tiptap/docs/src/docPages/guide/build-custom-extensions.md

384 lines
12 KiB
Markdown
Raw Normal View History

2020-10-28 23:32:06 +08:00
# Build custom extensions
## toc
## Introduction
2020-11-06 04:14:21 +08:00
One of the strength of tiptap is its extendability. You dont depend on the provided extensions, its intended to extend the editor to your liking. With custom extensions you can add new content types and new functionalities, on top of what already exists or from scratch.
2020-10-28 23:32:06 +08:00
2020-11-06 04:14:21 +08:00
## How extensions work
Although tiptap tries to hide most of the complexity of ProseMirror, its built on top of its APIs and we recommend you to read through the [ProseMirror Guide](https://ProseMirror.net/docs/guide/) for advanced usage. Youll have a better understanding of how everything works under the hood and get more familiar with many terms and jargon used by tiptap.
Existing [nodes](/api/nodes), [marks](/api/marks) and [extensions](/api/extensions) can give you a good impression on how to approach your own extensions. To make it easier to switch between the documentation and the source code, we linked to the file on GitHub from every single extension documentation page.
We recommend to start with customizing existing extensions first, and create your own extensions with the gained knowledge later. Thats why all the below examples extend existing extensions, but all examples will work on newly created extensions aswell.
## Customize existing extensions
2020-10-28 23:32:06 +08:00
Lets say you want to change the keyboard shortcuts for the bullet list. You should start by looking at [the source code of the `BulletList` extension](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-bullet-list/index.ts) and find the part you would like to change. In that case, the keyboard shortcut, and just that.
Every extension has an `extend()` method, which takes an object with everything you want to change or add to it. For the bespoken example, your code could like that:
```js
// 1. Import the extension
import BulletList from '@tiptap/extension-bullet-list'
// 2. Overwrite the keyboard shortcuts
const CustomBulletList = BulletList.extend({
addKeyboardShortcuts() {
return {
'Mod-l': () => this.editor.bulletList(),
}
},
})
// 3. Add the custom extension to your editor
new Editor({
extensions: [
CustomBulletList(),
// …
]
})
```
The same applies to every aspect of an existing extension, except to the name. Lets look at all the things that you can change through the extend method. We focus on one aspect in every example, but you can combine all those examples and change multiple aspects in one `extend()` call too.
### Name
2020-10-30 23:13:47 +08:00
The extension name is used in a whole lot of places and changing it isnt too easy. If you want to change the name of an existing extension, we would recommended to copy the whole extension and change the name in all occurrences.
2020-10-28 23:32:06 +08:00
2020-10-30 23:13:47 +08:00
The extension name is also part of the JSON. If you [store your content as JSON](/guide/store-content#option-1-json), you need to change the name there too.
2020-10-28 23:32:06 +08:00
### Settings
2020-10-30 23:13:47 +08:00
All settings can be configured through the extension anyway, but if you want to change the default settings, for example to provide a library on top of tiptap for other developers, you can do it like that:
2020-10-28 23:32:06 +08:00
```js
import Heading from '@tiptap/extension-heading'
const CustomHeading = Heading.extend({
defaultOptions: {
levels: [1, 2, 3],
},
})
```
2020-10-30 23:13:47 +08:00
### Schema
tiptap works with a strict schema, which configures how the content can be structured, nested, how it behaves and many more things. You [can change all aspects of the schema](/api/schema) for existing extensions. Lets walk through a few common use cases.
2020-10-28 23:32:06 +08:00
The default `Blockquote` extension can wrap other nodes, like headings. If you want to allow nothing but paragraphs in your blockquotes, this is how you could achieve it:
```js
2020-10-30 23:13:47 +08:00
// Blockquotes must only include paragraphs
2020-10-28 23:32:06 +08:00
import Blockquote from '@tiptap/extension-blockquote'
const CustomBlockquote = Blockquote.extend({
2020-10-28 23:54:11 +08:00
content: 'paragraph*',
2020-10-28 23:32:06 +08:00
})
```
2020-10-30 23:13:47 +08:00
The schema even allows to make your nodes draggable, thats what the `draggable` option is for, which defaults to `false`.
2020-10-28 23:32:06 +08:00
```js
2020-10-30 21:59:27 +08:00
// Draggable paragraphs
2020-10-28 23:32:06 +08:00
import Paragraph from '@tiptap/extension-paragraph'
const CustomParagraph = Paragraph.extend({
draggable: true,
})
```
2020-10-30 23:13:47 +08:00
Thats just two tiny examples, but [the underlying ProseMirror schema](https://prosemirror.net/docs/ref/#model.SchemaSpec) is really powerful. You should definitely read the documentation to understand all the nifty details.
2020-10-30 21:59:27 +08:00
### Attributes
2020-10-30 23:13:47 +08:00
You can use attributes to store additional information in the content. Lets say you want to extend the default paragraph extension to enable paragraphs to have different colors:
2020-10-30 21:59:27 +08:00
```js
const CustomParagraph = Paragraph.extend({
addAttributes() {
2020-10-30 23:13:47 +08:00
// Return an object with attribute configuration
2020-10-30 21:59:27 +08:00
return {
color: {
default: 'pink',
},
},
},
})
// Result:
2020-11-06 04:14:21 +08:00
// <p color="pink">Example Text</p>
2020-10-30 21:59:27 +08:00
```
2020-11-06 04:14:21 +08:00
Thats already enough to tell tiptap about the new attribute, and set `'pink'` as the default value. All attributes will be rendered as a HTML attribute by default, and parsed as attributes from the content.
2020-10-30 21:59:27 +08:00
2020-10-30 23:13:47 +08:00
Lets stick with the color example and assume youll want to add an inline style to actually color the text. With the `renderHTML` function you can return HTML attributes which will be rendered in the output.
This examples adds a style HTML attribute based on the value of color:
2020-10-30 21:59:27 +08:00
```js
const CustomParagraph = Paragraph.extend({
addAttributes() {
return {
color: {
default: null,
2020-10-30 23:13:47 +08:00
// Take the attribute values
2020-10-30 21:59:27 +08:00
renderHTML: attributes => {
2020-10-30 23:13:47 +08:00
// … and return an object with HTML attributes.
2020-10-30 21:59:27 +08:00
return {
style: `color: ${attributes.color}`,
}
},
},
}
},
})
// Result:
2020-11-10 22:28:29 +08:00
// <p style="color: pink">Example Text</p>
2020-10-30 21:59:27 +08:00
```
2020-11-06 04:14:21 +08:00
You can also control how the attribute is parsed from the HTML. Lets say you want to store the color in an attribute called `data-color`, heres how you would do that:
2020-10-30 21:59:27 +08:00
```js
const CustomParagraph = Paragraph.extend({
addAttributes() {
return {
color: {
default: null,
2020-10-30 23:13:47 +08:00
// Customize the HTML parsing (for example, to load the initial content)
2020-10-30 21:59:27 +08:00
parseHTML: element => {
return {
2020-11-06 04:14:21 +08:00
color: element.getAttribute('data-color'),
2020-10-30 21:59:27 +08:00
}
},
2020-10-30 23:13:47 +08:00
// … and customize the HTML rendering.
2020-10-30 21:59:27 +08:00
renderHTML: attributes => {
return {
2020-11-06 04:14:21 +08:00
'data-color': atttributes.color,
2020-10-30 21:59:27 +08:00
style: `color: ${attributes.color}`,
}
},
},
}
},
})
// Result:
2020-11-06 04:14:21 +08:00
// <p data-color="pink" style="color: pink">Example Text</p>
2020-10-30 21:59:27 +08:00
```
2020-11-06 04:14:21 +08:00
You can disable the rendering of attributes, if you pass `rendered: false`.
2020-10-30 21:59:27 +08:00
### Global Attributes
2020-10-30 23:13:47 +08:00
Attributes can be applied to multiple extensions at once. Thats useful for text alignment, line height, color, font family, and other styling related attributes.
2020-10-30 23:19:46 +08:00
Take a closer look at [the full source code](https://github.com/ueberdosis/tiptap-next/tree/main/packages/extension-text-align) of the [`TextAlign`](/api/extensions/text-align) extension to see a more complex example. But here is how it works in a nutshell:
2020-10-30 23:13:47 +08:00
```js
import { createExtension } from '@tiptap/core'
const TextAlign = createExtension({
addGlobalAttributes() {
return [
{
// Extend the following extensions
types: [
'heading',
'paragraph',
],
// … with those attributes
attributes: {
textAlign: {
default: 'left',
renderHTML: attributes => ({
style: `text-align: ${attributes.textAlign}`,
}),
parseHTML: element => ({
textAlign: element.style.textAlign || 'left',
}),
},
},
},
]
},
})
```
2020-10-30 21:59:27 +08:00
2020-10-31 03:11:11 +08:00
### Render HTML
With the `renderHTML` function you can control how an extension is rendered to HTML. We pass an attributes object to it, with all local attributes, global attributes, and configured CSS classes. Here is an example from the `Bold` extension:
```js
renderHTML({ attributes }) {
return ['strong', attributes, 0]
},
```
The first value in the array should be the name of HTML tag. If the second element is an object, its interpreted as a set of attributes. Any elements after that are rendered as children.
The number zero (representing a hole) is used to indicate where the content should be inserted. Lets look at the rendering of the `CodeBlock` extension with two nested tags:
```js
renderHTML({ attributes }) {
return ['pre', ['code', attributes, 0]]
},
```
If you want to add some specific attributes there, import the `mergeAttributes` helper from `@tiptap/core`:
```js
renderHTML({ attributes }) {
return ['a', mergeAttributes(attributes, { rel: this.options.rel }), 0]
},
```
2020-10-30 21:59:27 +08:00
### Parse HTML
2020-11-06 04:14:21 +08:00
The `parseHTML()` function tries to load the editor document from HTML. The function gets the HTML DOM element passed as a parameter, and is expected to return an object with attributes and their values. Here is a simplified example from the [`Bold`](/api/marks/bold) mark:
2020-10-30 21:59:27 +08:00
2020-11-06 04:14:21 +08:00
```js
parseHTML() {
return [
{
tag: 'strong',
},
]
},
```
This defines a rule to convert all `<strong>` tags to `Bold` marks. But you can get more advanced with this, here is the full example from the extension:
2020-10-28 23:32:06 +08:00
2020-11-06 04:14:21 +08:00
```js
parseHTML() {
return [
// <strong>
{
tag: 'strong',
},
// <b>
{
tag: 'b',
getAttrs: node => node.style.fontWeight !== 'normal' && null,
},
// <span style="font-weight: bold">
{
style: 'font-weight',
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null,
},
]
},
```
This looks for `<strong>` and `<b>` tags, and any HTML tag with an inline style setting the `font-weight` to bold.
As you can see, you can optionally pass a `getAttrs` callback, to add more complex checks, for example for specific HTML attributes. The callback gets passed the HTML DOM node, except when checking for the `style` attribute, then its the value.
### Commands
2020-10-28 23:32:06 +08:00
```js
import Paragraph from '@tiptap/extension-paragraph'
const CustomParagraph = Paragraph.extend({
addCommands() {
return {
paragraph: () => ({ commands }) => {
return commands.toggleBlockType('paragraph', 'paragraph')
},
}
},
})
```
### Keyboard shortcuts
Most core extensions come with sensible keyboard shortcut defaults. Depending on what you want to build, youll likely want to change them though. With the `addKeyboardShortcuts()` method you can overwrite the predefined shortcut map:
```js
2020-10-30 21:59:27 +08:00
// Change the bullet list keyboard shortcut
2020-10-28 23:32:06 +08:00
import BulletList from '@tiptap/extension-bullet-list'
const CustomBulletList = BulletList.extend({
addKeyboardShortcuts() {
return {
'Mod-l': () => this.editor.bulletList(),
}
},
})
```
### Input rules
```js
2020-10-30 21:59:27 +08:00
// Use the ~single tilde~ markdown shortcut
2020-10-28 23:32:06 +08:00
import Strike from '@tiptap/extension-strike'
import { markInputRule } from '@tiptap/core'
const inputRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/gm
const CustomStrike = Strike.extend({
addInputRules() {
return [
markInputRule(inputRegex, this.type),
]
},
})
```
### Paste rules
```js
2020-10-30 21:59:27 +08:00
// Overwrite the underline regex for pasted text
2020-10-28 23:32:06 +08:00
import Underline from '@tiptap/extension-underline'
import { markPasteRule } from '@tiptap/core'
const pasteRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/gm
const CustomUnderline = Underline.extend({
addPasteRules() {
return [
markPasteRule(inputRegex, this.type),
]
},
})
```
2020-11-06 04:14:21 +08:00
### Node views (Advanced)
```js
import Link from '@tiptap/extension-underline'
2020-11-06 04:14:21 +08:00
const CustomLink = Link.extend({
addNodeView() {
return () => {
const container = document.createElement('div')
2020-10-28 23:32:06 +08:00
2020-11-06 04:14:21 +08:00
container.addEventListener('change', event => {
alert('clicked on the container')
})
return {
dom: container,
}
}
},
})
```
2020-10-28 23:32:06 +08:00
2020-11-06 04:14:21 +08:00
## Start from scratch
2020-10-28 23:32:06 +08:00
2020-11-06 04:14:21 +08:00
### Create a node
```js
import { createNode } from '@tiptap/core'
2020-10-28 23:32:06 +08:00
2020-11-06 04:14:21 +08:00
const CustomNode = createNode({
// Your code goes here.
})
```
2020-10-28 23:32:06 +08:00
2020-11-06 04:14:21 +08:00
### Create a mark
```js
import { createMark } from '@tiptap/core'
const CustomMark = createMark({
// Your code goes here.
})
```
### Create an extension
```js
import { createExtension } from '@tiptap/core'
const CustomExtension = createExtension({
// Your code goes here.
})
```