New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@tiptap/core

Package Overview
Dependencies
Maintainers
6
Versions
343
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@tiptap/core - npm Package Compare versions

Comparing version 3.0.0-next.4 to 3.0.0-next.5

dist/jsx-runtime/jsx-runtime.cjs

2

LICENSE.md
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 {}

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

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc