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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
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 @@
---
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 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