tiptap/docs/src/docPages/guide/node-views.md

315 lines
10 KiB
Markdown
Raw Normal View History

2021-03-09 18:55:14 +08:00
# Interactive node views
2020-11-06 23:25:46 +08:00
## toc
## Introduction
2021-03-09 18:55:14 +08:00
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 content. That can literally be everything. If you can write it in JavaScript, you can use it in your editor.
2020-11-28 00:25:32 +08:00
2020-11-30 22:33:20 +08:00
## Different types of node views
2021-03-11 04:52:32 +08:00
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: Does your node has editable content?
### Editable text
Yes, node views can have editable text, just like regular nodes. Thats simple. The caret will exactly behave like you would expect it from a regular node. Existing commands work very well with those nodes.
2020-11-30 22:33:20 +08:00
2020-11-28 00:25:01 +08:00
```html
<div class="Prosemirror" contenteditable="true">
<p>text</p>
<node-view>text</node-view>
<p>text</p>
</div>
```
2021-03-11 04:52:32 +08:00
Thats how the [`TaskItem`](/api/nodes/task-item) node works.
### Non-editable text
Nodes can also have text, which is not edtiable. The caret cant jump into those, but you dont want that anyway.
Those just get a `contenteditable="false"` from tiptap by default.
2020-11-28 00:25:01 +08:00
```html
<div class="Prosemirror" contenteditable="true">
<p>text</p>
<node-view contenteditable="false">text</node-view>
<p>text</p>
</div>
```
2021-03-11 04:52:32 +08:00
Thats how you could render mentions, which shouldnt be editable. Users can add or delete them, but not delete single characters.
Statamic uses those for their Bard editor, which renders complex modules inside tiptap, which can have their own text inputs.
2021-03-10 04:54:16 +08:00
### Mixed content
2021-03-11 04:52:32 +08:00
You can even mix non-editable and editable text. Thats great because you can build complex things, and even use marks like bold and italic inside the editable content.
2020-11-28 00:25:01 +08:00
```html
<div class="Prosemirror" contenteditable="true">
<p>text</p>
<node-view>
<div>
non-editable text
</div>
<div>
editable text
</div>
</node-view>
<p>text</p>
</div>
2021-03-10 04:54:16 +08:00
```
2021-03-09 01:33:50 +08:00
2021-03-11 04:52:32 +08:00
**BUT**, if there are other elements with non-editable text in your node view, the caret can jump there. You can improve that with manually adding `contenteditable` attributes to the specific parts of your node view.
```html
<div class="Prosemirror" contenteditable="true">
<p>text</p>
<node-view contenteditable="false">
<div>
non-editable text
</div>
<div contenteditable="true">
editable text
</div>
</node-view>
<p>text</p>
</div>
```
**BUT**, that also means the caret cant just move from outside of the node view to the inside. Users have to manually place their caret to edit the content inside the node view. Just so you know.
2021-03-10 04:54:16 +08:00
## Node views with JavaScript
TODO
2020-11-28 00:25:01 +08:00
2021-03-10 04:54:16 +08:00
```js
import { Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-2'
import Component from './Component.vue'
export default Node.create({
addNodeView() {
return ({ editor, node, getPos, HTMLAttributes, decorations, extension }) => {
const dom = document.createElement('div')
2021-03-10 05:14:23 +08:00
dom.innerHTML = 'Hello, Im a node view!'
2021-03-10 04:54:16 +08:00
return {
dom,
}
})
},
})
```
## Node views with Vue
2021-03-11 04:52:32 +08:00
Using Vanilla JavaScript can feel complex if you are used to work in Vue. Good news: You can use regular Vue components in your node views, too. There is just a little bit you need to know, but lets go through this one by one.
2020-11-06 23:25:46 +08:00
2021-03-09 01:33:50 +08:00
### Render a Vue component
2021-03-10 04:54:16 +08:00
Here is what you need to do to render Vue components inside your text editor:
2021-03-09 18:55:14 +08:00
2021-03-10 04:54:16 +08:00
1. [Create a node extension](/guide/build-extensions)
2. Create a Vue component
2021-03-09 18:55:14 +08:00
3. Pass that component to the provided `VueNodeViewRenderer`
4. Register it with `addNodeView()`
5. [Configure tiptap to use your new node](/guide/configuration)
This is how your node could look like:
2021-03-09 18:17:48 +08:00
```js
import { Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-2'
import Component from './Component.vue'
export default Node.create({
2021-03-09 18:55:14 +08:00
// configuration …
2021-03-09 18:17:48 +08:00
addNodeView() {
return VueNodeViewRenderer(Component)
},
})
```
2021-03-11 04:52:32 +08:00
There is a little bit of magic required to make this work. But dont worry, we provide a wrapper component you can use to get started easily. Dont forget to add it to your custom Vue component, like shown below:
2021-03-09 18:55:14 +08:00
```html
<template>
<node-view-wrapper>
Vue Component
</node-view-wrapper>
</template>
```
2021-03-11 04:52:32 +08:00
Got it? Lets see it in action. Feel free to copy the below example to get started.
2021-03-09 18:55:14 +08:00
2021-03-09 01:33:50 +08:00
<demo name="Guide/NodeViews/VueComponent" />
2020-11-06 23:25:46 +08:00
2021-03-11 04:52:32 +08:00
That component doesnt interactive with the editor, though. Time to connect it to the editor output.
2021-03-09 01:33:50 +08:00
### Access node attributes
2021-03-11 04:52:32 +08:00
The `VueNodeViewRenderer` which you use in your node, passes a few very helpful props to your custom view component. One of them is the `node` prop. Add this snippet to your Vue component to directly access the node:
2021-03-09 01:33:50 +08:00
2021-03-09 18:17:48 +08:00
```js
props: {
node: {
type: Object,
required: true,
},
},
```
2021-03-11 04:52:32 +08:00
That makes it super easy to access node attributes in your Vue component. Lets say you have [added an attribute](/guide/extend-extensions#attributes) named `count` to your node, like we did in the above example, you could access it like this:
2021-03-09 18:17:48 +08:00
```js
this.node.attrs.count
```
### Update node attributes
2021-03-11 04:52:32 +08:00
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:
2021-03-09 18:17:48 +08:00
```js
props: {
updateAttributes: {
type: Function,
required: true,
},
},
```
2021-03-11 04:52:32 +08:00
Pass an object with updated attributes to the function:
2021-03-09 18:17:48 +08:00
```js
this.updateAttributes({
count: this.node.attrs.count + 1,
})
```
2021-03-11 04:52:32 +08:00
And yes, all of that is reactive, too. A pretty seemless communication, isnt it?
2021-03-09 18:17:48 +08:00
### Adding a content editable
2021-03-11 04:52:32 +08:00
There is another component called `NodeViewContent` which helps you adding editable content to your node view. Here is an example:
2021-03-09 18:17:48 +08:00
2021-03-10 04:54:16 +08:00
```html
<template>
<node-view-wrapper class="dom">
<node-view-content class="content-dom" />
</node-view-wrapper>
</template>
2020-12-14 19:18:05 +08:00
2021-03-10 04:54:16 +08:00
<script>
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2'
2020-12-14 19:18:05 +08:00
2021-03-10 04:54:16 +08:00
export default {
components: {
NodeViewWrapper,
NodeViewContent,
2020-12-14 19:18:05 +08:00
},
2021-03-10 04:54:16 +08:00
}
2020-12-14 19:18:05 +08:00
```
2021-03-11 04:52:32 +08:00
You dont need to add those `class` attributes, feel free to remove them or pass other class names. Try it out in the following example:
2021-03-10 04:54:16 +08:00
<demo name="Guide/NodeViews/VueComponentContent" />
2021-03-11 04:52:32 +08:00
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 (thats what we use in the above example).
2021-03-10 04:54:16 +08:00
### All available props
2021-03-11 04:52:32 +08:00
For advanced use cases, we pass a few more props to the component. Here is the full list of what you can expect:
2020-12-14 19:18:05 +08:00
```html
<template>
<node-view-wrapper />
2020-12-14 19:18:05 +08:00
</template>
<script>
import { NodeViewWrapper } from '@tiptap/vue-2'
2020-12-14 19:18:05 +08:00
export default {
components: {
NodeViewWrapper,
},
2020-12-14 19:18:05 +08:00
props: {
2021-03-11 04:52:32 +08:00
// the editor instance
2020-12-14 19:18:05 +08:00
editor: {
type: Object,
},
2021-03-11 04:52:32 +08:00
// the current node
2020-12-14 19:18:05 +08:00
node: {
type: Object,
},
2021-03-11 04:52:32 +08:00
// an array of decorations
2020-12-14 19:18:05 +08:00
decorations: {
type: Array,
},
2021-03-11 04:52:32 +08:00
// true when the caret is inside the node view
2020-12-14 19:18:05 +08:00
selected: {
type: Boolean,
},
extension: {
type: Object,
},
2021-03-11 04:52:32 +08:00
// get the document position of the current node
2020-12-14 19:18:05 +08:00
getPos: {
type: Function,
},
2021-03-11 04:52:32 +08:00
// update attributes of the current node
2020-12-14 19:18:05 +08:00
updateAttributes: {
type: Function,
},
},
}
</script>
```
2021-03-10 04:54:16 +08:00
## Node views with React
TODO
2021-03-10 04:54:16 +08:00
## Rendered content
2021-03-10 04:54:16 +08:00
```js
parseHTML() {
return [{
tag: 'vue-component',
}]
},
2021-03-10 04:54:16 +08:00
renderHTML({ HTMLAttributes }) {
return ['vue-component', mergeAttributes(HTMLAttributes)]
},
```
## Examples
2021-03-11 05:06:18 +08:00
Weve put together [a list of interactive examples](/guide/node-views/examples). :-)
2021-02-11 23:08:58 +08:00
## 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.