Skip to content
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

feat: markdown pasting & custom paste handlers #1490

Merged
merged 21 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
74 changes: 74 additions & 0 deletions docs/pages/docs/advanced/paste-handling.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
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
- Files
- BlockNote HTML
- Markdown
- HTML
- Plain text

> In certain cases, BlockNote will attempt to detect markdown in the clipboard and paste that into the editor as rich text.

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.

## `pasteHandler` option

The `pasteHandler` option is a function that receives the following arguments:

```ts
type PasteHandler = (context: {
event: ClipboardEvent;
editor: BlockNoteEditor;
defaultPasteHandler: (context?: {
prioritizeMarkdownOverHTML?: boolean;
plainTextAsMarkdown?: boolean;
}) => boolean;
}) => boolean;
```

- `event`: The paste event.
- `editor`: The current editor instance.
- `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.

The `defaultPasteHandler` function can be called with the following options:

- `prioritizeMarkdownOverHTML`: Whether to prioritize Markdown content in `text/plain` over `text/html` when pasting from the clipboard.
- `plainTextAsMarkdown`: Whether to interpret plain text as markdown and paste that as rich text or to paste the text directly into the editor.


## Custom Paste Handler

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

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.

```ts
const editor = new BlockNoteEditor({
pasteHandler: ({ event, editor, defaultPasteHandler }) => {
if (event.clipboardData?.types.includes("text/my-custom-format")) {
// You can do any custom logic here, for example you could transform the clipboard data before pasting it
const markdown = customToMarkdown(event.clipboardData.getData("text/my-custom-format"));

// The editor is able paste markdown (`pasteMarkdown`), HTML (`pasteHTML`), or plain text (`pasteText`)
editor.pasteMarkdown(markdown);
// We handled the paste event, so return true, returning false will cancel the paste event
return true;
}

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

See an example of this in the [Custom Paste Handler](/examples/basic/custom-paste-handler) example.
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 sample markdown to clipboard (text/plain)
</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 sample HTML to clipboard (text/html)
</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 sample plain text to clipboard (text/plain)
</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 sample markdown, HTML, and plain text to clipboard (Safari only)
</button>
</div>
</div>
);
}
9 changes: 9 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,9 @@
# Custom Paste Handler

In this example, we change the default paste handler to append some text to the pasted content when the content is plain text.

**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.

**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