@tiptap/core 2.6.6 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,6 @@
1
- import { Node as ProseMirrorNode } from '@tiptap/pm/model';
2
1
  import { NodeView as ProseMirrorNodeView } from '@tiptap/pm/view';
3
2
  import { Editor as CoreEditor } from './Editor.js';
4
- import { Node } from './Node.js';
5
- import { DecorationWithType, NodeViewRendererOptions, NodeViewRendererProps } from './types.js';
3
+ import { NodeViewRendererOptions, NodeViewRendererProps } from './types.js';
6
4
  /**
7
5
  * Node views are used to customize the rendered DOM structure of a node.
8
6
  * @see https://tiptap.dev/guide/node-views
@@ -11,10 +9,13 @@ export declare class NodeView<Component, NodeEditor extends CoreEditor = CoreEdi
11
9
  component: Component;
12
10
  editor: NodeEditor;
13
11
  options: Options;
14
- extension: Node;
15
- node: ProseMirrorNode;
16
- decorations: DecorationWithType[];
17
- getPos: any;
12
+ extension: NodeViewRendererProps['extension'];
13
+ node: NodeViewRendererProps['node'];
14
+ decorations: NodeViewRendererProps['decorations'];
15
+ innerDecorations: NodeViewRendererProps['innerDecorations'];
16
+ view: NodeViewRendererProps['view'];
17
+ getPos: NodeViewRendererProps['getPos'];
18
+ HTMLAttributes: NodeViewRendererProps['HTMLAttributes'];
18
19
  isDragging: boolean;
19
20
  constructor(component: Component, props: NodeViewRendererProps, options?: Partial<Options>);
20
21
  mount(): void;
@@ -22,10 +23,21 @@ export declare class NodeView<Component, NodeEditor extends CoreEditor = CoreEdi
22
23
  get contentDOM(): HTMLElement | null;
23
24
  onDragStart(event: DragEvent): void;
24
25
  stopEvent(event: Event): boolean;
26
+ /**
27
+ * Called when a DOM [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) or a selection change happens within the view.
28
+ * @return `false` if the editor should re-read the selection or re-parse the range around the mutation
29
+ * @return `true` if it can safely be ignored.
30
+ */
25
31
  ignoreMutation(mutation: MutationRecord | {
26
32
  type: 'selection';
27
33
  target: Element;
28
34
  }): boolean;
29
- updateAttributes(attributes: {}): void;
35
+ /**
36
+ * Update the attributes of the prosemirror node.
37
+ */
38
+ updateAttributes(attributes: Record<string, any>): void;
39
+ /**
40
+ * Delete the node.
41
+ */
30
42
  deleteNode(): void;
31
43
  }
@@ -11,6 +11,8 @@ export * from './NodePos.js';
11
11
  export * from './NodeView.js';
12
12
  export * from './PasteRule.js';
13
13
  export * from './pasteRules/index.js';
14
+ export * from './plugins/DropPlugin.js';
15
+ export * from './plugins/PastePlugin.js';
14
16
  export * from './Tracker.js';
15
17
  export * from './types.js';
16
18
  export * from './utilities/index.js';
@@ -0,0 +1,3 @@
1
+ import { Plugin } from '@tiptap/pm/state';
2
+ import { Slice } from 'packages/pm/model';
3
+ export declare const DropPlugin: (onDrop: (e: DragEvent, slice: Slice, moved: boolean) => void) => Plugin<any>;
@@ -0,0 +1,3 @@
1
+ import { Slice } from '@tiptap/pm/model';
2
+ import { Plugin } from '@tiptap/pm/state';
3
+ export declare const PastePlugin: (onPaste: (e: ClipboardEvent, slice: Slice) => void) => Plugin<any>;
@@ -1,6 +1,6 @@
1
- import { Mark as ProseMirrorMark, Node as ProseMirrorNode, NodeType, ParseOptions } from '@tiptap/pm/model';
1
+ import { Mark as ProseMirrorMark, Node as ProseMirrorNode, NodeType, ParseOptions, Slice } from '@tiptap/pm/model';
2
2
  import { EditorState, Transaction } from '@tiptap/pm/state';
3
- import { Decoration, EditorProps, EditorView, NodeView } from '@tiptap/pm/view';
3
+ import { Decoration, EditorProps, EditorView, NodeView, NodeViewConstructor } from '@tiptap/pm/view';
4
4
  import { Editor } from './Editor.js';
5
5
  import { Extension } from './Extension.js';
6
6
  import { Commands, ExtensionConfig, MarkConfig, NodeConfig } from './index.js';
@@ -79,7 +79,24 @@ export interface EditorOptions {
79
79
  };
80
80
  enableInputRules: EnableRules;
81
81
  enablePasteRules: EnableRules;
82
- enableCoreExtensions: boolean;
82
+ /**
83
+ * Determines whether core extensions are enabled.
84
+ *
85
+ * If set to `false`, all core extensions will be disabled.
86
+ * To disable specific core extensions, provide an object where the keys are the extension names and the values are `false`.
87
+ * Extensions not listed in the object will remain enabled.
88
+ *
89
+ * @example
90
+ * // Disable all core extensions
91
+ * enabledCoreExtensions: false
92
+ *
93
+ * @example
94
+ * // Disable only the keymap core extension
95
+ * enabledCoreExtensions: { keymap: false }
96
+ *
97
+ * @default true
98
+ */
99
+ enableCoreExtensions?: boolean | Partial<Record<'editable' | 'clipboardTextSerializer' | 'commands' | 'focusEvents' | 'keymap' | 'tabindex', false>>;
83
100
  /**
84
101
  * If `true`, the editor will check the content for errors on initialization.
85
102
  * Emitting the `contentError` event if the content is invalid.
@@ -100,6 +117,8 @@ export interface EditorOptions {
100
117
  onFocus: (props: EditorEvents['focus']) => void;
101
118
  onBlur: (props: EditorEvents['blur']) => void;
102
119
  onDestroy: (props: EditorEvents['destroy']) => void;
120
+ onPaste: (e: ClipboardEvent, slice: Slice) => void;
121
+ onDrop: (e: DragEvent, slice: Slice, moved: boolean) => void;
103
122
  }
104
123
  export type HTMLContent = string;
105
124
  export type JSONContent = {
@@ -170,19 +189,18 @@ export type ValuesOf<T> = T[keyof T];
170
189
  export type KeysWithTypeOf<T, Type> = {
171
190
  [P in keyof T]: T[P] extends Type ? P : never;
172
191
  }[keyof T];
192
+ export type Simplify<T> = {
193
+ [KeyType in keyof T]: T[KeyType];
194
+ } & {};
173
195
  export type DecorationWithType = Decoration & {
174
196
  type: NodeType;
175
197
  };
176
- export type NodeViewProps = {
177
- editor: Editor;
178
- node: ProseMirrorNode;
179
- decorations: DecorationWithType[];
198
+ export type NodeViewProps = Simplify<Omit<NodeViewRendererProps, 'decorations'> & {
199
+ decorations: readonly DecorationWithType[];
180
200
  selected: boolean;
181
- extension: Node;
182
- getPos: () => number;
183
201
  updateAttributes: (attributes: Record<string, any>) => void;
184
202
  deleteNode: () => void;
185
- };
203
+ }>;
186
204
  export interface NodeViewRendererOptions {
187
205
  stopEvent: ((props: {
188
206
  event: Event;
@@ -196,14 +214,16 @@ export interface NodeViewRendererOptions {
196
214
  contentDOMElementTag: string;
197
215
  }
198
216
  export type NodeViewRendererProps = {
217
+ node: Parameters<NodeViewConstructor>[0];
218
+ view: Parameters<NodeViewConstructor>[1];
219
+ getPos: () => number;
220
+ decorations: Parameters<NodeViewConstructor>[3];
221
+ innerDecorations: Parameters<NodeViewConstructor>[4];
199
222
  editor: Editor;
200
- node: ProseMirrorNode;
201
- getPos: (() => number) | boolean;
202
- HTMLAttributes: Record<string, any>;
203
- decorations: Decoration[];
204
223
  extension: Node;
224
+ HTMLAttributes: Record<string, any>;
205
225
  };
206
- export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView | {};
226
+ export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView;
207
227
  export type AnyCommands = Record<string, (...args: any[]) => Command>;
208
228
  export type UnionCommands<T = Command> = UnionToIntersection<ValuesOf<Pick<Commands<T>, KeysWithTypeOf<Commands<T>, {}>>>>;
209
229
  export type RawCommands = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/core",
3
3
  "description": "headless rich text editor",
4
- "version": "2.6.6",
4
+ "version": "2.7.0",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -32,10 +32,10 @@
32
32
  "dist"
33
33
  ],
34
34
  "devDependencies": {
35
- "@tiptap/pm": "^2.6.6"
35
+ "@tiptap/pm": "^2.7.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@tiptap/pm": "^2.6.6"
38
+ "@tiptap/pm": "^2.7.0-pre.0"
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",
package/src/Editor.ts CHANGED
@@ -24,6 +24,8 @@ import { isActive } from './helpers/isActive.js'
24
24
  import { isNodeEmpty } from './helpers/isNodeEmpty.js'
25
25
  import { resolveFocusPosition } from './helpers/resolveFocusPosition.js'
26
26
  import { NodePos } from './NodePos.js'
27
+ import { DropPlugin } from './plugins/DropPlugin.js'
28
+ import { PastePlugin } from './plugins/PastePlugin.js'
27
29
  import { style } from './style.js'
28
30
  import {
29
31
  CanCommands,
@@ -88,6 +90,8 @@ export class Editor extends EventEmitter<EditorEvents> {
88
90
  onBlur: () => null,
89
91
  onDestroy: () => null,
90
92
  onContentError: ({ error }) => { throw error },
93
+ onPaste: () => null,
94
+ onDrop: () => null,
91
95
  }
92
96
 
93
97
  constructor(options: Partial<EditorOptions> = {}) {
@@ -109,6 +113,14 @@ export class Editor extends EventEmitter<EditorEvents> {
109
113
  this.on('blur', this.options.onBlur)
110
114
  this.on('destroy', this.options.onDestroy)
111
115
 
116
+ if (this.options.onPaste) {
117
+ this.registerPlugin(PastePlugin(this.options.onPaste))
118
+ }
119
+
120
+ if (this.options.onDrop) {
121
+ this.registerPlugin(DropPlugin(this.options.onDrop))
122
+ }
123
+
112
124
  window.setTimeout(() => {
113
125
  if (this.isDestroyed) {
114
126
  return
@@ -261,7 +273,12 @@ export class Editor extends EventEmitter<EditorEvents> {
261
273
  FocusEvents,
262
274
  Keymap,
263
275
  Tabindex,
264
- ] : []
276
+ ].filter(ext => {
277
+ if (typeof this.options.enableCoreExtensions === 'object') {
278
+ return this.options.enableCoreExtensions[ext.name as keyof typeof this.options.enableCoreExtensions] !== false
279
+ }
280
+ return true
281
+ }) : []
265
282
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => {
266
283
  return ['extension', 'node', 'mark'].includes(extension?.type)
267
284
  })
@@ -1,7 +1,7 @@
1
1
  import { keymap } from '@tiptap/pm/keymap'
2
- import { Node as ProsemirrorNode, Schema } from '@tiptap/pm/model'
2
+ import { Schema } from '@tiptap/pm/model'
3
3
  import { Plugin } from '@tiptap/pm/state'
4
- import { Decoration, EditorView } from '@tiptap/pm/view'
4
+ import { NodeViewConstructor } from '@tiptap/pm/view'
5
5
 
6
6
  import type { Editor } from './Editor.js'
7
7
  import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions.js'
@@ -288,21 +288,26 @@ export class ExtensionManager {
288
288
  return []
289
289
  }
290
290
 
291
- const nodeview = (
292
- node: ProsemirrorNode,
293
- view: EditorView,
294
- getPos: (() => number) | boolean,
295
- decorations: Decoration[],
291
+ const nodeview: NodeViewConstructor = (
292
+ node,
293
+ view,
294
+ getPos,
295
+ decorations,
296
+ innerDecorations,
296
297
  ) => {
297
298
  const HTMLAttributes = getRenderedAttributes(node, extensionAttributes)
298
299
 
299
300
  return addNodeView()({
300
- editor,
301
+ // pass-through
301
302
  node,
302
- getPos,
303
+ view,
304
+ getPos: getPos as () => number,
303
305
  decorations,
304
- HTMLAttributes,
306
+ innerDecorations,
307
+ // tiptap-specific
308
+ editor,
305
309
  extension,
310
+ HTMLAttributes,
306
311
  })
307
312
  }
308
313
 
package/src/NodeView.ts CHANGED
@@ -1,9 +1,7 @@
1
- import { Node as ProseMirrorNode } from '@tiptap/pm/model'
2
1
  import { NodeSelection } from '@tiptap/pm/state'
3
2
  import { NodeView as ProseMirrorNodeView } from '@tiptap/pm/view'
4
3
 
5
4
  import { Editor as CoreEditor } from './Editor.js'
6
- import { Node } from './Node.js'
7
5
  import { DecorationWithType, NodeViewRendererOptions, NodeViewRendererProps } from './types.js'
8
6
  import { isAndroid } from './utilities/isAndroid.js'
9
7
  import { isiOS } from './utilities/isiOS.js'
@@ -23,13 +21,19 @@ export class NodeView<
23
21
 
24
22
  options: Options
25
23
 
26
- extension: Node
24
+ extension: NodeViewRendererProps['extension']
27
25
 
28
- node: ProseMirrorNode
26
+ node: NodeViewRendererProps['node']
29
27
 
30
- decorations: DecorationWithType[]
28
+ decorations: NodeViewRendererProps['decorations']
31
29
 
32
- getPos: any
30
+ innerDecorations: NodeViewRendererProps['innerDecorations']
31
+
32
+ view: NodeViewRendererProps['view']
33
+
34
+ getPos: NodeViewRendererProps['getPos']
35
+
36
+ HTMLAttributes: NodeViewRendererProps['HTMLAttributes']
33
37
 
34
38
  isDragging = false
35
39
 
@@ -44,6 +48,9 @@ export class NodeView<
44
48
  this.extension = props.extension
45
49
  this.node = props.node
46
50
  this.decorations = props.decorations as DecorationWithType[]
51
+ this.innerDecorations = props.innerDecorations
52
+ this.view = props.view
53
+ this.HTMLAttributes = props.HTMLAttributes
47
54
  this.getPos = props.getPos
48
55
  this.mount()
49
56
  }
@@ -93,9 +100,14 @@ export class NodeView<
93
100
 
94
101
  event.dataTransfer?.setDragImage(this.dom, x, y)
95
102
 
103
+ const pos = this.getPos()
104
+
105
+ if (typeof pos !== 'number') {
106
+ return
107
+ }
96
108
  // we need to tell ProseMirror that we want to move the whole node
97
109
  // so we create a NodeSelection
98
- const selection = NodeSelection.create(view.state.doc, this.getPos())
110
+ const selection = NodeSelection.create(view.state.doc, pos)
99
111
  const transaction = view.state.tr.setSelection(selection)
100
112
 
101
113
  view.dispatch(transaction)
@@ -197,6 +209,11 @@ export class NodeView<
197
209
  return true
198
210
  }
199
211
 
212
+ /**
213
+ * Called when a DOM [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) or a selection change happens within the view.
214
+ * @return `false` if the editor should re-read the selection or re-parse the range around the mutation
215
+ * @return `true` if it can safely be ignored.
216
+ */
200
217
  ignoreMutation(mutation: MutationRecord | { type: 'selection'; target: Element }) {
201
218
  if (!this.dom || !this.contentDOM) {
202
219
  return true
@@ -254,10 +271,17 @@ export class NodeView<
254
271
  return true
255
272
  }
256
273
 
257
- updateAttributes(attributes: {}) {
274
+ /**
275
+ * Update the attributes of the prosemirror node.
276
+ */
277
+ updateAttributes(attributes: Record<string, any>): void {
258
278
  this.editor.commands.command(({ tr }) => {
259
279
  const pos = this.getPos()
260
280
 
281
+ if (typeof pos !== 'number') {
282
+ return false
283
+ }
284
+
261
285
  tr.setNodeMarkup(pos, undefined, {
262
286
  ...this.node.attrs,
263
287
  ...attributes,
@@ -267,8 +291,15 @@ export class NodeView<
267
291
  })
268
292
  }
269
293
 
294
+ /**
295
+ * Delete the node.
296
+ */
270
297
  deleteNode(): void {
271
298
  const from = this.getPos()
299
+
300
+ if (typeof from !== 'number') {
301
+ return
302
+ }
272
303
  const to = from + this.node.nodeSize
273
304
 
274
305
  this.editor.commands.deleteRange({ from, to })
@@ -28,9 +28,18 @@ export const toggleNode: RawCommands['toggleNode'] = (typeOrName, toggleTypeOrNa
28
28
  const toggleType = getNodeType(toggleTypeOrName, state.schema)
29
29
  const isActive = isNodeActive(state, type, attributes)
30
30
 
31
+ let attributesToCopy: Record<string, any> | undefined
32
+
33
+ if (state.selection.$anchor.sameParent(state.selection.$head)) {
34
+ // only copy attributes if the selection is pointing to a node of the same type
35
+ attributesToCopy = state.selection.$anchor.parent.attrs
36
+ }
37
+
31
38
  if (isActive) {
32
- return commands.setNode(toggleType)
39
+ return commands.setNode(toggleType, attributesToCopy)
33
40
  }
34
41
 
35
- return commands.setNode(type, attributes)
42
+ // If the node is not active, we want to set the new node type with the given attributes
43
+ // Copying over the attributes from the current node if the selection is pointing to a node of the same type
44
+ return commands.setNode(type, { ...attributesToCopy, ...attributes })
36
45
  }
@@ -3,6 +3,7 @@ import { Plugin, PluginKey, Selection } from '@tiptap/pm/state'
3
3
  import { CommandManager } from '../CommandManager.js'
4
4
  import { Extension } from '../Extension.js'
5
5
  import { createChainableState } from '../helpers/createChainableState.js'
6
+ import { isNodeEmpty } from '../helpers/isNodeEmpty.js'
6
7
  import { isiOS } from '../utilities/isiOS.js'
7
8
  import { isMacOS } from '../utilities/isMacOS.js'
8
9
 
@@ -106,7 +107,9 @@ export const Keymap = Extension.create({
106
107
  const docChanges = transactions.some(transaction => transaction.docChanged)
107
108
  && !oldState.doc.eq(newState.doc)
108
109
 
109
- if (!docChanges) {
110
+ const ignoreTr = transactions.some(transaction => transaction.getMeta('preventClearDocument'))
111
+
112
+ if (!docChanges || ignoreTr) {
110
113
  return
111
114
  }
112
115
 
@@ -119,7 +122,7 @@ export const Keymap = Extension.create({
119
122
  return
120
123
  }
121
124
 
122
- const isEmpty = newState.doc.textBetween(0, newState.doc.content.size, ' ', ' ').length === 0
125
+ const isEmpty = isNodeEmpty(newState.doc)
123
126
 
124
127
  if (!isEmpty) {
125
128
  return
package/src/index.ts CHANGED
@@ -11,6 +11,8 @@ export * from './NodePos.js'
11
11
  export * from './NodeView.js'
12
12
  export * from './PasteRule.js'
13
13
  export * from './pasteRules/index.js'
14
+ export * from './plugins/DropPlugin.js'
15
+ export * from './plugins/PastePlugin.js'
14
16
  export * from './Tracker.js'
15
17
  export * from './types.js'
16
18
  export * from './utilities/index.js'
@@ -0,0 +1,14 @@
1
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
2
+ import { Slice } from 'packages/pm/model'
3
+
4
+ export const DropPlugin = (onDrop: (e: DragEvent, slice: Slice, moved: boolean) => void) => {
5
+ return new Plugin({
6
+ key: new PluginKey('tiptapDrop'),
7
+
8
+ props: {
9
+ handleDrop: (_, e, slice, moved) => {
10
+ onDrop(e, slice, moved)
11
+ },
12
+ },
13
+ })
14
+ }
@@ -0,0 +1,14 @@
1
+ import { Slice } from '@tiptap/pm/model'
2
+ import { Plugin, PluginKey } from '@tiptap/pm/state'
3
+
4
+ export const PastePlugin = (onPaste: (e: ClipboardEvent, slice: Slice) => void) => {
5
+ return new Plugin({
6
+ key: new PluginKey('tiptapPaste'),
7
+
8
+ props: {
9
+ handlePaste: (_view, e, slice) => {
10
+ onPaste(e, slice)
11
+ },
12
+ },
13
+ })
14
+ }
package/src/types.ts CHANGED
@@ -3,10 +3,15 @@ import {
3
3
  Node as ProseMirrorNode,
4
4
  NodeType,
5
5
  ParseOptions,
6
+ Slice,
6
7
  } from '@tiptap/pm/model'
7
8
  import { EditorState, Transaction } from '@tiptap/pm/state'
8
9
  import {
9
- Decoration, EditorProps, EditorView, NodeView,
10
+ Decoration,
11
+ EditorProps,
12
+ EditorView,
13
+ NodeView,
14
+ NodeViewConstructor,
10
15
  } from '@tiptap/pm/view'
11
16
 
12
17
  import { Editor } from './Editor.js'
@@ -79,7 +84,24 @@ export interface EditorOptions {
79
84
  };
80
85
  enableInputRules: EnableRules;
81
86
  enablePasteRules: EnableRules;
82
- enableCoreExtensions: boolean;
87
+ /**
88
+ * Determines whether core extensions are enabled.
89
+ *
90
+ * If set to `false`, all core extensions will be disabled.
91
+ * To disable specific core extensions, provide an object where the keys are the extension names and the values are `false`.
92
+ * Extensions not listed in the object will remain enabled.
93
+ *
94
+ * @example
95
+ * // Disable all core extensions
96
+ * enabledCoreExtensions: false
97
+ *
98
+ * @example
99
+ * // Disable only the keymap core extension
100
+ * enabledCoreExtensions: { keymap: false }
101
+ *
102
+ * @default true
103
+ */
104
+ enableCoreExtensions?: boolean | Partial<Record<'editable' | 'clipboardTextSerializer' | 'commands' | 'focusEvents' | 'keymap' | 'tabindex', false>>;
83
105
  /**
84
106
  * If `true`, the editor will check the content for errors on initialization.
85
107
  * Emitting the `contentError` event if the content is invalid.
@@ -100,6 +122,8 @@ export interface EditorOptions {
100
122
  onFocus: (props: EditorEvents['focus']) => void;
101
123
  onBlur: (props: EditorEvents['blur']) => void;
102
124
  onDestroy: (props: EditorEvents['destroy']) => void;
125
+ onPaste: (e: ClipboardEvent, slice: Slice) => void
126
+ onDrop: (e: DragEvent, slice: Slice, moved: boolean) => void
103
127
  }
104
128
 
105
129
  export type HTMLContent = string;
@@ -184,20 +208,21 @@ export type ValuesOf<T> = T[keyof T];
184
208
 
185
209
  export type KeysWithTypeOf<T, Type> = { [P in keyof T]: T[P] extends Type ? P : never }[keyof T];
186
210
 
211
+ export type Simplify<T> = { [KeyType in keyof T]: T[KeyType] } & {};
212
+
187
213
  export type DecorationWithType = Decoration & {
188
214
  type: NodeType;
189
215
  };
190
216
 
191
- export type NodeViewProps = {
192
- editor: Editor;
193
- node: ProseMirrorNode;
194
- decorations: DecorationWithType[];
195
- selected: boolean;
196
- extension: Node;
197
- getPos: () => number;
198
- updateAttributes: (attributes: Record<string, any>) => void;
199
- deleteNode: () => void;
200
- };
217
+ export type NodeViewProps = Simplify<
218
+ Omit<NodeViewRendererProps, 'decorations'> & {
219
+ // TODO this type is not technically correct, but it's the best we can do for now since prosemirror doesn't expose the type of decorations
220
+ decorations: readonly DecorationWithType[];
221
+ selected: boolean;
222
+ updateAttributes: (attributes: Record<string, any>) => void;
223
+ deleteNode: () => void;
224
+ }
225
+ >;
201
226
 
202
227
  export interface NodeViewRendererOptions {
203
228
  stopEvent: ((props: { event: Event }) => boolean) | null;
@@ -208,15 +233,19 @@ export interface NodeViewRendererOptions {
208
233
  }
209
234
 
210
235
  export type NodeViewRendererProps = {
236
+ // pass-through from prosemirror
237
+ node: Parameters<NodeViewConstructor>[0];
238
+ view: Parameters<NodeViewConstructor>[1];
239
+ getPos: () => number; // TODO getPos was incorrectly typed before, change to `Parameters<NodeViewConstructor>[2];` in the next major version
240
+ decorations: Parameters<NodeViewConstructor>[3];
241
+ innerDecorations: Parameters<NodeViewConstructor>[4];
242
+ // tiptap-specific
211
243
  editor: Editor;
212
- node: ProseMirrorNode;
213
- getPos: (() => number) | boolean;
214
- HTMLAttributes: Record<string, any>;
215
- decorations: Decoration[];
216
244
  extension: Node;
245
+ HTMLAttributes: Record<string, any>;
217
246
  };
218
247
 
219
- export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView | {};
248
+ export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView;
220
249
 
221
250
  export type AnyCommands = Record<string, (...args: any[]) => Command>;
222
251
 
@@ -23,7 +23,24 @@ export function mergeAttributes(...objects: Record<string, any>[]): Record<strin
23
23
 
24
24
  mergedAttributes[key] = [...existingClasses, ...insertClasses].join(' ')
25
25
  } else if (key === 'style') {
26
- mergedAttributes[key] = [mergedAttributes[key], value].join('; ')
26
+ const newStyles: string[] = value ? value.split(';').map((style: string) => style.trim()).filter(Boolean) : []
27
+ const existingStyles: string[] = mergedAttributes[key] ? mergedAttributes[key].split(';').map((style: string) => style.trim()).filter(Boolean) : []
28
+
29
+ const styleMap = new Map<string, string>()
30
+
31
+ existingStyles.forEach(style => {
32
+ const [property, val] = style.split(':').map(part => part.trim())
33
+
34
+ styleMap.set(property, val)
35
+ })
36
+
37
+ newStyles.forEach(style => {
38
+ const [property, val] = style.split(':').map(part => part.trim())
39
+
40
+ styleMap.set(property, val)
41
+ })
42
+
43
+ mergedAttributes[key] = Array.from(styleMap.entries()).map(([property, val]) => `${property}: ${val}`).join('; ')
27
44
  } else {
28
45
  mergedAttributes[key] = value
29
46
  }