feat: Add extension storage (#2069)

This commit is contained in:
Philipp Kühn 2021-10-22 08:52:54 +02:00 committed by GitHub
parent 6987505fda
commit 7ffabf251c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 555 additions and 105 deletions

View File

@ -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: {

View File

@ -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
},
})

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/react.ts'
import source from '@source'
setup('Experiments/ExtensionStorage', source)
</script>
</body>
</html>

View File

@ -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: `
<p>
This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though.
</p>
<p>
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.
</p>
`,
})
return (
<>
reactive storage: {editor?.storage.custom.foo}
<EditorContent editor={editor} />
</>
)
}

View File

@ -0,0 +1,6 @@
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
}

View File

@ -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
},
})

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
</head>
<body>
<div id="app"></div>
<script type="module">
import setup from '../../../../setup/vue.ts'
import source from '@source'
setup('Experiments/ExtensionStorage', source)
</script>
</body>
</html>

View File

@ -0,0 +1,56 @@
<template>
reactive storage: {{ editor?.storage.custom.foo }}
<editor-content :editor="editor" />
</template>
<script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { CustomExtension } from './CustomExtension'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
Document,
Paragraph,
Text,
CustomExtension,
],
content: `
<p>
This is a radically reduced version of tiptap. It has support for a document, with paragraphs and text. Thats it. Its probably too much for real minimalists though.
</p>
<p>
The paragraph extension is not really required, but you need at least one node. Sure, that node can be something different.
</p>
`,
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
}
</style>

View File

@ -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. Lets walk through a few common use cases.

View File

@ -32,6 +32,25 @@ const CustomExtension = Extension.create<CustomExtensionOptions>({
})
```
### Storage types
To add types for your extension storage, youll 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:

View File

@ -50,6 +50,8 @@ export class Editor extends EventEmitter<EditorEvents> {
public isFocused = false
public extensionStorage: Record<string, any> = {}
public options: EditorOptions = {
element: document.createElement('div'),
content: '',
@ -100,6 +102,13 @@ export class Editor extends EventEmitter<EditorEvents> {
}, 0)
}
/**
* Returns the editor storage.
*/
public get storage(): Record<string, any> {
return this.extensionStorage
}
/**
* An object of all registered commands.
*/

View File

@ -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<Options = any> {
interface ExtensionConfig<Options = any, Storage = any> {
[key: string]: any;
/**
@ -33,13 +36,23 @@ declare module '@tiptap/core' {
*/
defaultOptions?: Options,
/**
* Default Storage
*/
addStorage?: (this: {
name: string,
options: Options,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addGlobalAttributes'],
}) => Storage,
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
parent: ParentConfig<ExtensionConfig<Options>>['addGlobalAttributes'],
storage: Storage,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addGlobalAttributes'],
}) => GlobalAttributes | {},
/**
@ -48,8 +61,9 @@ declare module '@tiptap/core' {
addCommands?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['addCommands'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addCommands'],
}) => Partial<RawCommands>,
/**
@ -58,8 +72,9 @@ declare module '@tiptap/core' {
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['addKeyboardShortcuts'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addKeyboardShortcuts'],
}) => {
[key: string]: KeyboardShortcutCommand,
},
@ -70,8 +85,9 @@ declare module '@tiptap/core' {
addInputRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['addInputRules'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addInputRules'],
}) => InputRule[],
/**
@ -80,8 +96,9 @@ declare module '@tiptap/core' {
addPasteRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['addPasteRules'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addPasteRules'],
}) => PasteRule[],
/**
@ -90,8 +107,9 @@ declare module '@tiptap/core' {
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['addProseMirrorPlugins'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addProseMirrorPlugins'],
}) => Plugin[],
/**
@ -100,7 +118,8 @@ declare module '@tiptap/core' {
addExtensions?: (this: {
name: string,
options: Options,
parent: ParentConfig<ExtensionConfig<Options>>['addExtensions'],
storage: Storage,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addExtensions'],
}) => Extensions,
/**
@ -110,7 +129,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<ExtensionConfig<Options>>['extendNodeSchema'],
storage: Storage,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['extendNodeSchema'],
},
extension: Node,
) => Record<string, any>) | null,
@ -122,7 +142,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<ExtensionConfig<Options>>['extendMarkSchema'],
storage: Storage,
parent: ParentConfig<ExtensionConfig<Options, Storage>>['extendMarkSchema'],
},
extension: Mark,
) => Record<string, any>) | null,
@ -133,8 +154,9 @@ declare module '@tiptap/core' {
onBeforeCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['onBeforeCreate'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onBeforeCreate'],
}) => void) | null,
/**
@ -143,8 +165,9 @@ declare module '@tiptap/core' {
onCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['onCreate'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onCreate'],
}) => void) | null,
/**
@ -153,8 +176,9 @@ declare module '@tiptap/core' {
onUpdate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['onUpdate'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onUpdate'],
}) => void) | null,
/**
@ -163,8 +187,9 @@ declare module '@tiptap/core' {
onSelectionUpdate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['onSelectionUpdate'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onSelectionUpdate'],
}) => void) | null,
/**
@ -174,8 +199,9 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['onTransaction'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onTransaction'],
},
props: {
transaction: Transaction,
@ -189,8 +215,9 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['onFocus'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onFocus'],
},
props: {
event: FocusEvent,
@ -204,8 +231,9 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['onBlur'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onBlur'],
},
props: {
event: FocusEvent,
@ -218,13 +246,14 @@ declare module '@tiptap/core' {
onDestroy?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
parent: ParentConfig<ExtensionConfig<Options>>['onDestroy'],
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onDestroy'],
}) => void) | null,
}
}
export class Extension<Options = any> {
export class Extension<Options = any, Storage = any> {
type = 'extension'
name = 'extension'
@ -235,12 +264,14 @@ export class Extension<Options = any> {
options: Options
storage: Storage
config: ExtensionConfig = {
name: this.name,
defaultOptions: {},
}
constructor(config: Partial<ExtensionConfig<Options>> = {}) {
constructor(config: Partial<ExtensionConfig<Options, Storage>> = {}) {
this.config = {
...this.config,
...config,
@ -248,10 +279,18 @@ export class Extension<Options = any> {
this.name = this.config.name
this.options = this.config.defaultOptions
this.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
this,
'addStorage',
{
name: this.name,
options: this.options,
},
))
}
static create<O>(config: Partial<ExtensionConfig<O>> = {}) {
return new Extension<O>(config)
static create<O = any, S = any>(config: Partial<ExtensionConfig<O, S>> = {}) {
return new Extension<O, S>(config)
}
configure(options: Partial<Options> = {}) {
@ -264,8 +303,8 @@ export class Extension<Options = any> {
return extension
}
extend<ExtendedOptions = Options>(extendedConfig: Partial<ExtensionConfig<ExtendedOptions>> = {}) {
const extension = new Extension<ExtendedOptions>(extendedConfig)
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>> = {}) {
const extension = new Extension<ExtendedOptions, ExtendedStorage>(extendedConfig)
extension.parent = this
@ -279,6 +318,15 @@ export class Extension<Options = any> {
? extendedConfig.defaultOptions
: extension.parent.options
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
name: extension.name,
options: extension.options,
},
))
return extension
}
}

View File

@ -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<AnyConfig['addExtensions']>(
@ -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),
}

View File

@ -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<Options = any> {
export interface MarkConfig<Options = any, Storage = any> {
[key: string]: any;
/**
@ -39,13 +42,23 @@ declare module '@tiptap/core' {
*/
defaultOptions?: Options,
/**
* Default Storage
*/
addStorage?: (this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options, Storage>>['addGlobalAttributes'],
}) => Storage,
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['addGlobalAttributes'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['addGlobalAttributes'],
}) => GlobalAttributes | {},
/**
@ -54,9 +67,10 @@ declare module '@tiptap/core' {
addCommands?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options>>['addCommands'],
parent: ParentConfig<MarkConfig<Options, Storage>>['addCommands'],
}) => Partial<RawCommands>,
/**
@ -65,9 +79,10 @@ declare module '@tiptap/core' {
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options>>['addKeyboardShortcuts'],
parent: ParentConfig<MarkConfig<Options, Storage>>['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<MarkConfig<Options>>['addInputRules'],
parent: ParentConfig<MarkConfig<Options, Storage>>['addInputRules'],
}) => InputRule[],
/**
@ -89,9 +105,10 @@ declare module '@tiptap/core' {
addPasteRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options>>['addPasteRules'],
parent: ParentConfig<MarkConfig<Options, Storage>>['addPasteRules'],
}) => PasteRule[],
/**
@ -100,9 +117,10 @@ declare module '@tiptap/core' {
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options>>['addProseMirrorPlugins'],
parent: ParentConfig<MarkConfig<Options, Storage>>['addProseMirrorPlugins'],
}) => Plugin[],
/**
@ -111,7 +129,8 @@ declare module '@tiptap/core' {
addExtensions?: (this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['addExtensions'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['addExtensions'],
}) => Extensions,
/**
@ -121,7 +140,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['extendNodeSchema'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['extendNodeSchema'],
},
extension: Node,
) => Record<string, any>) | null,
@ -133,7 +153,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['extendMarkSchema'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['extendMarkSchema'],
},
extension: Mark,
) => Record<string, any>) | null,
@ -144,9 +165,10 @@ declare module '@tiptap/core' {
onBeforeCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options>>['onBeforeCreate'],
parent: ParentConfig<MarkConfig<Options, Storage>>['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<MarkConfig<Options>>['onCreate'],
parent: ParentConfig<MarkConfig<Options, Storage>>['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<MarkConfig<Options>>['onUpdate'],
parent: ParentConfig<MarkConfig<Options, Storage>>['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<MarkConfig<Options>>['onSelectionUpdate'],
parent: ParentConfig<MarkConfig<Options, Storage>>['onSelectionUpdate'],
}) => void) | null,
/**
@ -189,9 +214,10 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: MarkType,
parent: ParentConfig<MarkConfig<Options>>['onTransaction'],
parent: ParentConfig<MarkConfig<Options, Storage>>['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<MarkConfig<Options>>['onFocus'],
parent: ParentConfig<MarkConfig<Options, Storage>>['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<MarkConfig<Options>>['onBlur'],
parent: ParentConfig<MarkConfig<Options, Storage>>['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<MarkConfig<Options>>['onDestroy'],
parent: ParentConfig<MarkConfig<Options, Storage>>['onDestroy'],
}) => void) | null,
/**
@ -252,7 +281,8 @@ declare module '@tiptap/core' {
inclusive?: MarkSpec['inclusive'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['inclusive'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['inclusive'],
}) => MarkSpec['inclusive']),
/**
@ -261,7 +291,8 @@ declare module '@tiptap/core' {
excludes?: MarkSpec['excludes'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['excludes'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'],
}) => MarkSpec['excludes']),
/**
@ -270,7 +301,8 @@ declare module '@tiptap/core' {
group?: MarkSpec['group'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['group'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['group'],
}) => MarkSpec['group']),
/**
@ -279,7 +311,8 @@ declare module '@tiptap/core' {
spanning?: MarkSpec['spanning'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['spanning'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['spanning'],
}) => MarkSpec['spanning']),
/**
@ -288,7 +321,8 @@ declare module '@tiptap/core' {
code?: boolean | ((this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['code'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['code'],
}) => boolean),
/**
@ -298,7 +332,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['parseHTML'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['parseHTML'],
},
) => MarkSpec['parseDOM'],
@ -309,7 +344,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['renderHTML'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['renderHTML'],
},
props: {
mark: ProseMirrorMark,
@ -324,13 +360,14 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<MarkConfig<Options>>['addAttributes'],
storage: Storage,
parent: ParentConfig<MarkConfig<Options, Storage>>['addAttributes'],
},
) => Attributes | {},
}
}
export class Mark<Options = any> {
export class Mark<Options = any, Storage = any> {
type = 'mark'
name = 'mark'
@ -341,12 +378,14 @@ export class Mark<Options = any> {
options: Options
storage: Storage
config: MarkConfig = {
name: this.name,
defaultOptions: {},
}
constructor(config: Partial<MarkConfig<Options>> = {}) {
constructor(config: Partial<MarkConfig<Options, Storage>> = {}) {
this.config = {
...this.config,
...config,
@ -354,10 +393,18 @@ export class Mark<Options = any> {
this.name = this.config.name
this.options = this.config.defaultOptions
this.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
this,
'addStorage',
{
name: this.name,
options: this.options,
},
))
}
static create<O>(config: Partial<MarkConfig<O>> = {}) {
return new Mark<O>(config)
static create<O = any, S = any>(config: Partial<MarkConfig<O, S>> = {}) {
return new Mark<O, S>(config)
}
configure(options: Partial<Options> = {}) {
@ -370,8 +417,8 @@ export class Mark<Options = any> {
return extension
}
extend<ExtendedOptions = Options>(extendedConfig: Partial<MarkConfig<ExtendedOptions>> = {}) {
const extension = new Mark<ExtendedOptions>(extendedConfig)
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>> = {}) {
const extension = new Mark<ExtendedOptions, ExtendedStorage>(extendedConfig)
extension.parent = this
@ -385,6 +432,15 @@ export class Mark<Options = any> {
? extendedConfig.defaultOptions
: extension.parent.options
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
name: extension.name,
options: extension.options,
},
))
return extension
}
}

View File

@ -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<Options = any> {
interface NodeConfig<Options = any, Storage = any> {
[key: string]: any;
/**
@ -39,13 +42,23 @@ declare module '@tiptap/core' {
*/
defaultOptions?: Options,
/**
* Default Storage
*/
addStorage?: (this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options, Storage>>['addGlobalAttributes'],
}) => Storage,
/**
* Global attributes
*/
addGlobalAttributes?: (this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['addGlobalAttributes'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['addGlobalAttributes'],
}) => GlobalAttributes | {},
/**
@ -54,9 +67,10 @@ declare module '@tiptap/core' {
addCommands?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options>>['addCommands'],
parent: ParentConfig<NodeConfig<Options, Storage>>['addCommands'],
}) => Partial<RawCommands>,
/**
@ -65,9 +79,10 @@ declare module '@tiptap/core' {
addKeyboardShortcuts?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options>>['addKeyboardShortcuts'],
parent: ParentConfig<NodeConfig<Options, Storage>>['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<NodeConfig<Options>>['addInputRules'],
parent: ParentConfig<NodeConfig<Options, Storage>>['addInputRules'],
}) => InputRule[],
/**
@ -89,9 +105,10 @@ declare module '@tiptap/core' {
addPasteRules?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options>>['addPasteRules'],
parent: ParentConfig<NodeConfig<Options, Storage>>['addPasteRules'],
}) => PasteRule[],
/**
@ -100,9 +117,10 @@ declare module '@tiptap/core' {
addProseMirrorPlugins?: (this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options>>['addProseMirrorPlugins'],
parent: ParentConfig<NodeConfig<Options, Storage>>['addProseMirrorPlugins'],
}) => Plugin[],
/**
@ -111,7 +129,8 @@ declare module '@tiptap/core' {
addExtensions?: (this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['addExtensions'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['addExtensions'],
}) => Extensions,
/**
@ -121,7 +140,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['extendNodeSchema'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['extendNodeSchema'],
},
extension: Node,
) => Record<string, any>) | null,
@ -133,7 +153,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['extendMarkSchema'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['extendMarkSchema'],
},
extension: Node,
) => Record<string, any>) | null,
@ -144,9 +165,10 @@ declare module '@tiptap/core' {
onBeforeCreate?: ((this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options>>['onBeforeCreate'],
parent: ParentConfig<NodeConfig<Options, Storage>>['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<NodeConfig<Options>>['onCreate'],
parent: ParentConfig<NodeConfig<Options, Storage>>['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<NodeConfig<Options>>['onUpdate'],
parent: ParentConfig<NodeConfig<Options, Storage>>['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<NodeConfig<Options>>['onSelectionUpdate'],
parent: ParentConfig<NodeConfig<Options, Storage>>['onSelectionUpdate'],
}) => void) | null,
/**
@ -189,9 +214,10 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
storage: Storage,
editor: Editor,
type: NodeType,
parent: ParentConfig<NodeConfig<Options>>['onTransaction'],
parent: ParentConfig<NodeConfig<Options, Storage>>['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<NodeConfig<Options>>['onFocus'],
parent: ParentConfig<NodeConfig<Options, Storage>>['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<NodeConfig<Options>>['onBlur'],
parent: ParentConfig<NodeConfig<Options, Storage>>['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<NodeConfig<Options>>['onDestroy'],
parent: ParentConfig<NodeConfig<Options, Storage>>['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<NodeConfig<Options>>['addNodeView'],
parent: ParentConfig<NodeConfig<Options, Storage>>['addNodeView'],
}) => NodeViewRenderer) | null,
/**
@ -263,7 +293,8 @@ declare module '@tiptap/core' {
content?: NodeSpec['content'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['content'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['content'],
}) => NodeSpec['content']),
/**
@ -272,7 +303,8 @@ declare module '@tiptap/core' {
marks?: NodeSpec['marks'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['marks'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['marks'],
}) => NodeSpec['marks']),
/**
@ -281,7 +313,8 @@ declare module '@tiptap/core' {
group?: NodeSpec['group'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['group'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['group'],
}) => NodeSpec['group']),
/**
@ -290,7 +323,8 @@ declare module '@tiptap/core' {
inline?: NodeSpec['inline'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['inline'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['inline'],
}) => NodeSpec['inline']),
/**
@ -299,7 +333,8 @@ declare module '@tiptap/core' {
atom?: NodeSpec['atom'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['atom'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['atom'],
}) => NodeSpec['atom']),
/**
@ -308,7 +343,8 @@ declare module '@tiptap/core' {
selectable?: NodeSpec['selectable'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['selectable'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['selectable'],
}) => NodeSpec['selectable']),
/**
@ -317,7 +353,8 @@ declare module '@tiptap/core' {
draggable?: NodeSpec['draggable'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['draggable'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['draggable'],
}) => NodeSpec['draggable']),
/**
@ -326,7 +363,8 @@ declare module '@tiptap/core' {
code?: NodeSpec['code'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['code'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['code'],
}) => NodeSpec['code']),
/**
@ -335,7 +373,8 @@ declare module '@tiptap/core' {
defining?: NodeSpec['defining'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['defining'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['defining'],
}) => NodeSpec['defining']),
/**
@ -344,7 +383,8 @@ declare module '@tiptap/core' {
isolating?: NodeSpec['isolating'] | ((this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['isolating'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['isolating'],
}) => NodeSpec['isolating']),
/**
@ -354,7 +394,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['parseHTML'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['parseHTML'],
},
) => NodeSpec['parseDOM'],
@ -365,7 +406,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['renderHTML'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['renderHTML'],
},
props: {
node: ProseMirrorNode,
@ -380,7 +422,8 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['renderText'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['renderText'],
},
props: {
node: ProseMirrorNode,
@ -397,13 +440,14 @@ declare module '@tiptap/core' {
this: {
name: string,
options: Options,
parent: ParentConfig<NodeConfig<Options>>['addAttributes'],
storage: Storage,
parent: ParentConfig<NodeConfig<Options, Storage>>['addAttributes'],
},
) => Attributes | {},
}
}
export class Node<Options = any> {
export class Node<Options = any, Storage = any> {
type = 'node'
name = 'node'
@ -414,12 +458,14 @@ export class Node<Options = any> {
options: Options
storage: Storage
config: NodeConfig = {
name: this.name,
defaultOptions: {},
}
constructor(config: Partial<NodeConfig<Options>> = {}) {
constructor(config: Partial<NodeConfig<Options, Storage>> = {}) {
this.config = {
...this.config,
...config,
@ -427,10 +473,18 @@ export class Node<Options = any> {
this.name = this.config.name
this.options = this.config.defaultOptions
this.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
this,
'addStorage',
{
name: this.name,
options: this.options,
},
))
}
static create<O>(config: Partial<NodeConfig<O>> = {}) {
return new Node<O>(config)
static create<O = any, S = any>(config: Partial<NodeConfig<O, S>> = {}) {
return new Node<O, S>(config)
}
configure(options: Partial<Options> = {}) {
@ -443,8 +497,8 @@ export class Node<Options = any> {
return extension
}
extend<ExtendedOptions = Options>(extendedConfig: Partial<NodeConfig<ExtendedOptions>> = {}) {
const extension = new Node<ExtendedOptions>(extendedConfig)
extend<ExtendedOptions = Options, ExtendedStorage = Storage>(extendedConfig: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>> = {}) {
const extension = new Node<ExtendedOptions, ExtendedStorage>(extendedConfig)
extension.parent = this
@ -458,6 +512,15 @@ export class Node<Options = any> {
? extendedConfig.defaultOptions
: extension.parent.options
extension.storage = callOrReturn(getExtensionField<AnyConfig['addStorage']>(
extension,
'addStorage',
{
name: extension.name,
options: extension.options,
},
))
return extension
}
}

View File

@ -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<AnyConfig['addGlobalAttributes']>(
@ -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<NodeConfig['addAttributes'] | MarkConfig['addAttributes']>(

View File

@ -1,9 +1,9 @@
import { AnyExtension, RemoveThis } from '../types'
import { AnyExtension, RemoveThis, MaybeThisParameterType } from '../types'
export default function getExtensionField<T = any>(
extension: AnyExtension,
field: string,
context: Record<string, any> = {},
context?: Omit<MaybeThisParameterType<T>, 'parent'>,
): RemoveThis<T> {
if (extension.config[field] === undefined && extension.parent) {

View File

@ -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) => {

View File

@ -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<NodeConfig['group']>(extension, 'group', context))

View File

@ -56,10 +56,10 @@ export { default as posToDOMRect } from './helpers/posToDOMRect'
export interface Commands<ReturnType = any> {}
// eslint-disable-next-line
export interface ExtensionConfig<Options = any> {}
export interface ExtensionConfig<Options = any, Storage = any> {}
// eslint-disable-next-line
export interface NodeConfig<Options = any> {}
export interface NodeConfig<Options = any, Storage = any> {}
// eslint-disable-next-line
export interface MarkConfig<Options = any> {}
export interface MarkConfig<Options = any, Storage = any> {}

View File

@ -31,6 +31,15 @@ export type ParentConfig<T> = Partial<{
: T[P]
}>
export type Primitive =
| null
| undefined
| string
| number
| boolean
| symbol
| bigint
export type RemoveThis<T> = T extends (...args: any) => any
? (...args: Parameters<T>) => ReturnType<T>
: T
@ -39,6 +48,10 @@ export type MaybeReturnType<T> = T extends (...args: any) => any
? ReturnType<T>
: T
export type MaybeThisParameterType<T> = Exclude<T, Primitive> extends (...args: any) => any
? ThisParameterType<Exclude<T, Primitive>>
: any
export interface EditorEvents {
beforeCreate: { editor: Editor },
create: { editor: Editor },

View File

@ -0,0 +1,5 @@
export default function findDuplicates(items: any[]): any[] {
const filtered = items.filter((el, index) => items.indexOf(el) !== index)
return [...new Set(filtered)]
}

View File

@ -7,7 +7,7 @@ import {
import { gapCursor } from 'prosemirror-gapcursor'
declare module '@tiptap/core' {
interface NodeConfig<Options> {
interface NodeConfig<Options, Storage> {
/**
* Allow gap cursor
*/
@ -17,6 +17,7 @@ declare module '@tiptap/core' {
| ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options>>['allowGapCursor'],
}) => boolean | null),
}
@ -35,6 +36,7 @@ export const Gapcursor = Extension.create({
const context = {
name: extension.name,
options: extension.options,
storage: extension.storage,
}
return {

View File

@ -66,13 +66,14 @@ declare module '@tiptap/core' {
}
}
interface NodeConfig<Options> {
interface NodeConfig<Options, Storage> {
/**
* Table Role
*/
tableRole?: string | ((this: {
name: string,
options: Options,
storage: Storage,
parent: ParentConfig<NodeConfig<Options>>['tableRole'],
}) => string),
}
@ -245,6 +246,7 @@ export const Table = Node.create<TableOptions>({
const context = {
name: extension.name,
options: extension.options,
storage: extension.storage,
}
return {

View File

@ -17,7 +17,13 @@ export const useEditor = (options: Partial<EditorOptions> = {}, deps: Dependency
setEditor(instance)
instance.on('transaction', forceUpdate)
instance.on('transaction', () => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
forceUpdate()
})
})
})
return () => {
instance.destroy()

View File

@ -39,6 +39,8 @@ export type ContentComponent = ComponentInternalInstance & {
export class Editor extends CoreEditor {
private reactiveState: Ref<EditorState>
private reactiveExtensionStorage: Ref<Record<string, any>>
public vueRenderers = reactive<Map<string, VueRenderer>>(new Map())
public contentComponent: ContentComponent | null = null
@ -47,9 +49,11 @@ export class Editor extends CoreEditor {
super(options)
this.reactiveState = useDebouncedRef(this.view.state)
this.reactiveExtensionStorage = useDebouncedRef(this.extensionStorage)
this.on('transaction', () => {
this.reactiveState.value = this.view.state
this.reactiveExtensionStorage.value = this.extensionStorage
})
return markRaw(this)
@ -61,6 +65,12 @@ export class Editor extends CoreEditor {
: this.view.state
}
get storage() {
return this.reactiveExtensionStorage
? this.reactiveExtensionStorage.value
: super.storage
}
/**
* Register a ProseMirror plugin.
*/