2018-08-21 03:58:59 +08:00
# tiptap
2018-08-24 15:09:32 +08:00
A renderless and extendable rich-text editor for [Vue.js ](https://github.com/vuejs/vue )
2018-08-21 04:49:24 +08:00
2018-08-26 14:54:41 +08:00
[![ ](https://img.shields.io/npm/v/tiptap.svg?label=version )](https://www.npmjs.com/package/tiptap)
[![ ](https://img.shields.io/npm/dm/tiptap.svg )](https://npmcharts.com/compare/tiptap?minimal=true)
[![ ](https://img.shields.io/npm/l/tiptap.svg )](https://www.npmjs.com/package/tiptap)
[![ ](http://img.badgesize.io/https://unpkg.com/tiptap/dist/tiptap.min.js?compression=gzip&label=size&colorB=000000 )](https://www.npmjs.com/package/tiptap)
2018-08-25 21:27:41 +08:00
## Why I built tiptap
2018-08-25 21:34:14 +08:00
I was looking for a text editor for [Vue.js ](https://github.com/vuejs/vue ) and found some solutions that didn't really satisfy me. The editor should be easy to extend and not based on old dependencies such as jQuery. For React there is already a great editor called [Slate.js ](https://github.com/ianstormtaylor/slate ), which impresses with its modularity. I came across [Prosemirror ](https://github.com/prosemirror ) and decided to build on it. Prosemirror is a toolkit for building rich-text editors that is already in use at many well-known companies such as *Atlassian* or *New York Times* .
2018-08-24 15:09:32 +08:00
2018-08-28 14:39:33 +08:00
### What means `renderless`?
Will renderless components you'll have (almost) full control over markup and styling. I don't want to tell you what a menu should look like or where it should be rendered in the DOM. That's all up to you. There is also a [good article ](https://adamwathan.me/renderless-components-in-vuejs/ ) about renderless components by Adam Wathan.
### How is the data stored under the hood?
You can save your data as a raw `HTML` string or can get a `JSON` -serializeable representation of your document. And of course you can pass these two types back to the editor.
2018-08-24 15:13:31 +08:00
## Examples
To check out some live examples, visit [tiptap.scrumpy.io ](https://tiptap.scrumpy.io/ ).
2018-08-21 16:11:17 +08:00
2018-08-23 05:51:55 +08:00
## Installation
```
npm install tiptap
```
2018-08-25 21:27:41 +08:00
or
```
yarn add tiptap
```
2018-08-23 05:51:55 +08:00
2018-08-24 05:02:28 +08:00
## Basic Setup
2018-08-23 05:51:55 +08:00
```vue
< template >
< editor >
2018-08-25 21:34:14 +08:00
<!-- Add HTML to the scoped slot called `content` -->
2018-08-23 05:51:55 +08:00
< div slot = "content" slot-scope = "props" >
2018-08-24 05:02:28 +08:00
< p > Hi, I'm just a boring paragraph< / p >
2018-08-23 05:51:55 +08:00
< / div >
< / editor >
< / template >
< script >
// Import the editor
import { Editor } from 'tiptap'
export default {
components: {
Editor,
},
}
< / script >
```
2018-08-23 14:33:21 +08:00
## Editor Properties
2018-08-24 15:35:59 +08:00
| **Property** | **Type** | **Default** | **Description** |
2018-08-26 03:33:06 +08:00
| --- | :---: | :---: | --- |
2018-08-24 16:11:00 +08:00
| `editable` | `Boolean` | `true` | When set to `false` the editor is read-only. |
| `doc` | `Object` | `null` | The editor state object used by Prosemirror. You can also pass HTML to the `content` slot. When used both, the `content` slot will be ignored. |
| `extensions` | `Array` | `[]` | A list of extensions used, by the editor. This can be `Nodes` , `Marks` or `Plugins` . |
2018-09-03 19:26:46 +08:00
| `@init` | `Object` | `undefined` | This will return an Object with the current `state` and `view` of Prosemirror on init. |
2018-08-28 15:31:08 +08:00
| `@update` | `Object` | `undefined` | This will return an Object with the current `state` of Prosemirror, a `getJSON()` and `getHTML()` function on every change. |
2018-08-23 14:33:21 +08:00
2018-08-26 21:51:35 +08:00
## Scoped Slots
| **Name** | **Description** |
| --- | --- |
| `editor` | Here the content will be rendered. |
| `menubar` | Here a menu bar will be rendered. |
| `menububble` | Here a menu bubble will be rendered. |
### Slot Properties
The `menubar` and `menububble` slot will receive some properties.
| **Property** | **Type** | **Description** |
| --- | :---: | --- |
| `nodes` | `Object` | A list of available nodes with active state and command. |
| `marks` | `Object` | A list of available marks with active state and command. |
| `focused` | `Boolean` | Whether the editor is focused. |
| `focus` | `Function` | A function to focus the editor. |
2018-08-23 14:33:21 +08:00
## Extensions
2018-08-27 00:47:08 +08:00
By default the editor will only support paragraphs. Other nodes and marks are available as **extensions** . There is a package called `tiptap-extensions` with the most basic nodes, marks and plugins.
2018-08-24 05:02:28 +08:00
2018-08-26 03:33:06 +08:00
### Available Extensions
2018-08-24 05:02:28 +08:00
```vue
< template >
< editor :extensions = "extensions" >
< div slot = "content" slot-scope = "props" >
< h1 > Yay Headlines!< / h1 >
< p > All these < strong > cool tags< / strong > are working now.< / p >
< / div >
< / editor >
< / template >
< script >
import { Editor } from 'tiptap'
import {
2018-09-07 05:35:13 +08:00
// Nodes
2018-08-27 01:43:02 +08:00
BlockquoteNode,
BulletListNode,
CodeBlockNode,
2018-09-07 05:35:13 +08:00
CodeBlockHighlightNode,
2018-08-27 01:43:02 +08:00
HardBreakNode,
HeadingNode,
2018-09-01 04:32:28 +08:00
ImageNode,
2018-08-27 01:43:02 +08:00
ListItemNode,
OrderedListNode,
TodoItemNode,
TodoListNode,
2018-09-07 05:35:13 +08:00
// Marks
2018-08-27 01:43:02 +08:00
BoldMark,
CodeMark,
ItalicMark,
LinkMark,
2018-09-07 09:21:46 +08:00
StrikeMark,
2018-09-14 03:33:41 +08:00
UnderlineMark,
2018-09-07 05:35:13 +08:00
// General Extensions
2018-08-27 01:43:02 +08:00
HistoryExtension,
2018-09-07 05:35:13 +08:00
PlaceholderExtension,
2018-08-24 05:02:28 +08:00
} from 'tiptap-extensions'
export default {
components: {
Editor,
},
data() {
return {
extensions: [
2018-08-27 01:43:02 +08:00
new BlockquoteNode(),
new BulletListNode(),
new CodeBlockNode(),
new HardBreakNode(),
new HeadingNode({ maxLevel: 3 }),
2018-09-01 04:32:28 +08:00
new ImageNode(),
2018-08-27 01:43:02 +08:00
new ListItemNode(),
new OrderedListNode(),
new TodoItemNode(),
new TodoListNode(),
new BoldMark(),
new CodeMark(),
new ItalicMark(),
new LinkMark(),
2018-09-07 05:35:13 +08:00
new StrikeMark(),
2018-09-14 03:33:41 +08:00
new UnderlineMark(),
2018-08-27 01:43:02 +08:00
new HistoryExtension(),
2018-09-07 05:35:13 +08:00
new PlaceholderExtension(),
2018-08-24 05:02:28 +08:00
],
}
},
}
< / script >
```
2018-08-26 03:33:06 +08:00
### Create Custom Extensions
2018-08-24 05:02:28 +08:00
2018-08-27 00:47:08 +08:00
The most powerful feature of tiptap is that you can create your own extensions. There are 3 types of extensions.
2018-08-24 15:35:59 +08:00
| **Type** | **Description** |
2018-08-26 03:33:06 +08:00
| --- | --- |
2018-08-24 16:11:00 +08:00
| `Extension` | The most basic type. It's useful to register some [Prosemirror plugins ](https://prosemirror.net/docs/guide/ ) or some input rules. |
| `Node` | Add a custom node. Nodes are basically block elements like a headline or a paragraph. |
| `Mark` | Add a custom mark. Marks are used to add extra styling or other information to inline content like a strong tag or links. |
### Extension Class
| **Method** | **Type** | **Default** | **Description** |
2018-08-26 03:33:06 +08:00
| --- | :---: | :---: | --- |
2018-08-24 16:11:00 +08:00
| `get name()` | `String` | `null` | Define a name for your extension. |
| `get defaultOptions()` | `Object` | `{}` | Define some default options. The options are available as `this.$options` . |
| `get plugins()` | `Array` | `[]` | Define a list of [Prosemirror plugins ](https://prosemirror.net/docs/guide/ ). |
2018-08-26 03:33:06 +08:00
| `keys({ schema })` | `Object` | `null` | Define some keybindings. |
| `inputRules({ schema })` | `Array` | `[]` | Define a list of input rules. |
2018-08-24 16:11:00 +08:00
### Node|Mark Class
| **Method** | **Type** | **Default** | **Description** |
2018-08-26 03:33:06 +08:00
| --- | :---: | :---: | --- |
2018-08-24 16:11:00 +08:00
| `get name()` | `String` | `null` | Define a name for your node or mark. |
| `get defaultOptions()` | `Object` | `{}` | Define some default options. The options are available as `this.$options` . |
| `get schema()` | `Object` | `null` | Define a [schema ](https://prosemirror.net/docs/guide/#schema ). |
| `get view()` | `Object` | `null` | Define a node view as a vue component. |
| `keys({ type, schema })` | `Object` | `null` | Define some keybindings. |
| `command({ type, schema, attrs })` | `Object` | `null` | Define a command. This is used for menus to convert to this node or mark. |
2018-08-24 16:12:40 +08:00
| `inputRules({ type, schema })` | `Array` | `[]` | Define a list of input rules. |
2018-08-24 16:11:00 +08:00
| `get plugins()` | `Array` | `[]` | Define a list of [Prosemirror plugins ](https://prosemirror.net/docs/guide/ ). |
2018-08-24 15:35:59 +08:00
2018-08-25 22:39:33 +08:00
### Create a Node
2018-08-24 22:52:42 +08:00
Let's take a look at a real example. This is basically how the default `blockquote` node from [`tiptap-extensions` ](https://www.npmjs.com/package/tiptap-extensions ) looks like.
```js
import { Node } from 'tiptap'
import { wrappingInputRule, setBlockType, wrapIn } from 'tiptap-commands'
export default class BlockquoteNode extends Node {
2018-08-26 03:33:06 +08:00
2018-08-24 22:52:42 +08:00
// choose a unique name
get name() {
return 'blockquote'
}
2018-08-26 03:33:06 +08:00
2018-08-24 22:52:42 +08:00
// the prosemirror schema object
2018-08-25 21:34:14 +08:00
// take a look at https://prosemirror.net/docs/guide/#schema for a detailed explanation
2018-08-24 22:52:42 +08:00
get schema() {
return {
content: 'block+',
group: 'block',
defining: true,
draggable: false,
2018-08-25 21:34:14 +08:00
// define how the editor will detect your node from pasted HTML
// every blockquote tag will be converted to this blockquote node
2018-08-24 22:52:42 +08:00
parseDOM: [
{ tag: 'blockquote' },
],
// this is how this node will be rendered
2018-08-25 21:34:14 +08:00
// in this case a blockquote tag with a class called `awesome-blockquote` will be rendered
// the '0' stands for its text content inside
2018-08-24 22:52:42 +08:00
toDOM: () => ['blockquote', { class: 'awesome-blockquote' }, 0],
}
}
2018-08-26 03:33:06 +08:00
2018-08-24 22:52:42 +08:00
// this command will be called from menus to add a blockquote
2018-08-25 21:34:14 +08:00
// `type` is the prosemirror schema object for this blockquote
// `schema` is a collection of all registered nodes and marks
2018-08-24 22:52:42 +08:00
command({ type, schema }) {
return wrapIn(type)
}
2018-08-26 03:33:06 +08:00
2018-08-24 22:52:42 +08:00
// here you can register some shortcuts
2018-08-25 21:34:14 +08:00
// in this case you can create a blockquote with `ctrl` + `>`
2018-08-24 22:52:42 +08:00
keys({ type }) {
return {
'Ctrl->': wrapIn(type),
}
}
2018-08-26 03:33:06 +08:00
2018-08-25 21:34:14 +08:00
// a blockquote will be created when you are on a new line and type `>` followed by a space
2018-08-24 22:52:42 +08:00
inputRules({ type }) {
return [
wrappingInputRule(/^\s*>\s$/, type),
]
}
}
```
2018-08-25 22:39:33 +08:00
### Create a Node as a Vue Component
2018-08-24 22:52:42 +08:00
2018-08-27 00:47:08 +08:00
The real power of the nodes comes in combination with Vue components. Lets build an iframe node, where you can change its url (this can also be found in our [examples ](https://github.com/heyscrumpy/tiptap/tree/master/examples/Components/Routes/Embeds )).
2018-08-25 22:39:33 +08:00
```js
import { Node } from 'tiptap'
export default class IframeNode extends Node {
get name() {
return 'iframe'
}
get schema() {
return {
// here you have to specify all values that can be stored in this node
attrs: {
src: {
default: null,
},
},
group: 'block',
selectable: false,
// parseDOM and toDOM is still required to make copy and paste work
parseDOM: [{
tag: 'iframe',
getAttrs: dom => ({
src: dom.getAttribute('src'),
}),
}],
toDOM: node => ['iframe', {
src: node.attrs.src,
frameborder: 0,
allowfullscreen: 'true',
}],
}
}
2018-08-26 03:33:06 +08:00
2018-08-25 22:39:33 +08:00
// return a vue component
// this can be an object or an imported component
get view() {
return {
// there are some props available
// `node` is a Prosemirror Node Object
// `updateAttrs` is a function to update attributes defined in `schema`
// `editable` is the global editor prop whether the content can be edited
props: ['node', 'updateAttrs', 'editable'],
data() {
return {
2018-08-25 23:01:19 +08:00
// save the iframe src in a new variable because `this.node.attrs` is immutable
2018-08-25 22:39:33 +08:00
url: this.node.attrs.src,
}
},
methods: {
onChange(event) {
this.url = event.target.value
2018-08-26 03:33:06 +08:00
2018-08-25 22:39:33 +08:00
// update the iframe url
this.updateAttrs({
src: this.url,
})
},
},
template: `
< div class = "iframe" >
< iframe class = "iframe__embed" :src = "url" > < / iframe >
< input class = "iframe__input" type = "text" :value = "url" @input =" onChange " v-if = "editable" />
< / div >
`,
}
}
}
```
2018-08-23 14:33:21 +08:00
2018-08-26 21:51:35 +08:00
## Building a Menu
This is a basic example of building a custom menu. A more advanced menu can be found at the [examples page ](https://tiptap.scrumpy.io ).
```vue
< template >
< editor :extensions = "extensions" >
< div slot = "menubar" slot-scope = "{ nodes, marks }" >
2018-09-06 23:09:07 +08:00
< div v-if = "nodes && marks" >
< button :class = "{ 'is-active': nodes.heading.active({ level: 1 }) }" @click =" nodes . heading . command ({ level: 1 })" >
H1
< / button >
< button :class = "{ 'is-active': marks.bold.active() }" @click =" marks . bold . command ()" >
Bold
< / button >
< / div >
2018-08-26 21:51:35 +08:00
< / div >
< div slot = "content" slot-scope = "props" >
< p > This text can be made bold.< / p >
< / div >
< / editor >
< / template >
< script >
import { Editor } from 'tiptap'
2018-08-27 01:44:14 +08:00
import { HeadingNode, BoldMark } from 'tiptap-extensions'
2018-08-26 21:51:35 +08:00
export default {
components: {
Editor,
},
data() {
return {
extensions: [
2018-08-27 01:44:14 +08:00
new HeadingNode({ maxLevel: 3 }),
new BoldMark(),
2018-08-26 21:51:35 +08:00
],
}
},
}
< / script >
```
2018-09-01 15:35:39 +08:00
## Development Setup
Currently only Yarn is supported for development because of a feature called workspaces we are using here.
``` bash
# install deps
yarn install
# serve examples at localhost:3000
yarn start
# build dist files for packages
yarn build:packages
# build dist files for examples
yarn build:examples
```
2018-08-21 16:11:17 +08:00
## Contributing
Please see [CONTRIBUTING ](CONTRIBUTING.md ) for details.
## Credits
- [Philipp Kühn ](https://github.com/philippkuehn )
- [All Contributors ](../../contributors )
## License
The MIT License (MIT). Please see [License File ](LICENSE.md ) for more information.