mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-23 19:19:03 +08:00
feat(core): beginnings of a decorations API
This commit is contained in:
parent
f7453a3292
commit
36489ba873
557
packages/core/src/Decoration.ts
Normal file
557
packages/core/src/Decoration.ts
Normal file
@ -0,0 +1,557 @@
|
|||||||
|
import { Plugin, Transaction } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
import { Editor } from './Editor.js'
|
||||||
|
import { getExtensionField } from './helpers/getExtensionField.js'
|
||||||
|
import { DecorationConfig } 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'
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface DecorationConfig<Options = any, Storage = any, Attrs = any, Spec = 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 'myDecoration'
|
||||||
|
*/
|
||||||
|
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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<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<DecorationConfig<Options, Storage>>['extendMarkSchema'];
|
||||||
|
},
|
||||||
|
extension: Mark
|
||||||
|
) => Record<string, any>)
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function extends the schema of the decoration.
|
||||||
|
* @example
|
||||||
|
* extendMarkSchema() {
|
||||||
|
* return {
|
||||||
|
* group: 'inline',
|
||||||
|
* selectable: false,
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
extendDecorationSchema?:
|
||||||
|
| ((
|
||||||
|
this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
parent: ParentConfig<DecorationConfig<Options, Storage>>['extendDecorationSchema'];
|
||||||
|
},
|
||||||
|
extension: Decoration
|
||||||
|
) => Record<string, any>)
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The editor is not ready yet.
|
||||||
|
*/
|
||||||
|
onBeforeCreate?:
|
||||||
|
| ((this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
editor: Editor;
|
||||||
|
parent: ParentConfig<DecorationConfig<Options, Storage>>['onBeforeCreate'];
|
||||||
|
}) => void)
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The editor is ready.
|
||||||
|
*/
|
||||||
|
onCreate?:
|
||||||
|
| ((this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
editor: Editor;
|
||||||
|
parent: ParentConfig<DecorationConfig<Options, Storage>>['onCreate'];
|
||||||
|
}) => void)
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The content has changed.
|
||||||
|
*/
|
||||||
|
onUpdate?:
|
||||||
|
| ((this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
editor: Editor;
|
||||||
|
parent: ParentConfig<DecorationConfig<Options, Storage>>['onUpdate'];
|
||||||
|
}) => void)
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selection has changed.
|
||||||
|
*/
|
||||||
|
onSelectionUpdate?:
|
||||||
|
| ((this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
editor: Editor;
|
||||||
|
parent: ParentConfig<DecorationConfig<Options, Storage>>['onSelectionUpdate'];
|
||||||
|
}) => void)
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The editor state has changed.
|
||||||
|
*/
|
||||||
|
onTransaction?:
|
||||||
|
| ((
|
||||||
|
this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
editor: Editor;
|
||||||
|
parent: ParentConfig<DecorationConfig<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<DecorationConfig<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<DecorationConfig<Options, Storage>>['onBlur'];
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
event: FocusEvent;
|
||||||
|
}
|
||||||
|
) => void)
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The editor is destroyed.
|
||||||
|
*/
|
||||||
|
onDestroy?:
|
||||||
|
| ((this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
editor: Editor;
|
||||||
|
parent: ParentConfig<DecorationConfig<Options, Storage>>['onDestroy'];
|
||||||
|
}) => void)
|
||||||
|
| null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add attributes to the node
|
||||||
|
* @example addAttributes: () => ({ class: 'foo' })
|
||||||
|
*/
|
||||||
|
addAttributes?: (this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
parent: ParentConfig<DecorationConfig<Options, Storage>>['addAttributes'];
|
||||||
|
editor?: Editor;
|
||||||
|
}) => Partial<Attrs>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add attributes to the node
|
||||||
|
* @example addSpec: () => ({ ctx: 'foo' })
|
||||||
|
*/
|
||||||
|
addSpec?: (this: {
|
||||||
|
name: string;
|
||||||
|
options: Options;
|
||||||
|
storage: Storage;
|
||||||
|
parent: ParentConfig<DecorationConfig<Options, Storage>>['addAttributes'];
|
||||||
|
editor?: Editor;
|
||||||
|
}) => Partial<Spec>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Extension class is the base class for all extensions.
|
||||||
|
* @see https://tiptap.dev/api/extensions#create-a-new-extension
|
||||||
|
*/
|
||||||
|
export class Decoration<Options = any, Storage = any, Attrs = any, Spec = any> {
|
||||||
|
type = 'decoration'
|
||||||
|
|
||||||
|
name = 'decoration'
|
||||||
|
|
||||||
|
parent: Decoration | null = null
|
||||||
|
|
||||||
|
child: Decoration | null = null
|
||||||
|
|
||||||
|
options: Options
|
||||||
|
|
||||||
|
storage: Storage
|
||||||
|
|
||||||
|
config: DecorationConfig = {
|
||||||
|
name: this.name,
|
||||||
|
defaultOptions: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(config: Partial<DecorationConfig<Options, Storage, Attrs, Spec>> = {}) {
|
||||||
|
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<TOptions = any, TStorage = any, TAttrs = any, TSpec = any>(config: Partial<DecorationConfig<TOptions, TStorage, TAttrs, TSpec>> = {}) {
|
||||||
|
return new Decoration<TOptions, TStorage, TAttrs, TSpec>(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, Attrs, Spec>({
|
||||||
|
...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, ExtendedAttrs = Attrs, ExtendSpec = Spec>(
|
||||||
|
extendedConfig: Partial<DecorationConfig<ExtendedOptions, ExtendedStorage, ExtendedAttrs, ExtendSpec>> = {},
|
||||||
|
) {
|
||||||
|
const extension = new Decoration<ExtendedOptions, ExtendedStorage, ExtendedAttrs, ExtendSpec>({
|
||||||
|
...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
|
||||||
|
}
|
||||||
|
}
|
156
packages/core/src/DecorationManager.ts
Normal file
156
packages/core/src/DecorationManager.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { Node } from '@tiptap/pm/model'
|
||||||
|
import { Plugin, PluginKey, Selection } from '@tiptap/pm/state'
|
||||||
|
import { DecorationSet, DecorationSource } from '@tiptap/pm/view'
|
||||||
|
|
||||||
|
import { Editor } from './Editor.js'
|
||||||
|
import { NodePos } from './NodePos.js'
|
||||||
|
import { Range } from './types.js'
|
||||||
|
|
||||||
|
type AttachToOptions = {
|
||||||
|
/**
|
||||||
|
* Force the decoration to be a specific type.
|
||||||
|
*/
|
||||||
|
type?: 'node' | 'inline' | 'widget';
|
||||||
|
/**
|
||||||
|
* Offset the target position by a number or an object with start and end properties.
|
||||||
|
*/
|
||||||
|
offset?:
|
||||||
|
| number
|
||||||
|
| {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ResolvedDecorationOptions = {
|
||||||
|
type: 'widget' | 'inline' | 'node';
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DecorationManagerPluginState = {};
|
||||||
|
const pluginKey = new PluginKey<DecorationManagerPluginState>('tiptapDecorationManager')
|
||||||
|
|
||||||
|
export class DecorationManager {
|
||||||
|
editor: Editor
|
||||||
|
|
||||||
|
decorations: DecorationSource
|
||||||
|
|
||||||
|
constructor(props: { editor: Editor }) {
|
||||||
|
this.editor = props.editor
|
||||||
|
this.decorations = DecorationSet.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolvePositionsAndType(
|
||||||
|
target: Node | NodePos | Range | Selection | number,
|
||||||
|
options?: Pick<AttachToOptions, 'offset' | 'type'>,
|
||||||
|
): ResolvedDecorationOptions {
|
||||||
|
let offsetFrom = 0
|
||||||
|
let offsetTo = 0
|
||||||
|
|
||||||
|
if (options?.offset) {
|
||||||
|
if (typeof options.offset === 'number') {
|
||||||
|
offsetFrom = options.offset
|
||||||
|
offsetTo = options.offset
|
||||||
|
} else {
|
||||||
|
offsetFrom = options.offset.start
|
||||||
|
offsetTo = options.offset.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof target === 'number') {
|
||||||
|
return {
|
||||||
|
type: options?.type ?? 'widget',
|
||||||
|
from: target + offsetFrom,
|
||||||
|
to: target + offsetTo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target instanceof Selection) {
|
||||||
|
return {
|
||||||
|
type: options?.type ?? 'inline',
|
||||||
|
from: target.from + offsetFrom,
|
||||||
|
to: target.to + offsetFrom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target instanceof NodePos) {
|
||||||
|
return {
|
||||||
|
type: options?.type ?? (offsetFrom !== 0 || offsetTo !== 0 ? 'inline' : 'node'),
|
||||||
|
from: target.pos + offsetFrom,
|
||||||
|
to: target.pos + target.node.nodeSize + offsetTo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('from' in target && 'to' in target) {
|
||||||
|
return {
|
||||||
|
type: options?.type ?? 'inline',
|
||||||
|
from: target.from + offsetFrom,
|
||||||
|
to: target.to + offsetTo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target instanceof Node) {
|
||||||
|
// This may be too expensive for large documents, but it’s the only way to find the correct position
|
||||||
|
let from = -1
|
||||||
|
let to = -1
|
||||||
|
|
||||||
|
// Perhaps we can find the node faster than doing a full traversal
|
||||||
|
this.editor.state.doc.descendants((node, pos) => {
|
||||||
|
if (from !== -1 && to !== -1) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (node === target) {
|
||||||
|
from = pos
|
||||||
|
to = pos + node.nodeSize
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (from === -1 || to === -1) {
|
||||||
|
throw new Error('Node not found', { cause: target })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// a node can’t have an offset, so we need to convert it to an inline decoration
|
||||||
|
type: options?.type ?? (offsetFrom !== 0 || offsetTo !== 0 ? 'inline' : 'node'),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Invalid target: ${target}`, { cause: target })
|
||||||
|
}
|
||||||
|
|
||||||
|
attachTo(targetNode: Node, options?: AttachToOptions): void;
|
||||||
|
attachTo(targetNode: NodePos, options?: AttachToOptions): void;
|
||||||
|
attachTo(range: Range, options?: AttachToOptions): void;
|
||||||
|
attachTo(selection: Selection, options?: AttachToOptions): void;
|
||||||
|
attachTo(pos: number, options?: AttachToOptions): void;
|
||||||
|
attachTo(target: Node | NodePos | Range | Selection | number, options?: AttachToOptions): void {
|
||||||
|
this.resolvePositionsAndType(target, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
static get pluginKey() {
|
||||||
|
return pluginKey
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlugin() {
|
||||||
|
return new Plugin<DecorationManagerPluginState>({
|
||||||
|
key: DecorationManager.pluginKey,
|
||||||
|
state: {
|
||||||
|
init(config, instance) {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
apply(tr, value, oldState, newState) {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,9 @@ export interface Commands<ReturnType = any> {}
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
export interface ExtensionConfig<Options = any, Storage = any> {}
|
export interface ExtensionConfig<Options = any, Storage = any> {}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export interface DecorationConfig<Options = any, Storage = any> {}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
export interface NodeConfig<Options = any, Storage = any> {}
|
export interface NodeConfig<Options = any, Storage = any> {}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import {
|
|||||||
import { EditorState, Transaction } from '@tiptap/pm/state'
|
import { EditorState, Transaction } from '@tiptap/pm/state'
|
||||||
import { Mappable } from '@tiptap/pm/transform'
|
import { Mappable } from '@tiptap/pm/transform'
|
||||||
import {
|
import {
|
||||||
Decoration,
|
Decoration as PMDecoration,
|
||||||
DecorationAttrs,
|
DecorationAttrs,
|
||||||
EditorProps,
|
EditorProps,
|
||||||
EditorView,
|
EditorView,
|
||||||
@ -15,16 +15,17 @@ import {
|
|||||||
NodeViewConstructor,
|
NodeViewConstructor,
|
||||||
} from '@tiptap/pm/view'
|
} from '@tiptap/pm/view'
|
||||||
|
|
||||||
|
import { Decoration } from './Decoration.js'
|
||||||
import { Editor } from './Editor.js'
|
import { Editor } from './Editor.js'
|
||||||
import { Extension } from './Extension.js'
|
import { Extension } from './Extension.js'
|
||||||
import {
|
import {
|
||||||
Commands, ExtensionConfig, MarkConfig, NodeConfig,
|
Commands, DecorationConfig, ExtensionConfig, MarkConfig, NodeConfig,
|
||||||
} from './index.js'
|
} from './index.js'
|
||||||
import { Mark } from './Mark.js'
|
import { Mark } from './Mark.js'
|
||||||
import { Node } from './Node.js'
|
import { Node } from './Node.js'
|
||||||
|
|
||||||
export type AnyConfig = ExtensionConfig | NodeConfig | MarkConfig;
|
export type AnyConfig = ExtensionConfig | NodeConfig | MarkConfig | DecorationConfig;
|
||||||
export type AnyExtension = Extension | Node | Mark;
|
export type AnyExtension = Extension | Node | Mark | Decoration;
|
||||||
export type Extensions = AnyExtension[];
|
export type Extensions = AnyExtension[];
|
||||||
|
|
||||||
export type ParentConfig<T> = Partial<{
|
export type ParentConfig<T> = Partial<{
|
||||||
@ -233,8 +234,8 @@ export type DOMNode = InstanceType<typeof window.Node>
|
|||||||
*/
|
*/
|
||||||
export interface DecorationType {
|
export interface DecorationType {
|
||||||
spec: any
|
spec: any
|
||||||
map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null
|
map(mapping: Mappable, span: PMDecoration, offset: number, oldOffset: number): PMDecoration | null
|
||||||
valid(node: Node, span: Decoration): boolean
|
valid(node: Node, span: PMDecoration): boolean
|
||||||
eq(other: DecorationType): boolean
|
eq(other: DecorationType): boolean
|
||||||
destroy(dom: DOMNode): void
|
destroy(dom: DOMNode): void
|
||||||
readonly attrs: DecorationAttrs
|
readonly attrs: DecorationAttrs
|
||||||
@ -244,7 +245,7 @@ export interface DecorationType {
|
|||||||
* prosemirror-view does not export the `type` property of `Decoration`.
|
* prosemirror-view does not export the `type` property of `Decoration`.
|
||||||
* This adds the `type` property to the `Decoration` type.
|
* This adds the `type` property to the `Decoration` type.
|
||||||
*/
|
*/
|
||||||
export type DecorationWithType = Decoration & {
|
export type DecorationWithType = PMDecoration & {
|
||||||
type: DecorationType;
|
type: DecorationType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user