@tiptap/core
Advanced tools
Comparing version 3.0.0-next.4 to 3.0.0-next.5
MIT License | ||
Copyright (c) 2024, Tiptap GmbH | ||
Copyright (c) 2025, Tiptap GmbH | ||
@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy |
{ | ||
"name": "@tiptap/core", | ||
"description": "headless rich text editor", | ||
"version": "3.0.0-next.4", | ||
"version": "3.0.0-next.5", | ||
"homepage": "https://tiptap.dev", | ||
@@ -27,2 +27,18 @@ "keywords": [ | ||
"require": "./dist/index.cjs" | ||
}, | ||
"./jsx-runtime": { | ||
"types": { | ||
"import": "./jsx-runtime/index.d.ts", | ||
"require": "./jsx-runtime/index.d.cts" | ||
}, | ||
"import": "./jsx-runtime/index.js", | ||
"require": "./jsx-runtime/index.cjs" | ||
}, | ||
"./jsx-dev-runtime": { | ||
"types": { | ||
"import": "./jsx-dev-runtime/index.d.ts", | ||
"require": "./jsx-dev-runtime/index.d.cts" | ||
}, | ||
"import": "./jsx-dev-runtime/index.js", | ||
"require": "./jsx-dev-runtime/index.cjs" | ||
} | ||
@@ -35,6 +51,7 @@ }, | ||
"src", | ||
"dist" | ||
"dist", | ||
"jsx-runtime" | ||
], | ||
"devDependencies": { | ||
"@tiptap/pm": "^3.0.0-next.4" | ||
"@tiptap/pm": "^3.0.0-next.5" | ||
}, | ||
@@ -41,0 +58,0 @@ "peerDependencies": { |
@@ -8,6 +8,11 @@ import { RawCommands } from '../types.js' | ||
* Clear the whole document. | ||
* @param emitUpdate Whether to emit an update event. | ||
* @example editor.commands.clearContent() | ||
*/ | ||
clearContent: (emitUpdate?: boolean) => ReturnType | ||
clearContent: ( | ||
/** | ||
* Whether to emit an update event. | ||
* @default true | ||
*/ | ||
emitUpdate?: boolean, | ||
) => ReturnType | ||
} | ||
@@ -18,5 +23,5 @@ } | ||
export const clearContent: RawCommands['clearContent'] = | ||
(emitUpdate = false) => | ||
(emitUpdate = true) => | ||
({ commands }) => { | ||
return commands.setContent('', emitUpdate) | ||
return commands.setContent('', { emitUpdate }) | ||
} |
import { isTextSelection } from '../helpers/isTextSelection.js' | ||
import { resolveFocusPosition } from '../helpers/resolveFocusPosition.js' | ||
import { FocusPosition, RawCommands } from '../types.js' | ||
import { isAndroid } from '../utilities/isAndroid.js' | ||
import { isiOS } from '../utilities/isiOS.js' | ||
@@ -42,3 +44,7 @@ declare module '@tiptap/core' { | ||
const delayedFocus = () => { | ||
;(view.dom as HTMLElement).focus() | ||
// focus within `requestAnimationFrame` breaks focus on iOS and Android | ||
// so we have to call this | ||
if (isiOS() || isAndroid()) { | ||
;(view.dom as HTMLElement).focus() | ||
} | ||
@@ -45,0 +51,0 @@ // For React we have to focus asynchronously. Otherwise wild things happen. |
@@ -91,4 +91,8 @@ import { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model' | ||
disableCollaboration: () => { | ||
if (editor.storage.collaboration) { | ||
editor.storage.collaboration.isDisabled = true | ||
if ( | ||
'collaboration' in editor.storage && | ||
typeof editor.storage.collaboration === 'object' && | ||
editor.storage.collaboration | ||
) { | ||
;(editor.storage.collaboration as any).isDisabled = true | ||
} | ||
@@ -95,0 +99,0 @@ }, |
@@ -23,13 +23,2 @@ import { Fragment, Node as ProseMirrorNode, ParseOptions } from '@tiptap/pm/model' | ||
/** | ||
* Whether to emit an update event. | ||
* @default false | ||
*/ | ||
emitUpdate?: boolean, | ||
/** | ||
* Options for parsing the content. | ||
* @default {} | ||
*/ | ||
parseOptions?: ParseOptions, | ||
/** | ||
* Options for `setContent`. | ||
@@ -39,5 +28,17 @@ */ | ||
/** | ||
* Options for parsing the content. | ||
* @default {} | ||
*/ | ||
parseOptions?: ParseOptions | ||
/** | ||
* Whether to throw an error if the content is invalid. | ||
*/ | ||
errorOnInvalidContent?: boolean | ||
/** | ||
* Whether to emit an update event. | ||
* @default true | ||
*/ | ||
emitUpdate?: boolean | ||
}, | ||
@@ -50,3 +51,3 @@ ) => ReturnType | ||
export const setContent: RawCommands['setContent'] = | ||
(content, emitUpdate = false, parseOptions = {}, options = {}) => | ||
(content, { errorOnInvalidContent, emitUpdate = true, parseOptions = {} } = {}) => | ||
({ editor, tr, dispatch, commands }) => { | ||
@@ -59,3 +60,3 @@ const { doc } = tr | ||
const document = createDocument(content, editor.schema, parseOptions, { | ||
errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck, | ||
errorOnInvalidContent: errorOnInvalidContent ?? editor.options.enableContentCheck, | ||
}) | ||
@@ -75,4 +76,4 @@ | ||
parseOptions, | ||
errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck, | ||
errorOnInvalidContent: errorOnInvalidContent ?? editor.options.enableContentCheck, | ||
}) | ||
} |
@@ -12,2 +12,3 @@ /* eslint-disable @typescript-eslint/no-empty-object-type */ | ||
Commands, | ||
Delete, | ||
Drop, | ||
@@ -28,2 +29,3 @@ Editable, | ||
import { resolveFocusPosition } from './helpers/resolveFocusPosition.js' | ||
import type { Storage } from './index.js' | ||
import { NodePos } from './NodePos.js' | ||
@@ -61,6 +63,8 @@ import { style } from './style.js' | ||
public view!: EditorView | ||
private editorView: EditorView | null = null | ||
public isFocused = false | ||
private editorState!: EditorState | ||
/** | ||
@@ -71,3 +75,3 @@ * The editor is considered initialized after the `create` event has been emitted. | ||
public extensionStorage: Record<string, any> = {} | ||
public extensionStorage: Storage = {} as Storage | ||
@@ -80,3 +84,3 @@ /** | ||
public options: EditorOptions = { | ||
element: document.createElement('div'), | ||
element: typeof document !== 'undefined' ? document.createElement('div') : null, | ||
content: '', | ||
@@ -108,2 +112,3 @@ injectCSS: true, | ||
onDrop: () => null, | ||
onDelete: () => null, | ||
} | ||
@@ -120,4 +125,2 @@ | ||
this.on('contentError', this.options.onContentError) | ||
this.createView() | ||
this.injectCSS() | ||
this.on('create', this.options.onCreate) | ||
@@ -132,3 +135,27 @@ this.on('update', this.options.onUpdate) | ||
this.on('paste', ({ event, slice }) => this.options.onPaste(event, slice)) | ||
this.on('delete', this.options.onDelete) | ||
const initialDoc = this.createDoc() | ||
const selection = resolveFocusPosition(initialDoc, this.options.autofocus) | ||
// Set editor state immediately, so that it's available independently from the view | ||
this.editorState = EditorState.create({ | ||
doc: initialDoc, | ||
schema: this.schema, | ||
selection: selection || undefined, | ||
}) | ||
if (this.options.element) { | ||
this.mount(this.options.element) | ||
} | ||
} | ||
public mount(el: NonNullable<EditorOptions['element']> & {}) { | ||
if (typeof document === 'undefined') { | ||
throw new Error( | ||
`[tiptap error]: The editor cannot be mounted because there is no 'document' defined in this environment.`, | ||
) | ||
} | ||
this.createView(el) | ||
window.setTimeout(() => { | ||
@@ -148,3 +175,3 @@ if (this.isDestroyed) { | ||
*/ | ||
public get storage(): Record<string, any> { | ||
public get storage(): Storage { | ||
return this.extensionStorage | ||
@@ -178,3 +205,3 @@ } | ||
private injectCSS(): void { | ||
if (this.options.injectCSS && document) { | ||
if (this.options.injectCSS && typeof document !== 'undefined') { | ||
this.css = createStyleTag(style, this.options.injectNonce) | ||
@@ -195,3 +222,3 @@ } | ||
if (!this.view || !this.state || this.isDestroyed) { | ||
if (!this.editorView || !this.state || this.isDestroyed) { | ||
return | ||
@@ -231,4 +258,50 @@ } | ||
*/ | ||
public get view(): EditorView { | ||
if (this.editorView) { | ||
return this.editorView | ||
} | ||
return new Proxy( | ||
{ | ||
state: this.editorState, | ||
updateState: (state: EditorState): ReturnType<EditorView['updateState']> => { | ||
this.editorState = state | ||
}, | ||
dispatch: (tr: Transaction): ReturnType<EditorView['dispatch']> => { | ||
this.editorState = this.state.apply(tr) | ||
}, | ||
// Stub some commonly accessed properties to prevent errors | ||
composing: false, | ||
dragging: null, | ||
editable: true, | ||
} as EditorView, | ||
{ | ||
get: (obj, key) => { | ||
// Specifically always return the most recent editorState | ||
if (key === 'state') { | ||
return this.editorState | ||
} | ||
if (key in obj) { | ||
return Reflect.get(obj, key) | ||
} | ||
// We throw an error here, because we know the view is not available | ||
throw new Error( | ||
`[tiptap error]: The editor view is not available. Cannot access view['${key as string}']. The editor may not be mounted yet.`, | ||
) | ||
}, | ||
}, | ||
) as EditorView | ||
} | ||
/** | ||
* Returns the editor state. | ||
*/ | ||
public get state(): EditorState { | ||
return this.view.state | ||
if (this.editorView) { | ||
this.editorState = this.view.state | ||
} | ||
return this.editorState | ||
} | ||
@@ -312,2 +385,3 @@ | ||
Paste, | ||
Delete, | ||
].filter(ext => { | ||
@@ -346,5 +420,5 @@ if (typeof this.options.enableCoreExtensions === 'object') { | ||
/** | ||
* Creates a ProseMirror view. | ||
* Creates the initial document. | ||
*/ | ||
private createView(): void { | ||
private createDoc(): ProseMirrorNode { | ||
let doc: ProseMirrorNode | ||
@@ -368,4 +442,8 @@ | ||
disableCollaboration: () => { | ||
if (this.storage.collaboration) { | ||
this.storage.collaboration.isDisabled = true | ||
if ( | ||
'collaboration' in this.storage && | ||
typeof this.storage.collaboration === 'object' && | ||
this.storage.collaboration | ||
) { | ||
;(this.storage.collaboration as any).isDisabled = true | ||
} | ||
@@ -385,5 +463,10 @@ // To avoid syncing back invalid content, reinitialize the extensions without the collaboration extension | ||
} | ||
const selection = resolveFocusPosition(doc, this.options.autofocus) | ||
return doc | ||
} | ||
this.view = new EditorView(this.options.element, { | ||
/** | ||
* Creates a ProseMirror view. | ||
*/ | ||
private createView(element: NonNullable<EditorOptions['element']> & {}): void { | ||
this.editorView = new EditorView(element, { | ||
...this.options.editorProps, | ||
@@ -396,6 +479,3 @@ attributes: { | ||
dispatchTransaction: this.dispatchTransaction.bind(this), | ||
state: EditorState.create({ | ||
doc, | ||
selection: selection || undefined, | ||
}), | ||
state: this.editorState, | ||
}) | ||
@@ -413,2 +493,3 @@ | ||
this.prependClass() | ||
this.injectCSS() | ||
@@ -432,2 +513,3 @@ // Let’s store the editor instance in the DOM element. | ||
this.view.setProps({ | ||
markViews: this.extensionManager.markViews, | ||
nodeViews: this.extensionManager.nodeViews, | ||
@@ -624,6 +706,6 @@ }) | ||
if (this.view) { | ||
if (this.editorView) { | ||
// Cleanup our reference to prevent circular references which caused memory leaks | ||
// @ts-ignore | ||
const dom = this.view.dom as TiptapEditorHTMLElement | ||
const dom = this.editorView.dom as TiptapEditorHTMLElement | ||
@@ -633,3 +715,3 @@ if (dom && dom.editor) { | ||
} | ||
this.view.destroy() | ||
this.editorView.destroy() | ||
} | ||
@@ -636,0 +718,0 @@ |
@@ -1,392 +0,7 @@ | ||
import { Plugin, Transaction } from '@tiptap/pm/state' | ||
import { type ExtendableConfig, Extendable } from './Extendable.js' | ||
import { Editor } from './Editor.js' | ||
import { getExtensionField } from './helpers/getExtensionField.js' | ||
import { ExtensionConfig } from './index.js' | ||
import { InputRule } from './InputRule.js' | ||
import { Mark } from './Mark.js' | ||
import { Node } from './Node.js' | ||
import { PasteRule } from './PasteRule.js' | ||
import { AnyConfig, Extensions, GlobalAttributes, KeyboardShortcutCommand, ParentConfig, RawCommands } from './types.js' | ||
import { callOrReturn } from './utilities/callOrReturn.js' | ||
import { mergeDeep } from './utilities/mergeDeep.js' | ||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
export interface ExtensionConfig<Options = any, Storage = any> | ||
extends ExtendableConfig<Options, Storage, ExtensionConfig<Options, Storage>, null> {} | ||
declare module '@tiptap/core' { | ||
interface ExtensionConfig<Options = any, Storage = any> { | ||
// @ts-ignore - this is a dynamic key | ||
[key: string]: any | ||
/** | ||
* The extension name - this must be unique. | ||
* It will be used to identify the extension. | ||
* | ||
* @example 'myExtension' | ||
*/ | ||
name: string | ||
/** | ||
* The priority of your extension. The higher, the earlier it will be called | ||
* and will take precedence over other extensions with a lower priority. | ||
* @default 100 | ||
* @example 101 | ||
*/ | ||
priority?: number | ||
/** | ||
* The default options for this extension. | ||
* @example | ||
* defaultOptions: { | ||
* myOption: 'foo', | ||
* myOtherOption: 10, | ||
* } | ||
*/ | ||
defaultOptions?: Options | ||
/** | ||
* This method will add options to this extension | ||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#settings | ||
* @example | ||
* addOptions() { | ||
* return { | ||
* myOption: 'foo', | ||
* myOtherOption: 10, | ||
* } | ||
*/ | ||
addOptions?: (this: { | ||
name: string | ||
parent: Exclude<ParentConfig<ExtensionConfig<Options, Storage>>['addOptions'], undefined> | ||
}) => Options | ||
/** | ||
* The default storage this extension can save data to. | ||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#storage | ||
* @example | ||
* defaultStorage: { | ||
* prefetchedUsers: [], | ||
* loading: false, | ||
* } | ||
*/ | ||
addStorage?: (this: { | ||
name: string | ||
options: Options | ||
parent: Exclude<ParentConfig<ExtensionConfig<Options, Storage>>['addStorage'], undefined> | ||
}) => Storage | ||
/** | ||
* This function adds globalAttributes to specific nodes. | ||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#global-attributes | ||
* @example | ||
* addGlobalAttributes() { | ||
* return [ | ||
* { | ||
// Extend the following extensions | ||
* types: [ | ||
* 'heading', | ||
* 'paragraph', | ||
* ], | ||
* // … with those attributes | ||
* attributes: { | ||
* textAlign: { | ||
* default: 'left', | ||
* renderHTML: attributes => ({ | ||
* style: `text-align: ${attributes.textAlign}`, | ||
* }), | ||
* parseHTML: element => element.style.textAlign || 'left', | ||
* }, | ||
* }, | ||
* }, | ||
* ] | ||
* } | ||
*/ | ||
addGlobalAttributes?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
extensions: (Node | Mark)[] | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addGlobalAttributes'] | ||
}) => GlobalAttributes | ||
/** | ||
* This function adds commands to the editor | ||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#commands | ||
* @example | ||
* addCommands() { | ||
* return { | ||
* myCommand: () => ({ chain }) => chain().setMark('type', 'foo').run(), | ||
* } | ||
* } | ||
*/ | ||
addCommands?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addCommands'] | ||
}) => Partial<RawCommands> | ||
/** | ||
* This function registers keyboard shortcuts. | ||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#keyboard-shortcuts | ||
* @example | ||
* addKeyboardShortcuts() { | ||
* return { | ||
* 'Mod-l': () => this.editor.commands.toggleBulletList(), | ||
* } | ||
* }, | ||
*/ | ||
addKeyboardShortcuts?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addKeyboardShortcuts'] | ||
}) => { | ||
[key: string]: KeyboardShortcutCommand | ||
} | ||
/** | ||
* This function adds input rules to the editor. | ||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#input-rules | ||
* @example | ||
* addInputRules() { | ||
* return [ | ||
* markInputRule({ | ||
* find: inputRegex, | ||
* type: this.type, | ||
* }), | ||
* ] | ||
* }, | ||
*/ | ||
addInputRules?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addInputRules'] | ||
}) => InputRule[] | ||
/** | ||
* This function adds paste rules to the editor. | ||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#paste-rules | ||
* @example | ||
* addPasteRules() { | ||
* return [ | ||
* markPasteRule({ | ||
* find: pasteRegex, | ||
* type: this.type, | ||
* }), | ||
* ] | ||
* }, | ||
*/ | ||
addPasteRules?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addPasteRules'] | ||
}) => PasteRule[] | ||
/** | ||
* This function adds Prosemirror plugins to the editor | ||
* @see https://tiptap.dev/docs/editor/guide/custom-extensions#prosemirror-plugins | ||
* @example | ||
* addProseMirrorPlugins() { | ||
* return [ | ||
* customPlugin(), | ||
* ] | ||
* } | ||
*/ | ||
addProseMirrorPlugins?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addProseMirrorPlugins'] | ||
}) => Plugin[] | ||
/** | ||
* This function adds additional extensions to the editor. This is useful for | ||
* building extension kits. | ||
* @example | ||
* addExtensions() { | ||
* return [ | ||
* BulletList, | ||
* OrderedList, | ||
* ListItem | ||
* ] | ||
* } | ||
*/ | ||
addExtensions?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['addExtensions'] | ||
}) => Extensions | ||
/** | ||
* This function extends the schema of the node. | ||
* @example | ||
* extendNodeSchema() { | ||
* return { | ||
* group: 'inline', | ||
* selectable: false, | ||
* } | ||
* } | ||
*/ | ||
extendNodeSchema?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['extendNodeSchema'] | ||
}, | ||
extension: Node, | ||
) => Record<string, any>) | ||
| null | ||
/** | ||
* This function extends the schema of the mark. | ||
* @example | ||
* extendMarkSchema() { | ||
* return { | ||
* group: 'inline', | ||
* selectable: false, | ||
* } | ||
* } | ||
*/ | ||
extendMarkSchema?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['extendMarkSchema'] | ||
}, | ||
extension: Mark, | ||
) => Record<string, any>) | ||
| null | ||
/** | ||
* The editor is not ready yet. | ||
*/ | ||
onBeforeCreate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onBeforeCreate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The editor is ready. | ||
*/ | ||
onCreate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onCreate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The content has changed. | ||
*/ | ||
onUpdate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onUpdate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The selection has changed. | ||
*/ | ||
onSelectionUpdate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onSelectionUpdate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The editor state has changed. | ||
*/ | ||
onTransaction?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onTransaction'] | ||
}, | ||
props: { | ||
editor: Editor | ||
transaction: Transaction | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor is focused. | ||
*/ | ||
onFocus?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onFocus'] | ||
}, | ||
props: { | ||
event: FocusEvent | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor isn’t focused anymore. | ||
*/ | ||
onBlur?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onBlur'] | ||
}, | ||
props: { | ||
event: FocusEvent | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor is destroyed. | ||
*/ | ||
onDestroy?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
parent: ParentConfig<ExtensionConfig<Options, Storage>>['onDestroy'] | ||
}) => void) | ||
| null | ||
} | ||
} | ||
/** | ||
@@ -396,108 +11,8 @@ * The Extension class is the base class for all extensions. | ||
*/ | ||
export class Extension<Options = any, Storage = any> { | ||
export class Extension<Options = any, Storage = any> extends Extendable<Options, Storage> { | ||
type = 'extension' | ||
name = 'extension' | ||
parent: Extension | null = null | ||
child: Extension | null = null | ||
options: Options | ||
storage: Storage | ||
config: ExtensionConfig = { | ||
name: this.name, | ||
defaultOptions: {}, | ||
} | ||
constructor(config: Partial<ExtensionConfig<Options, Storage>> = {}) { | ||
this.config = { | ||
...this.config, | ||
...config, | ||
} | ||
this.name = this.config.name | ||
if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) { | ||
console.warn( | ||
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`, | ||
) | ||
} | ||
// TODO: remove `addOptions` fallback | ||
this.options = this.config.defaultOptions | ||
if (this.config.addOptions) { | ||
this.options = callOrReturn( | ||
getExtensionField<AnyConfig['addOptions']>(this, 'addOptions', { | ||
name: this.name, | ||
}), | ||
) | ||
} | ||
this.storage = | ||
callOrReturn( | ||
getExtensionField<AnyConfig['addStorage']>(this, 'addStorage', { | ||
name: this.name, | ||
options: this.options, | ||
}), | ||
) || {} | ||
} | ||
static create<O = any, S = any>(config: Partial<ExtensionConfig<O, S>> = {}) { | ||
return new Extension<O, S>(config) | ||
} | ||
configure(options: Partial<Options> = {}) { | ||
// return a new instance so we can use the same extension | ||
// with different calls of `configure` | ||
const extension = this.extend<Options, Storage>({ | ||
...this.config, | ||
addOptions: () => { | ||
return mergeDeep(this.options as Record<string, any>, options) as Options | ||
}, | ||
}) | ||
// Always preserve the current name | ||
extension.name = this.name | ||
// Set the parent to be our parent | ||
extension.parent = this.parent | ||
return extension | ||
} | ||
extend<ExtendedOptions = Options, ExtendedStorage = Storage>( | ||
extendedConfig: Partial<ExtensionConfig<ExtendedOptions, ExtendedStorage>> = {}, | ||
) { | ||
const extension = new Extension<ExtendedOptions, ExtendedStorage>({ ...this.config, ...extendedConfig }) | ||
extension.parent = this | ||
this.child = extension | ||
extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name | ||
if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { | ||
console.warn( | ||
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`, | ||
) | ||
} | ||
extension.options = callOrReturn( | ||
getExtensionField<AnyConfig['addOptions']>(extension, 'addOptions', { | ||
name: extension.name, | ||
}), | ||
) | ||
extension.storage = callOrReturn( | ||
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', { | ||
name: extension.name, | ||
options: extension.options, | ||
}), | ||
) | ||
return extension | ||
} | ||
} |
import { keymap } from '@tiptap/pm/keymap' | ||
import { Schema } from '@tiptap/pm/model' | ||
import { Plugin } from '@tiptap/pm/state' | ||
import { NodeViewConstructor } from '@tiptap/pm/view' | ||
import { MarkViewConstructor, NodeViewConstructor } from '@tiptap/pm/view' | ||
@@ -20,3 +20,3 @@ import type { Editor } from './Editor.js' | ||
} from './helpers/index.js' | ||
import type { NodeConfig } from './index.js' | ||
import { type MarkConfig, type NodeConfig, type Storage, getMarkType } from './index.js' | ||
import { InputRule, inputRulesPlugin } from './InputRule.js' | ||
@@ -59,3 +59,3 @@ import { Mark } from './Mark.js' | ||
options: extension.options, | ||
storage: extension.storage, | ||
storage: this.editor.extensionStorage[extension.name as keyof Storage], | ||
editor: this.editor, | ||
@@ -100,3 +100,3 @@ type: getSchemaTypeByName(extension.name, this.schema), | ||
options: extension.options, | ||
storage: extension.storage, | ||
storage: this.editor.extensionStorage[extension.name as keyof Storage], | ||
editor, | ||
@@ -117,3 +117,3 @@ type: getSchemaTypeByName(extension.name, this.schema), | ||
// bind exit handling | ||
if (extension.type === 'mark' && getExtensionField<AnyConfig['exitable']>(extension, 'exitable', context)) { | ||
if (extension.type === 'mark' && getExtensionField<MarkConfig['exitable']>(extension, 'exitable', context)) { | ||
defaultBindings.ArrowRight = () => Mark.handleExit({ editor, mark: extension as Mark }) | ||
@@ -201,3 +201,3 @@ } | ||
options: extension.options, | ||
storage: extension.storage, | ||
storage: this.editor.extensionStorage[extension.name as keyof Storage], | ||
editor, | ||
@@ -234,2 +234,44 @@ type: getNodeType(extension.name, this.schema), | ||
get markViews(): Record<string, MarkViewConstructor> { | ||
const { editor } = this | ||
const { markExtensions } = splitExtensions(this.extensions) | ||
return Object.fromEntries( | ||
markExtensions | ||
.filter(extension => !!getExtensionField(extension, 'addMarkView')) | ||
.map(extension => { | ||
const extensionAttributes = this.attributes.filter(attribute => attribute.type === extension.name) | ||
const context = { | ||
name: extension.name, | ||
options: extension.options, | ||
storage: this.editor.extensionStorage[extension.name as keyof Storage], | ||
editor, | ||
type: getMarkType(extension.name, this.schema), | ||
} | ||
const addMarkView = getExtensionField<MarkConfig['addMarkView']>(extension, 'addMarkView', context) | ||
if (!addMarkView) { | ||
return [] | ||
} | ||
const markView: MarkViewConstructor = (mark, view, inline) => { | ||
const HTMLAttributes = getRenderedAttributes(mark, extensionAttributes) | ||
return addMarkView()({ | ||
// pass-through | ||
mark, | ||
view, | ||
inline, | ||
// tiptap-specific | ||
editor, | ||
extension, | ||
HTMLAttributes, | ||
}) | ||
} | ||
return [extension.name, markView] | ||
}), | ||
) | ||
} | ||
/** | ||
@@ -240,10 +282,13 @@ * Go through all extensions, create extension storages & setup marks | ||
private setupExtensions() { | ||
this.extensions.forEach(extension => { | ||
// store extension storage in editor | ||
this.editor.extensionStorage[extension.name] = extension.storage | ||
const extensions = this.extensions | ||
// re-initialize the extension storage object instance | ||
this.editor.extensionStorage = Object.fromEntries( | ||
extensions.map(extension => [extension.name, extension.storage]), | ||
) as unknown as Storage | ||
extensions.forEach(extension => { | ||
const context = { | ||
name: extension.name, | ||
options: extension.options, | ||
storage: extension.storage, | ||
storage: this.editor.extensionStorage[extension.name as keyof Storage], | ||
editor: this.editor, | ||
@@ -250,0 +295,0 @@ type: getSchemaTypeByName(extension.name, this.schema), |
export { ClipboardTextSerializer } from './clipboardTextSerializer.js' | ||
export { Commands } from './commands.js' | ||
export { Delete } from './delete.js' | ||
export { Drop } from './drop.js' | ||
@@ -4,0 +5,0 @@ export { Editable } from './editable.js' |
@@ -112,2 +112,6 @@ import { Plugin, PluginKey, Selection } from '@tiptap/pm/state' | ||
appendTransaction: (transactions, oldState, newState) => { | ||
if (transactions.some(tr => tr.getMeta('composition'))) { | ||
return | ||
} | ||
const docChanges = transactions.some(transaction => transaction.docChanged) && !oldState.doc.eq(newState.doc) | ||
@@ -114,0 +118,0 @@ |
@@ -0,1 +1,4 @@ | ||
import { ExtensionConfig } from '../Extension.js' | ||
import { MarkConfig } from '../Mark.js' | ||
import { NodeConfig } from '../Node.js' | ||
import { AnyExtension, MaybeThisParameterType, RemoveThis } from '../types.js' | ||
@@ -10,13 +13,13 @@ | ||
*/ | ||
export function getExtensionField<T = any>( | ||
extension: AnyExtension, | ||
field: string, | ||
export function getExtensionField<T = any, E extends AnyExtension = any>( | ||
extension: E, | ||
field: keyof ExtensionConfig | keyof MarkConfig | keyof NodeConfig, | ||
context?: Omit<MaybeThisParameterType<T>, 'parent'>, | ||
): RemoveThis<T> { | ||
if (extension.config[field] === undefined && extension.parent) { | ||
if (extension.config[field as keyof typeof extension.config] === undefined && extension.parent) { | ||
return getExtensionField(extension.parent, field, context) | ||
} | ||
if (typeof extension.config[field] === 'function') { | ||
const value = extension.config[field].bind({ | ||
if (typeof extension.config[field as keyof typeof extension.config] === 'function') { | ||
const value = (extension.config[field as keyof typeof extension.config] as any).bind({ | ||
...context, | ||
@@ -29,3 +32,3 @@ parent: extension.parent ? getExtensionField(extension.parent, field, context) : null, | ||
return extension.config[field] | ||
return extension.config[field as keyof typeof extension.config] as RemoveThis<T> | ||
} |
@@ -8,3 +8,5 @@ export * from './CommandManager.js' | ||
export * from './inputRules/index.js' | ||
export { createElement, Fragment, createElement as h } from './jsx-runtime.js' | ||
export * from './Mark.js' | ||
export * from './MarkView.js' | ||
export * from './Node.js' | ||
@@ -23,8 +25,2 @@ export * from './NodePos.js' | ||
// eslint-disable-next-line | ||
export interface ExtensionConfig<Options = any, Storage = any> {} | ||
// eslint-disable-next-line | ||
export interface NodeConfig<Options = any, Storage = any> {} | ||
// eslint-disable-next-line | ||
export interface MarkConfig<Options = any, Storage = any> {} | ||
export interface Storage {} |
736
src/Mark.ts
@@ -1,528 +0,140 @@ | ||
import { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType } from '@tiptap/pm/model' | ||
import { Plugin, Transaction } from '@tiptap/pm/state' | ||
import type { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType } from '@tiptap/pm/model' | ||
import { Editor } from './Editor.js' | ||
import { getExtensionField } from './helpers/getExtensionField.js' | ||
import { MarkConfig } from './index.js' | ||
import { InputRule } from './InputRule.js' | ||
import { Node } from './Node.js' | ||
import { PasteRule } from './PasteRule.js' | ||
import { | ||
AnyConfig, | ||
Attributes, | ||
Extensions, | ||
GlobalAttributes, | ||
KeyboardShortcutCommand, | ||
ParentConfig, | ||
RawCommands, | ||
} from './types.js' | ||
import { callOrReturn } from './utilities/callOrReturn.js' | ||
import { mergeDeep } from './utilities/mergeDeep.js' | ||
import type { Editor } from './Editor.js' | ||
import type { ExtendableConfig } from './Extendable.js' | ||
import { Extendable } from './Extendable.js' | ||
import type { Attributes, MarkViewRenderer, ParentConfig } from './types.js' | ||
declare module '@tiptap/core' { | ||
export interface MarkConfig<Options = any, Storage = any> { | ||
// @ts-ignore - this is a dynamic key | ||
[key: string]: any | ||
export interface MarkConfig<Options = any, Storage = any> | ||
extends ExtendableConfig<Options, Storage, MarkConfig<Options, Storage>, MarkType> { | ||
/** | ||
* Mark View | ||
*/ | ||
addMarkView?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addMarkView'] | ||
}) => MarkViewRenderer) | ||
| null | ||
/** | ||
* The extension name - this must be unique. | ||
* It will be used to identify the extension. | ||
* | ||
* @example 'myExtension' | ||
*/ | ||
name: string | ||
/** | ||
* Keep mark after split node | ||
*/ | ||
keepOnSplit?: boolean | (() => boolean) | ||
/** | ||
* The priority of your extension. The higher, the earlier it will be called | ||
* and will take precedence over other extensions with a lower priority. | ||
* @default 100 | ||
* @example 101 | ||
*/ | ||
priority?: number | ||
/** | ||
* Inclusive | ||
*/ | ||
inclusive?: | ||
| MarkSpec['inclusive'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['inclusive'] | ||
editor?: Editor | ||
}) => MarkSpec['inclusive']) | ||
/** | ||
* The default options for this extension. | ||
* @example | ||
* defaultOptions: { | ||
* myOption: 'foo', | ||
* myOtherOption: 10, | ||
* } | ||
*/ | ||
defaultOptions?: Options | ||
/** | ||
* Excludes | ||
*/ | ||
excludes?: | ||
| MarkSpec['excludes'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'] | ||
editor?: Editor | ||
}) => MarkSpec['excludes']) | ||
/** | ||
* This method will add options to this extension | ||
* @see https://tiptap.dev/guide/custom-extensions#settings | ||
* @example | ||
* addOptions() { | ||
* return { | ||
* myOption: 'foo', | ||
* myOtherOption: 10, | ||
* } | ||
*/ | ||
addOptions?: (this: { | ||
name: string | ||
parent: Exclude<ParentConfig<MarkConfig<Options, Storage>>['addOptions'], undefined> | ||
}) => Options | ||
/** | ||
* Marks this Mark as exitable | ||
*/ | ||
exitable?: boolean | (() => boolean) | ||
/** | ||
* The default storage this extension can save data to. | ||
* @see https://tiptap.dev/guide/custom-extensions#storage | ||
* @example | ||
* defaultStorage: { | ||
* prefetchedUsers: [], | ||
* loading: false, | ||
* } | ||
*/ | ||
addStorage?: (this: { | ||
name: string | ||
options: Options | ||
parent: Exclude<ParentConfig<MarkConfig<Options, Storage>>['addStorage'], undefined> | ||
}) => Storage | ||
/** | ||
* Group | ||
*/ | ||
group?: | ||
| MarkSpec['group'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['group'] | ||
editor?: Editor | ||
}) => MarkSpec['group']) | ||
/** | ||
* This function adds globalAttributes to specific nodes. | ||
* @see https://tiptap.dev/guide/custom-extensions#global-attributes | ||
* @example | ||
* addGlobalAttributes() { | ||
* return [ | ||
* { | ||
// Extend the following extensions | ||
* types: [ | ||
* 'heading', | ||
* 'paragraph', | ||
* ], | ||
* // … with those attributes | ||
* attributes: { | ||
* textAlign: { | ||
* default: 'left', | ||
* renderHTML: attributes => ({ | ||
* style: `text-align: ${attributes.textAlign}`, | ||
* }), | ||
* parseHTML: element => element.style.textAlign || 'left', | ||
* }, | ||
* }, | ||
* }, | ||
* ] | ||
* } | ||
*/ | ||
addGlobalAttributes?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
extensions: (Node | Mark)[] | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addGlobalAttributes'] | ||
}) => GlobalAttributes | ||
/** | ||
* Spanning | ||
*/ | ||
spanning?: | ||
| MarkSpec['spanning'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['spanning'] | ||
editor?: Editor | ||
}) => MarkSpec['spanning']) | ||
/** | ||
* This function adds commands to the editor | ||
* @see https://tiptap.dev/guide/custom-extensions#keyboard-shortcuts | ||
* @example | ||
* addCommands() { | ||
* return { | ||
* myCommand: () => ({ chain }) => chain().setMark('type', 'foo').run(), | ||
* } | ||
* } | ||
*/ | ||
addCommands?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addCommands'] | ||
}) => Partial<RawCommands> | ||
/** | ||
* Code | ||
*/ | ||
code?: | ||
| boolean | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['code'] | ||
editor?: Editor | ||
}) => boolean) | ||
/** | ||
* This function registers keyboard shortcuts. | ||
* @see https://tiptap.dev/guide/custom-extensions#keyboard-shortcuts | ||
* @example | ||
* addKeyboardShortcuts() { | ||
* return { | ||
* 'Mod-l': () => this.editor.commands.toggleBulletList(), | ||
* } | ||
* }, | ||
*/ | ||
addKeyboardShortcuts?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addKeyboardShortcuts'] | ||
}) => { | ||
[key: string]: KeyboardShortcutCommand | ||
} | ||
/** | ||
* Parse HTML | ||
*/ | ||
parseHTML?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['parseHTML'] | ||
editor?: Editor | ||
}) => MarkSpec['parseDOM'] | ||
/** | ||
* This function adds input rules to the editor. | ||
* @see https://tiptap.dev/guide/custom-extensions#input-rules | ||
* @example | ||
* addInputRules() { | ||
* return [ | ||
* markInputRule({ | ||
* find: inputRegex, | ||
* type: this.type, | ||
* }), | ||
* ] | ||
* }, | ||
*/ | ||
addInputRules?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addInputRules'] | ||
}) => InputRule[] | ||
/** | ||
* This function adds paste rules to the editor. | ||
* @see https://tiptap.dev/guide/custom-extensions#paste-rules | ||
* @example | ||
* addPasteRules() { | ||
* return [ | ||
* markPasteRule({ | ||
* find: pasteRegex, | ||
* type: this.type, | ||
* }), | ||
* ] | ||
* }, | ||
*/ | ||
addPasteRules?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addPasteRules'] | ||
}) => PasteRule[] | ||
/** | ||
* This function adds Prosemirror plugins to the editor | ||
* @see https://tiptap.dev/guide/custom-extensions#prosemirror-plugins | ||
* @example | ||
* addProseMirrorPlugins() { | ||
* return [ | ||
* customPlugin(), | ||
* ] | ||
* } | ||
*/ | ||
addProseMirrorPlugins?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addProseMirrorPlugins'] | ||
}) => Plugin[] | ||
/** | ||
* This function adds additional extensions to the editor. This is useful for | ||
* building extension kits. | ||
* @example | ||
* addExtensions() { | ||
* return [ | ||
* BulletList, | ||
* OrderedList, | ||
* ListItem | ||
* ] | ||
* } | ||
*/ | ||
addExtensions?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addExtensions'] | ||
}) => Extensions | ||
/** | ||
* This function extends the schema of the node. | ||
* @example | ||
* extendNodeSchema() { | ||
* return { | ||
* group: 'inline', | ||
* selectable: false, | ||
* } | ||
* } | ||
*/ | ||
extendNodeSchema?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['extendNodeSchema'] | ||
}, | ||
extension: Node, | ||
) => Record<string, any>) | ||
| null | ||
/** | ||
* This function extends the schema of the mark. | ||
* @example | ||
* extendMarkSchema() { | ||
* return { | ||
* group: 'inline', | ||
* selectable: false, | ||
* } | ||
* } | ||
*/ | ||
extendMarkSchema?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['extendMarkSchema'] | ||
}, | ||
extension: Mark, | ||
) => Record<string, any>) | ||
| null | ||
/** | ||
* The editor is not ready yet. | ||
*/ | ||
onBeforeCreate?: | ||
| ((this: { | ||
/** | ||
* Render HTML | ||
*/ | ||
renderHTML?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['onBeforeCreate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The editor is ready. | ||
*/ | ||
onCreate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['onCreate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The content has changed. | ||
*/ | ||
onUpdate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['onUpdate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The selection has changed. | ||
*/ | ||
onSelectionUpdate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['onSelectionUpdate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The editor state has changed. | ||
*/ | ||
onTransaction?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['onTransaction'] | ||
}, | ||
props: { | ||
editor: Editor | ||
transaction: Transaction | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor is focused. | ||
*/ | ||
onFocus?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['onFocus'] | ||
}, | ||
props: { | ||
event: FocusEvent | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor isn’t focused anymore. | ||
*/ | ||
onBlur?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['onBlur'] | ||
}, | ||
props: { | ||
event: FocusEvent | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor is destroyed. | ||
*/ | ||
onDestroy?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: MarkType | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['onDestroy'] | ||
}) => void) | ||
| null | ||
/** | ||
* Keep mark after split node | ||
*/ | ||
keepOnSplit?: boolean | (() => boolean) | ||
/** | ||
* Inclusive | ||
*/ | ||
inclusive?: | ||
| MarkSpec['inclusive'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['inclusive'] | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['renderHTML'] | ||
editor?: Editor | ||
}) => MarkSpec['inclusive']) | ||
}, | ||
props: { | ||
mark: ProseMirrorMark | ||
HTMLAttributes: Record<string, any> | ||
}, | ||
) => DOMOutputSpec) | ||
| null | ||
/** | ||
* Excludes | ||
*/ | ||
excludes?: | ||
| MarkSpec['excludes'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'] | ||
editor?: Editor | ||
}) => MarkSpec['excludes']) | ||
/** | ||
* Marks this Mark as exitable | ||
*/ | ||
exitable?: boolean | (() => boolean) | ||
/** | ||
* Group | ||
*/ | ||
group?: | ||
| MarkSpec['group'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['group'] | ||
editor?: Editor | ||
}) => MarkSpec['group']) | ||
/** | ||
* Spanning | ||
*/ | ||
spanning?: | ||
| MarkSpec['spanning'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['spanning'] | ||
editor?: Editor | ||
}) => MarkSpec['spanning']) | ||
/** | ||
* Code | ||
*/ | ||
code?: | ||
| boolean | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['code'] | ||
editor?: Editor | ||
}) => boolean) | ||
/** | ||
* Parse HTML | ||
*/ | ||
parseHTML?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['parseHTML'] | ||
editor?: Editor | ||
}) => MarkSpec['parseDOM'] | ||
/** | ||
* Render HTML | ||
*/ | ||
renderHTML?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['renderHTML'] | ||
editor?: Editor | ||
}, | ||
props: { | ||
mark: ProseMirrorMark | ||
HTMLAttributes: Record<string, any> | ||
}, | ||
) => DOMOutputSpec) | ||
| null | ||
/** | ||
* Attributes | ||
*/ | ||
addAttributes?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addAttributes'] | ||
editor?: Editor | ||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
}) => Attributes | {} | ||
} | ||
/** | ||
* Attributes | ||
*/ | ||
addAttributes?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<MarkConfig<Options, Storage>>['addAttributes'] | ||
editor?: Editor | ||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
}) => Attributes | {} | ||
} | ||
@@ -534,54 +146,5 @@ | ||
*/ | ||
export class Mark<Options = any, Storage = any> { | ||
export class Mark<Options = any, Storage = any> extends Extendable<Options, Storage> { | ||
type = 'mark' | ||
name = 'mark' | ||
parent: Mark | null = null | ||
child: Mark | null = null | ||
options: Options | ||
storage: Storage | ||
config: MarkConfig = { | ||
name: this.name, | ||
defaultOptions: {}, | ||
} | ||
constructor(config: Partial<MarkConfig<Options, Storage>> = {}) { | ||
this.config = { | ||
...this.config, | ||
...config, | ||
} | ||
this.name = this.config.name | ||
if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) { | ||
console.warn( | ||
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`, | ||
) | ||
} | ||
// TODO: remove `addOptions` fallback | ||
this.options = this.config.defaultOptions | ||
if (this.config.addOptions) { | ||
this.options = callOrReturn( | ||
getExtensionField<AnyConfig['addOptions']>(this, 'addOptions', { | ||
name: this.name, | ||
}), | ||
) | ||
} | ||
this.storage = | ||
callOrReturn( | ||
getExtensionField<AnyConfig['addStorage']>(this, 'addStorage', { | ||
name: this.name, | ||
options: this.options, | ||
}), | ||
) || {} | ||
} | ||
static create<O = any, S = any>(config: Partial<MarkConfig<O, S>> = {}) { | ||
@@ -591,53 +154,2 @@ return new Mark<O, S>(config) | ||
configure(options: Partial<Options> = {}) { | ||
// return a new instance so we can use the same extension | ||
// with different calls of `configure` | ||
const extension = this.extend<Options, Storage>({ | ||
...this.config, | ||
addOptions: () => { | ||
return mergeDeep(this.options as Record<string, any>, options) as Options | ||
}, | ||
}) | ||
// Always preserve the current name | ||
extension.name = this.name | ||
// Set the parent to be our parent | ||
extension.parent = this.parent | ||
return extension | ||
} | ||
extend<ExtendedOptions = Options, ExtendedStorage = Storage>( | ||
extendedConfig: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>> = {}, | ||
) { | ||
const extension = new Mark<ExtendedOptions, ExtendedStorage>(extendedConfig) | ||
extension.parent = this | ||
this.child = extension | ||
extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name | ||
if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { | ||
console.warn( | ||
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`, | ||
) | ||
} | ||
extension.options = callOrReturn( | ||
getExtensionField<AnyConfig['addOptions']>(extension, 'addOptions', { | ||
name: extension.name, | ||
}), | ||
) | ||
extension.storage = callOrReturn( | ||
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', { | ||
name: extension.name, | ||
options: extension.options, | ||
}), | ||
) | ||
return extension | ||
} | ||
static handleExit({ editor, mark }: { editor: Editor; mark: Mark }) { | ||
@@ -644,0 +156,0 @@ const { tr } = editor.state |
1118
src/Node.ts
@@ -1,738 +0,334 @@ | ||
import { DOMOutputSpec, Node as ProseMirrorNode, NodeSpec, NodeType } from '@tiptap/pm/model' | ||
import { Plugin, Transaction } from '@tiptap/pm/state' | ||
import type { DOMOutputSpec, Node as ProseMirrorNode, NodeSpec, NodeType } from '@tiptap/pm/model' | ||
import { Editor } from './Editor.js' | ||
import { getExtensionField } from './helpers/getExtensionField.js' | ||
import { NodeConfig } from './index.js' | ||
import { InputRule } from './InputRule.js' | ||
import { Mark } from './Mark.js' | ||
import { PasteRule } from './PasteRule.js' | ||
import { | ||
AnyConfig, | ||
Attributes, | ||
Extensions, | ||
GlobalAttributes, | ||
KeyboardShortcutCommand, | ||
NodeViewRenderer, | ||
ParentConfig, | ||
RawCommands, | ||
} from './types.js' | ||
import { callOrReturn } from './utilities/callOrReturn.js' | ||
import { mergeDeep } from './utilities/mergeDeep.js' | ||
import type { Editor } from './Editor.js' | ||
import type { ExtendableConfig } from './Extendable.js' | ||
import { Extendable } from './Extendable.js' | ||
import type { Attributes, NodeViewRenderer, ParentConfig } from './types.js' | ||
declare module '@tiptap/core' { | ||
interface NodeConfig<Options = any, Storage = any> { | ||
// @ts-ignore - this is a dynamic key | ||
[key: string]: any | ||
export interface NodeConfig<Options = any, Storage = any> | ||
extends ExtendableConfig<Options, Storage, NodeConfig<Options, Storage>, NodeType> { | ||
/** | ||
* Node View | ||
*/ | ||
addNodeView?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addNodeView'] | ||
}) => NodeViewRenderer) | ||
| null | ||
/** | ||
* The extension name - this must be unique. | ||
* It will be used to identify the extension. | ||
* | ||
* @example 'myExtension' | ||
*/ | ||
name: string | ||
/** | ||
* Defines if this node should be a top level node (doc) | ||
* @default false | ||
* @example true | ||
*/ | ||
topNode?: boolean | ||
/** | ||
* The priority of your extension. The higher, the earlier it will be called | ||
* and will take precedence over other extensions with a lower priority. | ||
* @default 100 | ||
* @example 101 | ||
*/ | ||
priority?: number | ||
/** | ||
* The content expression for this node, as described in the [schema | ||
* guide](/docs/guide/#schema.content_expressions). When not given, | ||
* the node does not allow any content. | ||
* | ||
* You can read more about it on the Prosemirror documentation here | ||
* @see https://prosemirror.net/docs/guide/#schema.content_expressions | ||
* @default undefined | ||
* @example content: 'block+' | ||
* @example content: 'headline paragraph block*' | ||
*/ | ||
content?: | ||
| NodeSpec['content'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['content'] | ||
editor?: Editor | ||
}) => NodeSpec['content']) | ||
/** | ||
* The default options for this extension. | ||
* @example | ||
* defaultOptions: { | ||
* myOption: 'foo', | ||
* myOtherOption: 10, | ||
* } | ||
*/ | ||
defaultOptions?: Options | ||
/** | ||
* The marks that are allowed inside of this node. May be a | ||
* space-separated string referring to mark names or groups, `"_"` | ||
* to explicitly allow all marks, or `""` to disallow marks. When | ||
* not given, nodes with inline content default to allowing all | ||
* marks, other nodes default to not allowing marks. | ||
* | ||
* @example marks: 'strong em' | ||
*/ | ||
marks?: | ||
| NodeSpec['marks'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['marks'] | ||
editor?: Editor | ||
}) => NodeSpec['marks']) | ||
/** | ||
* This method will add options to this extension | ||
* @see https://tiptap.dev/guide/custom-extensions#settings | ||
* @example | ||
* addOptions() { | ||
* return { | ||
* myOption: 'foo', | ||
* myOtherOption: 10, | ||
* } | ||
*/ | ||
addOptions?: (this: { | ||
name: string | ||
parent: Exclude<ParentConfig<NodeConfig<Options, Storage>>['addOptions'], undefined> | ||
}) => Options | ||
/** | ||
* The group or space-separated groups to which this node belongs, | ||
* which can be referred to in the content expressions for the | ||
* schema. | ||
* | ||
* By default Tiptap uses the groups 'block' and 'inline' for nodes. You | ||
* can also use custom groups if you want to group specific nodes together | ||
* and handle them in your schema. | ||
* @example group: 'block' | ||
* @example group: 'inline' | ||
* @example group: 'customBlock' // this uses a custom group | ||
*/ | ||
group?: | ||
| NodeSpec['group'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['group'] | ||
editor?: Editor | ||
}) => NodeSpec['group']) | ||
/** | ||
* The default storage this extension can save data to. | ||
* @see https://tiptap.dev/guide/custom-extensions#storage | ||
* @example | ||
* defaultStorage: { | ||
* prefetchedUsers: [], | ||
* loading: false, | ||
* } | ||
*/ | ||
addStorage?: (this: { | ||
name: string | ||
options: Options | ||
parent: Exclude<ParentConfig<NodeConfig<Options, Storage>>['addStorage'], undefined> | ||
}) => Storage | ||
/** | ||
* Should be set to true for inline nodes. (Implied for text nodes.) | ||
*/ | ||
inline?: | ||
| NodeSpec['inline'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['inline'] | ||
editor?: Editor | ||
}) => NodeSpec['inline']) | ||
/** | ||
* This function adds globalAttributes to specific nodes. | ||
* @see https://tiptap.dev/guide/custom-extensions#global-attributes | ||
* @example | ||
* addGlobalAttributes() { | ||
* return [ | ||
* { | ||
// Extend the following extensions | ||
* types: [ | ||
* 'heading', | ||
* 'paragraph', | ||
* ], | ||
* // … with those attributes | ||
* attributes: { | ||
* textAlign: { | ||
* default: 'left', | ||
* renderHTML: attributes => ({ | ||
* style: `text-align: ${attributes.textAlign}`, | ||
* }), | ||
* parseHTML: element => element.style.textAlign || 'left', | ||
* }, | ||
* }, | ||
* }, | ||
* ] | ||
* } | ||
*/ | ||
addGlobalAttributes?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
extensions: (Node | Mark)[] | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addGlobalAttributes'] | ||
}) => GlobalAttributes | ||
/** | ||
* Can be set to true to indicate that, though this isn't a [leaf | ||
* node](https://prosemirror.net/docs/ref/#model.NodeType.isLeaf), it doesn't have directly editable | ||
* content and should be treated as a single unit in the view. | ||
* | ||
* @example atom: true | ||
*/ | ||
atom?: | ||
| NodeSpec['atom'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['atom'] | ||
editor?: Editor | ||
}) => NodeSpec['atom']) | ||
/** | ||
* This function adds commands to the editor | ||
* @see https://tiptap.dev/guide/custom-extensions#keyboard-shortcuts | ||
* @example | ||
* addCommands() { | ||
* return { | ||
* myCommand: () => ({ chain }) => chain().setMark('type', 'foo').run(), | ||
* } | ||
* } | ||
*/ | ||
addCommands?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addCommands'] | ||
}) => Partial<RawCommands> | ||
/** | ||
* Controls whether nodes of this type can be selected as a [node | ||
* selection](https://prosemirror.net/docs/ref/#state.NodeSelection). Defaults to true for non-text | ||
* nodes. | ||
* | ||
* @default true | ||
* @example selectable: false | ||
*/ | ||
selectable?: | ||
| NodeSpec['selectable'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['selectable'] | ||
editor?: Editor | ||
}) => NodeSpec['selectable']) | ||
/** | ||
* This function registers keyboard shortcuts. | ||
* @see https://tiptap.dev/guide/custom-extensions#keyboard-shortcuts | ||
* @example | ||
* addKeyboardShortcuts() { | ||
* return { | ||
* 'Mod-l': () => this.editor.commands.toggleBulletList(), | ||
* } | ||
* }, | ||
*/ | ||
addKeyboardShortcuts?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addKeyboardShortcuts'] | ||
}) => { | ||
[key: string]: KeyboardShortcutCommand | ||
} | ||
/** | ||
* Determines whether nodes of this type can be dragged without | ||
* being selected. Defaults to false. | ||
* | ||
* @default: false | ||
* @example: draggable: true | ||
*/ | ||
draggable?: | ||
| NodeSpec['draggable'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['draggable'] | ||
editor?: Editor | ||
}) => NodeSpec['draggable']) | ||
/** | ||
* This function adds input rules to the editor. | ||
* @see https://tiptap.dev/guide/custom-extensions#input-rules | ||
* @example | ||
* addInputRules() { | ||
* return [ | ||
* markInputRule({ | ||
* find: inputRegex, | ||
* type: this.type, | ||
* }), | ||
* ] | ||
* }, | ||
*/ | ||
addInputRules?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addInputRules'] | ||
}) => InputRule[] | ||
/** | ||
* Can be used to indicate that this node contains code, which | ||
* causes some commands to behave differently. | ||
*/ | ||
code?: | ||
| NodeSpec['code'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['code'] | ||
editor?: Editor | ||
}) => NodeSpec['code']) | ||
/** | ||
* This function adds paste rules to the editor. | ||
* @see https://tiptap.dev/guide/custom-extensions#paste-rules | ||
* @example | ||
* addPasteRules() { | ||
* return [ | ||
* markPasteRule({ | ||
* find: pasteRegex, | ||
* type: this.type, | ||
* }), | ||
* ] | ||
* }, | ||
*/ | ||
addPasteRules?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addPasteRules'] | ||
}) => PasteRule[] | ||
/** | ||
* Controls way whitespace in this a node is parsed. The default is | ||
* `"normal"`, which causes the [DOM parser](https://prosemirror.net/docs/ref/#model.DOMParser) to | ||
* collapse whitespace in normal mode, and normalize it (replacing | ||
* newlines and such with spaces) otherwise. `"pre"` causes the | ||
* parser to preserve spaces inside the node. When this option isn't | ||
* given, but [`code`](https://prosemirror.net/docs/ref/#model.NodeSpec.code) is true, `whitespace` | ||
* will default to `"pre"`. Note that this option doesn't influence | ||
* the way the node is rendered—that should be handled by `toDOM` | ||
* and/or styling. | ||
*/ | ||
whitespace?: | ||
| NodeSpec['whitespace'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['whitespace'] | ||
editor?: Editor | ||
}) => NodeSpec['whitespace']) | ||
/** | ||
* This function adds Prosemirror plugins to the editor | ||
* @see https://tiptap.dev/guide/custom-extensions#prosemirror-plugins | ||
* @example | ||
* addProseMirrorPlugins() { | ||
* return [ | ||
* customPlugin(), | ||
* ] | ||
* } | ||
*/ | ||
addProseMirrorPlugins?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addProseMirrorPlugins'] | ||
}) => Plugin[] | ||
/** | ||
* Allows a **single** node to be set as linebreak equivalent (e.g. hardBreak). | ||
* When converting between block types that have whitespace set to "pre" | ||
* and don't support the linebreak node (e.g. codeBlock) and other block types | ||
* that do support the linebreak node (e.g. paragraphs) - this node will be used | ||
* as the linebreak instead of stripping the newline. | ||
* | ||
* See [linebreakReplacement](https://prosemirror.net/docs/ref/#model.NodeSpec.linebreakReplacement). | ||
*/ | ||
linebreakReplacement?: | ||
| NodeSpec['linebreakReplacement'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['linebreakReplacement'] | ||
editor?: Editor | ||
}) => NodeSpec['linebreakReplacement']) | ||
/** | ||
* This function adds additional extensions to the editor. This is useful for | ||
* building extension kits. | ||
* @example | ||
* addExtensions() { | ||
* return [ | ||
* BulletList, | ||
* OrderedList, | ||
* ListItem | ||
* ] | ||
* } | ||
*/ | ||
addExtensions?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addExtensions'] | ||
}) => Extensions | ||
/** | ||
* When enabled, enables both | ||
* [`definingAsContext`](https://prosemirror.net/docs/ref/#model.NodeSpec.definingAsContext) and | ||
* [`definingForContent`](https://prosemirror.net/docs/ref/#model.NodeSpec.definingForContent). | ||
* | ||
* @default false | ||
* @example isolating: true | ||
*/ | ||
defining?: | ||
| NodeSpec['defining'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['defining'] | ||
editor?: Editor | ||
}) => NodeSpec['defining']) | ||
/** | ||
* This function extends the schema of the node. | ||
* @example | ||
* extendNodeSchema() { | ||
* return { | ||
* group: 'inline', | ||
* selectable: false, | ||
* } | ||
* } | ||
*/ | ||
extendNodeSchema?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['extendNodeSchema'] | ||
}, | ||
extension: Node, | ||
) => Record<string, any>) | ||
| null | ||
/** | ||
* When enabled (default is false), the sides of nodes of this type | ||
* count as boundaries that regular editing operations, like | ||
* backspacing or lifting, won't cross. An example of a node that | ||
* should probably have this enabled is a table cell. | ||
*/ | ||
isolating?: | ||
| NodeSpec['isolating'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['isolating'] | ||
editor?: Editor | ||
}) => NodeSpec['isolating']) | ||
/** | ||
* This function extends the schema of the mark. | ||
* @example | ||
* extendMarkSchema() { | ||
* return { | ||
* group: 'inline', | ||
* selectable: false, | ||
* } | ||
* } | ||
*/ | ||
extendMarkSchema?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['extendMarkSchema'] | ||
editor?: Editor | ||
}, | ||
extension: Node, | ||
) => Record<string, any>) | ||
| null | ||
/** | ||
* Associates DOM parser information with this node, which can be | ||
* used by [`DOMParser.fromSchema`](https://prosemirror.net/docs/ref/#model.DOMParser^fromSchema) to | ||
* automatically derive a parser. The `node` field in the rules is | ||
* implied (the name of this node will be filled in automatically). | ||
* If you supply your own parser, you do not need to also specify | ||
* parsing rules in your schema. | ||
* | ||
* @example parseHTML: [{ tag: 'div', attrs: { 'data-id': 'my-block' } }] | ||
*/ | ||
parseHTML?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['parseHTML'] | ||
editor?: Editor | ||
}) => NodeSpec['parseDOM'] | ||
/** | ||
* The editor is not ready yet. | ||
*/ | ||
onBeforeCreate?: | ||
| ((this: { | ||
/** | ||
* A description of a DOM structure. Can be either a string, which is | ||
* interpreted as a text node, a DOM node, which is interpreted as | ||
* itself, a `{dom, contentDOM}` object, or an array. | ||
* | ||
* An array describes a DOM element. The first value in the array | ||
* should be a string—the name of the DOM element, optionally prefixed | ||
* by a namespace URL and a space. If the second element is plain | ||
* object, it is interpreted as a set of attributes for the element. | ||
* Any elements after that (including the 2nd if it's not an attribute | ||
* object) are interpreted as children of the DOM elements, and must | ||
* either be valid `DOMOutputSpec` values, or the number zero. | ||
* | ||
* The number zero (pronounced “hole”) is used to indicate the place | ||
* where a node's child nodes should be inserted. If it occurs in an | ||
* output spec, it should be the only child element in its parent | ||
* node. | ||
* | ||
* @example toDOM: ['div[data-id="my-block"]', { class: 'my-block' }, 0] | ||
*/ | ||
renderHTML?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['onBeforeCreate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The editor is ready. | ||
*/ | ||
onCreate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['onCreate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The content has changed. | ||
*/ | ||
onUpdate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['onUpdate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The selection has changed. | ||
*/ | ||
onSelectionUpdate?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['onSelectionUpdate'] | ||
}) => void) | ||
| null | ||
/** | ||
* The editor state has changed. | ||
*/ | ||
onTransaction?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['onTransaction'] | ||
}, | ||
props: { | ||
editor: Editor | ||
transaction: Transaction | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor is focused. | ||
*/ | ||
onFocus?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['onFocus'] | ||
}, | ||
props: { | ||
event: FocusEvent | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor isn’t focused anymore. | ||
*/ | ||
onBlur?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['onBlur'] | ||
}, | ||
props: { | ||
event: FocusEvent | ||
}, | ||
) => void) | ||
| null | ||
/** | ||
* The editor is destroyed. | ||
*/ | ||
onDestroy?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['onDestroy'] | ||
}) => void) | ||
| null | ||
/** | ||
* Node View | ||
*/ | ||
addNodeView?: | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
editor: Editor | ||
type: NodeType | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addNodeView'] | ||
}) => NodeViewRenderer) | ||
| null | ||
/** | ||
* Defines if this node should be a top level node (doc) | ||
* @default false | ||
* @example true | ||
*/ | ||
topNode?: boolean | ||
/** | ||
* The content expression for this node, as described in the [schema | ||
* guide](/docs/guide/#schema.content_expressions). When not given, | ||
* the node does not allow any content. | ||
* | ||
* You can read more about it on the Prosemirror documentation here | ||
* @see https://prosemirror.net/docs/guide/#schema.content_expressions | ||
* @default undefined | ||
* @example content: 'block+' | ||
* @example content: 'headline paragraph block*' | ||
*/ | ||
content?: | ||
| NodeSpec['content'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['content'] | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['renderHTML'] | ||
editor?: Editor | ||
}) => NodeSpec['content']) | ||
}, | ||
props: { | ||
node: ProseMirrorNode | ||
HTMLAttributes: Record<string, any> | ||
}, | ||
) => DOMOutputSpec) | ||
| null | ||
/** | ||
* The marks that are allowed inside of this node. May be a | ||
* space-separated string referring to mark names or groups, `"_"` | ||
* to explicitly allow all marks, or `""` to disallow marks. When | ||
* not given, nodes with inline content default to allowing all | ||
* marks, other nodes default to not allowing marks. | ||
* | ||
* @example marks: 'strong em' | ||
*/ | ||
marks?: | ||
| NodeSpec['marks'] | ||
| ((this: { | ||
/** | ||
* renders the node as text | ||
* @example renderText: () => 'foo | ||
*/ | ||
renderText?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['marks'] | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['renderText'] | ||
editor?: Editor | ||
}) => NodeSpec['marks']) | ||
}, | ||
props: { | ||
node: ProseMirrorNode | ||
pos: number | ||
parent: ProseMirrorNode | ||
index: number | ||
}, | ||
) => string) | ||
| null | ||
/** | ||
* The group or space-separated groups to which this node belongs, | ||
* which can be referred to in the content expressions for the | ||
* schema. | ||
* | ||
* By default Tiptap uses the groups 'block' and 'inline' for nodes. You | ||
* can also use custom groups if you want to group specific nodes together | ||
* and handle them in your schema. | ||
* @example group: 'block' | ||
* @example group: 'inline' | ||
* @example group: 'customBlock' // this uses a custom group | ||
*/ | ||
group?: | ||
| NodeSpec['group'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['group'] | ||
editor?: Editor | ||
}) => NodeSpec['group']) | ||
/** | ||
* Should be set to true for inline nodes. (Implied for text nodes.) | ||
*/ | ||
inline?: | ||
| NodeSpec['inline'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['inline'] | ||
editor?: Editor | ||
}) => NodeSpec['inline']) | ||
/** | ||
* Can be set to true to indicate that, though this isn't a [leaf | ||
* node](https://prosemirror.net/docs/ref/#model.NodeType.isLeaf), it doesn't have directly editable | ||
* content and should be treated as a single unit in the view. | ||
* | ||
* @example atom: true | ||
*/ | ||
atom?: | ||
| NodeSpec['atom'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['atom'] | ||
editor?: Editor | ||
}) => NodeSpec['atom']) | ||
/** | ||
* Controls whether nodes of this type can be selected as a [node | ||
* selection](https://prosemirror.net/docs/ref/#state.NodeSelection). Defaults to true for non-text | ||
* nodes. | ||
* | ||
* @default true | ||
* @example selectable: false | ||
*/ | ||
selectable?: | ||
| NodeSpec['selectable'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['selectable'] | ||
editor?: Editor | ||
}) => NodeSpec['selectable']) | ||
/** | ||
* Determines whether nodes of this type can be dragged without | ||
* being selected. Defaults to false. | ||
* | ||
* @default: false | ||
* @example: draggable: true | ||
*/ | ||
draggable?: | ||
| NodeSpec['draggable'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['draggable'] | ||
editor?: Editor | ||
}) => NodeSpec['draggable']) | ||
/** | ||
* Can be used to indicate that this node contains code, which | ||
* causes some commands to behave differently. | ||
*/ | ||
code?: | ||
| NodeSpec['code'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['code'] | ||
editor?: Editor | ||
}) => NodeSpec['code']) | ||
/** | ||
* Controls way whitespace in this a node is parsed. The default is | ||
* `"normal"`, which causes the [DOM parser](https://prosemirror.net/docs/ref/#model.DOMParser) to | ||
* collapse whitespace in normal mode, and normalize it (replacing | ||
* newlines and such with spaces) otherwise. `"pre"` causes the | ||
* parser to preserve spaces inside the node. When this option isn't | ||
* given, but [`code`](https://prosemirror.net/docs/ref/#model.NodeSpec.code) is true, `whitespace` | ||
* will default to `"pre"`. Note that this option doesn't influence | ||
* the way the node is rendered—that should be handled by `toDOM` | ||
* and/or styling. | ||
*/ | ||
whitespace?: | ||
| NodeSpec['whitespace'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['whitespace'] | ||
editor?: Editor | ||
}) => NodeSpec['whitespace']) | ||
/** | ||
* Allows a **single** node to be set as linebreak equivalent (e.g. hardBreak). | ||
* When converting between block types that have whitespace set to "pre" | ||
* and don't support the linebreak node (e.g. codeBlock) and other block types | ||
* that do support the linebreak node (e.g. paragraphs) - this node will be used | ||
* as the linebreak instead of stripping the newline. | ||
* | ||
* See [linebreakReplacement](https://prosemirror.net/docs/ref/#model.NodeSpec.linebreakReplacement). | ||
*/ | ||
linebreakReplacement?: | ||
| NodeSpec['linebreakReplacement'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['linebreakReplacement'] | ||
editor?: Editor | ||
}) => NodeSpec['linebreakReplacement']) | ||
/** | ||
* When enabled, enables both | ||
* [`definingAsContext`](https://prosemirror.net/docs/ref/#model.NodeSpec.definingAsContext) and | ||
* [`definingForContent`](https://prosemirror.net/docs/ref/#model.NodeSpec.definingForContent). | ||
* | ||
* @default false | ||
* @example isolating: true | ||
*/ | ||
defining?: | ||
| NodeSpec['defining'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['defining'] | ||
editor?: Editor | ||
}) => NodeSpec['defining']) | ||
/** | ||
* When enabled (default is false), the sides of nodes of this type | ||
* count as boundaries that regular editing operations, like | ||
* backspacing or lifting, won't cross. An example of a node that | ||
* should probably have this enabled is a table cell. | ||
*/ | ||
isolating?: | ||
| NodeSpec['isolating'] | ||
| ((this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['isolating'] | ||
editor?: Editor | ||
}) => NodeSpec['isolating']) | ||
/** | ||
* Associates DOM parser information with this node, which can be | ||
* used by [`DOMParser.fromSchema`](https://prosemirror.net/docs/ref/#model.DOMParser^fromSchema) to | ||
* automatically derive a parser. The `node` field in the rules is | ||
* implied (the name of this node will be filled in automatically). | ||
* If you supply your own parser, you do not need to also specify | ||
* parsing rules in your schema. | ||
* | ||
* @example parseHTML: [{ tag: 'div', attrs: { 'data-id': 'my-block' } }] | ||
*/ | ||
parseHTML?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['parseHTML'] | ||
editor?: Editor | ||
}) => NodeSpec['parseDOM'] | ||
/** | ||
* A description of a DOM structure. Can be either a string, which is | ||
* interpreted as a text node, a DOM node, which is interpreted as | ||
* itself, a `{dom, contentDOM}` object, or an array. | ||
* | ||
* An array describes a DOM element. The first value in the array | ||
* should be a string—the name of the DOM element, optionally prefixed | ||
* by a namespace URL and a space. If the second element is plain | ||
* object, it is interpreted as a set of attributes for the element. | ||
* Any elements after that (including the 2nd if it's not an attribute | ||
* object) are interpreted as children of the DOM elements, and must | ||
* either be valid `DOMOutputSpec` values, or the number zero. | ||
* | ||
* The number zero (pronounced “hole”) is used to indicate the place | ||
* where a node's child nodes should be inserted. If it occurs in an | ||
* output spec, it should be the only child element in its parent | ||
* node. | ||
* | ||
* @example toDOM: ['div[data-id="my-block"]', { class: 'my-block' }, 0] | ||
*/ | ||
renderHTML?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['renderHTML'] | ||
editor?: Editor | ||
}, | ||
props: { | ||
node: ProseMirrorNode | ||
HTMLAttributes: Record<string, any> | ||
}, | ||
) => DOMOutputSpec) | ||
| null | ||
/** | ||
* renders the node as text | ||
* @example renderText: () => 'foo | ||
*/ | ||
renderText?: | ||
| (( | ||
this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['renderText'] | ||
editor?: Editor | ||
}, | ||
props: { | ||
node: ProseMirrorNode | ||
pos: number | ||
parent: ProseMirrorNode | ||
index: number | ||
}, | ||
) => string) | ||
| null | ||
/** | ||
* Add attributes to the node | ||
* @example addAttributes: () => ({ class: 'foo' }) | ||
*/ | ||
addAttributes?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addAttributes'] | ||
editor?: Editor | ||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
}) => Attributes | {} | ||
} | ||
/** | ||
* Add attributes to the node | ||
* @example addAttributes: () => ({ class: 'foo' }) | ||
*/ | ||
addAttributes?: (this: { | ||
name: string | ||
options: Options | ||
storage: Storage | ||
parent: ParentConfig<NodeConfig<Options, Storage>>['addAttributes'] | ||
editor?: Editor | ||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
}) => Attributes | {} | ||
} | ||
@@ -744,108 +340,8 @@ | ||
*/ | ||
export class Node<Options = any, Storage = any> { | ||
export class Node<Options = any, Storage = any> extends Extendable<Options, Storage> { | ||
type = 'node' | ||
name = 'node' | ||
parent: Node | null = null | ||
child: Node | null = null | ||
options: Options | ||
storage: Storage | ||
config: NodeConfig = { | ||
name: this.name, | ||
defaultOptions: {}, | ||
} | ||
constructor(config: Partial<NodeConfig<Options, Storage>> = {}) { | ||
this.config = { | ||
...this.config, | ||
...config, | ||
} | ||
this.name = this.config.name | ||
if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) { | ||
console.warn( | ||
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`, | ||
) | ||
} | ||
// TODO: remove `addOptions` fallback | ||
this.options = this.config.defaultOptions | ||
if (this.config.addOptions) { | ||
this.options = callOrReturn( | ||
getExtensionField<AnyConfig['addOptions']>(this, 'addOptions', { | ||
name: this.name, | ||
}), | ||
) | ||
} | ||
this.storage = | ||
callOrReturn( | ||
getExtensionField<AnyConfig['addStorage']>(this, 'addStorage', { | ||
name: this.name, | ||
options: this.options, | ||
}), | ||
) || {} | ||
} | ||
static create<O = any, S = any>(config: Partial<NodeConfig<O, S>> = {}) { | ||
return new Node<O, S>(config) | ||
} | ||
configure(options: Partial<Options> = {}) { | ||
// return a new instance so we can use the same extension | ||
// with different calls of `configure` | ||
const extension = this.extend<Options, Storage>({ | ||
...this.config, | ||
addOptions: () => { | ||
return mergeDeep(this.options as Record<string, any>, options) as Options | ||
}, | ||
}) | ||
// Always preserve the current name | ||
extension.name = this.name | ||
// Set the parent to be our parent | ||
extension.parent = this.parent | ||
return extension | ||
} | ||
extend<ExtendedOptions = Options, ExtendedStorage = Storage>( | ||
extendedConfig: Partial<NodeConfig<ExtendedOptions, ExtendedStorage>> = {}, | ||
) { | ||
const extension = new Node<ExtendedOptions, ExtendedStorage>(extendedConfig) | ||
extension.parent = this | ||
this.child = extension | ||
extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name | ||
if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { | ||
console.warn( | ||
`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`, | ||
) | ||
} | ||
extension.options = callOrReturn( | ||
getExtensionField<AnyConfig['addOptions']>(extension, 'addOptions', { | ||
name: extension.name, | ||
}), | ||
) | ||
extension.storage = callOrReturn( | ||
getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', { | ||
name: extension.name, | ||
options: extension.options, | ||
}), | ||
) | ||
return extension | ||
} | ||
} |
353
src/types.ts
import { Mark as ProseMirrorMark, Node as ProseMirrorNode, ParseOptions, Slice } from '@tiptap/pm/model' | ||
import { EditorState, Transaction } from '@tiptap/pm/state' | ||
import { Mappable } from '@tiptap/pm/transform' | ||
import { Mappable, Transform } from '@tiptap/pm/transform' | ||
import { | ||
@@ -9,2 +9,4 @@ Decoration, | ||
EditorView, | ||
MarkView, | ||
MarkViewConstructor, | ||
NodeView, | ||
@@ -41,6 +43,22 @@ NodeViewConstructor, | ||
export interface EditorEvents { | ||
beforeCreate: { editor: Editor } | ||
create: { editor: Editor } | ||
beforeCreate: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
} | ||
create: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
} | ||
contentError: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The error that occurred while parsing the content | ||
*/ | ||
error: Error | ||
@@ -53,11 +71,179 @@ /** | ||
} | ||
update: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[] } | ||
selectionUpdate: { editor: Editor; transaction: Transaction } | ||
beforeTransaction: { editor: Editor; transaction: Transaction; nextState: EditorState } | ||
transaction: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[] } | ||
focus: { editor: Editor; event: FocusEvent; transaction: Transaction } | ||
blur: { editor: Editor; event: FocusEvent; transaction: Transaction } | ||
update: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The transaction that caused the update | ||
*/ | ||
transaction: Transaction | ||
/** | ||
* Appended transactions that were added to the initial transaction by plugins | ||
*/ | ||
appendedTransactions: Transaction[] | ||
} | ||
selectionUpdate: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The transaction that caused the selection update | ||
*/ | ||
transaction: Transaction | ||
} | ||
beforeTransaction: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The transaction that will be applied | ||
*/ | ||
transaction: Transaction | ||
/** | ||
* The next state of the editor after the transaction is applied | ||
*/ | ||
nextState: EditorState | ||
} | ||
transaction: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The initial transaction | ||
*/ | ||
transaction: Transaction | ||
/** | ||
* Appended transactions that were added to the initial transaction by plugins | ||
*/ | ||
appendedTransactions: Transaction[] | ||
} | ||
focus: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The focus event | ||
*/ | ||
event: FocusEvent | ||
/** | ||
* The transaction that caused the focus | ||
*/ | ||
transaction: Transaction | ||
} | ||
blur: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The focus event | ||
*/ | ||
event: FocusEvent | ||
/** | ||
* The transaction that caused the blur | ||
*/ | ||
transaction: Transaction | ||
} | ||
destroy: void | ||
paste: { editor: Editor; event: ClipboardEvent; slice: Slice } | ||
drop: { editor: Editor; event: DragEvent; slice: Slice; moved: boolean } | ||
paste: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The clipboard event | ||
*/ | ||
event: ClipboardEvent | ||
/** | ||
* The slice that was pasted | ||
*/ | ||
slice: Slice | ||
} | ||
drop: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The drag event | ||
*/ | ||
event: DragEvent | ||
/** | ||
* The slice that was dropped | ||
*/ | ||
slice: Slice | ||
/** | ||
* Whether the content was moved (true) or copied (false) | ||
*/ | ||
moved: boolean | ||
} | ||
delete: { | ||
/** | ||
* The editor instance | ||
*/ | ||
editor: Editor | ||
/** | ||
* The range of the deleted content (before the deletion) | ||
*/ | ||
deletedRange: Range | ||
/** | ||
* The new range of positions of where the deleted content was in the new document (after the deletion) | ||
*/ | ||
newRange: Range | ||
/** | ||
* The transaction that caused the deletion | ||
*/ | ||
transaction: Transaction | ||
/** | ||
* The combined transform (including all appended transactions) that caused the deletion | ||
*/ | ||
combinedTransform: Transform | ||
/** | ||
* Whether the deletion was partial (only a part of this content was deleted) | ||
*/ | ||
partial: boolean | ||
/** | ||
* This is the start position of the mark in the document (before the deletion) | ||
*/ | ||
from: number | ||
/** | ||
* This is the end position of the mark in the document (before the deletion) | ||
*/ | ||
to: number | ||
} & ( | ||
| { | ||
/** | ||
* The content that was deleted | ||
*/ | ||
type: 'node' | ||
/** | ||
* The node which the deletion occurred in | ||
* @note This can be a parent node of the deleted content | ||
*/ | ||
node: ProseMirrorNode | ||
/** | ||
* The new start position of the node in the document (after the deletion) | ||
*/ | ||
newFrom: number | ||
/** | ||
* The new end position of the node in the document (after the deletion) | ||
*/ | ||
newTo: number | ||
} | ||
| { | ||
/** | ||
* The content that was deleted | ||
*/ | ||
type: 'mark' | ||
/** | ||
* The mark that was deleted | ||
*/ | ||
mark: ProseMirrorMark | ||
} | ||
) | ||
} | ||
@@ -68,11 +254,43 @@ | ||
export interface EditorOptions { | ||
element: Element | ||
/** | ||
* The element or selector to bind the editor to | ||
* If `null` is passed, the editor will not be mounted automatically | ||
* If a function is passed, it will be called with the editor's root element | ||
*/ | ||
element: Element | null | ||
/** | ||
* The content of the editor (HTML, JSON, or a JSON array) | ||
*/ | ||
content: Content | ||
/** | ||
* The extensions to use | ||
*/ | ||
extensions: Extensions | ||
/** | ||
* Whether to inject base CSS styles | ||
*/ | ||
injectCSS: boolean | ||
/** | ||
* A nonce to use for CSP while injecting styles | ||
*/ | ||
injectNonce: string | undefined | ||
/** | ||
* The editor's initial focus position | ||
*/ | ||
autofocus: FocusPosition | ||
/** | ||
* Whether the editor is editable | ||
*/ | ||
editable: boolean | ||
/** | ||
* The editor's props | ||
*/ | ||
editorProps: EditorProps | ||
/** | ||
* The editor's content parser options | ||
*/ | ||
parseOptions: ParseOptions | ||
/** | ||
* The editor's core extension options | ||
*/ | ||
coreExtensionOptions?: { | ||
@@ -82,4 +300,22 @@ clipboardTextSerializer?: { | ||
} | ||
delete?: { | ||
/** | ||
* Whether the `delete` extension should be called asynchronously to avoid blocking the editor while processing deletions | ||
* @default true deletion events are called asynchronously | ||
*/ | ||
async?: boolean | ||
/** | ||
* Allows filtering the transactions that are processed by the `delete` extension. | ||
* If the function returns `true`, the transaction will be ignored. | ||
*/ | ||
filterTransaction?: (transaction: Transaction) => boolean | ||
} | ||
} | ||
/** | ||
* Whether to enable input rules behavior | ||
*/ | ||
enableInputRules: EnableRules | ||
/** | ||
* Whether to enable paste rules behavior | ||
*/ | ||
enablePasteRules: EnableRules | ||
@@ -114,3 +350,4 @@ /** | ||
| 'drop' | ||
| 'paste', | ||
| 'paste' | ||
| 'delete', | ||
false | ||
@@ -126,3 +363,9 @@ > | ||
enableContentCheck: boolean | ||
/** | ||
* Called before the editor is constructed. | ||
*/ | ||
onBeforeCreate: (props: EditorEvents['beforeCreate']) => void | ||
/** | ||
* Called after the editor is constructed. | ||
*/ | ||
onCreate: (props: EditorEvents['create']) => void | ||
@@ -134,14 +377,48 @@ /** | ||
onContentError: (props: EditorEvents['contentError']) => void | ||
/** | ||
* Called when the editor's content is updated. | ||
*/ | ||
onUpdate: (props: EditorEvents['update']) => void | ||
/** | ||
* Called when the editor's selection is updated. | ||
*/ | ||
onSelectionUpdate: (props: EditorEvents['selectionUpdate']) => void | ||
/** | ||
* Called after a transaction is applied to the editor. | ||
*/ | ||
onTransaction: (props: EditorEvents['transaction']) => void | ||
/** | ||
* Called on focus events. | ||
*/ | ||
onFocus: (props: EditorEvents['focus']) => void | ||
/** | ||
* Called on blur events. | ||
*/ | ||
onBlur: (props: EditorEvents['blur']) => void | ||
/** | ||
* Called when the editor is destroyed. | ||
*/ | ||
onDestroy: (props: EditorEvents['destroy']) => void | ||
/** | ||
* Called when content is pasted into the editor. | ||
*/ | ||
onPaste: (e: ClipboardEvent, slice: Slice) => void | ||
/** | ||
* Called when content is dropped into the editor. | ||
*/ | ||
onDrop: (e: DragEvent, slice: Slice, moved: boolean) => void | ||
/** | ||
* Called when content is deleted from the editor. | ||
*/ | ||
onDelete: (props: EditorEvents['delete']) => void | ||
} | ||
/** | ||
* The editor's content as HTML | ||
*/ | ||
export type HTMLContent = string | ||
/** | ||
* Loosely describes a JSON representation of a Prosemirror document or node | ||
*/ | ||
export type JSONContent = { | ||
@@ -165,6 +442,6 @@ type?: string | ||
Type extends string | { name: string } = any, | ||
Attributes extends undefined | Record<string, any> = any, | ||
TAttributes extends undefined | Record<string, any> = any, | ||
> = { | ||
type: Type | ||
attrs: Attributes | ||
attrs: TAttributes | ||
} | ||
@@ -177,9 +454,9 @@ | ||
Type extends string | { name: string } = any, | ||
Attributes extends undefined | Record<string, any> = any, | ||
TAttributes extends undefined | Record<string, any> = any, | ||
NodeMarkType extends MarkType = any, | ||
Content extends (NodeType | TextType)[] = any, | ||
TContent extends (NodeType | TextType)[] = any, | ||
> = { | ||
type: Type | ||
attrs: Attributes | ||
content?: Content | ||
attrs: TAttributes | ||
content?: TContent | ||
marks?: NodeMarkType[] | ||
@@ -359,2 +636,40 @@ } | ||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type | ||
export interface MarkViewProps extends MarkViewRendererProps {} | ||
export interface MarkViewRendererProps { | ||
// pass-through from prosemirror | ||
/** | ||
* The node that is being rendered. | ||
*/ | ||
mark: Parameters<MarkViewConstructor>[0] | ||
/** | ||
* The editor's view. | ||
*/ | ||
view: Parameters<MarkViewConstructor>[1] | ||
/** | ||
* indicates whether the mark's content is inline | ||
*/ | ||
inline: Parameters<MarkViewConstructor>[2] | ||
// tiptap-specific | ||
/** | ||
* The editor instance. | ||
*/ | ||
editor: Editor | ||
/** | ||
* The extension that is responsible for the mark. | ||
*/ | ||
extension: Mark | ||
/** | ||
* The HTML attributes that should be added to the mark's DOM element. | ||
*/ | ||
HTMLAttributes: Record<string, any> | ||
} | ||
export type MarkViewRenderer = (props: MarkViewRendererProps) => MarkView | ||
export interface MarkViewRendererOptions { | ||
ignoreMutation: ((props: { mutation: ViewMutationRecord }) => boolean) | null | ||
} | ||
export type AnyCommands = Record<string, (...args: any[]) => Command> | ||
@@ -361,0 +676,0 @@ |
@@ -18,2 +18,5 @@ const removeWhitespaces = (node: HTMLElement) => { | ||
export function elementFromString(value: string): HTMLElement { | ||
if (typeof window === 'undefined') { | ||
throw new Error('[tiptap error]: there is no window object available, so this function cannot be used') | ||
} | ||
// add a wrapper to preserve leading and trailing whitespace | ||
@@ -20,0 +23,0 @@ const wrappedValue = `<body>${value}</body>` |
@@ -8,2 +8,3 @@ export * from './callOrReturn.js' | ||
export * from './fromString.js' | ||
export * from './isAndroid.js' | ||
export * from './isEmptyObject.js' | ||
@@ -10,0 +11,0 @@ export * from './isFunction.js' |
@@ -17,3 +17,3 @@ export function mergeAttributes(...objects: Record<string, any>[]): Record<string, any> { | ||
if (key === 'class') { | ||
const valueClasses: string[] = value ? value.split(' ') : [] | ||
const valueClasses: string[] = value ? String(value).split(' ') : [] | ||
const existingClasses: string[] = mergedAttributes[key] ? mergedAttributes[key].split(' ') : [] | ||
@@ -20,0 +20,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
190
1668060
22589