Skip to content

Commit e4d045f

Browse files
authored
feat: markdown pasting & custom paste handlers (#1490)
Adds - `editor.pasteText` which will paste text content into the editor - `editor.pasteHTML` which will convert HTML into BlockNote HTML and then paste that into the editor - `editor.pasteMarkdown` which will convert the markdown into BlockNote HTML and then paste that into the editor - `editorOptions.pasteHandler` for allowing the developer the ability to customize how content is pasted into the editor while still allowing the default paste behavior.
1 parent ed5ccc3 commit e4d045f

File tree

18 files changed

+633
-63
lines changed

18 files changed

+633
-63
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
title: Paste Handling
3+
description: This section explains how to handle paste events in BlockNote.
4+
imageTitle: Paste Handling
5+
---
6+
7+
import { Example } from "@/components/example";
8+
9+
# Paste Handling
10+
11+
BlockNote, by default, attempts to paste content in the following order:
12+
13+
- VS Code compatible content
14+
- Files
15+
- BlockNote HTML
16+
- Markdown
17+
- HTML
18+
- Plain text
19+
20+
> In certain cases, BlockNote will attempt to detect markdown in the clipboard and paste that into the editor as rich text.
21+
22+
You can change the default paste behavior by providing a custom paste handler, which will give you full control over how pasted content is inserted into the editor.
23+
24+
## `pasteHandler` option
25+
26+
The `pasteHandler` option is a function that receives the following arguments:
27+
28+
```ts
29+
type PasteHandler = (context: {
30+
event: ClipboardEvent;
31+
editor: BlockNoteEditor;
32+
defaultPasteHandler: (context?: {
33+
prioritizeMarkdownOverHTML?: boolean;
34+
plainTextAsMarkdown?: boolean;
35+
}) => boolean;
36+
}) => boolean;
37+
```
38+
39+
- `event`: The paste event.
40+
- `editor`: The current editor instance.
41+
- `defaultPasteHandler`: The default paste handler. If you only need to customize the paste behavior a little bit, you can fall back on the default paste handler.
42+
43+
The `defaultPasteHandler` function can be called with the following options:
44+
45+
- `prioritizeMarkdownOverHTML`: Whether to prioritize Markdown content in `text/plain` over `text/html` when pasting from the clipboard.
46+
- `plainTextAsMarkdown`: Whether to interpret plain text as markdown and paste that as rich text or to paste the text directly into the editor.
47+
48+
49+
## Custom Paste Handler
50+
51+
You can also provide your own paste handler by providing a function to the `pasteHandler` option.
52+
53+
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.
54+
55+
```ts
56+
const editor = new BlockNoteEditor({
57+
pasteHandler: ({ event, editor, defaultPasteHandler }) => {
58+
if (event.clipboardData?.types.includes("text/my-custom-format")) {
59+
// You can do any custom logic here, for example you could transform the clipboard data before pasting it
60+
const markdown = customToMarkdown(event.clipboardData.getData("text/my-custom-format"));
61+
62+
// The editor is able paste markdown (`pasteMarkdown`), HTML (`pasteHTML`), or plain text (`pasteText`)
63+
editor.pasteMarkdown(markdown);
64+
// We handled the paste event, so return true, returning false will cancel the paste event
65+
return true;
66+
}
67+
68+
// If we didn't handle the paste event, call the default paste handler to do the default behavior
69+
return defaultPasteHandler();
70+
},
71+
});
72+
```
73+
74+
See an example of this in the [Custom Paste Handler](/examples/basic/custom-paste-handler) example.

docs/pages/docs/editor-basics/setup.mdx

+9
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ type BlockNoteEditorOptions = {
3333
class?: string;
3434
}) => Plugin;
3535
initialContent?: PartialBlock[];
36+
pasteHandler?: (context: {
37+
event: ClipboardEvent;
38+
editor: BlockNoteEditor;
39+
defaultPasteHandler: (context: {
40+
pasteBehavior?: "prefer-markdown" | "prefer-html";
41+
}) => boolean | undefined;
42+
}) => boolean | undefined;
3643
resolveFileUrl: (url: string) => Promise<string>
3744
schema?: BlockNoteSchema;
3845
setIdAttribute?: boolean;
@@ -66,6 +73,8 @@ The hook takes two optional parameters:
6673

6774
`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).
6875

76+
`pasteHandler`: A function that can be used to override the default paste behavior. See [Paste Handling](/docs/advanced/paste-handling) for more.
77+
6978
`resolveFileUrl:` Function to resolve file URLs for display/download. Useful for creating authenticated URLs or implementing custom protocols.
7079

7180
`resolveUsers`: Function to resolve user information for comments. See [Comments](/docs/collaboration/comments) for more.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"playground": true,
3+
"docs": true,
4+
"author": "nperez0111",
5+
"tags": ["Basic"]
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import "@blocknote/core/fonts/inter.css";
2+
import { BlockNoteView } from "@blocknote/mantine";
3+
import "@blocknote/mantine/style.css";
4+
import { useCreateBlockNote } from "@blocknote/react";
5+
6+
import "./styles.css";
7+
8+
export default function App() {
9+
// Creates a new editor instance.
10+
const editor = useCreateBlockNote({
11+
initialContent: [
12+
{
13+
type: "paragraph",
14+
content: [
15+
{
16+
styles: {},
17+
type: "text",
18+
text: "Paste some text here",
19+
},
20+
],
21+
},
22+
],
23+
pasteHandler: ({ event, editor, defaultPasteHandler }) => {
24+
if (event.clipboardData?.types.includes("text/plain")) {
25+
editor.pasteMarkdown(
26+
event.clipboardData.getData("text/plain") +
27+
" - inserted by the custom paste handler"
28+
);
29+
return true;
30+
}
31+
return defaultPasteHandler();
32+
},
33+
});
34+
35+
// Renders the editor instance using a React component.
36+
return (
37+
<div>
38+
<BlockNoteView editor={editor} />
39+
<div className={"edit-buttons"}>
40+
<button
41+
className={"edit-button"}
42+
onClick={async () => {
43+
try {
44+
await navigator.clipboard.writeText(
45+
"**This is markdown in the plain text format**"
46+
);
47+
} catch (error) {
48+
window.alert("Failed to copy plain text with markdown content");
49+
}
50+
}}>
51+
Copy sample markdown to clipboard (text/plain)
52+
</button>
53+
<button
54+
className={"edit-button"}
55+
onClick={async () => {
56+
try {
57+
await navigator.clipboard.write([
58+
new ClipboardItem({
59+
"text/html": "<p><strong>HTML</strong></p>",
60+
}),
61+
]);
62+
} catch (error) {
63+
window.alert("Failed to copy HTML content");
64+
}
65+
}}>
66+
Copy sample HTML to clipboard (text/html)
67+
</button>
68+
<button
69+
className={"edit-button"}
70+
onClick={async () => {
71+
try {
72+
await navigator.clipboard.writeText(
73+
"This is plain text in the plain text format"
74+
);
75+
} catch (error) {
76+
window.alert("Failed to copy plain text");
77+
}
78+
}}>
79+
Copy sample plain text to clipboard (text/plain)
80+
</button>
81+
<button
82+
className={"edit-button"}
83+
onClick={async () => {
84+
try {
85+
await navigator.clipboard.write([
86+
new ClipboardItem({
87+
"text/plain": "Plain text",
88+
}),
89+
new ClipboardItem({
90+
"text/html": "<p><strong>HTML</strong></p>",
91+
}),
92+
new ClipboardItem({
93+
"text/markdown": "**Markdown**",
94+
}),
95+
]);
96+
} catch (error) {
97+
window.alert("Failed to copy multiple formats");
98+
}
99+
}}>
100+
Copy sample markdown, HTML, and plain text to clipboard (Safari only)
101+
</button>
102+
</div>
103+
</div>
104+
);
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Custom Paste Handler
2+
3+
In this example, we change the default paste handler to append some text to the pasted content when the content is plain text.
4+
5+
**Try it out:** Use the buttons to copy some content to the clipboard and paste it in the editor to trigger our custom paste handler.
6+
7+
**Relevant Docs:**
8+
9+
- [Paste Handling](/docs/advanced/paste-handling)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<html lang="en">
2+
<head>
3+
<script>
4+
<!-- AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY -->
5+
</script>
6+
<meta charset="UTF-8" />
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8+
<title>Custom Paste Handler</title>
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="./main.tsx"></script>
13+
</body>
14+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
2+
import React from "react";
3+
import { createRoot } from "react-dom/client";
4+
import App from "./App";
5+
6+
const root = createRoot(document.getElementById("root")!);
7+
root.render(
8+
<React.StrictMode>
9+
<App />
10+
</React.StrictMode>
11+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "@blocknote/example-custom-paste-handler",
3+
"description": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
4+
"private": true,
5+
"version": "0.12.4",
6+
"scripts": {
7+
"start": "vite",
8+
"dev": "vite",
9+
"build": "tsc && vite build",
10+
"preview": "vite preview",
11+
"lint": "eslint . --max-warnings 0"
12+
},
13+
"dependencies": {
14+
"@blocknote/core": "latest",
15+
"@blocknote/react": "latest",
16+
"@blocknote/ariakit": "latest",
17+
"@blocknote/mantine": "latest",
18+
"@blocknote/shadcn": "latest",
19+
"react": "^18.3.1",
20+
"react-dom": "^18.3.1"
21+
},
22+
"devDependencies": {
23+
"@types/react": "^18.0.25",
24+
"@types/react-dom": "^18.0.9",
25+
"@vitejs/plugin-react": "^4.3.1",
26+
"eslint": "^8.10.0",
27+
"vite": "^5.3.4"
28+
},
29+
"eslintConfig": {
30+
"extends": [
31+
"../../../.eslintrc.js"
32+
]
33+
},
34+
"eslintIgnore": [
35+
"dist"
36+
]
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.edit-buttons {
2+
display: flex;
3+
justify-content: space-between;
4+
margin-top: 8px;
5+
}
6+
7+
.edit-button {
8+
border: 1px solid gray;
9+
border-radius: 4px;
10+
padding-inline: 4px;
11+
}
12+
13+
.edit-button:hover {
14+
border: 1px solid lightgrey;
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"__comment": "AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY",
3+
"compilerOptions": {
4+
"target": "ESNext",
5+
"useDefineForClassFields": true,
6+
"lib": [
7+
"DOM",
8+
"DOM.Iterable",
9+
"ESNext"
10+
],
11+
"allowJs": false,
12+
"skipLibCheck": true,
13+
"esModuleInterop": false,
14+
"allowSyntheticDefaultImports": true,
15+
"strict": true,
16+
"forceConsistentCasingInFileNames": true,
17+
"module": "ESNext",
18+
"moduleResolution": "bundler",
19+
"resolveJsonModule": true,
20+
"isolatedModules": true,
21+
"noEmit": true,
22+
"jsx": "react-jsx",
23+
"composite": true
24+
},
25+
"include": [
26+
"."
27+
],
28+
"__ADD_FOR_LOCAL_DEV_references": [
29+
{
30+
"path": "../../../packages/core/"
31+
},
32+
{
33+
"path": "../../../packages/react/"
34+
}
35+
]
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// AUTO-GENERATED FILE, DO NOT EDIT DIRECTLY
2+
import react from "@vitejs/plugin-react";
3+
import * as fs from "fs";
4+
import * as path from "path";
5+
import { defineConfig } from "vite";
6+
// import eslintPlugin from "vite-plugin-eslint";
7+
// https://vitejs.dev/config/
8+
export default defineConfig((conf) => ({
9+
plugins: [react()],
10+
optimizeDeps: {},
11+
build: {
12+
sourcemap: true,
13+
},
14+
resolve: {
15+
alias:
16+
conf.command === "build" ||
17+
!fs.existsSync(path.resolve(__dirname, "../../packages/core/src"))
18+
? {}
19+
: ({
20+
// Comment out the lines below to load a built version of blocknote
21+
// or, keep as is to load live from sources with live reload working
22+
"@blocknote/core": path.resolve(
23+
__dirname,
24+
"../../packages/core/src/"
25+
),
26+
"@blocknote/react": path.resolve(
27+
__dirname,
28+
"../../packages/react/src/"
29+
),
30+
} as any),
31+
},
32+
}));

packages/core/src/api/clipboard/fromClipboard/acceptedMIMETypes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const acceptedMIMETypes = [
22
"vscode-editor-data",
33
"blocknote/html",
4+
"text/markdown",
45
"text/html",
56
"text/plain",
67
"Files",

0 commit comments

Comments
 (0)