Merge branch 'main' into feature/generate-html-from-json-document

# Conflicts:
#	packages/core/src/ExtensionManager.ts
This commit is contained in:
Philipp Kühn
2020-09-09 20:50:53 +02:00
59 changed files with 929 additions and 785 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -9,12 +9,7 @@ import { Editor, EditorContent } from '@tiptap/vue-starter-kit'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import History from '@tiptap/extension-history'
import Bold from '@tiptap/extension-bold'
import Italic from '@tiptap/extension-italic'
import Code from '@tiptap/extension-code' import Code from '@tiptap/extension-code'
import CodeBlock from '@tiptap/extension-codeblock'
import Heading from '@tiptap/extension-heading'
import Focus from '@tiptap/extension-focus' import Focus from '@tiptap/extension-focus'
export default { export default {
@@ -31,16 +26,11 @@ export default {
mounted() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
extensions: [ extensions: [
new Document(), Document(),
new History(), Paragraph(),
new Paragraph(), Text(),
new Text(), Code(),
new Bold(), Focus({
new Italic(),
new Code(),
new CodeBlock(),
new Heading(),
new Focus({
className: 'has-focus', className: 'has-focus',
nested: true, nested: true,
}), }),
@@ -48,16 +38,12 @@ export default {
autoFocus: true, autoFocus: true,
content: ` content: `
<p> <p>
With the focus extension you can add custom classes to focused nodes. Default options: The focus extension adds custom classes to focused nodes. By default, itll add a <code>has-focus</code> class, even to nested nodes:
</p> </p>
<pre><code>{\n className: 'has-focus',\n nested: true,\n}</code></pre> <pre><code>{ className: 'has-focus', nested: true }</code></pre>
<ul> <ul>
<li> <li>With <code>nested: true</code> nested elements like this list item will be focused.</li>
When set <code>nested</code> to <code>true</code> also nested elements like this list item will be captured. <li>Otherwise the whole list will get the focus class, even if only a single list item is selected.</li>
</li>
<li>
Otherwise only the wrapping list will get this class.
</li>
</ul> </ul>
`, `,
}) })

View File

@@ -24,9 +24,9 @@ export default {
this.editor = new Editor({ this.editor = new Editor({
content: '<p>This is a radically reduced version of tiptap for minimalisits. It has only support for a document, paragraphs and text, thats it.</p>', content: '<p>This is a radically reduced version of tiptap for minimalisits. It has only support for a document, paragraphs and text, thats it.</p>',
extensions: [ extensions: [
new Document(), Document(),
new Paragraph(), Paragraph(),
new Text(), Text(),
], ],
}) })

View File

@@ -26,10 +26,10 @@ export default {
this.editor = new Editor({ this.editor = new Editor({
content: '<p>Im running tiptap with Vue.js. This demo is interactive, try to edit the text.</p>', content: '<p>Im running tiptap with Vue.js. This demo is interactive, try to edit the text.</p>',
extensions: [ extensions: [
new Document(), Document(),
new Paragraph(), Paragraph(),
new Text(), Text(),
new Bold(), Bold(),
], ],
}) })
}, },

View File

@@ -30,10 +30,10 @@ export default {
mounted() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
extensions: [ extensions: [
new Document(), Document(),
new Paragraph(), Paragraph(),
new Text(), Text(),
new Bold(), Bold(),
], ],
content: ` content: `
<p>This isnt bold.</p> <p>This isnt bold.</p>

View File

@@ -30,10 +30,10 @@ export default {
mounted() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
extensions: [ extensions: [
new Document(), Document(),
new Paragraph(), Paragraph(),
new Text(), Text(),
new Code(), Code(),
], ],
content: ` content: `
<p>This isnt code.</p> <p>This isnt code.</p>

View File

@@ -0,0 +1,5 @@
context('/api/extensions/document', () => {
beforeEach(() => {
cy.visit('/api/extensions/document')
})
})

View File

@@ -0,0 +1,44 @@
<template>
<div v-if="editor">
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import { EditorContent } from '@tiptap/vue'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document(),
Paragraph(),
Text(),
],
content: `
<p>The Document extension is required. Though, you can write your own implementation, e. g. to give it custom name.</p>
`,
})
window.editor = this.editor
},
beforeDestroy() {
this.editor.destroy()
}
}
</script>

View File

@@ -33,10 +33,10 @@ export default {
mounted() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
extensions: [ extensions: [
new Document(), Document(),
new Paragraph(), Paragraph(),
new Text(), Text(),
new History(), History(),
], ],
content: ` content: `
<p>Edit this text and press undo to test this extension.</p> <p>Edit this text and press undo to test this extension.</p>

View File

@@ -30,10 +30,10 @@ export default {
mounted() { mounted() {
this.editor = new Editor({ this.editor = new Editor({
extensions: [ extensions: [
new Document(), Document(),
new Paragraph(), Paragraph(),
new Text(), Text(),
new Italic(), Italic(),
], ],
content: ` content: `
<p>This isnt italic.</p> <p>This isnt italic.</p>

View File

@@ -0,0 +1,5 @@
context('/api/extensions/paragraph', () => {
beforeEach(() => {
cy.visit('/api/extensions/paragraph')
})
})

View File

@@ -0,0 +1,44 @@
<template>
<div v-if="editor">
<editor-content :editor="editor" />
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import { EditorContent } from '@tiptap/vue'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document(),
Paragraph(),
Text(),
],
content: `
<p>The Paragraph extension is not required, but its very likely you want to use it. Its needed to write paragraphs of text. 🤓</p>
`,
})
window.editor = this.editor
},
beforeDestroy() {
this.editor.destroy()
}
}
</script>

View File

@@ -55,15 +55,15 @@ export default {
this.editor = new Editor({ this.editor = new Editor({
content: '<h2>Hey there!</h2><p>This editor is based on Prosemirror, fully extendable and renderless. You can easily add custom nodes as Vue components.</p>', content: '<h2>Hey there!</h2><p>This editor is based on Prosemirror, fully extendable and renderless. You can easily add custom nodes as Vue components.</p>',
extensions: [ extensions: [
new Document(), Document(),
new Paragraph(), Paragraph(),
new Text(), Text(),
new CodeBlock(), CodeBlock(),
new History(), History(),
new Bold(), Bold(),
new Italic(), Italic(),
new Code(), Code(),
new Heading(), Heading(),
], ],
}) })
}, },

View File

@@ -1 +1,34 @@
# Events # Events
:::warning Out of date
This content is written for tiptap 1 and needs an update.
:::
There are some events you can listen for. A full list of events can be found [here](/api/classes.md#editor-options).
```js
const editor = new Editor({
onInit: () => {
// editor is initialized
},
onUpdate: ({ getHTML }) => {
// get new content on update
const newContent = getHTML()
},
})
```
It's also possible to register event listeners afterwards.
```js
const editor = new Editor()
editor.on('init', () => {
// editor is initialized
})
editor.on('update', ({ getHTML }) => {
// get new content on update
const newContent = getHTML()
})
```

View File

@@ -2,7 +2,9 @@
The Blockquote extension enables you to use the `<blockquote>` HTML tag in the editor. The Blockquote extension enables you to use the `<blockquote>` HTML tag in the editor.
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |
@@ -39,7 +41,7 @@ export default {
return { return {
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new Blockquote(), Blockquote(),
], ],
content: ` content: `
<blockquote> <blockquote>

View File

@@ -6,7 +6,9 @@ The extension will generate the corresponding `<strong>` HTML tags when reading
::: :::
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |

View File

@@ -6,7 +6,9 @@ Its intended to be used with the `ListItem` extension.
::: :::
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |
@@ -43,7 +45,7 @@ export default {
return { return {
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new BulletList(), BulletList(),
], ],
content: ` content: `
<ul> <ul>

View File

@@ -2,7 +2,9 @@
The Code extensions enables you to use the `<code>` HTML tag in the editor. If you paste in text with `<code>` tags it will rendered accordingly. The Code extensions enables you to use the `<code>` HTML tag in the editor. If you paste in text with `<code>` tags it will rendered accordingly.
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |

View File

@@ -1 +1,14 @@
# Document # Document
**The `Document` extension is required**, no matter what you build with tiptap. Its a so called “topNode”, a node thats the home to all other nodes. Think of it like the `<body>` tag for your document.
The node is very tiny though. It defines a name of the node (`document`), is configured to be a top node (`topNode: true`) and that it can contain multiple other nodes (`block`). Thats all. But have a look yourself:
:::warning Breaking Change from 1.x → 2.x
Tiptap 1 tried to hide that node from you, but it has always been there. A tiny, but important change though: **We renamed the default type from `doc` to `document`.** To keep it like that, use your own implementation of the `Document` node or migrate the stored JSON to use the new name.
:::
## Source Code
[packages/extension-document/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-document/)
## Usage
<demo name="Extensions/Document" highlight="10,28" />

View File

@@ -3,7 +3,8 @@ Enables you to use headline HTML tags in the editor.
## Options ## Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
| ------ | ---- | ---- | ----- | | ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
| levels | Array | [1, 2, 3, 4, 5, 6] | Specifies which headlines are supported. | | levels | Array | [1, 2, 3, 4, 5, 6] | Specifies which headlines are supported. |
## Commands ## Commands
@@ -51,7 +52,7 @@ export default {
return { return {
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new Heading({ Heading({
levels: [1, 2], levels: [1, 2],
}), }),
], ],

View File

@@ -2,7 +2,9 @@
Enables you to use the `<hr>` HTML tag in the editor. Enables you to use the `<hr>` HTML tag in the editor.
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |
@@ -39,7 +41,7 @@ export default {
return { return {
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new HorizontalRule(), HorizontalRule(),
], ],
content: ` content: `
<p>Some text.</p> <p>Some text.</p>

View File

@@ -6,7 +6,9 @@ The extension will generate the corresponding `<em>` HTML tags when reading cont
::: :::
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |

View File

@@ -3,5 +3,6 @@ Enables you to use the `<a>` HTML tag in the editor.
## Options ## Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
| ------ | ---- | ---- | ----- | | ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
| openOnClick | Boolean | true | Specifies if links will be opened on click. | | openOnClick | Boolean | true | Specifies if links will be opened on click. |

View File

@@ -4,3 +4,8 @@ Enables you to use the `<li>` HTML tag in the editor.
::: warning Restrictions ::: warning Restrictions
This extensions is intended to be used with the `BulletList` or `OrderedList` extension. This extensions is intended to be used with the `BulletList` or `OrderedList` extension.
::: :::
## Options
| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |

View File

@@ -6,7 +6,9 @@ This extensions is intended to be used with the `ListItem` extension.
::: :::
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |
@@ -43,7 +45,7 @@ export default {
return { return {
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new OrderedList(), OrderedList(),
], ],
content: ` content: `
<ol> <ol>

View File

@@ -1,2 +1,19 @@
# Paragraph # Paragraph
Enables you to use paragraphs in the editor. Yes, the schema is very strict. Without this extension you wont even be able to use paragraphs in the editor.
## Options
| Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands
*None*
## Keybindings
*None*
## Source Code
[packages/extension-paragraph/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-paragraph/)
## Usage
<demo name="Extensions/Paragraph" highlight="11,29" />

View File

@@ -2,7 +2,9 @@
Enables you to use the `<s>` HTML tag in the editor. Enables you to use the `<s>` HTML tag in the editor.
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |
@@ -40,7 +42,7 @@ export default {
return { return {
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new Strike(), Strike(),
], ],
content: ` content: `
<p><s>That's strikethrough.</s></p> <p><s>That's strikethrough.</s></p>

View File

@@ -7,7 +7,7 @@ This extensions is intended to be used with the `TodoList` extension.
## Options ## Options
| Option | Type | Default | Description | | Option | Type | Default | Description |
| ------ | ---- | ---- | ----- | | ------ | ---- | ------- | ----------- |
| nested | Boolean | false | Specifies if you can nest todo lists. | | nested | Boolean | false | Specifies if you can nest todo lists. |
## Commands ## Commands

View File

@@ -43,10 +43,10 @@ export default {
return { return {
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new TodoItem({ TodoItem({
nested: true, nested: true,
}), }),
new TodoList(), TodoList(),
], ],
content: ` content: `
<ul data-type="todo_list"> <ul data-type="todo_list">

View File

@@ -2,7 +2,9 @@
Enables you to use the `<u>` HTML tag in the editor. Enables you to use the `<u>` HTML tag in the editor.
## Options ## Options
*None* | Option | Type | Default | Description |
| ------ | ---- | ------- | ----------- |
| class | string | | Add a custom class to the rendered HTML tag. |
## Commands ## Commands
| Command | Options | Description | | Command | Options | Description |
@@ -40,7 +42,7 @@ export default {
return { return {
editor: new Editor({ editor: new Editor({
extensions: [ extensions: [
new Underline(), Underline(),
], ],
content: ` content: `
<p><u>This is underlined.</u></p> <p><u>This is underlined.</u></p>

View File

@@ -13,9 +13,9 @@ import Text from '@tiptap/extension-text'
const editor = new Editor({ const editor = new Editor({
extensions: [ extensions: [
new Document, Document(),
new Paragraph, Paragraph(),
new Text, Text(),
// add more extensions here // add more extensions here
]) ])
}) })
@@ -33,9 +33,9 @@ import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
const schema = getSchema([ const schema = getSchema([
new Document, Document(),
new Paragraph, Paragraph(),
new Text, Text(),
// add more extensions here // add more extensions here
]) ])
``` ```

View File

@@ -1,3 +1,3 @@
# Focus # Focus
<demo name="Examples/Focus" highlight="18,43-46,48" /> <demo name="Examples/Focus" highlight="13,33-36,38" />

View File

@@ -1,80 +0,0 @@
# Roadmap
## Tasks
1. Refactoring the API & Extension Manager
2. Improve testing: Add editor instance to the DOM element
3. Building the first batch of basic extensions (bold, italic), writing tests
4. Building more complex examples from the extensions
## New features
* generate schema without initializing tiptap, to make SSR easier (e. g. `getSchema([new Doc(), new Paragraph()])`)
## Requested features
* Basic Styling
* https://github.com/ueberdosis/tiptap/issues/507
* Support vor Vue.js 3
* Easily add custom classes to everything
* https://github.com/ueberdosis/tiptap/discussions/817
* Text snippets
* https://github.com/ueberdosis/tiptap/issues/737
* Markdown Support
## Requested extensions
* Alignment
* https://github.com/ueberdosis/tiptap/pull/544
* Font color
* Font family
* Font size
* Created embed from pasted YouTube URL
* Superscript/Subscript
* https://github.com/ueberdosis/tiptap/discussions/813
* Math Support
* https://github.com/ueberdosis/tiptap/issues/179
* https://github.com/ueberdosis/tiptap/issues/698
* Resizeable Images
* https://gist.github.com/zachjharris/a5442efbdff11948d085b6b1406dfbe6
## Ideas
* A `@tiptap/extensions` package would be helpful to make imports easier.
* Add more shorcuts:
* Ctrl+I → Italic ✅
* Ctrl+B → Bold ✅
* Ctrl+K → Link (Medium, Tumblr, Slack, Google Docs, Word)
* Ctrl+Shift+K → Code (Slack)
* Shift+Enter → Line break
* Ctrl+Shift+X → Strikethrough (Slack)
* Alt+Shift+5 → Strikethrough (Google Docs)
* Ctrl+Shift+6 → Strikethrough (Tumblr)
* Ctrl+Alt+0 → Paragraph (Google Docs)
* Ctrl+Alt+1 to 6 → Heading (Google Docs, Word, ~Medium, ~Slack)
* Ctrl+Shift+2 → Heading (Tumblr)
* Ctrl+Shift+7 → Ordered list (Google Docs, Slack, Tumblr)
* Ctrl+Shift+8 → Unordered list (Google Docs, Slack, Tumblr)
* Tab, Shift+Tab → Increase / decrease nesting in lists
* Ctrl+], Ctrl+[ → Same as above (when Tab needs to be used)
* Ctrl+Shift+9 → Blockquote (Tumblr)
* Ctrl+Alt+K → Code block (Slack)
* Ctrl+R → Horizontal ruler (Stack Overflow)
* Markdown shortcuts
* #+Space → Heading (the number of # determines the header level)
* *+Space, -+Space → Unordered list
* 1.+Space → Ordered list
* >+Space → Blockquote
* ```+Space → Code block
* ---- → Horizontal ruler
* ![] → Embedded resource (not part of Slack, but would it not be fancy?)
* :emoji: → Emoji (based on the name). A nice-to-have, most certainly.
* General shortcuts
* Ctrl+C, Ctrl+X, Ctrl+V: copy, cut, paste
* Ctrl+Z, Ctrl+Shift+Z, Ctrl+Y: undo, redo
* Ctrl+Backspace: delete previous word
* Ctrl+Delete: delete next word
* Ctrl+Home, Ctrl+End: go to the start / end of the whole document
* Ctrl+F, Ctrl+G: find, find next occurrence
* Ctrl+S: if there is no auto-saving, this should save the document
* Ctrl+/: show shortcuts (Medium, Slack)

View File

@@ -1,3 +0,0 @@
# Upgrade Guide
## Upgrading from 1.x to 2.x

View File

@@ -0,0 +1,11 @@
# Contributing
## What kind of contributions are welcome
## What kind of contributions wont be merged
## How to send your first Pull Request
## Code style
## Testing

View File

@@ -2,15 +2,15 @@
tiptap has a very modular package structure and is independent of any framework. Depending on what you want to do with tiptap there are a few different ways to install tiptap in your project. Choose the way that fits your project best. tiptap has a very modular package structure and is independent of any framework. Depending on what you want to do with tiptap there are a few different ways to install tiptap in your project. Choose the way that fits your project best.
## Vanilla JavaScript ## Plain JavaScript
Use tiptap with vanilla JavaScript for a very lightweight and raw experience. If you feel like it, you can even use it to connect the tiptap core with other frameworks not mentioned here. Use tiptap with vanilla JavaScript for a very lightweight and raw experience. If you feel like it, you can even use it to connect the tiptap core with other frameworks not mentioned here.
```bash ```bash
# Use npm # With npm
npm install @tiptap/core @tiptap/starter-kit npm install @tiptap/core @tiptap/starter-kit
# Or: Use Yarn # Or: With Yarn
yarn add @tiptap/core @tiptap/starter-kit yarn add @tiptap/core @tiptap/starter-kit
``` ```
@@ -18,11 +18,11 @@ Great, that should be enough to start. Here is the most essential code you need
```js ```js
import { Editor } from '@tiptap/core' import { Editor } from '@tiptap/core'
import extensions from '@tiptap/starter-kit' import defaultExtensions from '@tiptap/starter-kit'
new Editor({ new Editor({
element: document.getElementsByClassName('element'), element: document.getElementsByClassName('element'),
extensions: extensions(), extensions: defaultExtensions(),
content: '<p>Your content.</p>', content: '<p>Your content.</p>',
}) })
``` ```
@@ -32,10 +32,10 @@ new Editor({
To use tiptap with Vue.js (and tools that are based on Vue.js) install the Vue.js adapter in your project: To use tiptap with Vue.js (and tools that are based on Vue.js) install the Vue.js adapter in your project:
```bash ```bash
# Using npm # With npm
npm install @tiptap/vue @tiptap/vue-starter-kit npm install @tiptap/vue @tiptap/vue-starter-kit
# Using Yarn # Or: With Yarn
yarn add @tiptap/vue @tiptap/vue-starter-kit yarn add @tiptap/vue @tiptap/vue-starter-kit
``` ```

View File

@@ -0,0 +1,3 @@
# Roadmap
See https://github.com/ueberdosis/tiptap-next/projects/1

View File

@@ -0,0 +1,3 @@
# Sponsoring
https://github.com/sponsors/ueberdosis

View File

@@ -0,0 +1,35 @@
# Upgrade Guide
## Reasons to upgrade to tiptap 2.x
* TypeScript: auto complete, less bugs, generated API documentation
* Amazing documentation with 100+ pages
* Active maintenance, no more updates to 1.x
* Tons of new extensions planned
* Less bugs, tested code based
## Upgrading from 1.x to 2.x
The new API will look pretty familiar too you, but there are a ton of changes though. To make the upgrade a little bit easier, here is everything you should do:
### New document type
**We renamed the default `Document` type from `doc` to `document`.** To keep it like that, use your own implementation of the `Document` node or migrate the stored JSON to use the new name.
```js
import Document from '@tiptap/extension-document'
const CustomDocument = Document.name('doc').create()
new Editor({
extensions: [
CustomDocument(),
]
})
```
### New extension API
In case youve built some custom extensions for your project, youll need to rewrite them to fit the new API. No worries, though, you can keep a lot of your work though. The schema, commands, keys, inputRules, pasteRules all work like they did before. Its just different how you register them.
```js
const CustomExtension =
```

View File

@@ -1,14 +1,20 @@
- title: General - title: Overview
items: items:
- title: Introduction - title: Introduction
link: / link: /
- title: Installation - title: Installation
link: /general/installation link: /overview/installation
- title: Upgrade Guide - title: Upgrade Guide
link: /general/upgrade-guide link: /overview/upgrade-guide
draft: true
- title: Contributing
link: /overview/contributing
draft: true
- title: Sponsoring
link: /overview/sponsoring
draft: true draft: true
- title: Roadmap - title: Roadmap
link: /general/roadmap link: /overview/roadmap
draft: true draft: true
- title: Guide - title: Guide
@@ -40,62 +46,62 @@
link: /examples/basic link: /examples/basic
- title: Simple - title: Simple
link: /examples/simple link: /examples/simple
- title: Menu Bubble # - title: Menu Bubble
link: /examples/menu-bubble # link: /examples/menu-bubble
draft: true # draft: true
- title: Floating Menu # - title: Floating Menu
link: /examples/floating-menu # link: /examples/floating-menu
draft: true # draft: true
- title: Links # - title: Links
link: /examples/links # link: /examples/links
draft: true # draft: true
- title: Images # - title: Images
link: /examples/images # link: /examples/images
draft: true # draft: true
- title: Hiding Menu Bar # - title: Hiding Menu Bar
link: /examples/hiding-menu-bar # link: /examples/hiding-menu-bar
draft: true # draft: true
- title: Todo List # - title: Todo List
link: /examples/todo-list # link: /examples/todo-list
draft: true # draft: true
- title: Tables # - title: Tables
link: /examples/tables # link: /examples/tables
draft: true # draft: true
- title: Search and Replace # - title: Search and Replace
link: /examples/search-and-replace # link: /examples/search-and-replace
draft: true # draft: true
- title: Suggestions # - title: Suggestions
link: /examples/suggestions # link: /examples/suggestions
draft: true # draft: true
- title: Markdown Shortcuts - title: Markdown Shortcuts
link: /examples/markdown-shortcuts link: /examples/markdown-shortcuts
- title: Code Highlighting # - title: Code Highlighting
link: /examples/code-highlighting # link: /examples/code-highlighting
draft: true # draft: true
- title: History - title: History
link: /examples/history link: /examples/history
- title: Read-Only - title: Read-Only
link: /examples/read-only link: /examples/read-only
- title: Embeds # - title: Embeds
link: /examples/embeds # link: /examples/embeds
draft: true # draft: true
- title: Placeholder # - title: Placeholder
link: /examples/placeholder # link: /examples/placeholder
draft: true # draft: true
- title: Focus - title: Focus
link: /examples/focus link: /examples/focus
- title: Collaboration # - title: Collaboration
link: /examples/collaboration # link: /examples/collaboration
draft: true # draft: true
- title: Title # - title: Title
link: /examples/title # link: /examples/title
draft: true # draft: true
- title: Trailing Paragraph # - title: Trailing Paragraph
link: /examples/trailing-paragraph # link: /examples/trailing-paragraph
draft: true # draft: true
- title: Drag Handle # - title: Drag Handle
link: /examples/drag-handle # link: /examples/drag-handle
draft: true # draft: true
- title: Export HTML or JSON - title: Export HTML or JSON
link: /examples/export-html-or-json link: /examples/export-html-or-json
@@ -128,7 +134,6 @@
# draft: true # draft: true
- title: Document - title: Document
link: /api/extensions/document link: /api/extensions/document
draft: true
- title: Hardbreak - title: Hardbreak
link: /api/extensions/hard-break link: /api/extensions/hard-break
draft: true draft: true
@@ -156,7 +161,6 @@
draft: true draft: true
- title: Paragraph - title: Paragraph
link: /api/extensions/paragraph link: /api/extensions/paragraph
draft: true
# - title: Placeholder # - title: Placeholder
# link: /api/extensions/placeholder # link: /api/extensions/placeholder
# draft: true # draft: true

View File

@@ -12,9 +12,12 @@
"dist" "dist"
], ],
"dependencies": { "dependencies": {
"@types/clone-deep": "^4.0.1",
"@types/prosemirror-dropcursor": "^1.0.0", "@types/prosemirror-dropcursor": "^1.0.0",
"@types/prosemirror-gapcursor": "^1.0.1", "@types/prosemirror-gapcursor": "^1.0.1",
"clone-deep": "^4.0.1",
"collect.js": "^4.28.2", "collect.js": "^4.28.2",
"deepmerge": "^4.2.2",
"prosemirror-commands": "^1.1.3", "prosemirror-commands": "^1.1.3",
"prosemirror-dropcursor": "^1.3.2", "prosemirror-dropcursor": "^1.3.2",
"prosemirror-gapcursor": "^1.1.5", "prosemirror-gapcursor": "^1.1.5",
@@ -24,8 +27,7 @@
"prosemirror-state": "^1.3.3", "prosemirror-state": "^1.3.3",
"prosemirror-tables": "^1.1.1", "prosemirror-tables": "^1.1.1",
"prosemirror-utils": "^0.9.6", "prosemirror-utils": "^0.9.6",
"prosemirror-view": "^1.15.6", "prosemirror-view": "^1.15.6"
"verbal-expressions": "^1.0.2"
}, },
"scripts": { "scripts": {
"build": "microbundle" "build": "microbundle"

View File

@@ -12,10 +12,10 @@ import removeElement from './utils/removeElement'
import getSchemaTypeByName from './utils/getSchemaTypeByName' import getSchemaTypeByName from './utils/getSchemaTypeByName'
import getHtmlFromFragment from './utils/getHtmlFromFragment' import getHtmlFromFragment from './utils/getHtmlFromFragment'
import ExtensionManager from './ExtensionManager' import ExtensionManager from './ExtensionManager'
import EventEmitter from './EventEmitter'
import Extension from './Extension' import Extension from './Extension'
import Node from './Node' import Node from './Node'
import Mark from './Mark' import Mark from './Mark'
import EventEmitter from './EventEmitter'
import ComponentRenderer from './ComponentRenderer' import ComponentRenderer from './ComponentRenderer'
import defaultPlugins from './plugins' import defaultPlugins from './plugins'
import * as commands from './commands' import * as commands from './commands'

View File

@@ -1,54 +1,113 @@
import cloneDeep from 'clone-deep'
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
import { Editor, Command } from './Editor' import { Editor, CommandSpec } from './Editor'
export default abstract class Extension {
constructor(options = {}) {
this.options = {
...this.defaultOptions(),
...options,
}
}
editor!: Editor
options: { [key: string]: any } = {}
public abstract name: string
public extensionType = 'extension'
public created() {}
public bindEditor(editor: Editor): void {
this.editor = editor
}
defaultOptions(): { [key: string]: any } {
return {}
}
update(): any {
return () => {}
}
plugins(): Plugin[] {
return []
}
inputRules(): any {
return []
}
pasteRules(): any {
return []
}
keys(): { [key: string]: Function } {
return {}
}
commands(): { [key: string]: Command } {
return {}
}
type AnyObject = {
[key: string]: any
}
type NoInfer<T> = [T][T extends any ? 0 : never]
type MergeStrategy = 'extend' | 'overwrite'
type Configs = {
[key: string]: {
stategy: MergeStrategy
value: any
}[]
}
export interface ExtensionCallback<Options> {
name: string
editor: Editor
options: Options
}
export interface ExtensionExtends<Callback, Options> {
name: string
options: Options
commands: (params: Callback) => CommandSpec
inputRules: (params: Callback) => any[]
pasteRules: (params: Callback) => any[]
keys: (params: Callback) => {
[key: string]: Function
}
plugins: (params: Callback) => Plugin[]
}
export default class Extension<
Options = {},
Callback = ExtensionCallback<Options>,
Extends extends ExtensionExtends<Callback, Options> = ExtensionExtends<Callback, Options>
> {
type = 'extension'
config: AnyObject = {}
configs: Configs = {}
options: Partial<Options> = {}
protected storeConfig(key: string, value: any, stategy: MergeStrategy) {
const item = {
stategy,
value,
}
if (this.configs[key]) {
this.configs[key].push(item)
} else {
this.configs[key] = [item]
}
}
public configure(options: Partial<Options>) {
this.options = { ...this.options, ...options }
return this
}
public name(value: Extends['name']) {
this.storeConfig('name', value, 'overwrite')
return this
}
public defaults(value: Options) {
this.storeConfig('defaults', value, 'overwrite')
return this
}
public commands(value: Extends['commands']) {
this.storeConfig('commands', value, 'overwrite')
return this
}
public keys(value: Extends['keys']) {
this.storeConfig('keys', value, 'overwrite')
return this
}
public inputRules(value: Extends['inputRules']) {
this.storeConfig('inputRules', value, 'overwrite')
return this
}
public pasteRules(value: Extends['pasteRules']) {
this.storeConfig('pasteRules', value, 'overwrite')
return this
}
public plugins(value: Extends['plugins']) {
this.storeConfig('plugins', value, 'overwrite')
return this
}
public extend<T extends Extract<keyof Extends, string>>(key: T, value: Extends[T]) {
this.storeConfig(key, value, 'extend')
return this
}
public create() {
type ParentOptions = Options
return <Options = ParentOptions>(options?: Partial<NoInfer<Options>>) => {
return cloneDeep(this, true).configure(options as Options)
}
}
} }

View File

@@ -1,3 +1,4 @@
import deepmerge from 'deepmerge'
import collect from 'collect.js' import collect from 'collect.js'
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
import { keymap } from 'prosemirror-keymap' import { keymap } from 'prosemirror-keymap'
@@ -5,6 +6,7 @@ import { inputRules } from 'prosemirror-inputrules'
import { EditorView, Decoration } from 'prosemirror-view' import { EditorView, Decoration } from 'prosemirror-view'
import { Node as ProsemirrorNode } from 'prosemirror-model' import { Node as ProsemirrorNode } from 'prosemirror-model'
import { Editor } from './Editor' import { Editor } from './Editor'
import Extension from './Extension'
import Node from './Node' import Node from './Node'
import Mark from './Mark' import Mark from './Mark'
import capitalize from './utils/capitalize' import capitalize from './utils/capitalize'
@@ -21,30 +23,116 @@ export default class ExtensionManager {
constructor(extensions: Extensions, editor: Editor) { constructor(extensions: Extensions, editor: Editor) {
this.editor = editor this.editor = editor
this.extensions = extensions this.extensions = extensions
this.extensions.forEach(extension => { this.extensions.forEach(extension => {
extension.bindEditor(editor) this.resolveConfig(extension, 'name')
this.resolveConfig(extension, 'defaults')
this.resolveConfig(extension, 'topNode')
this.resolveConfig(extension, 'schema', ['name', 'options'])
editor.on('schemaCreated', () => { editor.on('schemaCreated', () => {
this.editor.registerCommands(extension.commands()) this.resolveConfig(extension, 'commands', ['name', 'options', 'editor', 'type'])
extension.created() this.resolveConfig(extension, 'inputRules', ['name', 'options', 'editor', 'type'])
this.resolveConfig(extension, 'pasteRules', ['name', 'options', 'editor', 'type'])
this.resolveConfig(extension, 'keys', ['name', 'options', 'editor', 'type'])
this.resolveConfig(extension, 'plugins', ['name', 'options', 'editor', 'type'])
if (extension.config.commands) {
this.editor.registerCommands(extension.config.commands)
}
}) })
}) })
} }
get topNode() { resolveConfig(
return getTopNodeFromExtensions(this.extensions) extension: Extension | Node | Mark,
name: string,
propValues: ('name' | 'options' | 'editor' | 'type')[] = []
) {
if (!extension.configs[name]) {
return
}
extension.config[name] = extension.configs[name]
.reduce((accumulator, { stategy, value: rawValue }) => {
const props: any = {}
if (propValues.includes('name')) {
props.name = extension.config.name
}
if (propValues.includes('options')) {
props.options = deepmerge(extension.config.defaults, extension.options)
}
if (propValues.includes('editor')) {
props.editor = this.editor
}
if (propValues.includes('type')) {
props.type = extension.type === 'node'
? this.editor.schema.nodes[extension.config.name]
: this.editor.schema.marks[extension.config.name]
}
const value = typeof rawValue === 'function'
? rawValue(props)
: rawValue
if (accumulator === undefined) {
return value
}
if (stategy === 'overwrite') {
return value
}
if (stategy === 'extend') {
return deepmerge(accumulator, value)
}
return accumulator
}, undefined)
}
// get topNode() {
// return getTopNodeFromExtensions(this.extensions)
// }
// get nodes(): any {
// return getNodesFromExtensions(this.extensions)
// }
// get marks(): any {
// return getMarksFromExtensions(this.extensions)
// }
get topNode(): any {
const topNode = collect(this.extensions).firstWhere('config.topNode', true)
if (topNode) {
return topNode.config.name
}
} }
get nodes(): any { get nodes(): any {
return getNodesFromExtensions(this.extensions) return collect(this.extensions)
.where('type', 'node')
.mapWithKeys((extension: Node) => [extension.config.name, extension.config.schema])
.all()
} }
get marks(): any { get marks(): any {
return getMarksFromExtensions(this.extensions) return collect(this.extensions)
.where('type', 'mark')
.mapWithKeys((extension: Mark) => [extension.config.name, extension.config.schema])
.all()
} }
get plugins(): Plugin[] { get plugins(): Plugin[] {
const plugins = collect(this.extensions) const plugins = collect(this.extensions)
.flatMap(extension => extension.plugins()) .flatMap(extension => extension.config.plugins)
.filter(plugin => plugin)
.toArray() .toArray()
return [ return [
@@ -57,54 +145,57 @@ export default class ExtensionManager {
get inputRules(): any { get inputRules(): any {
return collect(this.extensions) return collect(this.extensions)
.flatMap(extension => extension.inputRules()) .flatMap(extension => extension.config.inputRules)
.filter(plugin => plugin)
.toArray() .toArray()
} }
get pasteRules(): any { get pasteRules(): any {
return collect(this.extensions) return collect(this.extensions)
.flatMap(extension => extension.pasteRules()) .flatMap(extension => extension.config.pasteRules)
.filter(plugin => plugin)
.toArray() .toArray()
} }
get keymaps() { get keymaps() {
return collect(this.extensions) return collect(this.extensions)
.map(extension => extension.keys()) .map(extension => extension.config.keys)
.filter(keys => !!Object.keys(keys).length) .filter(keys => keys)
// @ts-ignore
.map(keys => keymap(keys)) .map(keys => keymap(keys))
.toArray() .toArray()
} }
get nodeViews() { get nodeViews() {
const { renderer: Renderer } = this.editor // const { renderer: Renderer } = this.editor
// if (!Renderer || !Renderer.type) {
// return {}
// }
// const prop = `to${capitalize(Renderer.type)}`
// return collect(this.extensions)
// .where('extensionType', 'node')
// .filter((extension: any) => extension.schema()[prop])
// .map((extension: any) => {
// return (
// node: ProsemirrorNode,
// view: EditorView,
// getPos: (() => number) | boolean,
// decorations: Decoration[],
// ) => {
// return new Renderer(extension.schema()[prop], {
// extension,
// editor: this.editor,
// node,
// getPos,
// decorations,
// })
// }
// })
// .all()
if (!Renderer || !Renderer.type) {
return {} return {}
} }
const prop = `to${capitalize(Renderer.type)}`
return collect(this.extensions)
.where('extensionType', 'node')
.filter((extension: any) => extension.schema()[prop])
.map((extension: any) => {
return (
node: ProsemirrorNode,
view: EditorView,
getPos: (() => number) | boolean,
decorations: Decoration[],
) => {
return new Renderer(extension.schema()[prop], {
extension,
editor: this.editor,
node,
getPos,
decorations,
})
}
})
.all()
}
} }

View File

@@ -1,18 +1,28 @@
import Extension from './Extension' import { MarkSpec, MarkType } from 'prosemirror-model'
import { MarkSpec } from 'prosemirror-model' import Extension, { ExtensionCallback, ExtensionExtends } from './Extension'
import { Editor } from './Editor'
export default abstract class Mark extends Extension {
constructor(options = {}) {
super(options)
}
public extensionType = 'mark'
abstract schema(): MarkSpec
get type() {
return this.editor.schema.marks[this.name]
}
export interface MarkCallback<Options> {
name: string
editor: Editor
options: Options
type: MarkType
}
export interface MarkExtends<Callback, Options> extends ExtensionExtends<Callback, Options> {
topMark: boolean
schema: (params: Omit<Callback, 'type' | 'editor'>) => MarkSpec
}
export default class Mark<
Options = {},
Callback = MarkCallback<Options>,
Extends extends MarkExtends<Callback, Options> = MarkExtends<Callback, Options>
> extends Extension<Options, Callback, Extends> {
type = 'mark'
public schema(value: Extends['schema']) {
this.storeConfig('schema', value, 'overwrite')
return this
}
} }

View File

@@ -1,20 +1,33 @@
import Extension from './Extension' import { NodeSpec, NodeType } from 'prosemirror-model'
import { NodeSpec } from 'prosemirror-model' import Extension, { ExtensionCallback, ExtensionExtends } from './Extension'
import { Editor } from './Editor'
export default abstract class Node extends Extension {
constructor(options = {}) {
super(options)
}
public extensionType = 'node'
public topNode = false
abstract schema(): NodeSpec
get type() {
return this.editor.schema.nodes[this.name]
}
export interface NodeCallback<Options> {
name: string
editor: Editor
options: Options
type: NodeType
}
export interface NodeExtends<Callback, Options> extends ExtensionExtends<Callback, Options> {
topNode: boolean
schema: (params: Omit<Callback, 'type' | 'editor'>) => NodeSpec
}
export default class Node<
Options = {},
Callback = NodeCallback<Options>,
Extends extends NodeExtends<Callback, Options> = NodeExtends<Callback, Options>
> extends Extension<Options, Callback, Extends> {
type = 'node'
public topNode(value: Extends['topNode'] = true) {
this.storeConfig('topNode', value, 'overwrite')
return this
}
public schema(value: Extends['schema']) {
this.storeConfig('schema', value, 'overwrite')
return this
}
} }

View File

@@ -1,6 +1,4 @@
import { Mark, CommandSpec, markInputRule, markPasteRule } from '@tiptap/core' import { Mark, markInputRule, markPasteRule } from '@tiptap/core'
import { MarkSpec } from 'prosemirror-model'
import VerEx from 'verbal-expressions'
declare module '@tiptap/core/src/Editor' { declare module '@tiptap/core/src/Editor' {
interface Editor { interface Editor {
@@ -8,12 +6,14 @@ declare module '@tiptap/core/src/Editor' {
} }
} }
export default class Bold extends Mark { export const starInputRegex = /(?:^|\s)((?:\*\*)((?:[^\*\*]+))(?:\*\*))$/gm
export const starPasteRegex = /(?:^|\s)((?:\*\*)((?:[^\*\*]+))(?:\*\*))/gm
export const underscoreInputRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))$/gm
export const underscorePasteRegex = /(?:^|\s)((?:__)((?:[^__]+))(?:__))/gm
name = 'bold' export default new Mark()
.name('bold')
schema(): MarkSpec { .schema(() => ({
return {
parseDOM: [ parseDOM: [
{ {
tag: 'strong', tag: 'strong',
@@ -28,55 +28,22 @@ export default class Bold extends Mark {
}, },
], ],
toDOM: () => ['strong', 0], toDOM: () => ['strong', 0],
} }))
} .commands(({ editor, name, type }) => ({
commands(): CommandSpec {
return {
bold: next => () => { bold: next => () => {
this.editor.toggleMark(this.name) editor.toggleMark(name)
next() next()
}, },
} }))
} .keys(({ editor }) => ({
'Mod-b': () => editor.bold()
keys() { }))
return { .inputRules(({ type }) => [
'Mod-b': () => this.editor.bold() markInputRule(starInputRegex, type),
} markInputRule(underscoreInputRegex, type),
} ])
.pasteRules(({ type }) => [
inputRules() { markPasteRule(starPasteRegex, type),
return ['**', '__'].map(character => { markPasteRule(underscorePasteRegex, type),
const regex = VerEx() ])
.add('(?:^|\\s)') .create()
.beginCapture()
.find(character)
.beginCapture()
.somethingBut(character)
.endCapture()
.find(character)
.endCapture()
.endOfLine()
return markInputRule(regex, this.type)
})
}
pasteRules() {
return ['**', '__'].map(character => {
const regex = VerEx()
.add('(?:^|\\s)')
.beginCapture()
.find(character)
.beginCapture()
.somethingBut(character)
.endCapture()
.find(character)
.endCapture()
return markPasteRule(regex, this.type)
})
}
}

View File

@@ -1,6 +1,4 @@
import { Mark, markInputRule, markPasteRule, CommandSpec } from '@tiptap/core' import { Mark, markInputRule, markPasteRule } from '@tiptap/core'
import { MarkSpec } from 'prosemirror-model'
import VerEx from 'verbal-expressions'
declare module '@tiptap/core/src/Editor' { declare module '@tiptap/core/src/Editor' {
interface Editor { interface Editor {
@@ -8,62 +6,31 @@ declare module '@tiptap/core/src/Editor' {
} }
} }
export default class Code extends Mark { export const inputRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))$/gm
export const pasteRegex = /(?:^|\s)((?:`)((?:[^`]+))(?:`))/gm
name = 'code' export default new Mark()
.name('code')
schema(): MarkSpec { .schema(() => ({
return {
excludes: '_', excludes: '_',
parseDOM: [ parseDOM: [
{ tag: 'code' }, { tag: 'code' },
], ],
toDOM: () => ['code', 0], toDOM: () => ['code', 0],
} }))
} .commands(({ editor, name }) => ({
commands(): CommandSpec {
return {
code: next => () => { code: next => () => {
this.editor.toggleMark(this.name) editor.toggleMark(name)
next() next()
}, },
} }))
} .keys(({ editor }) => ({
'Mod-`': () => editor.code()
keys() { }))
return { .inputRules(({ type }) => [
'Mod-`': () => this.editor.code() markInputRule(inputRegex, type)
} ])
} .pasteRules(({ type }) => [
markPasteRule(inputRegex, type)
inputRules() { ])
const regex = VerEx() .create()
.add('(?:^|\\s)')
.beginCapture()
.find('`')
.beginCapture()
.somethingBut('`')
.endCapture()
.find('`')
.endCapture()
.endOfLine()
return markInputRule(regex, this.type)
}
pasteRules() {
const regex = VerEx()
.add('(?:^|\\s)')
.beginCapture()
.find('`')
.beginCapture()
.somethingBut('`')
.endCapture()
.find('`')
.endCapture()
return markPasteRule(regex, this.type)
}
}

View File

@@ -1,12 +1,8 @@
import { Node } from '@tiptap/core' import { Node } from '@tiptap/core'
import { NodeSpec } from 'prosemirror-model'
export default class CodeBlock extends Node { export default new Node()
.name('code_block')
name = 'code_block' .schema(() => ({
schema(): NodeSpec {
return {
content: 'text*', content: 'text*',
marks: '', marks: '',
group: 'block', group: 'block',
@@ -17,7 +13,5 @@ export default class CodeBlock extends Node {
{ tag: 'pre', preserveWhitespace: 'full' }, { tag: 'pre', preserveWhitespace: 'full' },
], ],
toDOM: () => ['pre', ['code', 0]], toDOM: () => ['pre', ['code', 0]],
} }))
} .create()
}

View File

@@ -1,16 +1,9 @@
import { Node } from '@tiptap/core' import { Node } from '@tiptap/core'
import { NodeSpec } from 'prosemirror-model'
export default class Document extends Node { export default new Node()
.name('document')
name = 'document' .topNode()
.schema(() => ({
topNode = true
schema(): NodeSpec {
return {
content: 'block+', content: 'block+',
} }))
} .create()
}

View File

@@ -2,32 +2,22 @@ import { Extension } from '@tiptap/core'
import { Plugin } from 'prosemirror-state' import { Plugin } from 'prosemirror-state'
import { DecorationSet, Decoration } from 'prosemirror-view' import { DecorationSet, Decoration } from 'prosemirror-view'
interface FocusOptions { export interface FocusOptions {
className: string, className: string,
nested: boolean, nested: boolean,
} }
export default class Focus extends Extension { export default new Extension<FocusOptions>()
.name('focus')
name = 'focus' .defaults({
constructor(options: Partial<FocusOptions> = {}) {
super(options)
}
defaultOptions(): FocusOptions {
return {
className: 'has-focus', className: 'has-focus',
nested: false, nested: false,
} })
} .plugins(({ editor, options }) => [
plugins() {
return [
new Plugin({ new Plugin({
props: { props: {
decorations: ({ doc, selection }) => { decorations: ({ doc, selection }) => {
const { isEditable, isFocused } = this.editor const { isEditable, isFocused } = editor
const { anchor } = selection const { anchor } = selection
const decorations: Decoration[] = [] const decorations: Decoration[] = []
@@ -40,19 +30,17 @@ export default class Focus extends Extension {
if (hasAnchor && !node.isText) { if (hasAnchor && !node.isText) {
const decoration = Decoration.node(pos, pos + node.nodeSize, { const decoration = Decoration.node(pos, pos + node.nodeSize, {
class: this.options.className, class: options.className,
}) })
decorations.push(decoration) decorations.push(decoration)
} }
return this.options.nested return options.nested
}) })
return DecorationSet.create(doc, decorations) return DecorationSet.create(doc, decorations)
}, },
}, },
}), }),
] ])
} .create()
}

View File

@@ -1,11 +1,9 @@
import { Node, CommandSpec } from '@tiptap/core' import { Node } from '@tiptap/core'
import { NodeSpec } from 'prosemirror-model'
import VerEx from 'verbal-expressions'
import { textblockTypeInputRule } from 'prosemirror-inputrules' import { textblockTypeInputRule } from 'prosemirror-inputrules'
type Level = 1 | 2 | 3 | 4 | 5 | 6 type Level = 1 | 2 | 3 | 4 | 5 | 6
interface HeadingOptions { export interface HeadingOptions {
levels: Level[], levels: Level[],
} }
@@ -15,22 +13,12 @@ declare module '@tiptap/core/src/Editor' {
} }
} }
export default class Heading extends Node { export default new Node<HeadingOptions>()
.name('heading')
name = 'heading' .defaults({
constructor(options: Partial<HeadingOptions> = {}) {
super(options)
}
defaultOptions(): HeadingOptions {
return {
levels: [1, 2, 3, 4, 5, 6], levels: [1, 2, 3, 4, 5, 6],
} })
} .schema(({ options }) => ({
schema(): NodeSpec {
return {
attrs: { attrs: {
level: { level: {
default: 1, default: 1,
@@ -40,35 +28,22 @@ export default class Heading extends Node {
group: 'block', group: 'block',
defining: true, defining: true,
draggable: false, draggable: false,
parseDOM: this.options.levels parseDOM: options.levels
.map((level: Level) => ({ .map((level: Level) => ({
tag: `h${level}`, tag: `h${level}`,
attrs: { level }, attrs: { level },
})), })),
toDOM: node => [`h${node.attrs.level}`, 0], toDOM: node => [`h${node.attrs.level}`, 0],
} }))
} .commands(({ editor, name }) => ({
[name]: next => attrs => {
commands(): CommandSpec { editor.toggleNode(name, 'paragraph', attrs)
return {
heading: next => attrs => {
this.editor.toggleNode(this.name, 'paragraph', attrs)
next() next()
}, },
} }))
} .inputRules(({ options, type }) => {
return options.levels.map((level: Level) => {
inputRules() { return textblockTypeInputRule(new RegExp(`^(#{1,${level}})\\s$`), type, { level })
return this.options.levels.map((level: Level) => {
const regex = VerEx()
.startOfLine()
.find('#')
.repeatPrevious(level)
.whitespace()
.endOfLine()
return textblockTypeInputRule(regex, this.type, { level })
}) })
} })
.create()
}

View File

@@ -1,4 +1,4 @@
import { Extension, CommandSpec } from '@tiptap/core' import { Extension } from '@tiptap/core'
import { import {
history, history,
undo, undo,
@@ -14,26 +14,16 @@ declare module '@tiptap/core/src/Editor' {
} }
} }
interface HistoryOptions { export interface HistoryOptions {
historyPluginOptions?: Object, historyPluginOptions: Object,
} }
export default class History extends Extension { export default new Extension<HistoryOptions>()
.name('history')
name = 'history' .defaults({
constructor(options: Partial<HistoryOptions> = {}) {
super(options)
}
defaultOptions(): HistoryOptions {
return {
historyPluginOptions: {}, historyPluginOptions: {},
} })
} .commands(() => ({
commands(): CommandSpec {
return {
undo: (next, { view }) => () => { undo: (next, { view }) => () => {
undo(view.state, view.dispatch) undo(view.state, view.dispatch)
next() next()
@@ -42,21 +32,13 @@ export default class History extends Extension {
redo(view.state, view.dispatch) redo(view.state, view.dispatch)
next() next()
}, },
} }))
} .keys(({ editor }) => ({
'Mod-z': () => editor.undo(),
keys() { 'Mod-y': () => editor.redo(),
return { 'Shift-Mod-z': () => editor.redo(),
'Mod-z': () => this.editor.undo(), }))
'Mod-y': () => this.editor.redo(), .plugins(({ options }) => [
'Shift-Mod-z': () => this.editor.redo(), history(options.historyPluginOptions)
} ])
} .create()
plugins() {
return [
history(this.options.historyPluginOptions)
]
}
}

View File

@@ -1,6 +1,4 @@
import { Mark, markInputRule, markPasteRule, CommandSpec } from '@tiptap/core' import { Mark, markInputRule, markPasteRule } from '@tiptap/core'
import { MarkSpec } from 'prosemirror-model'
import VerEx from 'verbal-expressions'
declare module '@tiptap/core/src/Editor' { declare module '@tiptap/core/src/Editor' {
interface Editor { interface Editor {
@@ -8,67 +6,36 @@ declare module '@tiptap/core/src/Editor' {
} }
} }
export default class Italic extends Mark { export const starInputRegex = /(?:^|\s)((?:\*)((?:[^\*]+))(?:\*))$/gm
export const starPasteRegex = /(?:^|\s)((?:\*)((?:[^\*]+))(?:\*))/gm
export const underscoreInputRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))$/gm
export const underscorePasteRegex = /(?:^|\s)((?:_)((?:[^_]+))(?:_))/gm
name = 'italic' export default new Mark()
.name('italic')
schema(): MarkSpec { .schema(() => ({
return {
parseDOM: [ parseDOM: [
{ tag: 'i' }, { tag: 'i' },
{ tag: 'em' }, { tag: 'em' },
{ style: 'font-style=italic' }, { style: 'font-style=italic' },
], ],
toDOM: () => ['em', 0], toDOM: () => ['em', 0],
} }))
} .commands(({ editor, name }) => ({
commands(): CommandSpec {
return {
italic: next => () => { italic: next => () => {
this.editor.toggleMark(this.name) editor.toggleMark(name)
next() next()
}, },
} }))
} .keys(({ editor }) => ({
'Mod-i': () => editor.italic()
keys() { }))
return { .inputRules(({ type }) => [
'Mod-i': () => this.editor.italic() markInputRule(starInputRegex, type),
} markInputRule(underscoreInputRegex, type),
} ])
.pasteRules(({ type }) => [
inputRules() { markPasteRule(starPasteRegex, type),
return ['*', '_'].map(character => { markPasteRule(underscorePasteRegex, type),
const regex = VerEx() ])
.add('(?:^|\\s)') .create()
.beginCapture()
.find(character)
.beginCapture()
.somethingBut(character)
.endCapture()
.find(character)
.endCapture()
.endOfLine()
return markInputRule(regex, this.type)
})
}
pasteRules() {
return ['*', '_'].map(character => {
const regex = VerEx()
.add('(?:^|\\s)')
.beginCapture()
.find(character)
.beginCapture()
.somethingBut(character)
.endCapture()
.find(character)
.endCapture()
return markPasteRule(regex, this.type)
})
}
}

View File

@@ -1,19 +1,13 @@
import { Node } from '@tiptap/core' import { Node } from '@tiptap/core'
import { NodeSpec } from 'prosemirror-model'
// import ParagraphComponent from './paragraph.vue' // import ParagraphComponent from './paragraph.vue'
export default class Paragraph extends Node { export default new Node()
.name('paragraph')
name = 'paragraph' .schema(() => ({
schema(): NodeSpec {
return {
content: 'inline*', content: 'inline*',
group: 'block', group: 'block',
parseDOM: [{ tag: 'p' }], parseDOM: [{ tag: 'p' }],
toDOM: () => ['p', 0], toDOM: () => ['p', 0],
// toVue: ParagraphComponent, // toVue: ParagraphComponent,
} }))
} .create()
}

View File

@@ -1,14 +1,8 @@
import { Node } from '@tiptap/core' import { Node } from '@tiptap/core'
import { NodeSpec } from 'prosemirror-model'
export default class Text extends Node { export default new Node()
.name('text')
name = 'text' .schema(() => ({
schema(): NodeSpec {
return {
group: 'inline', group: 'inline',
} }))
} .create()
}

View File

@@ -10,14 +10,14 @@ import Heading from '@tiptap/extension-heading'
export default function defaultExtensions() { export default function defaultExtensions() {
return [ return [
new Document(), Document(),
new History(), History(),
new Paragraph(), Paragraph(),
new Text(), Text(),
new Bold(), Bold(),
new Italic(), Italic(),
new Code(), Code(),
new CodeBlock(), CodeBlock(),
new Heading(), Heading(),
] ]
} }

View File

@@ -12,14 +12,14 @@ import Heading from '@tiptap/extension-heading'
export function defaultExtensions() { export function defaultExtensions() {
return [ return [
new Document(), Document(),
new History(), History(),
new Paragraph(), Paragraph(),
new Text(), Text(),
new Bold(), Bold(),
new Italic(), Italic(),
new Code(), Code(),
new CodeBlock(), CodeBlock(),
new Heading(), Heading(),
] ]
} }

View File

@@ -2259,6 +2259,11 @@
dependencies: dependencies:
defer-to-connect "^1.0.1" defer-to-connect "^1.0.1"
"@types/clone-deep@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/clone-deep/-/clone-deep-4.0.1.tgz#7c488443ab9f571cd343d774551b78e9264ea990"
integrity sha512-bdkCSkyVHsgl3Goe1y16T9k6JuQx7SiDREkq728QjKmTZkGJZuS8R3gGcnGzVuGBP0mssKrzM/GlMOQxtip9cg==
"@types/color-name@^1.1.1": "@types/color-name@^1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
@@ -13694,11 +13699,6 @@ vendors@^1.0.0:
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
verbal-expressions@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/verbal-expressions/-/verbal-expressions-1.0.2.tgz#1f2d28fdcf7169be270777ff5fadcdb2b3b905c5"
integrity sha512-LV8eG4ckcg1iIhGjOF+j1jb0b58m1DgGywce+2U8kbRrB5wZnGe4XCyUyOujZR9D/+rJGXTmxnL30o3zAgmC4w==
verror@1.10.0: verror@1.10.0:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"