Import and export custom nodes with .docx
One of the biggest advantages of the @tiptap-pro/extension-export-docx
and @tiptap-pro/extension-import-docx
extensions is the ability to define how custom nodes in your Tiptap schema should be rendered in DOCX.
This allows you to preserve application-specific content in the exported Word file.
Custom node conventions
Custom node converters must adhere to the underlying DOCX generation library’s requirements. In practice, a custom converter function for DOCX should return one of the allowed DOCX elements for that node: a Paragraph
class (or an array of Paragraph
classes), a Table
class, or null
if the node should be skipped in the output.
Export custom nodes to .docx
When calling editor.exportDocx()
, you can pass an array of custom node definitions in the ExportDocxOptions
argument. Each definition specifies the node type and a render function.
For the sake of the example, suppose your editor has a custom node type hintbox
(a callout-styled box). You can define how it should appear in DOCX.
Here's how the Hintbox
extension's custom node might look like:
import { mergeAttributes, Node } from '@tiptap/core'
export interface ParagraphOptions {
/**
* The HTML attributes for a paragraph node.
* @default {}
* @example { class: 'foo' }
*/
HTMLAttributes: Record<string, any>
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
hintbox: {
/**
* Set a hintbox
* @example editor.commands.setHintbox()
*/
setHintbox: () => ReturnType
/**
* Toggle a hintbox
* @example editor.commands.toggleHintbox()
*/
toggleHintbox: () => ReturnType
}
}
}
/**
* This extension allows you to create hintboxes.
* @see https://www.tiptap.dev/api/nodes/paragraph
*/
export const Hintbox = Node.create<ParagraphOptions>({
name: 'hintbox',
priority: 1000,
addOptions() {
return {
HTMLAttributes: {
style: 'padding: 20px; border: 1px solid #b8d8ff; border-radius: 5px; background-color: #e6f3ff;',
},
}
},
group: 'block',
content: 'inline*',
parseHTML() {
return [{ tag: 'p' }]
},
renderHTML({ HTMLAttributes }) {
return ['p', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
},
addCommands() {
return {
setHintbox:
() =>
({ commands }) => {
return commands.setNode(this.name)
},
toggleHintbox:
() =>
({ commands }) => {
return commands.toggleNode(this.name, 'paragraph')
},
}
},
addKeyboardShortcuts() {
return {
'Mod-Alt-h': () => this.editor.commands.toggleHintbox(),
}
},
})
And we will define how the Hintbox
custom node should be rendered in the DOCX
:
// Import the ExportDocx extension
import {
convertTextNode,
Docx,
ExportDocx,
lineHeightToDocx,
pixelsToHalfPoints,
pointsToTwips,
} from '@tiptap-pro/extension-export-docx'
const editor = new Editor({
extensions: [
// Other extensions ...
ExportDocx.configure({
onCompleteExport: result => {
setIsLoading(false)
const blob = new Blob([result], {
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'export.docx'
a.click()
URL.revokeObjectURL(url)
},
exportType: 'blob',
customNodes: [
{
type: 'hintbox',
render: node => {
// Here we define how our custom Hintbox node should be rendered in the DOCX.
// Per the documentation, we should return a Docx node
// that's either a Paragraph, an array of Paragraphs, or a Table.
return new Docx.Paragraph({
children: node.content.map(content => convertTextNode(content)),
style: 'Hintbox', // Here we apply our custom style to the Paragraph node.
})
},
},
], // Custom nodes
styleOverrides: {
paragraphStyles: [
// Here we define our custom styles for our custom Hintbox node.
{
id: 'Hintbox',
name: 'Hintbox',
basedOn: 'Normal',
next: 'Normal',
quickFormat: false,
run: {
font: 'Aptos Light',
size: pixelsToHalfPoints(16),
},
paragraph: {
spacing: {
before: pointsToTwips(12),
after: pointsToTwips(12),
line: lineHeightToDocx(1),
},
border: {
// DOCX colors are in Hexadecimal without the leading #
top: { style: Docx.BorderStyle.SINGLE, size: 1, color: 'b8d8ff', space: 5 },
bottom: { style: Docx.BorderStyle.SINGLE, size: 1, color: 'b8d8ff', space: 5 },
right: { style: Docx.BorderStyle.SINGLE, size: 1, color: 'b8d8ff', space: 5 },
left: { style: Docx.BorderStyle.SINGLE, size: 1, color: 'b8d8ff', space: 5 },
},
shading: {
type: Docx.ShadingType.SOLID,
color: 'e6f3ff',
},
},
},
],
}, // Style overrides
}),
// Other extensions ...
],
// Other editor settings ...
})
Then, at a later point in your application, you can export the editor content to a DOCX
file:
editor
.chain()
.exportDocx()
.run()
You can construct any supported DOCX
elements in the render
function using the Docx
library classes (Paragraph
, TextRun
, Table
, etc.) that are provided via the Docx
import from the @tiptap-pro/extension-export-docx
package.
Import custom nodes from .docx
When importing a DOCX file, you can also define how custom nodes should be converted back to Tiptap nodes. This is done by passing an array of custom node definitions to the import
command.
import { Import } from '@tiptap-pro/extension-import-docx'
// ... inside your Editor or useEditor setup:
Import.configure({
appId: 'your-app-id',
token: 'your-jwt',
// ATTENTION: This is for demo purposes only
endpoint: 'https://your-endpoint.com',
imageUploadCallbackUrl: 'https://your-endpoint.com/image-upload',
// Promisemirror custom node mapping
promisemirrorNodes: {
Hintbox: 'hintbox',
},
}),
The latest version of the @tiptap-pro/extension-import
has available the promisemirrorNodes
configuration option. This option allows you to map custom nodes from the DOCX to your Tiptap schema. In the example above, we are mapping the Hintbox
custom node from the DOCX to the hintbox
custom node in our Tiptap schema. By doing so, whenever the DOCX contains a Hintbox
custom node, it will be converted to a hintbox
node in Tiptap when imported.
DOCX, "prosemirrorNodes" and "prosemirrorMarks"
Please note that the promisemirrorNodes
and prosemirrorMarks
options will only work if you're importing a .docx
file. If you're importing another type of file, eg: an .odt
file, the /import
endpoint will be used instead of the /import-docx
endpoint, and the promisemirrorNodes
and prosemirrorMarks
options will not be available, and therefore you'd need to rely on the custom node and mark mapping API for those endpoints.