From 7ffabf251c408a652eec1931cc78a8bd43cccb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20K=C3=BChn?= Date: Fri, 22 Oct 2021 08:52:54 +0200 Subject: [PATCH] feat: Add extension storage (#2069) --- .eslintrc.js | 2 +- .../ExtensionStorage/React/CustomExtension.ts | 19 +++ .../ExtensionStorage/React/index.html | 15 ++ .../ExtensionStorage/React/index.jsx | 33 ++++ .../ExtensionStorage/React/styles.scss | 6 + .../ExtensionStorage/Vue/CustomExtension.ts | 19 +++ .../ExtensionStorage/Vue/index.html | 15 ++ .../ExtensionStorage/Vue/index.vue | 56 +++++++ docs/guide/custom-extensions.md | 33 ++++ docs/guide/typescript.md | 19 +++ packages/core/src/Editor.ts | 9 ++ packages/core/src/Extension.ts | 96 +++++++++--- packages/core/src/ExtensionManager.ts | 18 ++- packages/core/src/Mark.ts | 120 +++++++++++---- packages/core/src/Node.ts | 141 +++++++++++++----- .../helpers/getAttributesFromExtensions.ts | 2 + .../core/src/helpers/getExtensionField.ts | 4 +- .../helpers/getSchemaByResolvedExtensions.ts | 2 + packages/core/src/helpers/isList.ts | 1 + packages/core/src/index.ts | 6 +- packages/core/src/types.ts | 13 ++ packages/core/src/utilities/findDuplicates.ts | 5 + packages/extension-gapcursor/src/gapcursor.ts | 4 +- packages/extension-table/src/table.ts | 4 +- packages/react/src/useEditor.ts | 8 +- packages/vue-3/src/Editor.ts | 10 ++ 26 files changed, 555 insertions(+), 105 deletions(-) create mode 100644 demos/src/Experiments/ExtensionStorage/React/CustomExtension.ts create mode 100644 demos/src/Experiments/ExtensionStorage/React/index.html create mode 100644 demos/src/Experiments/ExtensionStorage/React/index.jsx create mode 100644 demos/src/Experiments/ExtensionStorage/React/styles.scss create mode 100644 demos/src/Experiments/ExtensionStorage/Vue/CustomExtension.ts create mode 100644 demos/src/Experiments/ExtensionStorage/Vue/index.html create mode 100644 demos/src/Experiments/ExtensionStorage/Vue/index.vue create mode 100644 packages/core/src/utilities/findDuplicates.ts diff --git a/.eslintrc.js b/.eslintrc.js index e6d6392bd..172905130 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/demos/src/Experiments/ExtensionStorage/React/CustomExtension.ts b/demos/src/Experiments/ExtensionStorage/React/CustomExtension.ts new file mode 100644 index 000000000..de407ad7e --- /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 000000000..538363687 --- /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 000000000..3f75a1da2 --- /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 000000000..46b51a4e1 --- /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 000000000..de407ad7e --- /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 000000000..f0485cddc --- /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 000000000..a09406c4f --- /dev/null +++ b/demos/src/Experiments/ExtensionStorage/Vue/index.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/docs/guide/custom-extensions.md b/docs/guide/custom-extensions.md index e5e0a8fba..eeb2e2102 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 5eb9c6aa4..c329a3544 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/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index 8ad0bf699..4788471b2 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 55ce6f21b..7befb1415 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 = {}) { @@ -264,8 +303,8 @@ export class Extension { return extension } - extend(extendedConfig: Partial> = {}) { - const extension = new Extension(extendedConfig) + extend(extendedConfig: Partial> = {}) { + const extension = new Extension(extendedConfig) extension.parent = this @@ -279,6 +318,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 0056179d8..afd5c0a7a 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 8c8a2cbf7..18212aede 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 = {}) { @@ -370,8 +417,8 @@ export class Mark { return extension } - extend(extendedConfig: Partial> = {}) { - const extension = new Mark(extendedConfig) + extend(extendedConfig: Partial> = {}) { + const extension = new Mark(extendedConfig) extension.parent = this @@ -385,6 +432,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 ec0cfd8aa..1f9b56ce3 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 = {}) { @@ -443,8 +497,8 @@ export class Node { return extension } - extend(extendedConfig: Partial> = {}) { - const extension = new Node(extendedConfig) + extend(extendedConfig: Partial> = {}) { + const extension = new Node(extendedConfig) extension.parent = this @@ -458,6 +512,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/helpers/getAttributesFromExtensions.ts b/packages/core/src/helpers/getAttributesFromExtensions.ts index 1a81bf5f9..d76a6a2dc 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 cfe03c75b..90df54ff2 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 2ef006396..bf899c013 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 424890b8e..1705458d7 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 241ceff09..8c622edce 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 {} // eslint-disable-next-line -export interface NodeConfig {} +export interface NodeConfig {} // eslint-disable-next-line -export interface MarkConfig {} +export interface MarkConfig {} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index deeb3a972..9691c014a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -31,6 +31,15 @@ export type ParentConfig = Partial<{ : T[P] }> +export type Primitive = + | null + | undefined + | string + | number + | boolean + | symbol + | bigint + export type RemoveThis = T extends (...args: any) => any ? (...args: Parameters) => ReturnType : T @@ -39,6 +48,10 @@ export type MaybeReturnType = T extends (...args: any) => any ? ReturnType : T +export type MaybeThisParameterType = Exclude extends (...args: any) => any + ? ThisParameterType> + : any + export interface EditorEvents { beforeCreate: { editor: Editor }, create: { editor: Editor }, diff --git a/packages/core/src/utilities/findDuplicates.ts b/packages/core/src/utilities/findDuplicates.ts new file mode 100644 index 000000000..541c062fc --- /dev/null +++ b/packages/core/src/utilities/findDuplicates.ts @@ -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)] +} diff --git a/packages/extension-gapcursor/src/gapcursor.ts b/packages/extension-gapcursor/src/gapcursor.ts index 214445b80..686b3302c 100644 --- a/packages/extension-gapcursor/src/gapcursor.ts +++ b/packages/extension-gapcursor/src/gapcursor.ts @@ -7,7 +7,7 @@ import { import { gapCursor } from 'prosemirror-gapcursor' declare module '@tiptap/core' { - interface NodeConfig { + interface NodeConfig { /** * Allow gap cursor */ @@ -17,6 +17,7 @@ declare module '@tiptap/core' { | ((this: { name: string, options: Options, + storage: Storage, parent: ParentConfig>['allowGapCursor'], }) => boolean | null), } @@ -35,6 +36,7 @@ export const Gapcursor = Extension.create({ const context = { name: extension.name, options: extension.options, + storage: extension.storage, } return { diff --git a/packages/extension-table/src/table.ts b/packages/extension-table/src/table.ts index 0c464f8c5..9df7be428 100644 --- a/packages/extension-table/src/table.ts +++ b/packages/extension-table/src/table.ts @@ -66,13 +66,14 @@ declare module '@tiptap/core' { } } - interface NodeConfig { + interface NodeConfig { /** * Table Role */ tableRole?: string | ((this: { name: string, options: Options, + storage: Storage, parent: ParentConfig>['tableRole'], }) => string), } @@ -245,6 +246,7 @@ export const Table = Node.create({ const context = { name: extension.name, options: extension.options, + storage: extension.storage, } return { diff --git a/packages/react/src/useEditor.ts b/packages/react/src/useEditor.ts index f2f3fcda7..32896f235 100644 --- a/packages/react/src/useEditor.ts +++ b/packages/react/src/useEditor.ts @@ -17,7 +17,13 @@ export const useEditor = (options: Partial = {}, deps: Dependency setEditor(instance) - instance.on('transaction', forceUpdate) + instance.on('transaction', () => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + forceUpdate() + }) + }) + }) return () => { instance.destroy() diff --git a/packages/vue-3/src/Editor.ts b/packages/vue-3/src/Editor.ts index 5c61c6c8d..68b69f671 100644 --- a/packages/vue-3/src/Editor.ts +++ b/packages/vue-3/src/Editor.ts @@ -39,6 +39,8 @@ export type ContentComponent = ComponentInternalInstance & { export class Editor extends CoreEditor { private reactiveState: Ref + private reactiveExtensionStorage: Ref> + public vueRenderers = reactive>(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. */