Convert Draft.js content to Tiptap-compatible content.
The best place to try this library is in the CodeSandbox.
Draft.js and Tiptap are both rich text editors, but they have different data structures. Draft.js preferring a flat structure of blocks with inline styles and entities, while Tiptap uses a nested structure of nodes and marks.
Draft.js has been archived, and Tiptap is a popular alternative for building rich text editors in multiple frameworks. This library aims to make it easier to migrate from Draft.js to Tiptap by providing a conversion tool.
npm install @tiptap/draftjs-to-tiptap
yarn add @tiptap/draftjs-to-tiptap
pnpm add @tiptap/draftjs-to-tiptap
bun add @tiptap/draftjs-to-tiptap
Unfortunately, there is no one-size-fits-all solution for converting Draft.js content to Tiptap content. The conversion process depends on the specific use case and the customizations made to the Draft.js content. Draft.js did not come with very many built-in features, so many developers added their own customizations to the content structure. So, the conversion process will likely need to be customized to fit your specific use case.
The conversion process involves mapping:
- Draft.js blocks to Tiptap nodes
- Draft.js entities to Tiptap nodes
- Draft.js inline styles to Tiptap marks
- Draft.js entities to Tiptap marks
- Blocks are the main structural units of content in Draft.js. They are the paragraphs, headings, lists, etc.
- Entities are the annotations on the content, like links, mentions, etc.
- Inline styles are the formatting applied to the text, like bold, italic, etc.
Here is an example of all the types of content Draft.js supports in a ContentState:
{
"blocks": [
{
"key": "1",
"text": "Hello, world!", // block text
"type": "unstyled", // block type
"depth": 0,
"inlineStyleRanges": [],
"entityRanges": [ // entities applied to the block text
{
"offset": 0, // start index of the entity
"length": 12, // length of the entity
"key": 0 // entity key in the entityMap below
}
],
"data": {}
}
],
"entityMap": {
"0": { // entity key
"type": "LINK", // entity type
"mutability": "MUTABLE",
"data": { // entity data
"url": "https://example.com"
}
}
}
}
In Tiptap:
- Nodes are the main structural units of content in Tiptap. They are the paragraphs, headings, lists, etc.
- Marks are the formatting applied to the text, like bold, italic, etc.
Here is an example of all the types of content Tiptap supports in a ContentNode:
{
"type": "paragraph", // node type
"content": [ // node content
{
"type": "text", // node type
"text": "Hello, world!",
"marks": [ // marks applied to the text
{
"type": "link", // mark type
"attrs": { // mark attributes
"href": "https://example.com"
}
}
]
}
]
}
As you can see, the structure of the content is quite different between Draft.js and Tiptap. This library aims to make it easier to convert Draft.js content to Tiptap content.
If you are using the default Draft.js content structure, you can use the default converter provided by this library. If you have a custom Draft.js content structure, you will need to add custom mappings to the converter.
Using the default converter is simple. Here is an example of how to convert Draft.js content to Tiptap content:
import { DraftConverter } from '@tiptap/draftjs-to-tiptap';
const draftContent = {
blocks: [
{
key: '1',
text: 'Hello, world!',
type: 'unstyled',
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {},
},
],
entityMap: {},
};
const convertDraftToTiptap = new DraftConverter();
const tiptapContent = convertDraftToTiptap.convert(draftContent);
It can be helpful to visualize the conversion process. You can use the visualizer web app to see the conversion process in action. The visualizer allows you to input Draft.js content and see the Tiptap content that is generated, inside a live Tiptap editor.
You can access the visualizer in CodeSandbox.
It's source is available in the visualizer
directory in this repository.
It looks like this:
You can configure the converter to use custom mappings for:
- Draft.js blocks to Tiptap nodes
- Draft.js inline styles to Tiptap marks
- Draft.js entities to Tiptap marks
- Draft.js entities to Tiptap nodes
Here is an example of how to use custom mappings:
import { DraftConverter } from '@tiptap/draftjs-to-tiptap';
const draftContent = {
blocks: [
{
key: '1',
text: 'Hello, world!',
type: 'unstyled',
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {},
},
],
entityMap: {},
};
const convertDraftToTiptap = new DraftConverter({
mapBlockToNode({ block, converter }) {
if (block.type === 'unstyled') {
return {
type: 'paragraph',
content: [
{
type: 'text',
text: block.text,
marks: [],
},
],
};
}
}
});
const tiptapContent = convertDraftToTiptap.convert(draftContent);
The custom mappings can be as simple or as complex as needed. You can use the converter
object to access the default mappings and helper functions provided by the library.
The library essentially iterates over each Draft.js block and applies the custom mappings to convert the block to a Tiptap node. It completely gives you control over the conversion process, even allowing you to control iteration over the blocks, which can be useful for more complex content structures.
Here is an example of a more complex custom mapping:
import { DraftConverter } from '@tiptap/draftjs-to-tiptap';
const draftContent = {
blocks: [
{
key: "1",
text: "Item 1",
type: "unordered-list-item",
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {},
},
{
key: "2",
text: "Item 2",
type: "unordered-list-item",
depth: 0,
inlineStyleRanges: [],
entityRanges: [],
data: {},
},
],
entityMap: {},
};
const convertDraftToTiptap = new DraftConverter({
// Mapping a block does not have to be a one-to-one mapping, it can be a many-to-one mapping
mapBlockToNode({ block, next, peek, converter, entityMap }) {
if (block.type === "unordered-list-item") {
// We need to check if the next block is also an unordered-list-item
const listNode = converter.createNode("bulletList");
do {
// For each unordered-list-item block, we create a listItem node
const itemNode = converter.createNode("listItem", {
content: [
// We create a paragraph node for the text content
converter.createNode("paragraph", {
content: [
converter.splitTextByEntityRangesAndInlineStyleRanges({
block,
entityMap,
}),
],
}),
],
});
// We add the listItem node to the bulletList node
converter.addChild(listNode, itemNode);
// The next item is not an unordered-list-item, so we break out of the loop
if (peek()?.type !== "unordered-list-item") {
break;
}
// `next` iterates to the next block
} while (next());
// We return the bulletList we've created
return listNode;
}
},
});
const tiptapContent = convertDraftToTiptap.convert(draftContent);
The DraftConverter
class is the main class provided by this library. It is used to convert Draft.js content to Tiptap content.
options
(optional): An object containing options for the converter.
mapBlockToNode?: (params: MapBlockToNodeContext) => NodeType | null | undefined
: A function that maps a Draft.js block to a Tiptap node.mapInlineStyleToMark?: (params: MapInlineStyleToMarkContext) => MarkType | null | undefined
: A function that maps a Draft.js inline style to a Tiptap mark.mapEntityToMark?: (params: MapEntityToMarkContext) => MarkType | null | undefined
: A function that maps a Draft.js entity to a Tiptap mark.mapEntityToNode?: (params: MapEntityToNodeContext) => NodeType | null | undefined
: A function that maps a Draft.js entity to a Tiptap node.
Available properties:
converter
: The draft converter instance.entityMap
: The entity map of the Draft.js content.doc
: The current document tree.getCurrentBlock
: A function that returns the current block.block
: The current block to convert.index
: The index of the current block.setIndex
: A function that sets the index of the current block.allBlocks
: All blocks in the content being converted.peek
: A function that peeks at the next block in the content without iterating.peekPrev
: A function that peeks at the previous block in the content without iterating.next
: A function that gets the next block in the content and iterates forward.prev
: A function that gets the previous block in the content and iterates backward.
Available properties:
converter
: The draft converter instance.range
: The inline style range to convert.entityMap
: The entity map of the Draft.js content.doc
: The current document tree.block
: The current block to convert.
Available properties:
converter
: The draft converter instance.range
: The entity range to convert.entityMap
: The entity map of the Draft.js content.doc
: The current document tree.block
: The current block to convert.
Available properties:
converter
: The draft converter instance.range
: The entity range to convert.entityMap
: The entity map of the Draft.js content.doc
: The current document tree.block
: The current block to convert.
draftContent
: The Draft.js content to convert.- Returns: The Tiptap content.
This library also provides utility functions for creating and manipulating Tiptap content. These functions can be used to create and manipulate Tiptap content directly without using the converter.
import { createNode, addChild } from '@tiptap/draftjs-to-tiptap';
const node = createNode('paragraph');
// { "type": "paragraph", "content": [] }
const child = createNode('text', { text: 'Hello, world!' });
// => { "type": "text", "text": "Hello, world!" }
const tree = addChild(node, child);
// => { "type": "paragraph", "content": [{ "type": "text", "text": "Hello, world!" }] }
This library is written in TypeScript and provides type definitions for the converter. It includes types for tiptap content, but any custom types will need to be defined by the user.
Here is an example of how to use TypeScript with the converter:
import { type NodeType } from '@tiptap/draftjs-to-tiptap';
declare module '@tiptap/draftjs-to-tiptap' {
// This extends the NodeMapping interface to include a custom node types
interface NodeMapping {
// This adds a custom node type to the NodeMapping allowing for correct typing in `createNode` and `addChild` methods
custom: NodeType<'custom'>;
}
}
const node = createNode('custom');
// => { "type": "custom", "content": [] }
This is not strictly necessary, but it can help with type checking and code completion in an IDE. As well as enforcing the correct structure of the content. For example, you can restrict the child nodes that can be added to a parent node, though this is only enforced on the type level and not at runtime.
import { type NodeType } from '@tiptap/draftjs-to-tiptap';
declare module '@tiptap/draftjs-to-tiptap' {
// This extends the NodeMapping interface to include a custom node types
interface NodeMapping {
// An image node can only have a src attribute and no content
image: NodeType<'image', { src: string }, never, never>;
// A figcaption node can only have text content
figcaption: NodeType<'figcaption', {}, MarkType, NodeMapping['text']>;
// A figure node can only have an image and a figcaption as children
figure: NodeType<
"figure",
{},
MarkType,
Array<NodeMapping['image'] | NodeMapping["figcaption"]>
>;
}
}