diff --git a/.eslintrc.js b/.eslintrc.js
index e6d6392bd2..1729051305 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -31,7 +31,7 @@ module.exports = {
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
- 'plugin:vue/strongly-recommended',
+ 'plugin:vue/vue3-strongly-recommended',
'airbnb-base',
],
rules: {
diff --git a/README.md b/README.md
index 49896d4bad..9b3a8c0903 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# tiptap
+# Tiptap
A headless, framework-agnostic and extendable rich text editor, based on [ProseMirror](https://github.com/ProseMirror/prosemirror).
[![Build Status](https://github.com/ueberdosis/tiptap/workflows/build/badge.svg)](https://github.com/ueberdosis/tiptap/actions)
diff --git a/demos/package.json b/demos/package.json
index c0eb787597..1ecf5bfa27 100644
--- a/demos/package.json
+++ b/demos/package.json
@@ -16,7 +16,7 @@
"y-indexeddb": "^9.0.6",
"y-webrtc": "^10.2.0",
"y-websocket": "^1.3.17",
- "yjs": "^13.5.13"
+ "yjs": "^13.5.16"
},
"devDependencies": {
"@types/uuid": "^8.3.1",
@@ -24,14 +24,14 @@
"@vitejs/plugin-vue": "^1.9.3",
"autoprefixer": "^10.3.7",
"iframe-resizer": "^4.3.2",
- "postcss": "^8.3.9",
+ "postcss": "^8.3.11",
"react": "^17.0.2",
"react-dom": "^17.0.2",
- "sass": "^1.43.2",
+ "sass": "^1.43.3",
"tailwindcss": "^2.2.17",
"typescript": "^4.4.4",
"uuid": "^8.3.2",
- "vite": "^2.6.7",
+ "vite": "^2.6.10",
"vite-plugin-checker": "^0.3.4",
"vue": "^3.0.5",
"vue-router": "^4.0.11"
diff --git a/demos/setup/style.scss b/demos/setup/style.scss
index 0da52a244f..66062a5fc4 100644
--- a/demos/setup/style.scss
+++ b/demos/setup/style.scss
@@ -56,9 +56,17 @@ body {
}
button,
-input {
+input,
+select {
font-size: inherit;
font-family: inherit;
+ color: black;
+ margin: 0.1rem;
+ border: 1px solid black;
+ border-radius: 0.3rem;
+ padding: 0.1rem 0.4rem;
+ background: white;
+ accent-color: black;
}
.ProseMirror:focus {
diff --git a/demos/src/Examples/CollaborativeEditing/React/index.jsx b/demos/src/Examples/CollaborativeEditing/React/index.jsx
index fa541c0ec9..23aa99046e 100644
--- a/demos/src/Examples/CollaborativeEditing/React/index.jsx
+++ b/demos/src/Examples/CollaborativeEditing/React/index.jsx
@@ -16,7 +16,7 @@ import MenuBar from './MenuBar'
import './styles.scss'
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']
-const rooms = ['rooms.7', 'rooms.8', 'rooms.9']
+const rooms = ['rooms.10', 'rooms.11', 'rooms.12']
const names = [
'Lea Thompson',
'Cyndi Lauper',
diff --git a/demos/src/Examples/CollaborativeEditing/Vue/index.vue b/demos/src/Examples/CollaborativeEditing/Vue/index.vue
index f254ca2e74..9bb26298c9 100644
--- a/demos/src/Examples/CollaborativeEditing/Vue/index.vue
+++ b/demos/src/Examples/CollaborativeEditing/Vue/index.vue
@@ -40,9 +40,9 @@ const getRandomElement = list => {
const getRandomRoom = () => {
return getRandomElement([
- 'rooms.7',
- 'rooms.8',
- 'rooms.9',
+ 'rooms.10',
+ 'rooms.11',
+ 'rooms.12',
])
}
diff --git a/demos/src/Examples/Community/React/MentionList.scss b/demos/src/Examples/Community/React/MentionList.scss
index 89d6e650b4..e4637c5a94 100644
--- a/demos/src/Examples/Community/React/MentionList.scss
+++ b/demos/src/Examples/Community/React/MentionList.scss
@@ -14,6 +14,7 @@
.item {
display: block;
+ margin: 0;
width: 100%;
text-align: left;
background: transparent;
diff --git a/demos/src/Examples/Community/React/suggestion.js b/demos/src/Examples/Community/React/suggestion.js
index ba9ad3aee1..5f803ac99c 100644
--- a/demos/src/Examples/Community/React/suggestion.js
+++ b/demos/src/Examples/Community/React/suggestion.js
@@ -3,7 +3,7 @@ import tippy from 'tippy.js'
import { MentionList } from './MentionList'
export default {
- items: query => {
+ items: ({ query }) => {
return [
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
diff --git a/demos/src/Examples/Community/Vue/MentionList.vue b/demos/src/Examples/Community/Vue/MentionList.vue
index 0dedfc3b95..086baf7d7c 100644
--- a/demos/src/Examples/Community/Vue/MentionList.vue
+++ b/demos/src/Examples/Community/Vue/MentionList.vue
@@ -98,6 +98,7 @@ export default {
.item {
display: block;
+ margin: 0;
width: 100%;
text-align: left;
background: transparent;
diff --git a/demos/src/Examples/Community/Vue/suggestion.js b/demos/src/Examples/Community/Vue/suggestion.js
index d1478598c9..3745660dec 100644
--- a/demos/src/Examples/Community/Vue/suggestion.js
+++ b/demos/src/Examples/Community/Vue/suggestion.js
@@ -3,7 +3,7 @@ import tippy from 'tippy.js'
import MentionList from './MentionList.vue'
export default {
- items: query => {
+ items: ({ query }) => {
return [
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
diff --git a/demos/src/Examples/Menus/React/index.spec.js b/demos/src/Examples/Menus/React/index.spec.js
index 9022fab666..ee2f3b0395 100644
--- a/demos/src/Examples/Menus/React/index.spec.js
+++ b/demos/src/Examples/Menus/React/index.spec.js
@@ -1,7 +1,57 @@
-context('/src/Examples/BubbleMenu/React/', () => {
+context('/src/Examples/Menus/React/', () => {
before(() => {
- cy.visit('/src/Examples/BubbleMenu/React/')
+ cy.visit('/src/Examples/Menus/React/')
})
- // TODO: Write tests
+ beforeEach(() => {
+ cy.get('.ProseMirror').then(([{ editor }]) => {
+ editor.chain().focus().clearContent().run()
+ })
+ })
+
+ // TODO: fix test
+ // it('should show menu when the editor is empty', () => {
+ // cy.get('#app')
+ // .find('[data-tippy-root]')
+ // })
+
+ it('should show menu when text is selected', () => {
+ cy.get('.ProseMirror')
+ .type('Test')
+ .type('{selectall}')
+
+ cy.get('#app')
+ .find('[data-tippy-root]')
+ })
+
+ const marks = [
+ {
+ button: 'Bold',
+ tag: 'strong',
+ },
+ {
+ button: 'Italic',
+ tag: 'em',
+ },
+ {
+ button: 'Strike',
+ tag: 's',
+ },
+ ]
+
+ marks.forEach(mark => {
+ it(`should apply ${mark.button} correctly`, () => {
+ cy.get('.ProseMirror')
+ .type('Test')
+ .type('{selectall}')
+
+ cy.get('#app')
+ .find('[data-tippy-root]')
+ .contains(mark.button)
+ .click()
+
+ cy.get('.ProseMirror')
+ .find(`p ${mark.tag}`)
+ })
+ })
})
diff --git a/demos/src/Examples/Menus/Vue/index.spec.js b/demos/src/Examples/Menus/Vue/index.spec.js
index 965e3411e2..c67018118d 100644
--- a/demos/src/Examples/Menus/Vue/index.spec.js
+++ b/demos/src/Examples/Menus/Vue/index.spec.js
@@ -1,7 +1,56 @@
-context('/src/Examples/BubbleMenu/Vue/', () => {
+context('/src/Examples/Menus/Vue/', () => {
before(() => {
- cy.visit('/src/Examples/BubbleMenu/Vue/')
+ cy.visit('/src/Examples/Menus/Vue/')
})
- // TODO: Write tests
+ beforeEach(() => {
+ cy.get('.ProseMirror').then(([{ editor }]) => {
+ editor.chain().focus().clearContent().run()
+ })
+ })
+
+ it('should show menu when the editor is empty', () => {
+ cy.get('#app')
+ .find('[data-tippy-root]')
+ })
+
+ it('should show menu when text is selected', () => {
+ cy.get('.ProseMirror')
+ .type('Test')
+ .type('{selectall}')
+
+ cy.get('#app')
+ .find('[data-tippy-root]')
+ })
+
+ const marks = [
+ {
+ button: 'Bold',
+ tag: 'strong',
+ },
+ {
+ button: 'Italic',
+ tag: 'em',
+ },
+ {
+ button: 'Strike',
+ tag: 's',
+ },
+ ]
+
+ marks.forEach(mark => {
+ it(`should apply ${mark.button} correctly`, () => {
+ cy.get('.ProseMirror')
+ .type('Test')
+ .type('{selectall}')
+
+ cy.get('#app div')
+ .find('[data-tippy-root]')
+ .contains(mark.button)
+ .click()
+
+ cy.get('.ProseMirror')
+ .find(`p ${mark.tag}`)
+ })
+ })
})
diff --git a/demos/src/Examples/Minimal/Vue/index.spec.js b/demos/src/Examples/Minimal/Vue/index.spec.js
index 2b6eeee01b..3227502670 100644
--- a/demos/src/Examples/Minimal/Vue/index.spec.js
+++ b/demos/src/Examples/Minimal/Vue/index.spec.js
@@ -3,5 +3,45 @@ context('/src/Examples/Minimal/Vue/', () => {
cy.visit('/src/Examples/Minimal/Vue/')
})
- // TODO: Write tests
+ beforeEach(() => {
+ cy.get('.ProseMirror').then(([{ editor }]) => {
+ editor.commands.clearContent()
+ })
+ })
+
+ it('text should be wrapped in a paragraph by default', () => {
+ cy.get('.ProseMirror')
+ .type('Example Text')
+ .find('p')
+ .should('contain', 'Example Text')
+ })
+
+ it('should parse paragraphs correctly', () => {
+ cy.get('.ProseMirror').then(([{ editor }]) => {
+ editor.commands.setContent('
Example Text
')
+ expect(editor.getHTML()).to.eq('Example Text
')
+
+ editor.commands.setContent('Example Text
')
+ expect(editor.getHTML()).to.eq('Example Text
')
+ })
+ })
+
+ it('enter should make a new paragraph', () => {
+ cy.get('.ProseMirror')
+ .type('First Paragraph{enter}Second Paragraph')
+ .find('p')
+ .should('have.length', 2)
+ })
+
+ it('backspace should remove the last paragraph', () => {
+ cy.get('.ProseMirror')
+ .type('{enter}')
+ .find('p')
+ .should('have.length', 2)
+
+ cy.get('.ProseMirror')
+ .type('{backspace}')
+ .find('p')
+ .should('have.length', 1)
+ })
})
diff --git a/demos/src/Examples/Savvy/Vue/index.spec.js b/demos/src/Examples/Savvy/Vue/index.spec.js
index e1975a51c1..8f1b4d67bc 100644
--- a/demos/src/Examples/Savvy/Vue/index.spec.js
+++ b/demos/src/Examples/Savvy/Vue/index.spec.js
@@ -3,5 +3,36 @@ context('/src/Examples/Savvy/Vue/', () => {
cy.visit('/src/Examples/Savvy/Vue/')
})
- // TODO: Write tests
+ beforeEach(() => {
+ cy.get('.ProseMirror').then(([{ editor }]) => {
+ editor.commands.clearContent()
+ })
+ })
+
+ const tests = [
+ ['(c)', '©'],
+ ['->', '→'],
+ ['>>', '»'],
+ ['1/2', '½'],
+ ['!=', '≠'],
+ ['--', '—'],
+ ['1x1', '1×1'],
+ [':-) ', '🙂'],
+ ['<3 ', '❤️'],
+ ['>:P ', '😜'],
+ ]
+
+ tests.forEach(test => {
+ it(`should parse ${test[0]} correctly`, () => {
+ cy.get('.ProseMirror')
+ .type(test[0])
+ .should('contain', test[1])
+ })
+ })
+
+ it('should parse hex colors correctly', () => {
+ cy.get('.ProseMirror')
+ .type('#FD9170')
+ .find('.color')
+ })
})
diff --git a/demos/src/Experiments/Commands/Vue/CommandsList.vue b/demos/src/Experiments/Commands/Vue/CommandsList.vue
index e9989c43b5..0082b085b9 100644
--- a/demos/src/Experiments/Commands/Vue/CommandsList.vue
+++ b/demos/src/Experiments/Commands/Vue/CommandsList.vue
@@ -98,6 +98,7 @@ export default {
.item {
display: block;
+ margin: 0;
width: 100%;
text-align: left;
background: transparent;
diff --git a/demos/src/Experiments/Commands/Vue/suggestion.js b/demos/src/Experiments/Commands/Vue/suggestion.js
index 388256d7de..9680693abc 100644
--- a/demos/src/Experiments/Commands/Vue/suggestion.js
+++ b/demos/src/Experiments/Commands/Vue/suggestion.js
@@ -3,7 +3,7 @@ import { VueRenderer } from '@tiptap/vue-3'
import CommandsList from './CommandsList.vue'
export default {
- items: query => {
+ items: ({ query }) => {
return [
{
title: 'H1',
diff --git a/demos/src/Experiments/ExtensionStorage/React/CustomExtension.ts b/demos/src/Experiments/ExtensionStorage/React/CustomExtension.ts
new file mode 100644
index 0000000000..de407ad7e0
--- /dev/null
+++ b/demos/src/Experiments/ExtensionStorage/React/CustomExtension.ts
@@ -0,0 +1,19 @@
+import { Extension } from '@tiptap/core'
+
+type CustomStorage = {
+ foo: number,
+}
+
+export const CustomExtension = Extension.create<{}, CustomStorage>({
+ name: 'custom',
+
+ addStorage() {
+ return {
+ foo: 123,
+ }
+ },
+
+ onUpdate() {
+ this.storage.foo += 1
+ },
+})
diff --git a/demos/src/Experiments/ExtensionStorage/React/index.html b/demos/src/Experiments/ExtensionStorage/React/index.html
new file mode 100644
index 0000000000..538363687b
--- /dev/null
+++ b/demos/src/Experiments/ExtensionStorage/React/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/src/Experiments/ExtensionStorage/React/index.jsx b/demos/src/Experiments/ExtensionStorage/React/index.jsx
new file mode 100644
index 0000000000..3f75a1da2b
--- /dev/null
+++ b/demos/src/Experiments/ExtensionStorage/React/index.jsx
@@ -0,0 +1,33 @@
+import React from 'react'
+import { useEditor, EditorContent } from '@tiptap/react'
+import Document from '@tiptap/extension-document'
+import Paragraph from '@tiptap/extension-paragraph'
+import Text from '@tiptap/extension-text'
+import { CustomExtension } from './CustomExtension'
+import './styles.scss'
+
+export default () => {
+ const editor = useEditor({
+ extensions: [
+ Document,
+ Paragraph,
+ Text,
+ CustomExtension,
+ ],
+ content: `
+
+ This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. That’s it. It’s probably too much for real minimalists though.
+
+
+ The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.
+
+ `,
+ })
+
+ return (
+ <>
+ reactive storage: {editor?.storage.custom.foo}
+
+ >
+ )
+}
diff --git a/demos/src/Experiments/ExtensionStorage/React/styles.scss b/demos/src/Experiments/ExtensionStorage/React/styles.scss
new file mode 100644
index 0000000000..46b51a4e14
--- /dev/null
+++ b/demos/src/Experiments/ExtensionStorage/React/styles.scss
@@ -0,0 +1,6 @@
+/* Basic editor styles */
+.ProseMirror {
+ > * + * {
+ margin-top: 0.75em;
+ }
+}
diff --git a/demos/src/Experiments/ExtensionStorage/Vue/CustomExtension.ts b/demos/src/Experiments/ExtensionStorage/Vue/CustomExtension.ts
new file mode 100644
index 0000000000..de407ad7e0
--- /dev/null
+++ b/demos/src/Experiments/ExtensionStorage/Vue/CustomExtension.ts
@@ -0,0 +1,19 @@
+import { Extension } from '@tiptap/core'
+
+type CustomStorage = {
+ foo: number,
+}
+
+export const CustomExtension = Extension.create<{}, CustomStorage>({
+ name: 'custom',
+
+ addStorage() {
+ return {
+ foo: 123,
+ }
+ },
+
+ onUpdate() {
+ this.storage.foo += 1
+ },
+})
diff --git a/demos/src/Experiments/ExtensionStorage/Vue/index.html b/demos/src/Experiments/ExtensionStorage/Vue/index.html
new file mode 100644
index 0000000000..f0485cddc3
--- /dev/null
+++ b/demos/src/Experiments/ExtensionStorage/Vue/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/src/Experiments/ExtensionStorage/Vue/index.vue b/demos/src/Experiments/ExtensionStorage/Vue/index.vue
new file mode 100644
index 0000000000..a09406c4f7
--- /dev/null
+++ b/demos/src/Experiments/ExtensionStorage/Vue/index.vue
@@ -0,0 +1,56 @@
+
+ reactive storage: {{ editor?.storage.custom.foo }}
+
+
+
+
+
+
diff --git a/demos/src/Nodes/Mention/Vue/MentionList.vue b/demos/src/Nodes/Mention/Vue/MentionList.vue
index 0dedfc3b95..086baf7d7c 100644
--- a/demos/src/Nodes/Mention/Vue/MentionList.vue
+++ b/demos/src/Nodes/Mention/Vue/MentionList.vue
@@ -98,6 +98,7 @@ export default {
.item {
display: block;
+ margin: 0;
width: 100%;
text-align: left;
background: transparent;
diff --git a/demos/src/Nodes/Mention/Vue/suggestion.js b/demos/src/Nodes/Mention/Vue/suggestion.js
index d1478598c9..3745660dec 100644
--- a/demos/src/Nodes/Mention/Vue/suggestion.js
+++ b/demos/src/Nodes/Mention/Vue/suggestion.js
@@ -3,7 +3,7 @@ import tippy from 'tippy.js'
import MentionList from './MentionList.vue'
export default {
- items: query => {
+ items: ({ query }) => {
return [
'Lea Thompson', 'Cyndi Lauper', 'Tom Cruise', 'Madonna', 'Jerry Hall', 'Joan Collins', 'Winona Ryder', 'Christina Applegate', 'Alyssa Milano', 'Molly Ringwald', 'Ally Sheedy', 'Debbie Harry', 'Olivia Newton-John', 'Elton John', 'Michael J. Fox', 'Axl Rose', 'Emilio Estevez', 'Ralph Macchio', 'Rob Lowe', 'Jennifer Grey', 'Mickey Rourke', 'John Cusack', 'Matthew Broderick', 'Justine Bateman', 'Lisa Bonet',
].filter(item => item.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5)
diff --git a/docs/api/nodes/mention.md b/docs/api/nodes/mention.md
index 8d52ce3d49..eee7169a04 100644
--- a/docs/api/nodes/mention.md
+++ b/docs/api/nodes/mention.md
@@ -31,51 +31,41 @@ npm install tippy.js
yarn add tippy.js
```
-## Rendering
-Currently, we’re supporting custom Vue.js components only. To get the required `VueRenderer` install our Vue.js package:
+## Settings
-```bash
-# with npm
-npm install @tiptap/vue-2
+### HTMLAttributes
+Custom HTML attributes that should be added to the rendered HTML tag.
-# with Yarn
-yarn add @tiptap/vue-2
-```
-If you are using `vue-3` then the `VueRenderer` requires different input:
```js
-new VueRenderer(MentionList, {
- props: props,
- editor: this.editor,
+Mention.configure({
+ HTMLAttributes: {
+ class: 'my-custom-class',
+ },
})
```
-and not
+
+### renderLabel
+Define how a mention label should be rendered.
+
```js
-new VueRenderer(MentionList, {
- parent: this,
- propsData: props,
+Mention.configure({
+ renderLabel({ options, node }) {
+ return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`
+ }
})
```
-And yes, we plan to support React, too. Meanwhile, you can roll your own `ReactRenderer`, but don’t forget to share it with the community.
-
-It’s also possible to use Vanilla JavaScript, but that is probably a lot more work.
-
-## Settings
-
-### HTMLAttributes
-Custom HTML attributes that should be added to the rendered HTML tag.
+### suggestion
+[Read more](/api/utilities/suggestion)
```js
Mention.configure({
- HTMLAttributes: {
- class: 'my-custom-class',
+ suggestion: {
+ // …
},
})
```
-| renderLabel | `String` | `({ options, node }) => …` | Define how a mention label should be rendered. |
-| suggestion | `Object` | `{ … }` | [Read more](/api/utilities/suggestion) |
-
## Source code
[packages/extension-mention/](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-mention/)
diff --git a/docs/api/utilities/suggestion.md b/docs/api/utilities/suggestion.md
index 92a16fa937..3e0cac1bea 100644
--- a/docs/api/utilities/suggestion.md
+++ b/docs/api/utilities/suggestion.md
@@ -44,7 +44,7 @@ Default: `() => {}'`
### items
Pass an array of filtered suggestions, can be async.
-Default: `() => {}`
+Default: `({ editor, query }) => []`
### render
A render function for the autocomplete popup.
diff --git a/docs/guide/custom-extensions.md b/docs/guide/custom-extensions.md
index e5e0a8fba7..eeb2e2102e 100644
--- a/docs/guide/custom-extensions.md
+++ b/docs/guide/custom-extensions.md
@@ -78,6 +78,39 @@ const CustomHeading = Heading.extend({
})
```
+### Storage
+At some point you probably want to save some data within your extension instance. This data is mutable. You can access it within the extension under `this.storage`.
+
+```js
+import { Extension } from '@tiptap/core'
+
+const CustomExtension = Extension.create({
+ name: 'customExtension',
+
+ addStorage() {
+ return {
+ awesomeness: 100,
+ }
+ },
+
+ onUpdate() {
+ this.storage.awesomeness += 1
+ },
+})
+```
+
+Outside the extension you have access via `editor.storage`. Make sure that each extension has a unique name.
+
+```js
+const editor = new Editor({
+ extensions: [
+ CustomExtension,
+ ],
+})
+
+const awesomeness = editor.storage.customExtension.awesomeness
+```
+
### Schema
tiptap works with a strict schema, which configures how the content can be structured, nested, how it behaves and many more things. You [can change all aspects of the schema](/api/schema) for existing extensions. Let’s walk through a few common use cases.
diff --git a/docs/guide/typescript.md b/docs/guide/typescript.md
index 5eb9c6aa48..c329a35448 100644
--- a/docs/guide/typescript.md
+++ b/docs/guide/typescript.md
@@ -32,6 +32,25 @@ const CustomExtension = Extension.create({
})
```
+### Storage types
+To add types for your extension storage, you’ll have to pass that as a second type parameter.
+
+```ts
+import { Extension } from '@tiptap/core'
+
+export interface CustomExtensionStorage {
+ awesomeness: number,
+}
+
+const CustomExtension = Extension.create<{}, CustomExtensionStorage>({
+ addStorage() {
+ return {
+ awesomeness: 100,
+ }
+ },
+})
+```
+
### Command type
The core package also exports a `Command` type, which needs to be added to all commands that you specify in your code. Here is an example:
diff --git a/package.json b/package.json
index 3d62af83a3..608c13d536 100644
--- a/package.json
+++ b/package.json
@@ -34,26 +34,26 @@
"@lerna/filter-packages": "^4.0.0",
"@lerna/project": "^4.0.0",
"@rollup/plugin-babel": "^5.3.0",
- "@rollup/plugin-commonjs": "^21.0.0",
- "@rollup/plugin-node-resolve": "^13.0.5",
- "@typescript-eslint/eslint-plugin": "^5.0.0",
- "@typescript-eslint/parser": "^5.0.0",
- "babel-loader": "^8.2.2",
+ "@rollup/plugin-commonjs": "^21.0.1",
+ "@rollup/plugin-node-resolve": "^13.0.6",
+ "@typescript-eslint/eslint-plugin": "^5.1.0",
+ "@typescript-eslint/parser": "^5.1.0",
+ "babel-loader": "^8.2.3",
"cypress": "^8.6.0",
- "eslint": "^8.0.1",
+ "eslint": "^8.1.0",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-import": "^2.25.2",
- "eslint-plugin-vue": "^7.19.1",
+ "eslint-plugin-vue": "^7.20.0",
"lerna": "^4.0.0",
"minimist": "^1.2.5",
- "rollup": "^2.58.0",
+ "rollup": "^2.58.1",
"rollup-plugin-auto-external": "^2.0.0",
"rollup-plugin-sourcemaps": "^0.6.3",
"rollup-plugin-typescript2": "^0.30.0",
"ts-loader": "^9.2.6",
"typescript": "^4.4.4",
- "webpack": "^5.58.2"
+ "webpack": "^5.59.1"
}
}
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 5d12285820..17dfa55431 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -3,6 +3,45 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+# [2.0.0-beta.128](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.127...@tiptap/core@2.0.0-beta.128) (2021-10-25)
+
+
+### Bug Fixes
+
+* fix storage context when using configure ([ef254ce](https://github.com/ueberdosis/tiptap/commit/ef254cead7b9be052ec0211849fb78ae577095dd))
+
+
+
+
+
+# [2.0.0-beta.127](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.126...@tiptap/core@2.0.0-beta.127) (2021-10-22)
+
+
+### Bug Fixes
+
+* improve default styling for .ProseMirror-separator ([0e94afe](https://github.com/ueberdosis/tiptap/commit/0e94afe42a5c15a47698152b3bc88e6bc4f8c01f))
+* Separate drags from drops in stopEvent ([#2070](https://github.com/ueberdosis/tiptap/issues/2070)) ([bebaa40](https://github.com/ueberdosis/tiptap/commit/bebaa4045e6be2e59d1b9c2e1f61f088a47fdf1b))
+
+
+
+
+
+# [2.0.0-beta.126](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.125...@tiptap/core@2.0.0-beta.126) (2021-10-22)
+
+
+### Bug Fixes
+
+* fix a bug where paste rules doesn’t worked at the start of the document, see [#1225](https://github.com/ueberdosis/tiptap/issues/1225) ([ff67ee1](https://github.com/ueberdosis/tiptap/commit/ff67ee1da380d8308e85fa4b0386ea6947ec7ff1))
+
+
+### Features
+
+* Add extension storage ([#2069](https://github.com/ueberdosis/tiptap/issues/2069)) ([7ffabf2](https://github.com/ueberdosis/tiptap/commit/7ffabf251c408a652eec1931cc78a8bd43cccb67))
+
+
+
+
+
# [2.0.0-beta.125](https://github.com/ueberdosis/tiptap/compare/@tiptap/core@2.0.0-beta.124...@tiptap/core@2.0.0-beta.125) (2021-10-14)
diff --git a/packages/core/package.json b/packages/core/package.json
index 81326f568c..82ba69e299 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,7 +1,7 @@
{
"name": "@tiptap/core",
"description": "headless rich text editor",
- "version": "2.0.0-beta.125",
+ "version": "2.0.0-beta.128",
"homepage": "https://tiptap.dev",
"keywords": [
"tiptap",
@@ -33,7 +33,7 @@
"@types/prosemirror-view": "^1.19.1",
"prosemirror-commands": "^1.1.11",
"prosemirror-keymap": "^1.1.3",
- "prosemirror-model": "^1.14.3",
+ "prosemirror-model": "^1.15.0",
"prosemirror-schema-list": "^1.1.6",
"prosemirror-state": "^1.3.4",
"prosemirror-transform": "^1.3.3",
diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts
index 8ad0bf6998..4788471b2f 100644
--- a/packages/core/src/Editor.ts
+++ b/packages/core/src/Editor.ts
@@ -50,6 +50,8 @@ export class Editor extends EventEmitter {
public isFocused = false
+ public extensionStorage: Record = {}
+
public options: EditorOptions = {
element: document.createElement('div'),
content: '',
@@ -100,6 +102,13 @@ export class Editor extends EventEmitter {
}, 0)
}
+ /**
+ * Returns the editor storage.
+ */
+ public get storage(): Record {
+ return this.extensionStorage
+ }
+
/**
* An object of all registered commands.
*/
diff --git a/packages/core/src/Extension.ts b/packages/core/src/Extension.ts
index 55ce6f21bc..51199d029a 100644
--- a/packages/core/src/Extension.ts
+++ b/packages/core/src/Extension.ts
@@ -5,7 +5,10 @@ import { Editor } from './Editor'
import { Node } from './Node'
import { Mark } from './Mark'
import mergeDeep from './utilities/mergeDeep'
+import callOrReturn from './utilities/callOrReturn'
+import getExtensionField from './helpers/getExtensionField'
import {
+ AnyConfig,
Extensions,
GlobalAttributes,
RawCommands,
@@ -15,7 +18,7 @@ import {
import { ExtensionConfig } from '.'
declare module '@tiptap/core' {
- interface ExtensionConfig {
+ interface ExtensionConfig {
[key: string]: any;
/**
@@ -33,13 +36,23 @@ declare module '@tiptap/core' {
*/
defaultOptions?: Options,
+ /**
+ * Default Storage
+ */
+ addStorage?: (this: {
+ name: string,
+ options: Options,
+ parent: ParentConfig>['addGlobalAttributes'],
+ }) => Storage,
+
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
- parent: ParentConfig>['addGlobalAttributes'],
+ storage: Storage,
+ parent: ParentConfig>['addGlobalAttributes'],
}) => GlobalAttributes | {},
/**
@@ -48,8 +61,9 @@ declare module '@tiptap/core' {
addCommands?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['addCommands'],
+ parent: ParentConfig>['addCommands'],
}) => Partial,
/**
@@ -58,8 +72,9 @@ declare module '@tiptap/core' {
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['addKeyboardShortcuts'],
+ parent: ParentConfig>['addKeyboardShortcuts'],
}) => {
[key: string]: KeyboardShortcutCommand,
},
@@ -70,8 +85,9 @@ declare module '@tiptap/core' {
addInputRules?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['addInputRules'],
+ parent: ParentConfig>['addInputRules'],
}) => InputRule[],
/**
@@ -80,8 +96,9 @@ declare module '@tiptap/core' {
addPasteRules?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['addPasteRules'],
+ parent: ParentConfig>['addPasteRules'],
}) => PasteRule[],
/**
@@ -90,8 +107,9 @@ declare module '@tiptap/core' {
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['addProseMirrorPlugins'],
+ parent: ParentConfig>['addProseMirrorPlugins'],
}) => Plugin[],
/**
@@ -100,7 +118,8 @@ declare module '@tiptap/core' {
addExtensions?: (this: {
name: string,
options: Options,
- parent: ParentConfig>['addExtensions'],
+ storage: Storage,
+ parent: ParentConfig>['addExtensions'],
}) => Extensions,
/**
@@ -110,7 +129,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['extendNodeSchema'],
+ storage: Storage,
+ parent: ParentConfig>['extendNodeSchema'],
},
extension: Node,
) => Record) | null,
@@ -122,7 +142,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['extendMarkSchema'],
+ storage: Storage,
+ parent: ParentConfig>['extendMarkSchema'],
},
extension: Mark,
) => Record) | null,
@@ -133,8 +154,9 @@ declare module '@tiptap/core' {
onBeforeCreate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['onBeforeCreate'],
+ parent: ParentConfig>['onBeforeCreate'],
}) => void) | null,
/**
@@ -143,8 +165,9 @@ declare module '@tiptap/core' {
onCreate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['onCreate'],
+ parent: ParentConfig>['onCreate'],
}) => void) | null,
/**
@@ -153,8 +176,9 @@ declare module '@tiptap/core' {
onUpdate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['onUpdate'],
+ parent: ParentConfig>['onUpdate'],
}) => void) | null,
/**
@@ -163,8 +187,9 @@ declare module '@tiptap/core' {
onSelectionUpdate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['onSelectionUpdate'],
+ parent: ParentConfig>['onSelectionUpdate'],
}) => void) | null,
/**
@@ -174,8 +199,9 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['onTransaction'],
+ parent: ParentConfig>['onTransaction'],
},
props: {
transaction: Transaction,
@@ -189,8 +215,9 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['onFocus'],
+ parent: ParentConfig>['onFocus'],
},
props: {
event: FocusEvent,
@@ -204,8 +231,9 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['onBlur'],
+ parent: ParentConfig>['onBlur'],
},
props: {
event: FocusEvent,
@@ -218,13 +246,14 @@ declare module '@tiptap/core' {
onDestroy?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
- parent: ParentConfig>['onDestroy'],
+ parent: ParentConfig>['onDestroy'],
}) => void) | null,
}
}
-export class Extension {
+export class Extension {
type = 'extension'
name = 'extension'
@@ -235,12 +264,14 @@ export class Extension {
options: Options
+ storage: Storage
+
config: ExtensionConfig = {
name: this.name,
defaultOptions: {},
}
- constructor(config: Partial> = {}) {
+ constructor(config: Partial> = {}) {
this.config = {
...this.config,
...config,
@@ -248,10 +279,18 @@ export class Extension {
this.name = this.config.name
this.options = this.config.defaultOptions
+ this.storage = callOrReturn(getExtensionField(
+ this,
+ 'addStorage',
+ {
+ name: this.name,
+ options: this.options,
+ },
+ ))
}
- static create(config: Partial> = {}) {
- return new Extension(config)
+ static create(config: Partial> = {}) {
+ return new Extension(config)
}
configure(options: Partial = {}) {
@@ -261,11 +300,20 @@ export class Extension {
extension.options = mergeDeep(this.options, options) as Options
+ extension.storage = callOrReturn(getExtensionField(
+ extension,
+ 'addStorage',
+ {
+ name: extension.name,
+ options: extension.options,
+ },
+ ))
+
return extension
}
- extend(extendedConfig: Partial> = {}) {
- const extension = new Extension(extendedConfig)
+ extend(extendedConfig: Partial> = {}) {
+ const extension = new Extension(extendedConfig)
extension.parent = this
@@ -279,6 +327,15 @@ export class Extension {
? extendedConfig.defaultOptions
: extension.parent.options
+ extension.storage = callOrReturn(getExtensionField(
+ extension,
+ 'addStorage',
+ {
+ name: extension.name,
+ options: extension.options,
+ },
+ ))
+
return extension
}
}
diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts
index 0056179d86..afd5c0a7ad 100644
--- a/packages/core/src/ExtensionManager.ts
+++ b/packages/core/src/ExtensionManager.ts
@@ -14,6 +14,7 @@ import splitExtensions from './helpers/splitExtensions'
import getAttributesFromExtensions from './helpers/getAttributesFromExtensions'
import getRenderedAttributes from './helpers/getRenderedAttributes'
import callOrReturn from './utilities/callOrReturn'
+import findDuplicates from './utilities/findDuplicates'
import { NodeConfig } from '.'
export default class ExtensionManager {
@@ -32,9 +33,13 @@ export default class ExtensionManager {
this.schema = getSchemaByResolvedExtensions(this.extensions)
this.extensions.forEach(extension => {
+ // store extension storage in editor
+ this.editor.extensionStorage[extension.name] = extension.storage
+
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
editor: this.editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
@@ -130,7 +135,14 @@ export default class ExtensionManager {
}
static resolve(extensions: Extensions): Extensions {
- return ExtensionManager.sort(ExtensionManager.flatten(extensions))
+ const resolvedExtensions = ExtensionManager.sort(ExtensionManager.flatten(extensions))
+ const duplicatedNames = findDuplicates(resolvedExtensions.map(extension => extension.name))
+
+ if (duplicatedNames.length) {
+ console.warn(`[tiptap warn]: Duplicate extension names found: [${duplicatedNames.map(item => `'${item}'`).join(', ')}]. This can lead to issues.`)
+ }
+
+ return resolvedExtensions
}
static flatten(extensions: Extensions): Extensions {
@@ -139,6 +151,7 @@ export default class ExtensionManager {
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
}
const addExtensions = getExtensionField(
@@ -184,6 +197,7 @@ export default class ExtensionManager {
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
editor: this.editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
@@ -223,6 +237,7 @@ export default class ExtensionManager {
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
editor,
type: getSchemaTypeByName(extension.name, this.schema),
}
@@ -313,6 +328,7 @@ export default class ExtensionManager {
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
editor,
type: getNodeType(extension.name, this.schema),
}
diff --git a/packages/core/src/Mark.ts b/packages/core/src/Mark.ts
index 8c8a2cbf7f..0947f69f08 100644
--- a/packages/core/src/Mark.ts
+++ b/packages/core/src/Mark.ts
@@ -8,7 +8,10 @@ import { Plugin, Transaction } from 'prosemirror-state'
import { InputRule } from './InputRule'
import { PasteRule } from './PasteRule'
import mergeDeep from './utilities/mergeDeep'
+import callOrReturn from './utilities/callOrReturn'
+import getExtensionField from './helpers/getExtensionField'
import {
+ AnyConfig,
Extensions,
Attributes,
RawCommands,
@@ -21,7 +24,7 @@ import { MarkConfig } from '.'
import { Editor } from './Editor'
declare module '@tiptap/core' {
- export interface MarkConfig {
+ export interface MarkConfig {
[key: string]: any;
/**
@@ -39,13 +42,23 @@ declare module '@tiptap/core' {
*/
defaultOptions?: Options,
+ /**
+ * Default Storage
+ */
+ addStorage?: (this: {
+ name: string,
+ options: Options,
+ parent: ParentConfig>['addGlobalAttributes'],
+ }) => Storage,
+
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
- parent: ParentConfig>['addGlobalAttributes'],
+ storage: Storage,
+ parent: ParentConfig>['addGlobalAttributes'],
}) => GlobalAttributes | {},
/**
@@ -54,9 +67,10 @@ declare module '@tiptap/core' {
addCommands?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['addCommands'],
+ parent: ParentConfig>['addCommands'],
}) => Partial,
/**
@@ -65,9 +79,10 @@ declare module '@tiptap/core' {
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['addKeyboardShortcuts'],
+ parent: ParentConfig>['addKeyboardShortcuts'],
}) => {
[key: string]: KeyboardShortcutCommand,
},
@@ -78,9 +93,10 @@ declare module '@tiptap/core' {
addInputRules?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['addInputRules'],
+ parent: ParentConfig>['addInputRules'],
}) => InputRule[],
/**
@@ -89,9 +105,10 @@ declare module '@tiptap/core' {
addPasteRules?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['addPasteRules'],
+ parent: ParentConfig>['addPasteRules'],
}) => PasteRule[],
/**
@@ -100,9 +117,10 @@ declare module '@tiptap/core' {
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['addProseMirrorPlugins'],
+ parent: ParentConfig>['addProseMirrorPlugins'],
}) => Plugin[],
/**
@@ -111,7 +129,8 @@ declare module '@tiptap/core' {
addExtensions?: (this: {
name: string,
options: Options,
- parent: ParentConfig>['addExtensions'],
+ storage: Storage,
+ parent: ParentConfig>['addExtensions'],
}) => Extensions,
/**
@@ -121,7 +140,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['extendNodeSchema'],
+ storage: Storage,
+ parent: ParentConfig>['extendNodeSchema'],
},
extension: Node,
) => Record) | null,
@@ -133,7 +153,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['extendMarkSchema'],
+ storage: Storage,
+ parent: ParentConfig>['extendMarkSchema'],
},
extension: Mark,
) => Record) | null,
@@ -144,9 +165,10 @@ declare module '@tiptap/core' {
onBeforeCreate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['onBeforeCreate'],
+ parent: ParentConfig>['onBeforeCreate'],
}) => void) | null,
/**
@@ -155,9 +177,10 @@ declare module '@tiptap/core' {
onCreate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['onCreate'],
+ parent: ParentConfig>['onCreate'],
}) => void) | null,
/**
@@ -166,9 +189,10 @@ declare module '@tiptap/core' {
onUpdate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['onUpdate'],
+ parent: ParentConfig>['onUpdate'],
}) => void) | null,
/**
@@ -177,9 +201,10 @@ declare module '@tiptap/core' {
onSelectionUpdate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['onSelectionUpdate'],
+ parent: ParentConfig>['onSelectionUpdate'],
}) => void) | null,
/**
@@ -189,9 +214,10 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['onTransaction'],
+ parent: ParentConfig>['onTransaction'],
},
props: {
transaction: Transaction,
@@ -205,9 +231,10 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['onFocus'],
+ parent: ParentConfig>['onFocus'],
},
props: {
event: FocusEvent,
@@ -221,9 +248,10 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['onBlur'],
+ parent: ParentConfig>['onBlur'],
},
props: {
event: FocusEvent,
@@ -236,9 +264,10 @@ declare module '@tiptap/core' {
onDestroy?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: MarkType,
- parent: ParentConfig>['onDestroy'],
+ parent: ParentConfig>['onDestroy'],
}) => void) | null,
/**
@@ -252,7 +281,8 @@ declare module '@tiptap/core' {
inclusive?: MarkSpec['inclusive'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['inclusive'],
+ storage: Storage,
+ parent: ParentConfig>['inclusive'],
}) => MarkSpec['inclusive']),
/**
@@ -261,7 +291,8 @@ declare module '@tiptap/core' {
excludes?: MarkSpec['excludes'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['excludes'],
+ storage: Storage,
+ parent: ParentConfig>['excludes'],
}) => MarkSpec['excludes']),
/**
@@ -270,7 +301,8 @@ declare module '@tiptap/core' {
group?: MarkSpec['group'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['group'],
+ storage: Storage,
+ parent: ParentConfig>['group'],
}) => MarkSpec['group']),
/**
@@ -279,7 +311,8 @@ declare module '@tiptap/core' {
spanning?: MarkSpec['spanning'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['spanning'],
+ storage: Storage,
+ parent: ParentConfig>['spanning'],
}) => MarkSpec['spanning']),
/**
@@ -288,7 +321,8 @@ declare module '@tiptap/core' {
code?: boolean | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['code'],
+ storage: Storage,
+ parent: ParentConfig>['code'],
}) => boolean),
/**
@@ -298,7 +332,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['parseHTML'],
+ storage: Storage,
+ parent: ParentConfig>['parseHTML'],
},
) => MarkSpec['parseDOM'],
@@ -309,7 +344,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['renderHTML'],
+ storage: Storage,
+ parent: ParentConfig>['renderHTML'],
},
props: {
mark: ProseMirrorMark,
@@ -324,13 +360,14 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['addAttributes'],
+ storage: Storage,
+ parent: ParentConfig>['addAttributes'],
},
) => Attributes | {},
}
}
-export class Mark {
+export class Mark {
type = 'mark'
name = 'mark'
@@ -341,12 +378,14 @@ export class Mark {
options: Options
+ storage: Storage
+
config: MarkConfig = {
name: this.name,
defaultOptions: {},
}
- constructor(config: Partial> = {}) {
+ constructor(config: Partial> = {}) {
this.config = {
...this.config,
...config,
@@ -354,10 +393,18 @@ export class Mark {
this.name = this.config.name
this.options = this.config.defaultOptions
+ this.storage = callOrReturn(getExtensionField(
+ this,
+ 'addStorage',
+ {
+ name: this.name,
+ options: this.options,
+ },
+ ))
}
- static create(config: Partial> = {}) {
- return new Mark(config)
+ static create(config: Partial> = {}) {
+ return new Mark(config)
}
configure(options: Partial = {}) {
@@ -367,11 +414,20 @@ export class Mark {
extension.options = mergeDeep(this.options, options) as Options
+ extension.storage = callOrReturn(getExtensionField(
+ extension,
+ 'addStorage',
+ {
+ name: extension.name,
+ options: extension.options,
+ },
+ ))
+
return extension
}
- extend(extendedConfig: Partial> = {}) {
- const extension = new Mark(extendedConfig)
+ extend(extendedConfig: Partial> = {}) {
+ const extension = new Mark(extendedConfig)
extension.parent = this
@@ -385,6 +441,15 @@ export class Mark {
? extendedConfig.defaultOptions
: extension.parent.options
+ extension.storage = callOrReturn(getExtensionField(
+ extension,
+ 'addStorage',
+ {
+ name: extension.name,
+ options: extension.options,
+ },
+ ))
+
return extension
}
}
diff --git a/packages/core/src/Node.ts b/packages/core/src/Node.ts
index ec0cfd8aaf..14e28117bf 100644
--- a/packages/core/src/Node.ts
+++ b/packages/core/src/Node.ts
@@ -8,7 +8,10 @@ import { Plugin, Transaction } from 'prosemirror-state'
import { InputRule } from './InputRule'
import { PasteRule } from './PasteRule'
import mergeDeep from './utilities/mergeDeep'
+import callOrReturn from './utilities/callOrReturn'
+import getExtensionField from './helpers/getExtensionField'
import {
+ AnyConfig,
Extensions,
Attributes,
NodeViewRenderer,
@@ -21,7 +24,7 @@ import { NodeConfig } from '.'
import { Editor } from './Editor'
declare module '@tiptap/core' {
- interface NodeConfig {
+ interface NodeConfig {
[key: string]: any;
/**
@@ -39,13 +42,23 @@ declare module '@tiptap/core' {
*/
defaultOptions?: Options,
+ /**
+ * Default Storage
+ */
+ addStorage?: (this: {
+ name: string,
+ options: Options,
+ parent: ParentConfig>['addGlobalAttributes'],
+ }) => Storage,
+
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
- parent: ParentConfig>['addGlobalAttributes'],
+ storage: Storage,
+ parent: ParentConfig>['addGlobalAttributes'],
}) => GlobalAttributes | {},
/**
@@ -54,9 +67,10 @@ declare module '@tiptap/core' {
addCommands?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['addCommands'],
+ parent: ParentConfig>['addCommands'],
}) => Partial,
/**
@@ -65,9 +79,10 @@ declare module '@tiptap/core' {
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['addKeyboardShortcuts'],
+ parent: ParentConfig>['addKeyboardShortcuts'],
}) => {
[key: string]: KeyboardShortcutCommand,
},
@@ -78,9 +93,10 @@ declare module '@tiptap/core' {
addInputRules?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['addInputRules'],
+ parent: ParentConfig>['addInputRules'],
}) => InputRule[],
/**
@@ -89,9 +105,10 @@ declare module '@tiptap/core' {
addPasteRules?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['addPasteRules'],
+ parent: ParentConfig>['addPasteRules'],
}) => PasteRule[],
/**
@@ -100,9 +117,10 @@ declare module '@tiptap/core' {
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['addProseMirrorPlugins'],
+ parent: ParentConfig>['addProseMirrorPlugins'],
}) => Plugin[],
/**
@@ -111,7 +129,8 @@ declare module '@tiptap/core' {
addExtensions?: (this: {
name: string,
options: Options,
- parent: ParentConfig>['addExtensions'],
+ storage: Storage,
+ parent: ParentConfig>['addExtensions'],
}) => Extensions,
/**
@@ -121,7 +140,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['extendNodeSchema'],
+ storage: Storage,
+ parent: ParentConfig>['extendNodeSchema'],
},
extension: Node,
) => Record) | null,
@@ -133,7 +153,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['extendMarkSchema'],
+ storage: Storage,
+ parent: ParentConfig>['extendMarkSchema'],
},
extension: Node,
) => Record) | null,
@@ -144,9 +165,10 @@ declare module '@tiptap/core' {
onBeforeCreate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['onBeforeCreate'],
+ parent: ParentConfig>['onBeforeCreate'],
}) => void) | null,
/**
@@ -155,9 +177,10 @@ declare module '@tiptap/core' {
onCreate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['onCreate'],
+ parent: ParentConfig>['onCreate'],
}) => void) | null,
/**
@@ -166,9 +189,10 @@ declare module '@tiptap/core' {
onUpdate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['onUpdate'],
+ parent: ParentConfig>['onUpdate'],
}) => void) | null,
/**
@@ -177,9 +201,10 @@ declare module '@tiptap/core' {
onSelectionUpdate?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['onSelectionUpdate'],
+ parent: ParentConfig>['onSelectionUpdate'],
}) => void) | null,
/**
@@ -189,9 +214,10 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['onTransaction'],
+ parent: ParentConfig>['onTransaction'],
},
props: {
transaction: Transaction,
@@ -205,9 +231,10 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['onFocus'],
+ parent: ParentConfig>['onFocus'],
},
props: {
event: FocusEvent,
@@ -221,9 +248,10 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['onBlur'],
+ parent: ParentConfig>['onBlur'],
},
props: {
event: FocusEvent,
@@ -236,9 +264,10 @@ declare module '@tiptap/core' {
onDestroy?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['onDestroy'],
+ parent: ParentConfig>['onDestroy'],
}) => void) | null,
/**
@@ -247,9 +276,10 @@ declare module '@tiptap/core' {
addNodeView?: ((this: {
name: string,
options: Options,
+ storage: Storage,
editor: Editor,
type: NodeType,
- parent: ParentConfig>['addNodeView'],
+ parent: ParentConfig>['addNodeView'],
}) => NodeViewRenderer) | null,
/**
@@ -263,7 +293,8 @@ declare module '@tiptap/core' {
content?: NodeSpec['content'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['content'],
+ storage: Storage,
+ parent: ParentConfig>['content'],
}) => NodeSpec['content']),
/**
@@ -272,7 +303,8 @@ declare module '@tiptap/core' {
marks?: NodeSpec['marks'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['marks'],
+ storage: Storage,
+ parent: ParentConfig>['marks'],
}) => NodeSpec['marks']),
/**
@@ -281,7 +313,8 @@ declare module '@tiptap/core' {
group?: NodeSpec['group'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['group'],
+ storage: Storage,
+ parent: ParentConfig>['group'],
}) => NodeSpec['group']),
/**
@@ -290,7 +323,8 @@ declare module '@tiptap/core' {
inline?: NodeSpec['inline'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['inline'],
+ storage: Storage,
+ parent: ParentConfig>['inline'],
}) => NodeSpec['inline']),
/**
@@ -299,7 +333,8 @@ declare module '@tiptap/core' {
atom?: NodeSpec['atom'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['atom'],
+ storage: Storage,
+ parent: ParentConfig>['atom'],
}) => NodeSpec['atom']),
/**
@@ -308,7 +343,8 @@ declare module '@tiptap/core' {
selectable?: NodeSpec['selectable'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['selectable'],
+ storage: Storage,
+ parent: ParentConfig>['selectable'],
}) => NodeSpec['selectable']),
/**
@@ -317,7 +353,8 @@ declare module '@tiptap/core' {
draggable?: NodeSpec['draggable'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['draggable'],
+ storage: Storage,
+ parent: ParentConfig>['draggable'],
}) => NodeSpec['draggable']),
/**
@@ -326,7 +363,8 @@ declare module '@tiptap/core' {
code?: NodeSpec['code'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['code'],
+ storage: Storage,
+ parent: ParentConfig>['code'],
}) => NodeSpec['code']),
/**
@@ -335,7 +373,8 @@ declare module '@tiptap/core' {
defining?: NodeSpec['defining'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['defining'],
+ storage: Storage,
+ parent: ParentConfig>['defining'],
}) => NodeSpec['defining']),
/**
@@ -344,7 +383,8 @@ declare module '@tiptap/core' {
isolating?: NodeSpec['isolating'] | ((this: {
name: string,
options: Options,
- parent: ParentConfig>['isolating'],
+ storage: Storage,
+ parent: ParentConfig>['isolating'],
}) => NodeSpec['isolating']),
/**
@@ -354,7 +394,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['parseHTML'],
+ storage: Storage,
+ parent: ParentConfig>['parseHTML'],
},
) => NodeSpec['parseDOM'],
@@ -365,7 +406,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['renderHTML'],
+ storage: Storage,
+ parent: ParentConfig>['renderHTML'],
},
props: {
node: ProseMirrorNode,
@@ -380,7 +422,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['renderText'],
+ storage: Storage,
+ parent: ParentConfig>['renderText'],
},
props: {
node: ProseMirrorNode,
@@ -397,13 +440,14 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
- parent: ParentConfig>['addAttributes'],
+ storage: Storage,
+ parent: ParentConfig>['addAttributes'],
},
) => Attributes | {},
}
}
-export class Node {
+export class Node {
type = 'node'
name = 'node'
@@ -414,12 +458,14 @@ export class Node {
options: Options
+ storage: Storage
+
config: NodeConfig = {
name: this.name,
defaultOptions: {},
}
- constructor(config: Partial> = {}) {
+ constructor(config: Partial> = {}) {
this.config = {
...this.config,
...config,
@@ -427,10 +473,18 @@ export class Node {
this.name = this.config.name
this.options = this.config.defaultOptions
+ this.storage = callOrReturn(getExtensionField(
+ this,
+ 'addStorage',
+ {
+ name: this.name,
+ options: this.options,
+ },
+ ))
}
- static create(config: Partial> = {}) {
- return new Node(config)
+ static create(config: Partial> = {}) {
+ return new Node(config)
}
configure(options: Partial = {}) {
@@ -440,11 +494,20 @@ export class Node {
extension.options = mergeDeep(this.options, options) as Options
+ extension.storage = callOrReturn(getExtensionField(
+ extension,
+ 'addStorage',
+ {
+ name: extension.name,
+ options: extension.options,
+ },
+ ))
+
return extension
}
- extend(extendedConfig: Partial> = {}) {
- const extension = new Node(extendedConfig)
+ extend(extendedConfig: Partial> = {}) {
+ const extension = new Node(extendedConfig)
extension.parent = this
@@ -458,6 +521,15 @@ export class Node {
? extendedConfig.defaultOptions
: extension.parent.options
+ extension.storage = callOrReturn(getExtensionField(
+ extension,
+ 'addStorage',
+ {
+ name: extension.name,
+ options: extension.options,
+ },
+ ))
+
return extension
}
}
diff --git a/packages/core/src/NodeView.ts b/packages/core/src/NodeView.ts
index 020582f536..16f9afc894 100644
--- a/packages/core/src/NodeView.ts
+++ b/packages/core/src/NodeView.ts
@@ -113,11 +113,12 @@ export class NodeView<
return false
}
+ const isDropEvent = event.type === 'drop'
const isInput = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA'].includes(target.tagName)
|| target.isContentEditable
// any input event within node views should be ignored by ProseMirror
- if (isInput) {
+ if (isInput && !isDropEvent) {
return true
}
@@ -129,7 +130,7 @@ export class NodeView<
const isPasteEvent = event.type === 'paste'
const isCutEvent = event.type === 'cut'
const isClickEvent = event.type === 'mousedown'
- const isDragEvent = event.type.startsWith('drag') || event.type === 'drop'
+ const isDragEvent = event.type.startsWith('drag')
// ProseMirror tries to drag selectable nodes
// even if `draggable` is set to `false`
@@ -165,6 +166,7 @@ export class NodeView<
// these events are handled by prosemirror
if (
isDragging
+ || isDropEvent
|| isCopyEvent
|| isPasteEvent
|| isCutEvent
diff --git a/packages/core/src/PasteRule.ts b/packages/core/src/PasteRule.ts
index 7d4e9c9677..3a03ce7c76 100644
--- a/packages/core/src/PasteRule.ts
+++ b/packages/core/src/PasteRule.ts
@@ -3,6 +3,7 @@ import { Editor } from './Editor'
import CommandManager from './CommandManager'
import createChainableState from './helpers/createChainableState'
import isRegExp from './utilities/isRegExp'
+import isNumber from './utilities/isNumber'
import {
Range,
ExtendedRegExpMatchArray,
@@ -177,7 +178,7 @@ export function pasteRulesPlugin(props: { editor: Editor, rules: PasteRule[] }):
const from = before.content.findDiffStart(doc.content)
const to = before.content.findDiffEnd(doc.content)
- if (!from || !to || from === to.b) {
+ if (!isNumber(from) || !to || from === to.b) {
return
}
diff --git a/packages/core/src/helpers/getAttributesFromExtensions.ts b/packages/core/src/helpers/getAttributesFromExtensions.ts
index 1a81bf5f9d..d76a6a2dc0 100644
--- a/packages/core/src/helpers/getAttributesFromExtensions.ts
+++ b/packages/core/src/helpers/getAttributesFromExtensions.ts
@@ -30,6 +30,7 @@ export default function getAttributesFromExtensions(extensions: Extensions): Ext
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
}
const addGlobalAttributes = getExtensionField(
@@ -67,6 +68,7 @@ export default function getAttributesFromExtensions(extensions: Extensions): Ext
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
}
const addAttributes = getExtensionField(
diff --git a/packages/core/src/helpers/getExtensionField.ts b/packages/core/src/helpers/getExtensionField.ts
index cfe03c75b7..90df54ff2e 100644
--- a/packages/core/src/helpers/getExtensionField.ts
+++ b/packages/core/src/helpers/getExtensionField.ts
@@ -1,9 +1,9 @@
-import { AnyExtension, RemoveThis } from '../types'
+import { AnyExtension, RemoveThis, MaybeThisParameterType } from '../types'
export default function getExtensionField(
extension: AnyExtension,
field: string,
- context: Record = {},
+ context?: Omit, 'parent'>,
): RemoveThis {
if (extension.config[field] === undefined && extension.parent) {
diff --git a/packages/core/src/helpers/getSchemaByResolvedExtensions.ts b/packages/core/src/helpers/getSchemaByResolvedExtensions.ts
index 2ef0063964..bf899c0135 100644
--- a/packages/core/src/helpers/getSchemaByResolvedExtensions.ts
+++ b/packages/core/src/helpers/getSchemaByResolvedExtensions.ts
@@ -29,6 +29,7 @@ export default function getSchemaByResolvedExtensions(extensions: Extensions): S
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
}
const extraNodeFields = extensions.reduce((fields, e) => {
@@ -91,6 +92,7 @@ export default function getSchemaByResolvedExtensions(extensions: Extensions): S
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
}
const extraMarkFields = extensions.reduce((fields, e) => {
diff --git a/packages/core/src/helpers/isList.ts b/packages/core/src/helpers/isList.ts
index 424890b8e1..1705458d7f 100644
--- a/packages/core/src/helpers/isList.ts
+++ b/packages/core/src/helpers/isList.ts
@@ -15,6 +15,7 @@ export default function isList(name: string, extensions: Extensions): boolean {
const context = {
name: extension.name,
options: extension.options,
+ storage: extension.storage,
}
const group = callOrReturn(getExtensionField(extension, 'group', context))
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 241ceff092..8c622edce4 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -56,10 +56,10 @@ export { default as posToDOMRect } from './helpers/posToDOMRect'
export interface Commands {}
// eslint-disable-next-line
-export interface ExtensionConfig {}
+export interface ExtensionConfig