remove gridsome
This commit is contained in:
41
docs/guide/accessibility.md
Normal file
41
docs/guide/accessibility.md
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
tableOfContents: true
|
||||
---
|
||||
|
||||
# Accessibility
|
||||
|
||||
:::pro Fund the development ♥
|
||||
We need your support to maintain, update, support and develop tiptap. If you’re 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, there’s not much work done now. From our current understanding, that’s 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 don’t even provide an interface, so for now that’s totally up to you.
|
||||
|
||||
But no worries, we’ll 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 that’s 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.
|
||||
367
docs/guide/collaborative-editing.md
Normal file
367
docs/guide/collaborative-editing.md
Normal 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. Don’t worry, a production-grade setup doesn’t 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: It’s reaaally good in merging changes. And to achieve that, changes don’t have to come in order. It’s 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 let’s 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 that’s 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, isn’t 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, that’s 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 it’s enough if all clients are connected indirectly, but even that isn’t possible at some point. Or in other words, it doesn’t scale well for more than 100+ clients in the same document.
|
||||
2. It’s likely you want to involve a server to persist changes anyway. But the WebRTC signaling server (which connects all clients with eachother) doesn’t receive the changes and therefore doesn’t know what’s 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. It’s very flexible and can scale very well. For the client, the example is nearly the same, only the provider is different. First, let’s 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 doesn’t work out of the box. As you can see, it’s 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). Let’s 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()
|
||||
```
|
||||
|
||||
That’s 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, isn’t it?
|
||||
|
||||
### Multiple network providers
|
||||
You can even combine multiple providers. That’s 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, that’s all.
|
||||
|
||||
Keep in mind that WebRTC needs a signaling server to connect clients. This signaling server doesn’t 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 it’s 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, it’s magic. As already mentioned, that is all based on the fantastic Y.js framework. And if you’re 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. Let’s 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 you’d 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 you’d 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 that’s not allowed according to the configured schema it’ll be thrown away. That can lead to a strange behaviour when multiple clients with different schemas share changes to a document.
|
||||
|
||||
Let’s 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 it’s not allowed according to their (old) schema. Those changes will be synced to other connected clients and oops, it’s 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).
|
||||
|
||||
It’s on our list to provide features to make that easier. If you’ve got an idea how to improve that, share it with us!
|
||||
149
docs/guide/configuration.md
Normal file
149
docs/guide/configuration.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
tableOfContents: true
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## toc
|
||||
|
||||
## Introduction
|
||||
For most cases it’s 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. Let’s 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 that’s 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,
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
Don’t 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.
|
||||
569
docs/guide/custom-extensions.md
Normal file
569
docs/guide/custom-extensions.md
Normal file
@@ -0,0 +1,569 @@
|
||||
---
|
||||
tableOfContents: true
|
||||
---
|
||||
|
||||
# Custom extensions
|
||||
|
||||
## toc
|
||||
|
||||
## Introduction
|
||||
One of the strengths of tiptap is its extendability. You don’t 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. Let’s start with a few common examples of how you can extend existing nodes, marks and extensions.
|
||||
|
||||
You’ll learn how you start from scratch at the end, but you’ll 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.
|
||||
|
||||
Let’s say, you’d 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. Let’s 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 isn’t 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`, that’s 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. Let’s 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, that’s 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,
|
||||
})
|
||||
```
|
||||
|
||||
That’s 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. Let’s 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.
|
||||
|
||||
Let’s 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`), here’s 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. That’s 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, it’s 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. Let’s 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 it’s 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 that’s passed to it.
|
||||
:::
|
||||
|
||||
### Keyboard shortcuts
|
||||
Most core extensions come with sensible keyboard shortcut defaults. Depending on what you want to build, you’ll 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 don’t 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 isn’t focused anymore.
|
||||
},
|
||||
onDestroy() {
|
||||
// The editor is being destroyed.
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### What’s available in this?
|
||||
Those extensions aren’t 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 that’s 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 you’re 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? It’s 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 don’t 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 you’ll read the word extension here very often, even for nodes and marks. But there are literal extensions. Those can’t 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, don’t forget to [share it with the community](https://github.com/ueberdosis/tiptap/issues/819).
|
||||
8
docs/guide/extend-extensions.md
Normal file
8
docs/guide/extend-extensions.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
tableOfContents: true
|
||||
---
|
||||
|
||||
# Overwrite & extend
|
||||
|
||||
## toc
|
||||
|
||||
117
docs/guide/menus.md
Normal file
117
docs/guide/menus.md
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
tableOfContents: true
|
||||
---
|
||||
|
||||
# Create menus
|
||||
|
||||
## toc
|
||||
|
||||
## Introduction
|
||||
tiptap comes very raw, but that’s 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. Let’s 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 don’t 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)
|
||||
It’s not an official extension yet, but [there’s 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, you’ve got your menu. But how do you wire things up?
|
||||
|
||||
### Commands
|
||||
You’ve 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, that’s a long command, right? Actually, it’s a [chain of commands](/api/commands#chain-commands). Let’s 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 it’s 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 what’s actually available or just surf through your code editor’s 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. It’s likely you want to add `focus()` to all your menu buttons, so the writing flow of your users isn’t 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 it’s 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
138
docs/guide/node-views.md
Normal 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. That’s 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>
|
||||
```
|
||||
|
||||
That’s how the [`TaskItem`](/api/nodes/task-item) node works.
|
||||
|
||||
### Non-editable text
|
||||
Nodes can also have text, which is not editable. The cursor can’t jump into those, but you don’t 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>
|
||||
```
|
||||
|
||||
That’s how you could render mentions, which shouldn’t 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. That’s 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 you’re working with HTML, you’ll 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 wouldn’t want that anyway.
|
||||
|
||||
Let’s 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, it’s not that easy. Just keep in mind, that you‘re in full control of the rendering inside the editor and of the output.
|
||||
|
||||
:::warning What if you store JSON?
|
||||
That doesn’t 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, you’ve set up your node with an interactive node view and now you want to control the output. Even if you’re 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 it’s something distinguishable, so it’s 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 you’re 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. -->
|
||||
29
docs/guide/node-views/examples.md
Normal file
29
docs/guide/node-views/examples.md
Normal 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 don’t have tests for them, and don’t plan to maintain them with the same attention as we do with official extensions.
|
||||
|
||||
## Drag handles
|
||||
Drag handles aren’t that easy to add. We are still on the lookout what’s the best way to add them. Official support will come at some point, but there’s 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>
|
||||
|
||||
It’s not working very well with the Collaboration extension. It’s 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
125
docs/guide/node-views/js.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
tableOfContents: true
|
||||
---
|
||||
|
||||
# Node views with JavaScript
|
||||
|
||||
## toc
|
||||
|
||||
## Introduction
|
||||
Using frameworks like Vue or React can feel too complex, if you’re 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 let’s 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, I’m a node view!'
|
||||
|
||||
return {
|
||||
dom,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Got it? Let’s 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. Let’s 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 it’s 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 get’s 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? You’re 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 (that’s what we use in the above example).
|
||||
118
docs/guide/node-views/react.md
Normal file
118
docs/guide/node-views/react.md
Normal 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 let’s 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 don’t worry, we provide a wrapper component you can use to get started easily. Don’t forget to add it to your custom React component, like shown below:
|
||||
|
||||
```html
|
||||
<NodeViewWrapper className="react-component">
|
||||
React Component
|
||||
</NodeViewWrapper>
|
||||
```
|
||||
|
||||
Got it? Let’s see it in action. Feel free to copy the below example to get started.
|
||||
|
||||
<tiptap-demo name="GuideNodeViews/ReactComponent"></tiptap-demo>
|
||||
|
||||
That component doesn’t 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. Let’s 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, isn’t 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 don’t 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 (that’s 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> -->
|
||||
205
docs/guide/node-views/vue.md
Normal file
205
docs/guide/node-views/vue.md
Normal 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 let’s 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 don’t worry, we provide a wrapper component you can use to get started easily. Don’t 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? Let’s see it in action. Feel free to copy the below example to get started.
|
||||
|
||||
<tiptap-demo name="GuideNodeViews/VueComponent"></tiptap-demo>
|
||||
|
||||
That component doesn’t 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. Let’s 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, isn’t 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 don’t 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 (that’s 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
138
docs/guide/output.md
Normal 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 it’s 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 it’s wildly used, so it’s 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, it’s 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 that’s more like an import/export format. It won’t be your single source. That’s important to consider when adding Y.js for one of the mentioned use cases.
|
||||
|
||||
That said, it’s amazing and we’re about to provide an amazing backend, that makes all that a breeze.
|
||||
|
||||
### Not an option: Markdown
|
||||
Unfortunately, **tiptap doesn’t 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.
|
||||
* tiptap’s strength is customization, that doesn’t 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 you’re 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. That’s how you can achieve the exact same rendering as it’s 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, you’ll probably want to do just that without an actual editor instance.
|
||||
|
||||
That’s what the `generateHTML()` is for. It’s 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 you’re migrating existing content to tiptap we would recommend to get your existing output to HTML. That’s 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 aren’t allowed (based on your configuration), tiptap just throws them away quietly.
|
||||
|
||||
We’re 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) We’d 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 doesn’t matter if it’s JSON or HTML. It doesn’t even matter if you’re using tiptap or not. You should always validate user input.
|
||||
113
docs/guide/styling.md
Normal file
113
docs/guide/styling.md
Normal 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 you’re rendering the stored content somewhere, there won’t 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). That’s 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, that’s 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 that’s 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 doesn’t 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
65
docs/guide/typescript.md
Normal file
@@ -0,0 +1,65 @@
|
||||
---
|
||||
tableOfContents: true
|
||||
---
|
||||
|
||||
# Working with TypeScript
|
||||
|
||||
## toc
|
||||
|
||||
## Introduction
|
||||
The whole tiptap is code base is written in TypeScript. If you haven’t heard of it or never used it, no worries. You don’t have to.
|
||||
|
||||
TypeScript extends JavaScript by adding types (hence the name). It adds new syntax, which doesn’t exist in Vanilla JavaScript. It’s 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, that’s very valuable. It means we’ll get notified of lot of bugs, before shipping code to you.
|
||||
|
||||
**Anyway, if you don’t use TypeScript in your project, that’s 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, you’ll 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 }) => {
|
||||
// …
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
That’s basically it. We’re doing all the rest automatically.
|
||||
Reference in New Issue
Block a user