remove gridsome

This commit is contained in:
Philipp Kühn
2021-09-16 14:41:25 +02:00
parent e012a29240
commit 2f15a11572
311 changed files with 157 additions and 10308 deletions

View File

@@ -0,0 +1,41 @@
---
tableOfContents: true
---
# Accessibility
:::pro Fund the development ♥
We need your support to maintain, update, support and develop tiptap. If youre waiting for progress here, [become a sponsor and fund our work](/sponsor).
:::
## toc
## Introduction
We strive to make tiptap accessible to everyone, but to be honest, theres not much work done now. From our current understanding, thats what needs to be done:
## Interface
An interface needs to have well-defined contrasts, big enough click areas, semantic markup, must be keyboard accessible and well documented. Currently, we dont even provide an interface, so for now thats totally up to you.
But no worries, well provide an interface soon and take accessibility into account early on.
## Output
The editor needs to produce semantic markup, must be keyboard accessible and well documented. The tiptap content is well structured so thats a good foundation already. That said, we can add support and encourage the usage of additional attributes, for example the Alt-attribute for images.
### Writing assistance (optional)
An optional writing assitance could help people writing content semanticly correct, for example pointing out an incorrect usage of heading levels. With that kind of assistance provided by the core developers, we could help to improve the content of a lot of applications.
## Resources
| Document | Section | Heading |
| -------- | ------- | -------------------------------------------------------------------------------------- |
| WCAG 3.0 | 7.1 | [Text Alternatives](https://www.w3.org/TR/wcag-3.0/#text-alternatives) |
| WCAG 2.1 | 1.1.1 | [Non-text Content](https://www.w3.org/WAI/WCAG21/Understanding/non-text-content) |
| WCAG 2.1 | 2.1 | [Keyboard Accessible](https://www.w3.org/WAI/WCAG21/Understanding/keyboard-accessible) |
| WCAG 2.1 | 2.1.1 | [Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard) |
| WCAG 2.1 | 4.1.1 | [Parsing](https://www.w3.org/WAI/WCAG21/Understanding/parsing) |
| WCAG 2.1 | 4.1.2 | [Name, Role, Value](https://www.w3.org/WAI/WCAG21/Understanding/name-role-value) |
| WCAG 2.1 | 2.1.2 | [No Keyboard Trap](https://www.w3.org/TR/WCAG21/#no-keyboard-trap) |
TODO: Update resources to point to WCAG 3.0 https://www.w3.org/TR/wcag-3.0/
Anything we should add here? [Please let us know](mailto:humans@tiptap.dev), so we can take it into account.

View File

@@ -0,0 +1,367 @@
---
tableOfContents: true
---
# Collaborative editing
## toc
## Introduction
Real-time collaboration, syncing between different devices and working offline used to be hard. We provide everything you need to keep everything in sync, conflict-free with the power of [Y.js](https://github.com/yjs/yjs). The following guide explains all things to take into account when you consider to make tiptap collaborative. Dont worry, a production-grade setup doesnt require much code.
## Configure the editor
The underyling schema tiptap uses is an excellent foundation to sync documents. With the [`Collaboration`](/api/extensions/collaboration) you can tell tiptap to track changes to the document with [Y.js](https://github.com/yjs/yjs).
Y.js is a conflict-free replicated data types implementation, or in other words: Its reaaally good in merging changes. And to achieve that, changes dont have to come in order. Its totally fine to change a document while being offline and merge it with other changes when the device is online again.
But somehow, all clients need to interchange document modifications at some point. The most popular technologies to do that are [WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) and [WebSockets](https://developer.mozilla.org/de/docs/Web/API/WebSocket), so lets have a closer look at those:
### WebRTC
WebRTC uses a server only to connect clients with each other. The actual data is then flowing between the clients, without the server knowing anything about it and thats great to take the first steps with collaborative editing.
First, install the dependencies:
```bash
# with npm
npm install @tiptap/extension-collaboration yjs y-webrtc
# with Yarn
yarn add @tiptap/extension-collaboration yjs y-webrtc
```
Now, create a new Y document, and register it with tiptap:
```js
import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
// A new Y document
const ydoc = new Y.Doc()
// Registered with a WebRTC provider
const provider = new WebrtcProvider('example-document', ydoc)
const editor = new Editor({
extensions: [
// …
// Register the document with tiptap
Collaboration.configure({
document: ydoc,
}),
],
})
```
This should be enough to create a collaborative instance of tiptap. Crazy, isnt it? Try it out, and open the editor in two different browsers. Changes should be synced between different windows.
So how does this magic work? All clients need to connect with eachother, thats the job of a *provider*. The [WebRTC provider](https://github.com/yjs/y-webrtc) is the easiest way to get started with, as it requires a public server to connect clients directly with each other, but not to sync the actual changes. This has two downsides, though.
1. Browsers refuse to connect with too many clients. With Y.js its enough if all clients are connected indirectly, but even that isnt possible at some point. Or in other words, it doesnt scale well for more than 100+ clients in the same document.
2. Its likely you want to involve a server to persist changes anyway. But the WebRTC signaling server (which connects all clients with eachother) doesnt receive the changes and therefore doesnt know whats in the document.
Anyway, if you want to dive deeper, head over to [the Y WebRTC repository](https://github.com/yjs/y-webrtc) on GitHub.
### WebSocket (Recommended)
For most uses cases, the WebSocket provider is the recommended choice. Its very flexible and can scale very well. For the client, the example is nearly the same, only the provider is different. First, lets install the dependencies:
```bash
# with npm
npm install @tiptap/extension-collaboration yjs y-websocket
# with Yarn
yarn add @tiptap/extension-collaboration yjs y-websocket
```
And then register the WebSocket provider with tiptap:
```js
import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
// A new Y document
const ydoc = new Y.Doc()
// Registered with a WebSocket provider
const provider = new WebsocketProvider('ws://127.0.0.1:1234', 'example-document', ydoc)
const editor = new Editor({
extensions: [
// …
// Register the document with tiptap
Collaboration.configure({
document: ydoc,
}),
],
})
```
That example doesnt work out of the box. As you can see, its configured to talk to a WebSocket server which is available under `ws://127.0.0.1:1234` (WebSocket protocol `ws://`, your local IP `127.0.0.1` and the port `1234`). You need to set this up, too.
#### The WebSocket backend
To make the server part as easy as possible, we provide [an opinionated server package, called hocuspocus](http://hocuspocus.dev/) (early access for sponsors). Lets go through, how this will work once its released.
Create a new project, and install the hocuspocus server as a dependency:
```bash
# with npm
npm install @hocuspocus/server
# with Yarn
yarn add @hocuspocus/server
```
Create an `index.js` and throw in the following content, to create, configure and start your very own WebSocket server:
```js
import { Server } from '@hocuspocus/server'
import { RocksDB } from '@hocuspocus/extension-rocksdb'
const server = Server.configure({
port: 1234,
extensions: [
new RocksDB({ path: './database' }),
],
})
server.listen()
```
Thats all. Start the script with:
```bash
node ./index.js
```
<!-- TODO: This should output something like “Listening on ws://127.0.0.1:1234”. -->
Try opening http://127.0.0.1:1234 in your browser. You should see a plain text `OK` if everything works fine.
Go back to your tiptap editor and hit reload, it should now connect to the WebSocket server and changes should sync with all other clients. Amazing, isnt it?
### Multiple network providers
You can even combine multiple providers. Thats not needed, but could keep clients connected, even if one connection - for example the WebSocket server - goes down for a while. Here is an example:
```js
new WebrtcProvider('example-document', ydoc)
new WebsocketProvider('ws://127.0.0.1:1234', 'example-document', ydoc)
```
Yes, thats all.
Keep in mind that WebRTC needs a signaling server to connect clients. This signaling server doesnt receive the synced data, but helps to let clients find each other. You can [run your own signaling server](https://github.com/yjs/y-webrtc#signaling), if you like. Otherwise its using a default URL baked into the package.
### Show other cursors
To enable users to see the cursor and text selections of each other, add the [`CollaborationCursor`](/api/extensions/collaboration-cursor) extension.
```js
import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
const ydoc = new Y.Doc()
const provider = new WebsocketProvider('ws://127.0.0.1:1234', 'example-document', ydoc)
const editor = new Editor({
extensions: [
Collaboration.configure({
document: ydoc,
}),
// Register the collaboration cursor extension
CollaborationCursor.configure({
provider: provider,
name: 'Cyndi Lauper',
color: '#f783ac',
}),
// …
],
})
```
As you can see, you can pass a name and color for every user. Look at the [collaborative editing example](/examples/collaborative-editing), to see a more advanced example.
### Offline support
Adding offline support to your collaborative editor is basically a one-liner, thanks to the fantastic [Y IndexedDB adapter](https://github.com/yjs/y-indexeddb). Install it:
```bash
# with npm
npm install y-indexeddb
# with Yarn
yarn add y-indexeddb
```
And connect it with a Y document:
```js
import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'
import { IndexeddbPersistence } from 'y-indexeddb'
const ydoc = new Y.Doc()
// Store the Y document in the browser
new IndexeddbPersistence('example-document', ydoc)
const editor = new Editor({
extensions: [
// …
Collaboration.configure({
document: ydoc,
}),
],
})
```
All changes will be stored in the browser then, even if you close the tab, go offline, or make changes while working offline. Next time you are online, the WebSocket provider will try to find a connection and eventually sync the changes.
Yes, its magic. As already mentioned, that is all based on the fantastic Y.js framework. And if youre using it, or our integration, you should definitely [sponsor Kevin Jahns on GitHub](https://github.com/dmonad), he is the brain behind Y.js.
## Our plug & play collaboration backend
Our collaborative editing backend handles the syncing, authorization, persistence and scaling. Lets go through a few common use cases here!
:::warning Request early access
Our plug & play collaboration backend hocuspocus is still work in progress. If you want to give it a try, [get early access](https://www.hocuspocus.dev).
:::
### The document name
The document name is `'example-document'` in all examples here, but it could be any string. In a real-world app youd probably add the name of your entity and the ID of the entity. Here is how that could look like:
```js
const documentName = 'page.140'
```
In the backend, you can split the string to know the user is typing on a page with the ID 140 to manage authorization and such accordingly. New documents are created on the fly, no need to tell the backend about them, besides passing a string to the provider.
And if youd like to sync multiple fields with one Y.js document, just pass different fragment names to the collaboration extension:
```js
// a tiptap instance for the field
Collaboration.configure({
document: ydoc,
field: 'title',
})
// and another instance for the summary, both in the same Y.js document
Collaboration.configure({
document: ydoc,
field: 'summary',
})
```
If your setup is somehow more complex, for example with nested fragments, you can pass a raw Y.js fragment too. `document` and `field` will be ignored then.
```js
// a raw Y.js fragment
Collaboration.configure({
fragment: ydoc.getXmlFragment('custom'),
})
```
### Authentication & Authorization
With the `onConnect` hook you can check if a client is authenticated and authorized to view the current document. In a real world application this would probably be a request to an API, a database query or something else.
When throwing an error (or rejecting the returned Promise), the connection to the client will be terminated. If the client is authorized and authenticated you can also return contextual data which will be accessible in other hooks. But you don't need to.
```js
import { Server } from '@hocuspocus/server'
const server = Server.configure({
async onConnect(data) {
const { requestParameters } = data
// Example test if a user is authenticated using a
// request parameter
if (requestParameters.access_token !== 'super-secret-token') {
throw new Error('Not authorized!')
}
// You can set contextual data to use it in other hooks
return {
user: {
id: 1234,
name: 'John',
},
}
},
})
server.listen()
```
### Handling Document changes
With the `onChange` hook you can listen to changes of the document and handle them. It should return
a Promise. It's payload contains the resulting document as well as the actual update in the Y-Doc
binary format.
In a real-world application you would probably save the current document to a database, send it via
webhook to an API or something else. If you want to send a webhook to an external API we already
have built a simple to use webhook extension you should check out.
It's **highly recommended** to debounce extensive operations (like API calls) as this hook can be
fired up to multiple times a second:
You need to serialize the Y-Doc that hocuspocus gives you to something you can actually display in
your views.
This example is **not intended** to be a primary storage as serializing to and deserializing from JSON will not store the collaboration history steps but only the resulting document. Make sure to always use the RocksDB extension as primary storage.
```typescript
import { debounce } from 'debounce'
import { Server } from '@hocuspocus/server'
import { TiptapTransformer } from '@hocuspocus/transformer'
import { writeFile } from 'fs'
let debounced
const hocuspocus = Server.configure({
async onChange(data) {
const save = () => {
// Convert the y-doc to something you can actually use in your views.
// In this example we use the TiptapTransformer to get JSON from the given
// ydoc.
const prosemirrorJSON = TiptapTransformer.fromYdoc(data.document)
// Save your document. In a real-world app this could be a database query
// a webhook or something else
writeFile(
`/path/to/your/documents/${data.documentName}.json`,
prosemirrorJSON
)
// Maybe you want to store the user who changed the document?
// Guess what, you have access to your custom context from the
// onConnect hook here.
console.log(`Document ${data.documentName} changed by ${data.context.user.name}`)
}
debounced?.clear()
debounced = debounce(() => save, 4000)
debounced()
},
})
hocuspocus.listen()
```
## Pitfalls
### Schema updates
tiptap is very strict with the [schema](/api/schema), that means, if you add something thats not allowed according to the configured schema itll be thrown away. That can lead to a strange behaviour when multiple clients with different schemas share changes to a document.
Lets say you added an editor to your app and the first people use it already. They have all a loaded instance of tiptap with all default extensions, and therefor a schema that only allows those. But you want to add task lists in the next update, so you add the extension and deploy again.
A new user opens your app and has the updated schema (with task lists), while all others still have the old schema (without task lists). The new user checks out the newly added tasks lists and adds it to a document to show that feature to other users in that document. But then, it magically disappears right after she added it. What happened?
When one user adds a new node (or mark), that change will be synced to all other connected clients. The other connected clients apply those changes to the editor, and tiptap, strict as it is, removes the newly added node, because its not allowed according to their (old) schema. Those changes will be synced to other connected clients and oops, its removed everywhere. To avoid this you have a few options:
1. Never change the schema (not cool).
2. Force clients to update when you deploy a new schema (tough).
3. Keep track of the schema version and disable the editor for clients with an outdated schema (depends on your setup).
Its on our list to provide features to make that easier. If youve got an idea how to improve that, share it with us!

149
docs/guide/configuration.md Normal file
View File

@@ -0,0 +1,149 @@
---
tableOfContents: true
---
# Configuration
## toc
## Introduction
For most cases its enough to say where tiptap should be rendered (`element`), what functionalities you want to enable (`extensions`) and what the initial document should be (`content`).
A few more things can be configured though. Lets look at a fully configured editor example.
## Configure the editor
To add your configuration, pass [an object with settings](/api/editor) to the `Editor` class, like shown here:
```js
import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
new Editor({
element: document.querySelector('.element'),
extensions: [
Document,
Paragraph,
Text,
],
content: '<p>Example Text</p>',
autofocus: true,
editable: true,
injectCSS: false,
})
```
This will do the following:
1. bind tiptap to `.element`,
2. load the `Document`, `Paragraph` and `Text` extensions,
3. set the initial content,
4. place the cursor in the editor after initialization,
5. make the text editable (but thats the default anyway), and
6. disable the loading of [the default CSS](https://github.com/ueberdosis/tiptap/tree/main/packages/core/src/style.ts) (which is not much anyway).
## Nodes, marks and extensions
Most editing features are bundled as [node](/api/nodes), [mark](/api/marks) or [extension](/api/extensions). Import what you need and pass them as an array to the editor.
Here is the minimal setup with only three extensions:
```js
import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
new Editor({
element: document.querySelector('.element'),
extensions: [
Document,
Paragraph,
Text,
],
})
```
### Configure extensions
Most extensions can be configured. Add a `.configure()` and pass an object to it.
The following example will disable the default heading levels 4, 5 and 6 and just allow 1, 2 and 3:
```js
import { Editor } from '@tiptap/core'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import Heading from '@tiptap/extension-heading'
new Editor({
element: document.querySelector('.element'),
extensions: [
Document,
Paragraph,
Text,
Heading.configure({
levels: [1, 2, 3],
}),
],
})
```
Have a look at the documentation of the extension you are using to learn more about their settings.
### Default extensions
We have bundled a few of the most common extensions into a `StarterKit` extension. Here is how you to use that:
```js
import StarterKit from '@tiptap/starter-kit'
new Editor({
extensions: [
StarterKit,
],
})
```
You can even pass a configuration for all included extensions as an object. Just prefix the configuration with the extension name:
```js
import StarterKit from '@tiptap/starter-kit'
new Editor({
extensions: StarterKit.configure({
heading: {
levels: [1, 2, 3],
},
}),
})
```
The `StarterKit` extension loads the most common extensions, but not all available extensions. If you want to load additional extensions or add a custom extension, add them to the `extensions` array:
```js
import StarterKit from '@tiptap/starter-kit'
import Strike from '@tiptap/extension-strike'
new Editor({
extensions: [
StarterKit,
Strike,
],
})
```
Dont want to load a specific extension from the `StarterKit`? Just pass `false` to the config:
```js
import StarterKit from '@tiptap/starter-kit'
new Editor({
extensions: [
StarterKit.configure({
history: false,
}),
],
})
```
You will probably see something like that in collaborative editing examples. The [`Collaboration`](/api/extensions/collaboration) comes with its own history extension. You need to remove or disable the default [`History`](/api/extensions/history) extension to avoid conflicts.

View File

@@ -0,0 +1,569 @@
---
tableOfContents: true
---
# Custom extensions
## toc
## Introduction
One of the strengths of tiptap is its extendability. You dont depend on the provided extensions, it is 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. Lets start with a few common examples of how you can extend existing nodes, marks and extensions.
Youll learn how you start from scratch at the end, but youll need the same knowledge for extending existing and creating new extensions.
## Extend existing extensions
Every extension has an `extend()` method, which takes an object with everything you want to change or add to it.
Lets say, youd like to change the keyboard shortcut for the bullet list. You should start with looking at the source code of the extension, in that case [the `BulletList` node](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-bullet-list/src/bullet-list.ts). For the bespoken example to overwrite the keyboard shortcut, your code could look 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.commands.toggleBulletList(),
}
},
})
// 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
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, you can copy the whole extension and change the name in all occurrences.
The extension name is also part of the JSON. If you [store your content as JSON](/guide/output#option-1-json), you need to change the name there too.
### Priority
The priority defines the order in which extensions are registered. The default priority is `100`, thats what most extension have. Extensions with a higher priority will be loaded earlier.
```js
import Link from '@tiptap/extension-link'
const CustomLink = Link.extend({
priority: 1000,
})
```
The order in which extensions are loaded influences two things:
1. #### Plugin order
ProseMirror plugins of extensions with a higher priority will run first.
2. #### Schema order
The [`Link`](/api/marks/link) mark for example has a higher priority, which means it will be rendered as `<a href="…"><strong>Example</strong></a>` instead of `<strong><a href="…">Example</a></strong>`.
### Settings
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:
```js
import Heading from '@tiptap/extension-heading'
const CustomHeading = Heading.extend({
defaultOptions: {
...Heading.options,
levels: [1, 2, 3],
},
})
```
### 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.
The default `Blockquote` extension can wrap other nodes, like headings. If you want to allow nothing but paragraphs in your blockquotes, set the `content` attribute accordingly:
```js
// Blockquotes must only include paragraphs
import Blockquote from '@tiptap/extension-blockquote'
const CustomBlockquote = Blockquote.extend({
content: 'paragraph*',
})
```
The schema even allows to make your nodes draggable, thats what the `draggable` option is for. It defaults to `false`, but you can override that.
```js
// Draggable paragraphs
import Paragraph from '@tiptap/extension-paragraph'
const CustomParagraph = Paragraph.extend({
draggable: true,
})
```
Thats just two tiny examples, but [the underlying ProseMirror schema](https://prosemirror.net/docs/ref/#model.SchemaSpec) is really powerful.
### Attributes
You can use attributes to store additional information in the content. Lets say you want to extend the default `Paragraph` node to have different colors:
```js
const CustomParagraph = Paragraph.extend({
addAttributes() {
// Return an object with attribute configuration
return {
color: {
default: 'pink',
},
},
},
})
// Result:
// <p color="pink">Example Text</p>
```
That is 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 from the content when initiated.
Lets stick with the color example and assume you 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`:
```js
const CustomParagraph = Paragraph.extend({
addAttributes() {
return {
color: {
default: null,
// Take the attribute values
renderHTML: attributes => {
// … and return an object with HTML attributes.
return {
style: `color: ${attributes.color}`,
}
},
},
}
},
})
// Result:
// <p style="color: pink">Example Text</p>
```
You can also control how the attribute is parsed from the HTML. Maybe you want to store the color in an attribute called `data-color` (and not just `color`), heres how you would do that:
```js
const CustomParagraph = Paragraph.extend({
addAttributes() {
return {
color: {
default: null,
// Customize the HTML parsing (for example, to load the initial content)
parseHTML: element => element.getAttribute('data-color'),
// … and customize the HTML rendering.
renderHTML: attributes => {
return {
'data-color': attributes.color,
style: `color: ${attributes.color}`,
}
},
},
}
},
})
// Result:
// <p data-color="pink" style="color: pink">Example Text</p>
```
You can completly disable the rendering of attributes with `rendered: false`.
#### Extend existing attributes
If you want to add an attribute to an extension and keep existing attributes, you can access them through `this.parent()`.
In some cases, it is undefined, so make sure to check for that case, or use optional chaining `this.parent?.()`
```js
const CustomTableCell = TableCell.extend({
addAttributes() {
return {
...this.parent?.(),
myCustomAttribute: {
// …
},
}
},
})
```
### Global attributes
Attributes can be applied to multiple extensions at once. Thats useful for text alignment, line height, color, font family, and other styling related attributes.
Take a closer look at [the full source code](https://github.com/ueberdosis/tiptap/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:
```js
import { Extension } from '@tiptap/core'
const TextAlign = Extension.create({
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 => element.style.textAlign || 'left',
},
},
},
]
},
})
```
### 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({ HTMLAttributes }) {
return ['strong', HTMLAttributes, 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({ HTMLAttributes }) {
return ['pre', ['code', HTMLAttributes, 0]]
},
```
If you want to add some specific attributes there, import the `mergeAttributes` helper from `@tiptap/core`:
```js
import { mergeAttributes } from '@tiptap/core'
// ...
renderHTML({ HTMLAttributes }) {
return ['a', mergeAttributes(HTMLAttributes, { rel: this.options.rel }), 0]
},
```
### Parse HTML
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:
```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:
```js
parseHTML() {
return [
// <strong>
{
tag: 'strong',
},
// <b>
{
tag: 'b',
getAttrs: node => node.style.fontWeight !== 'normal' && null,
},
// <span style="font-weight: bold"> and <span style="font-weight: 700">
{
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
```js
import Paragraph from '@tiptap/extension-paragraph'
const CustomParagraph = Paragraph.extend({
addCommands() {
return {
paragraph: () => ({ commands }) => {
return commands.toggleNode('paragraph', 'paragraph')
},
}
},
})
```
:::warning Use the commands parameter inside of addCommands
To access other commands inside `addCommands` use the `commands` parameter thats passed to it.
:::
### 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
// Change the bullet list keyboard shortcut
import BulletList from '@tiptap/extension-bullet-list'
const CustomBulletList = BulletList.extend({
addKeyboardShortcuts() {
return {
'Mod-l': () => this.editor.commands.toggleBulletList(),
}
},
})
```
### Input rules
With input rules you can define regular expressions to listen for user inputs. They are used for markdown shortcuts, or for example to convert text like `(c)` to a `©` (and many more) with the [`Typography`](/api/extensions/typography) extension. Use the `markInputRule` helper function for marks, and the `nodeInputRule` for nodes.
By default text between two tildes on both sides is transformed to ~~striked text~~. If you want to think one tilde on each side is enough, you can overwrite the input rule like this:
```js
// Use the ~single tilde~ markdown shortcut
import Strike from '@tiptap/extension-strike'
import { markInputRule } from '@tiptap/core'
// Default:
// const inputRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))$/gm
// New:
const inputRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))$/gm
const CustomStrike = Strike.extend({
addInputRules() {
return [
markInputRule(inputRegex, this.type),
]
},
})
```
### Paste rules
Paste rules work like input rules (see above) do. But instead of listening to what the user types, they are applied to pasted content.
There is one tiny difference in the regular expression. Input rules typically end with a `$` dollar sign (which means “asserts position at the end of a line”), paste rules typically look through all the content and dont have said `$` dollar sign.
Taking the example from above and applying it to the paste rule would look like the following example.
```js
// Check pasted content for the ~single tilde~ markdown syntax
import Strike from '@tiptap/extension-strike'
import { markPasteRule } from '@tiptap/core'
// Default:
// const pasteRegex = /(?:^|\s)((?:~~)((?:[^~]+))(?:~~))/gm
// New:
const pasteRegex = /(?:^|\s)((?:~)((?:[^~]+))(?:~))/gm
const CustomStrike = Strike.extend({
addPasteRules() {
return [
markPasteRule(pasteRegex, this.type),
]
},
})
```
### Events
You can even move your [event listeners](/api/events) to a separate extension. Here is an example with listeners for all events:
```js
import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create({
onCreate() {
// The editor is ready.
},
onUpdate() {
// The content has changed.
},
onSelectionUpdate({editor}) {
// The selection has changed.
},
onTransaction({ transaction }) {
// The editor state has changed.
},
onFocus({ event }) {
// The editor is focused.
},
onBlur({ event }) {
// The editor isnt focused anymore.
},
onDestroy() {
// The editor is being destroyed.
},
})
```
### Whats available in this?
Those extensions arent classes, but you still have a few important things available in `this` everywhere in the extension.
```js
// Name of the extension, for example 'bulletList'
this.name
// Editor instance
this.editor
// ProseMirror type
this.type
// Object with all settings
this.options
// Everything thats in the extended extension
this.parent
```
### ProseMirror Plugins (Advanced)
After all, tiptap is built on ProseMirror and ProseMirror has a pretty powerful plugin API, too. To access that directly, use `addProseMirrorPlugins()`.
#### Existing plugins
You can wrap existing ProseMirror plugins in tiptap extensions like shown in the example below.
```js
import { history } from 'prosemirror-history'
const History = Extension.create({
addProseMirrorPlugins() {
return [
history(),
// …
]
},
})
```
#### Access the ProseMirror API
To hook into events, for example a click, double click or when content is pasted, you can pass event handlers as `editorProps` to the [editor](/api/editor). Or you can add them to a tiptap extension like shown in the below example.
```js
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
export const EventHandler = Extension.create({
name: 'eventHandler',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('eventHandler'),
props: {
handleClick(view, pos, event) { /* … */ },
handleDoubleClick(view, pos, event) { /* … */ },
handlePaste(view, event, slice) { /* … */ },
// … and many, many more.
// Here is the full list: https://prosemirror.net/docs/ref/#view.EditorProps
},
}),
]
},
})
```
### Node views (Advanced)
For advanced use cases, where you need to execute JavaScript inside your nodes, for example to render a sophisticated link preview, you need to learn about node views.
They are really powerful, but also complex. In a nutshell, you need to return a parent DOM element, and a DOM element where the content should be rendered in. Look at the following, simplified example:
```js
import Link from '@tiptap/extension-link'
const CustomLink = Link.extend({
addNodeView() {
return () => {
const container = document.createElement('div')
container.addEventListener('click', event => {
alert('clicked on the container')
})
const content = document.createElement('div')
container.append(content)
return {
dom: container,
contentDOM: content,
}
}
},
})
```
There is a whole lot to learn about node views, so head over to the [dedicated section in our guide about node views](/guide/node-views) for more information. If youre looking for a real-world example, look at the source code of the [`TaskItem`](/api/nodes/task-item) node. This is using a node view to render the checkboxes.
## Create new extensions
You can build your own extensions from scratch and you know what? Its the same syntax as for extending existing extension described above.
### Create a node
If you think of the document as a tree, then [nodes](/api/nodes) are just a type of content in that tree. Good examples to learn from are [`Paragraph`](/api/nodes/paragraph), [`Heading`](/api/nodes/heading), or [`CodeBlock`](/api/nodes/code-block).
```js
import { Node } from '@tiptap/core'
const CustomNode = Node.create({
name: 'customNode',
// Your code goes here.
})
```
Nodes dont have to be blocks. They can also be rendered inline with the text, for example for [@mentions](/api/nodes/mention).
### Create a mark
One or multiple marks can be applied to [nodes](/api/nodes), for example to add inline formatting. Good examples to learn from are [`Bold`](/api/marks/bold), [`Italic`](/api/marks/italic) and [`Highlight`](/api/marks/highlight).
```js
import { Mark } from '@tiptap/core'
const CustomMark = Mark.create({
name: 'customMark',
// Your code goes here.
})
```
### Create an extension
Extensions add new capabilities to tiptap and youll read the word extension here very often, even for nodes and marks. But there are literal extensions. Those cant add to the schema (like marks and nodes do), but can add functionality or change the behaviour of the editor.
A good example to learn from is probably [`TextAlign`](/api/extensions/text-align).
```js
import { Extension } from '@tiptap/core'
const CustomExtension = Extension.create({
name: 'customExtension',
// Your code goes here.
})
```
## Sharing
When everything is working fine, dont forget to [share it with the community](https://github.com/ueberdosis/tiptap/issues/819).

View File

@@ -0,0 +1,8 @@
---
tableOfContents: true
---
# Overwrite & extend
## toc

117
docs/guide/menus.md Normal file
View File

@@ -0,0 +1,117 @@
---
tableOfContents: true
---
# Create menus
## toc
## Introduction
tiptap comes very raw, but thats a good thing. You have full control about the appearance of it.
When we say full control, we mean it. You can (and have to) build a menu on your own. We help you to wire everything up.
## Menus
The editor provides a fluent API to trigger commands and add active states. You can use any markup you like. To make the positioning of menus easier, we provide a few utilities and components. Lets go through the most typical use cases one by one.
### Fixed menu
A fixed menu, for example on top of the editor, can be anything. We dont provide such menu. Just add a `<div>` with a few `<button>`s. How those buttons can trigger [commands](/api/commands) is [explained below](#actions).
### Bubble menu
The [bubble menu](/api/extensions/bubble-menu) appears when selecting text. Markup and styling is totally up to you.
<tiptap-demo name="Extensions/BubbleMenu" hideSource></tiptap-demo>
### Floating menu
The [floating menu](/api/extensions/floating-menu) appears in empty lines. Markup and styling is totally up to you.
<tiptap-demo name="Extensions/FloatingMenu" hideSource></tiptap-demo>
### Slash commands (work in progress)
Its not an official extension yet, but [theres an experiment you can use to add what we call slash commands](/experiments/commands). It allows you to start a new line with `/` and will bring up a popup to select which node should be added.
## Buttons
Okay, youve got your menu. But how do you wire things up?
### Commands
Youve got the editor running already and want to add your first button. You need a `<button>` HTML tag with a click handler. Depending on your setup, that can look like the following example:
```html
<button onclick="editor.chain().toggleBold().focus().run()">
Bold
</button>
```
Oh, thats a long command, right? Actually, its a [chain of commands](/api/commands#chain-commands). Lets go through this one by one:
```js
editor.chain().focus().toggleBold().run()
```
1. `editor` should be a tiptap instance,
2. `chain()` is used to tell the editor you want to execute multiple commands,
3. `focus()` sets the focus back to the editor,
4. `toggleBold()` marks the selected text bold, or removes the bold mark from the text selection if its already applied and
5. `run()` will execute the chain.
In other words: This will be a typical **Bold** button for your text editor.
Which commands are available depends on what extensions you have registered with the editor. Most extensions come with a `set…()`, `unset…()` and `toggle…()` command. Read the extension documentation to see whats actually available or just surf through your code editors autocomplete.
### Keep the focus
You have already seen the `focus()` command in the above example. When you click on the button, the browser focuses that DOM element and the editor loses focus. Its likely you want to add `focus()` to all your menu buttons, so the writing flow of your users isnt interrupted.
### The active state
The editor provides an `isActive()` method to check if something is applied to the selected text already. In Vue.js you can toggle a CSS class with help of that function like that:
```html
<button :class="{ 'is-active': editor.isActive('bold') }" @click="editor.chain().toggleBold().focus().run()">
Bold
</button>
```
This toggles the `.is-active` class accordingly and works for nodes and marks. You can even check for specific attributes. Here is an example with the [`Highlight`](/api/marks/highlight) mark, that ignores different attributes:
```js
editor.isActive('highlight')
```
And an example that compares the given attribute(s):
```js
editor.isActive('highlight', { color: '#ffa8a8' })
```
There is even support for regular expressions:
```js
editor.isActive('textStyle', { color: /.*/ })
```
You can even nodes and marks, but check for the attributes only. Here is an example with the [`TextAlign`](/api/extensions/text-align) extension:
```js
editor.isActive({ textAlign: 'right' })
```
If your selection spans multiple nodes or marks, or only part of the selection has a mark, `isActive()` will return `false` and indicate nothing is active. This is how it is supposed to be, because it allows people to apply a new node or mark to that selection right-away.
## User experience
When designing a great user experience you should consider a few things.
### Accessibility
* Make sure users can navigate the menu with their keyboard
* Use proper [title attributes](https://developer.mozilla.org/de/docs/Web/HTML/Global_attributes/title)
* Use proper [aria attributes](https://developer.mozilla.org/en-US/docs/Learn/Accessibility/WAI-ARIA_basics)
* List available keyboard shortcuts
:::warning Incomplete
This section needs some work. Do you know what else needs to be taken into account when building an editor menu? Let us know on [GitHub](https://github.com/ueberdosis/tiptap) or send us an email to [humans@tiptap.dev](mailto:humans@tiptap.dev)!
:::
### Icons
Most editor menus use icons for their buttons. In some of our demos, we use the open source icon set [Remix Icon](https://remixicon.com/), which is free to use. But its totally up to you what you use. Here are a few icon sets you can consider:
* [Remix Icon](https://remixicon.com/#editor)
* [Font Awesome](https://fontawesome.com/icons?c=editors)
* [UI icons](https://www.ibm.com/design/language/iconography/ui-icons/library/)

138
docs/guide/node-views.md Normal file
View File

@@ -0,0 +1,138 @@
---
tableOfContents: true
---
# Interactive node views
## toc
## Introduction
Node views are the best thing since sliced bread, at least if you are a fan of customization (and bread). With node views you can add interactive nodes to your editor. That can literally be everything. If you can write it in JavaScript, you can use it in your editor.
Node views are amazing to improve the in-editor experience, but can also be used in a read-only instance of tiptap. They are unrelated to the HTML output by design, so you have full control about the in-editor experience *and* the output.
## Different types of node views
Depending on what you would like to build, node views work a little bit different and can have their verify specific capabilities, but also pitfalls. The main question is: How should your custom node look like?
### Editable text
Yes, node views can have editable text, just like a regular node. Thats simple. The cursor will exactly behave like you would expect it from a regular node. Existing commands work very well with those nodes.
```html
<div class="Prosemirror" contenteditable="true">
<p>text</p>
<node-view>text</node-view>
<p>text</p>
</div>
```
Thats how the [`TaskItem`](/api/nodes/task-item) node works.
### Non-editable text
Nodes can also have text, which is not editable. The cursor cant jump into those, but you dont want that anyway.
tiptap adds a `contenteditable="false"` to those by default.
```html
<div class="Prosemirror" contenteditable="true">
<p>text</p>
<node-view contenteditable="false">text</node-view>
<p>text</p>
</div>
```
Thats how you could render mentions, which shouldnt be editable. Users can add or delete them, but not delete single characters.
Statamic uses those for their Bard editor, which renders complex modules inside tiptap, which can have their own text inputs.
### Mixed content
You can even mix non-editable and editable text. Thats great to build complex things, and still use marks like bold and italic inside the editable content.
**BUT**, if there are other elements with non-editable text in your node view, the cursor can jump there. You can improve that with manually adding `contenteditable="false"` to the specific parts of your node view.
```html
<div class="Prosemirror" contenteditable="true">
<p>text</p>
<node-view>
<div contenteditable="false">
non-editable text
</div>
<div>
editable text
</div>
</node-view>
<p>text</p>
</div>
```
## Markup
But what happens if you [access the editor content](/guide/output)? If youre working with HTML, youll need to tell tiptap how your node should be serialized.
The editor **does not** export the rendered JavaScript node, and for a lot of use cases you wouldnt want that anyway.
Lets say you have a node view which lets users add a video player and configure the appearance (autoplay, controls …). You want the interface to do that in the editor, not in the output of the editor. The output of the editor should probably only have the video player.
I know, I know, its not that easy. Just keep in mind, that youre in full control of the rendering inside the editor and of the output.
:::warning What if you store JSON?
That doesnt apply to JSON. In JSON, everything is stored as an object. There is no need to configure the “translation” to and from HTML.
:::
### Render HTML
Okay, youve set up your node with an interactive node view and now you want to control the output. Even if youre node view is pretty complex, the rendered HTML can be simple:
```js
renderHTML({ HTMLAttributes }) {
return ['my-custom-node', mergeAttributes(HTMLAttributes)]
},
// Output: <my-custom-node count="1"></my-custom-node>
```
Make sure its something distinguishable, so its easier to restore the content from the HTML. If you just need something generic markup like a `<div>` consider to add a `data-type="my-custom-node"`.
### Parse HTML
The same applies to restoring the content. You can configure what markup you expect, that can be something completely unrelated to the node view markup. It just needs to contain all the information you want to restore.
Attributes are automagically restored, if you registered them through [`addAttributes`](/guide/custom-extensions#attributes).
```js
// Input: <my-custom-node count="1"></my-custom-node>
parseHTML() {
return [{
tag: 'my-custom-node',
}]
},
```
### Render JavaScript/Vue/React
But what if you want to render your actual JavaScript/Vue/React code? Consider using tiptap to render your output. Just set the editor to `editable: false` and no one will notice youre using an editor to render the content. :-)
<!-- ## Reference
### dom: ?dom.Node
> The outer DOM node that represents the document node. When not given, the default strategy is used to create a DOM node.
### contentDOM: ?dom.Node
> The DOM node that should hold the node's content. Only meaningful if the node view also defines a dom property and if its node type is not a leaf node type. When this is present, ProseMirror will take care of rendering the node's children into it. When it is not present, the node view itself is responsible for rendering (or deciding not to render) its child nodes.
### update: ?fn(node: Node, decorations: [Decoration]) → bool
> When given, this will be called when the view is updating itself. It will be given a node (possibly of a different type), and an array of active decorations (which are automatically drawn, and the node view may ignore if it isn't interested in them), and should return true if it was able to update to that node, and false otherwise. If the node view has a contentDOM property (or no dom property), updating its child nodes will be handled by ProseMirror.
### selectNode: ?fn()
> Can be used to override the way the node's selected status (as a node selection) is displayed.
### deselectNode: ?fn()
> When defining a selectNode method, you should also provide a deselectNode method to remove the effect again.
### setSelection: ?fn(anchor: number, head: number, root: dom.Document)
> This will be called to handle setting the selection inside the node. The anchor and head positions are relative to the start of the node. By default, a DOM selection will be created between the DOM positions corresponding to those positions, but if you override it you can do something else.
### stopEvent: ?fn(event: dom.Event) → bool
> Can be used to prevent the editor view from trying to handle some or all DOM events that bubble up from the node view. Events for which this returns true are not handled by the editor.
### ignoreMutation: ?fn(dom.MutationRecord) → bool
> Called when a DOM mutation or a selection change happens within the view. When the change is a selection change, the record will have a type property of "selection" (which doesn't occur for native mutation records). Return false if the editor should re-read the selection or re-parse the range around the mutation, true if it can safely be ignored.
### destroy: ?fn()
> Called when the node view is removed from the editor or the whole editor is destroyed. -->

View File

@@ -0,0 +1,29 @@
---
tableOfContents: true
---
# Examples
## toc
## Introduction
Node views enable you to fully customize your nodes. We are collecting a few different examples here. Feel free to copy them and start building on them.
Keep in mind that those are just examples to get you started, not officially supported extensions. We dont have tests for them, and dont plan to maintain them with the same attention as we do with official extensions.
## Drag handles
Drag handles arent that easy to add. We are still on the lookout whats the best way to add them. Official support will come at some point, but theres no timeline yet.
<tiptap-demo name="GuideNodeViews/DragHandle"></tiptap-demo>
## Table of contents
This one loops through the editor content, gives all headings an ID and renders a Table of Contents with Vue.
<tiptap-demo name="GuideNodeViews/TableOfContents"></tiptap-demo>
## Drawing in the editor
The drawing example shows a SVG that enables you to draw inside the editor.
<tiptap-demo name="Examples/Drawing"></tiptap-demo>
Its not working very well with the Collaboration extension. Its sending all data on every change, which can get pretty huge with Y.js. If you plan to use those two in combination, you need to improve it or your WebSocket backend will melt.

125
docs/guide/node-views/js.md Normal file
View File

@@ -0,0 +1,125 @@
---
tableOfContents: true
---
# Node views with JavaScript
## toc
## Introduction
Using frameworks like Vue or React can feel too complex, if youre used to work without those two. Good news: You can use Vanilla JavaScript in your node views. There is just a little bit you need to know, but lets go through this one by one.
## Render a node view with JavaScript
Here is what you need to do to render a node view inside your editor:
1. [Create a node extension](/guide/custom-extensions)
2. Register a new node view with `addNodeView()`
3. Write your render function
4. [Configure tiptap to use your new node extension](/guide/configuration)
This is how your node extension could look like:
```js
import { Node } from '@tiptap/core'
import Component from './Component.vue'
export default Node.create({
// configuration …
addNodeView() {
return ({ editor, node, getPos, HTMLAttributes, decorations, extension }) => {
const dom = document.createElement('div')
dom.innerHTML = 'Hello, Im a node view!'
return {
dom,
}
}
},
})
```
Got it? Lets see it in action. Feel free to copy the below example to get started.
<tiptap-demo name="GuideNodeViews/JavaScript"></tiptap-demo>
That node view even interacts with the editor. Time to see how that is wired up.
## Access node attributes
The editor passes a few helpful things to your render function. One of them is the the `node` prop. This one enables you to access node attributes in your node view. Lets say you have [added an attribute](/guide/custom-extensions#attributes) named `count` to your node extension. You could access the attribute like this:
```js
addNodeView() {
return ({ node }) => {
console.log(node.attrs.count)
// …
}
}
```
## Update node attributes
You can even update node attributes from your node view, with the help of the `getPos` prop passed to your render function. Dispatch a new transaction with an object of the updated attributes:
```js
addNodeView() {
return ({ editor, node, getPos }) => {
const { view } = editor
// Create a button …
const button = document.createElement('button')
button.innerHTML = `This button has been clicked ${node.attrs.count} times.`
// … and when its clicked …
button.addEventListener('click', () => {
if (typeof getPos === 'function') {
// … dispatch a transaction, for the current position in the document …
view.dispatch(view.state.tr.setNodeMarkup(getPos(), undefined, {
count: node.attrs.count + 1,
}))
// … and set the focus back to the editor.
editor.commands.focus()
}
})
// …
}
}
```
Does seem a little bit too complex? Consider using [React](/guide/node-views/react) or [Vue](/guide/node-views/vue), if you have one of those in your project anyway. It gets a little bit easier with those two.
## Adding a content editable
To add editable content to your node view, you need to pass a `contentDOM`, a container element for the content. Here is a simplified version of a node view with non-editable and editable text content:
```js
// Create a container for the node view
const dom = document.createElement('div')
// Give other elements containing text `contentEditable = false`
const label = document.createElement('span')
label.innerHTML = 'Node view'
label.contentEditable = false
// Create a container for the content
const content = document.createElement('div')
// Append all elements to the node view container
dom.append(label, content)
return {
// Pass the node view container …
dom,
// … and the content container:
contentDOM: content,
}
```
Got it? Youre free to do anything you like, as long as you return a container for the node view and another one for the content. Here is the above example in action:
<tiptap-demo name="GuideNodeViews/JavaScriptContent"></tiptap-demo>
Keep in mind that this content is rendered by tiptap. That means you need to tell what kind of content is allowed, for example with `content: 'inline*'` in your node extension (thats what we use in the above example).

View File

@@ -0,0 +1,118 @@
---
tableOfContents: true
---
# Node views with React
## toc
## Introduction
Using Vanilla JavaScript can feel complex if you are used to work in React. Good news: You can use regular React components in your node views, too. There is just a little bit you need to know, but lets go through this one by one.
## Render a React component
Here is what you need to do to render React components inside your editor:
1. [Create a node extension](/guide/custom-extensions)
2. Create a React component
3. Pass that component to the provided `ReactNodeViewRenderer`
4. Register it with `addNodeView()`
5. [Configure tiptap to use your new node extension](/guide/configuration)
This is how your node extension could look like:
```js
import { Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import Component from './Component.jsx'
export default Node.create({
// configuration …
addNodeView() {
return ReactNodeViewRenderer(Component)
},
})
```
There is a little bit of magic required to make this work. But dont worry, we provide a wrapper component you can use to get started easily. Dont forget to add it to your custom React component, like shown below:
```html
<NodeViewWrapper className="react-component">
React Component
</NodeViewWrapper>
```
Got it? Lets see it in action. Feel free to copy the below example to get started.
<tiptap-demo name="GuideNodeViews/ReactComponent"></tiptap-demo>
That component doesnt interact with the editor, though. Time to wire it up.
## Access node attributes
The `ReactNodeViewRenderer` which you use in your node extension, passes a few very helpful props to your custom React component. One of them is the `node` prop. Lets say you have [added an attribute](/guide/custom-extensions#attributes) named `count` to your node extension (like we did in the above example) you could access it like this:
```js
props.node.attrs.count
```
## Update node attributes
You can even update node attributes from your node, with the help of the `updateAttributes` prop passed to your component. Pass an object with updated attributes to the `updateAttributes` prop:
```js
export default props => {
const increase = () => {
props.updateAttributes({
count: props.node.attrs.count + 1,
})
}
// …
}
```
And yes, all of that is reactive, too. A pretty seemless communication, isnt it?
## Adding a content editable
There is another component called `NodeViewContent` which helps you adding editable content to your node view. Here is an example:
```jsx
import React from 'react'
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'
export default () => {
return (
<NodeViewWrapper className="react-component-with-content">
<span className="label" contentEditable={false}>React Component</span>
<NodeViewContent className="content" />
</NodeViewWrapper>
)
}
```
You dont need to add those `className` attributes, feel free to remove them or pass other class names. Try it out in the following example:
<tiptap-demo name="GuideNodeViews/ReactComponentContent"></tiptap-demo>
Keep in mind that this content is rendered by tiptap. That means you need to tell what kind of content is allowed, for example with `content: 'inline*'` in your node extension (thats what we use in the above example).
The `NodeViewWrapper` and `NodeViewContent` components render a `<div>` HTML tag (`<span>` for inline nodes), but you can change that. For example `<NodeViewContent as="p">` should render a paragraph. One limitation though: That tag must not change during runtime.
## All available props
Here is the full list of what props you can expect:
| Prop | Description |
| ------------------ | --------------------------------------------------------------- |
| `editor` | The editor instance |
| `node` | The current node |
| `decorations` | An array of decorations |
| `selected` | `true` when there is a `NodeSelection` at the current node view |
| `extension` | Access to the node extension, for example to get options |
| `getPos` | Get the document position of the current node |
| `updateAttributes` | Update attributes of the current node |
| `deleteNode` | Delete the current node |
## Dragging
To make your node views draggable, set `draggable: true` in the extension and add `data-drag-handle` to the DOM element that should function as the drag handle.
<!-- <tiptap-demo name="GuideNodeViews/DragHandle"></tiptap-demo> -->

View File

@@ -0,0 +1,205 @@
---
tableOfContents: true
---
# Node views with Vue
## toc
## Introduction
Using Vanilla JavaScript can feel complex if you are used to work in Vue. Good news: You can use regular Vue components in your node views, too. There is just a little bit you need to know, but lets go through this one by one.
## Render a Vue component
Here is what you need to do to render Vue components inside your editor:
1. [Create a node extension](/guide/custom-extensions)
2. Create a Vue component
3. Pass that component to the provided `VueNodeViewRenderer`
4. Register it with `addNodeView()`
5. [Configure tiptap to use your new node extension](/guide/configuration)
This is how your node extension could look like:
```js
import { Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-2'
import Component from './Component.vue'
export default Node.create({
// configuration …
addNodeView() {
return VueNodeViewRenderer(Component)
},
})
```
There is a little bit of magic required to make this work. But dont worry, we provide a wrapper component you can use to get started easily. Dont forget to add it to your custom Vue component, like shown below:
```html
<template>
<node-view-wrapper>
Vue Component
</node-view-wrapper>
</template>
```
Got it? Lets see it in action. Feel free to copy the below example to get started.
<tiptap-demo name="GuideNodeViews/VueComponent"></tiptap-demo>
That component doesnt interact with the editor, though. Time to wire it up.
## Access node attributes
The `VueNodeViewRenderer` which you use in your node extension, passes a few very helpful props to your custom Vue component. One of them is the `node` prop. Add this snippet to your Vue component to directly access the node:
```js
props: {
node: {
type: Object,
required: true,
},
},
```
That enables you to access node attributes in your Vue component. Lets say you have [added an attribute](/guide/custom-extensions#attributes) named `count` to your node extension (like we did in the above example) you could access it like this:
```js
this.node.attrs.count
```
## Update node attributes
You can even update node attributes from your node, with the help of the `updateAttributes` prop passed to your component. Just add this snippet to your component:
```js
props: {
updateAttributes: {
type: Function,
required: true,
},
},
```
Pass an object with updated attributes to the function:
```js
this.updateAttributes({
count: this.node.attrs.count + 1,
})
```
And yes, all of that is reactive, too. A pretty seemless communication, isnt it?
## Adding a content editable
There is another component called `NodeViewContent` which helps you adding editable content to your node view. Here is an example:
```html
<template>
<node-view-wrapper class="dom">
<node-view-content class="content-dom" />
</node-view-wrapper>
</template>
<script>
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'
export default {
components: {
NodeViewWrapper,
NodeViewContent,
},
}
</script>
```
You dont need to add those `class` attributes, feel free to remove them or pass other class names. Try it out in the following example:
<tiptap-demo name="GuideNodeViews/VueComponentContent"></tiptap-demo>
Keep in mind that this content is rendered by tiptap. That means you need to tell what kind of content is allowed, for example with `content: 'inline*'` in your node extension (thats what we use in the above example).
The `NodeViewWrapper` and `NodeViewContent` components render a `<div>` HTML tag (`<span>` for inline nodes), but you can change that. For example `<node-view-content as="p">` should render a paragraph. One limitation though: That tag must not change during runtime.
## All available props
For advanced use cases, we pass a few more props to the component. Here is the full list of what props you can expect:
```html
<template>
<node-view-wrapper />
</template>
<script>
import { NodeViewWrapper } from '@tiptap/vue-2'
export default {
components: {
NodeViewWrapper,
},
props: {
// the editor instance
editor: {
type: Object,
},
// the current node
node: {
type: Object,
},
// an array of decorations
decorations: {
type: Array,
},
// `true` when there is a `NodeSelection` at the current node view
selected: {
type: Boolean,
},
// access to the node extension, for example to get options
extension: {
type: Object,
},
// get the document position of the current node
getPos: {
type: Function,
},
// update attributes of the current node
updateAttributes: {
type: Function,
},
// delete the current node
deleteNode: {
type: Function,
},
},
}
</script>
```
If you just want to have all (and to have TypeScript support) you can import all props like that:
```js
// Vue 3
import { defineComponent } from 'vue'
import { nodeViewProps } from '@tiptap/vue-3'
export default defineComponent({
props: nodeViewProps,
})
// Vue 2
import Vue from 'vue'
import { nodeViewProps } from '@tiptap/vue-2'
export default Vue.extend({
props: nodeViewProps,
})
```
## Dragging
To make your node views draggable, set `draggable: true` in the extension and add `data-drag-handle` to the DOM element that should function as the drag handle.
<tiptap-demo name="GuideNodeViews/DragHandle"></tiptap-demo>

138
docs/guide/output.md Normal file
View File

@@ -0,0 +1,138 @@
---
tableOfContents: true
---
# Output
## toc
## Introduction
You can store your content as a JSON object or as a good old HTML string. Both work fine. And of course, you can pass both formats to the editor to restore your content. Here is an interactive example, that exports the content as HTML and JSON when the document is changed:
## Export
### Option 1: JSON
JSON is probably easier to loop through, for example to look for a mention and its more like what tiptap uses under the hood. Anyway, if you want to use JSON to store the content we provide a method to retrieve the content as JSON:
```js
const json = editor.getJSON()
```
You can store that in your database (or send it to an API) and restore the document initially like that:
```js
new Editor({
content: {
"type": "doc",
"content": [
// …
]
},
})
```
Or if you need to wait for something, you can do it later through the editor instance:
```js
editor.commands.setContent({
"type": "doc",
"content": [
// …
]
})
```
Here is an interactive example where you can see that in action:
<tiptap-demo name="GuideContent/ExportJSON" hideSource></tiptap-demo>
### Option 2: HTML
HTML can be easily rendered in other places, for example in emails and its wildly used, so its probably easier to switch the editor at some point. Anyway, every editor instance provides a method to get HTML from the current document:
```js
const html = editor.getHTML()
```
This can then be used to restore the document initially:
```js
new Editor({
content: `<p>Example Text</p>`,
})
```
Or if you want to restore the content later (e. g. after an API call has finished), you can do that too:
```js
editor.commands.setContent(`<p>Example Text</p>`)
```
Use this interactive example to fiddle around:
<tiptap-demo name="GuideContent/ExportHTML" hideSource></tiptap-demo>
### Option 3: Y.js
Our editor has top notch support for Y.js, which is amazing to add features like [realtime collaboration, offline editing, or syncing between devices](/guide/collaborative-editing).
Internally, Y.js stores a history of all changes. That can be in the browser, on a server, synced with other connected clients, or on a USB stick. But, its important to know that Y.js needs those stored changes. A simple JSON document is not enough to merge changes.
Sure, you can import existing JSON documents to get started and get a JSON out of Y.js, but thats more like an import/export format. It wont be your single source. Thats important to consider when adding Y.js for one of the mentioned use cases.
That said, its amazing and were about to provide an amazing backend, that makes all that a breeze.
### Not an option: Markdown
Unfortunately, **tiptap doesnt support Markdown as an input or output format**. We considered to add support for it, but those are the reasons why we decided to not do it:
* Both, HTML and JSON, can have deeply nested structures, Markdown is flat.
* Markdown standards vary.
* tiptaps strength is customization, that doesnt work very well with Markdown.
* There are enough packages to convert HTML to Markdown and vice-versa.
You should really consider to work with HTML or JSON to store your content, they are perfectly fine for most use cases.
If you still think you need Markdown, ProseMirror has an [example on how to deal with Markdown](https://prosemirror.net/examples/markdown/), [Nextcloud Text](https://github.com/nextcloud/text) uses tiptap 1 to work with Markdown. Maybe you can learn from them. Or if you are looking for a really good Markdown editor, try [CodeMirror](https://codemirror.net/).
That said, tiptap does support [Markdown shortcuts](/examples/markdown-shortcuts) to format your content. Also youre free to let your content look like Markdown, for example add a `#` before an `<h1>` with CSS.
## Listening for changes
If you want to continuously store the updated content while people write, you can [hook into events](/api/events). Here is an example how that could look like:
```js
const editor = new Editor({
// intial content
content: `<p>Example Content</p>`,
// triggered on every change
onUpdate() {
const json = this.getJSON()
// send the content to an API here
},
})
```
## Rendering
### Option 1: Read-only instance of tiptap
To render the saved content, set the editor to read-only. Thats how you can achieve the exact same rendering as its in the editor, without duplicating your CSS and other code.
<tiptap-demo name="GuideContent/ReadOnly"></tiptap-demo>
### Option 2: Generate HTML from ProseMirror JSON
If you need to render the content on the server side, for example to generate the HTML for a blog post which has been written in tiptap, youll probably want to do just that without an actual editor instance.
Thats what the `generateHTML()` is for. Its a helper function which renders HTML without an actual editor instance.
<tiptap-demo name="GuideContent/GenerateHTML"></tiptap-demo>
By the way, the other way is possible, too. The below examples shows how to generate JSON from HTML.
<tiptap-demo name="GuideContent/GenerateJSON"></tiptap-demo>
## Migration
If youre migrating existing content to tiptap we would recommend to get your existing output to HTML. Thats probably the best format to get your initial content into tiptap, because ProseMirror ensures there is nothing wrong with it. Even if there are some tags or attributes that arent allowed (based on your configuration), tiptap just throws them away quietly.
Were about to go through a few cases to help with that, for example we provide a PHP package to convert HTML to a compatible JSON structure: [ueberdosis/prosemirror-to-html](https://github.com/ueberdosis/html-to-prosemirror).
[Share your experiences with us!](mailto:humans@tiptap.dev) Wed like to add more information here.
## Security
There is no reason to use one or the other because of security concerns. If someone wants to send malicious content to your server, it doesnt matter if its JSON or HTML. It doesnt even matter if youre using tiptap or not. You should always validate user input.

113
docs/guide/styling.md Normal file
View File

@@ -0,0 +1,113 @@
---
tableOfContents: true
---
# Styling
## toc
## Introduction
tiptap is headless, that means there is no styling provided. That also means, you are in full control of how your editor looks. The following methods allow you to apply custom styles to the editor.
## Option 1: Style the plain HTML
The whole editor is rendered inside of a container with the class `.ProseMirror`. You can use that to scope your styling to the editor content:
```css
/* Scoped to the editor */
.ProseMirror p {
margin: 1em 0;
}
```
If youre rendering the stored content somewhere, there wont be a `.ProseMirror` container, so you can just globally add styling to the used HTML tags:
```css
/* Global styling */
p {
margin: 1em 0;
}
```
## Option 2: Add custom classes
You can control the whole rendering, including adding classes to everything.
### Extensions
Most extensions allow you to add attributes to the rendered HTML through the `HTMLAttributes` option. You can use that to add a custom class (or any other attribute). Thats also very helpful, when you work with [Tailwind CSS](https://tailwindcss.com/).
```js
new Editor({
extensions: [
Document,
Paragraph.configure({
HTMLAttributes: {
class: 'my-custom-paragraph',
},
}),
Heading.configure({
HTMLAttributes: {
class: 'my-custom-heading',
},
}),
Text,
],
})
```
The rendered HTML will look like that:
```html
<h1 class="my-custom-heading">Example Text</p>
<p class="my-custom-paragraph">Wow, thats really custom.</p>
```
If there are already classes defined by the extensions, your classes will be added.
### Editor
You can even pass classes to the element which contains the editor like that:
```js
new Editor({
editorProps: {
attributes: {
class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none',
},
},
})
```
### With Tailwind CSS
The editor works fine with Tailwind CSS, too. Find an example thats styled with the `@tailwindcss/typography` plugin below.
<iframe
src="https://codesandbox.io/embed/tiptap-demo-tailwind-iqjz0?fontsize=14&hidenavigation=1&module=%2Fsrc%2Findex.js&theme=dark&view=preview"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="tiptap-demo-tailwind"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
## Option 3: Customize the HTML
Or you can customize the markup for extensions. The following example will make a custom bold extension that doesnt render a `<strong>` tag, but a `<b>` tag:
```js
import Bold from '@tiptap/extension-bold'
const CustomBold = Bold.extend({
renderHTML({ HTMLAttributes }) {
// Original:
// return ['strong', HTMLAttributes, 0]
return ['b', HTMLAttributes, 0]
},
})
new Editor({
extensions: [
// …
CustomBold(),
],
})
```
You should put your custom extensions in separate files, but I think you got the idea.

65
docs/guide/typescript.md Normal file
View File

@@ -0,0 +1,65 @@
---
tableOfContents: true
---
# Working with TypeScript
## toc
## Introduction
The whole tiptap is code base is written in TypeScript. If you havent heard of it or never used it, no worries. You dont have to.
TypeScript extends JavaScript by adding types (hence the name). It adds new syntax, which doesnt exist in Vanilla JavaScript. Its actually removed before running in the browser, but this step the compilation is important to find bugs early. It checks if you passe the right types of data to functions. For a big and complex project, thats very valuable. It means well get notified of lot of bugs, before shipping code to you.
**Anyway, if you dont use TypeScript in your project, thats fine.** You will still be able to use tiptap and nevertheless get a nice autocomplete for the tiptap API (if your editor supports it, but most do).
If you are using TypeScript in your project and want to extend tiptap, there are two types that are good to know about.
## Types
### Options types
To extend or create default options for an extension, youll need to define a custom type, here is an example:
```ts
import { Extension } from '@tiptap/core'
export interface CustomExtensionOptions {
awesomeness: number,
}
const CustomExtension = Extension.create<CustomExtensionOptions>({
defaultOptions: {
awesomeness: 100,
},
})
```
### Command type
The core package also exports a `Command` type, which needs to be added to all commands that you specify in your code. Here is an example:
```ts
import { Extension } from '@tiptap/core'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
customExtension: {
/**
* Comments will be added to the autocomplete.
*/
yourCommand: (someProp: any) => ReturnType,
}
}
}
const CustomExtension = Extension.create({
addCommands() {
return {
yourCommand: someProp => ({ commands }) => {
// …
},
}
},
})
```
Thats basically it. Were doing all the rest automatically.