tiptap/README.md

402 lines
12 KiB
Markdown
Raw Normal View History

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
## Installation
```
npm install tiptap
```
2018-08-25 21:27:41 +08:00
or
```
yarn add tiptap
```
## Basic Setup
```vue
<template>
<editor>
2018-08-25 21:34:14 +08:00
<!-- Add HTML to the scoped slot called `content` -->
<div slot="content" slot-scope="props">
<p>Hi, I'm just a boring paragraph</p>
</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-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.
### Available Extensions
```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,
} 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(),
],
}
},
}
</script>
```
### Create Custom Extensions
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-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-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/). |
| `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-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-24 22:52:42 +08:00
// choose a unique name
get name() {
return 'blockquote'
}
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-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-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-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-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-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 }">
<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.