@tiptap/core 3.0.0-beta.3 → 3.0.0-beta.30

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.
@@ -0,0 +1 @@
1
+ module.exports = require('../dist/jsx-runtime/jsx-runtime.cjs')
@@ -0,0 +1 @@
1
+ export * from '../src/jsx-runtime.ts'
@@ -0,0 +1 @@
1
+ export type * from '../src/jsx-runtime.js'
@@ -0,0 +1 @@
1
+ export * from '../dist/jsx-runtime/jsx-runtime.js'
@@ -1 +1 @@
1
- export * from '../dist/jsx-runtime/jsx-runtime.cjs'
1
+ module.exports = require('../dist/jsx-runtime/jsx-runtime.cjs')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiptap/core",
3
3
  "description": "headless rich text editor",
4
- "version": "3.0.0-beta.3",
4
+ "version": "3.0.0-beta.30",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -48,13 +48,14 @@
48
48
  "files": [
49
49
  "src",
50
50
  "dist",
51
- "jsx-runtime"
51
+ "jsx-runtime",
52
+ "jsx-dev-runtime"
52
53
  ],
53
54
  "devDependencies": {
54
- "@tiptap/pm": "3.0.0-beta.3"
55
+ "@tiptap/pm": "3.0.0-beta.30"
55
56
  },
56
57
  "peerDependencies": {
57
- "@tiptap/pm": "3.0.0-beta.3"
58
+ "@tiptap/pm": "3.0.0-beta.30"
58
59
  },
59
60
  "repository": {
60
61
  "type": "git",
package/src/Editor.ts CHANGED
@@ -92,6 +92,7 @@ export class Editor extends EventEmitter<EditorEvents> {
92
92
  enablePasteRules: true,
93
93
  enableCoreExtensions: true,
94
94
  enableContentCheck: false,
95
+ emitContentError: false,
95
96
  onBeforeCreate: () => null,
96
97
  onCreate: () => null,
97
98
  onUpdate: () => null,
@@ -287,6 +288,7 @@ export class Editor extends EventEmitter<EditorEvents> {
287
288
  composing: false,
288
289
  dragging: null,
289
290
  editable: true,
291
+ isDestroyed: false,
290
292
  } as EditorView,
291
293
  {
292
294
  get: (obj, key) => {
@@ -361,7 +363,7 @@ export class Editor extends EventEmitter<EditorEvents> {
361
363
  const name = typeof nameOrPluginKey === 'string' ? `${nameOrPluginKey}$` : nameOrPluginKey.key
362
364
 
363
365
  // @ts-ignore
364
- plugins = prevPlugins.filter(plugin => !plugin.key.startsWith(name))
366
+ plugins = plugins.filter(plugin => !plugin.key.startsWith(name))
365
367
  })
366
368
 
367
369
  if (prevPlugins.length === plugins.length) {
@@ -717,8 +719,7 @@ export class Editor extends EventEmitter<EditorEvents> {
717
719
  * Check if the editor is already destroyed.
718
720
  */
719
721
  public get isDestroyed(): boolean {
720
- // @ts-ignore
721
- return !this.view?.docView
722
+ return this.editorView?.isDestroyed ?? true
722
723
  }
723
724
 
724
725
  public $node(selector: string, attributes?: { [key: string]: any }): NodePos | null {
package/src/Extension.ts CHANGED
@@ -15,8 +15,16 @@ export class Extension<Options = any, Storage = any> extends Extendable<
15
15
  > {
16
16
  type = 'extension'
17
17
 
18
- static create<O = any, S = any>(config: Partial<ExtensionConfig<O, S>> = {}) {
19
- return new Extension<O, S>(config)
18
+ /**
19
+ * Create a new Extension instance
20
+ * @param config - Extension configuration object or a function that returns a configuration object
21
+ */
22
+ static create<O = any, S = any>(
23
+ config: Partial<ExtensionConfig<O, S>> | (() => Partial<ExtensionConfig<O, S>>) = {},
24
+ ) {
25
+ // If the config is a function, execute it to get the configuration object
26
+ const resolvedConfig = typeof config === 'function' ? config() : config
27
+ return new Extension<O, S>(resolvedConfig)
20
28
  }
21
29
 
22
30
  configure(options?: Partial<Options>) {
@@ -27,7 +35,9 @@ export class Extension<Options = any, Storage = any> extends Extendable<
27
35
  ExtendedOptions = Options,
28
36
  ExtendedStorage = Storage,
29
37
  ExtendedConfig = ExtensionConfig<ExtendedOptions, ExtendedStorage>,
30
- >(extendedConfig?: Partial<ExtendedConfig>) {
31
- return super.extend(extendedConfig) as Extension<ExtendedOptions, ExtendedStorage>
38
+ >(extendedConfig?: Partial<ExtendedConfig> | (() => Partial<ExtendedConfig>)) {
39
+ // If the extended config is a function, execute it to get the configuration object
40
+ const resolvedConfig = typeof extendedConfig === 'function' ? extendedConfig() : extendedConfig
41
+ return super.extend(resolvedConfig) as Extension<ExtendedOptions, ExtendedStorage>
32
42
  }
33
43
  }
@@ -17,7 +17,7 @@ import {
17
17
  sortExtensions,
18
18
  splitExtensions,
19
19
  } from './helpers/index.js'
20
- import { type MarkConfig, type NodeConfig, type Storage, getMarkType } from './index.js'
20
+ import { type MarkConfig, type NodeConfig, type Storage, getMarkType, updateMarkViewAttributes } from './index.js'
21
21
  import type { InputRule } from './InputRule.js'
22
22
  import { inputRulesPlugin } from './InputRule.js'
23
23
  import { Mark } from './Mark.js'
@@ -262,6 +262,9 @@ export class ExtensionManager {
262
262
  editor,
263
263
  extension,
264
264
  HTMLAttributes,
265
+ updateAttributes: (attrs: Record<string, any>) => {
266
+ updateMarkViewAttributes(mark, editor, attrs)
267
+ },
265
268
  })
266
269
  }
267
270
 
package/src/Mark.ts CHANGED
@@ -146,8 +146,14 @@ export interface MarkConfig<Options = any, Storage = any>
146
146
  export class Mark<Options = any, Storage = any> extends Extendable<Options, Storage, MarkConfig<Options, Storage>> {
147
147
  type = 'mark'
148
148
 
149
- static create<O = any, S = any>(config: Partial<MarkConfig<O, S>> = {}) {
150
- return new Mark<O, S>(config)
149
+ /**
150
+ * Create a new Mark instance
151
+ * @param config - Mark configuration object or a function that returns a configuration object
152
+ */
153
+ static create<O = any, S = any>(config: Partial<MarkConfig<O, S>> | (() => Partial<MarkConfig<O, S>>) = {}) {
154
+ // If the config is a function, execute it to get the configuration object
155
+ const resolvedConfig = typeof config === 'function' ? config() : config
156
+ return new Mark<O, S>(resolvedConfig)
151
157
  }
152
158
 
153
159
  static handleExit({ editor, mark }: { editor: Editor; mark: Mark }) {
@@ -186,7 +192,9 @@ export class Mark<Options = any, Storage = any> extends Extendable<Options, Stor
186
192
  ExtendedOptions = Options,
187
193
  ExtendedStorage = Storage,
188
194
  ExtendedConfig = MarkConfig<ExtendedOptions, ExtendedStorage>,
189
- >(extendedConfig?: Partial<ExtendedConfig>) {
190
- return super.extend(extendedConfig) as Mark<ExtendedOptions, ExtendedStorage>
195
+ >(extendedConfig?: Partial<ExtendedConfig> | (() => Partial<ExtendedConfig>)) {
196
+ // If the extended config is a function, execute it to get the configuration object
197
+ const resolvedConfig = typeof extendedConfig === 'function' ? extendedConfig() : extendedConfig
198
+ return super.extend(resolvedConfig) as Mark<ExtendedOptions, ExtendedStorage>
191
199
  }
192
200
  }
package/src/MarkView.ts CHANGED
@@ -1,9 +1,57 @@
1
+ import type { Mark } from '@tiptap/pm/model'
1
2
  import type { ViewMutationRecord } from '@tiptap/pm/view'
2
3
 
3
4
  import type { Editor } from './Editor.js'
4
5
  import type { MarkViewProps, MarkViewRendererOptions } from './types.js'
5
6
  import { isAndroid, isiOS } from './utilities/index.js'
6
7
 
8
+ export function updateMarkViewAttributes(checkMark: Mark, editor: Editor, attrs: Record<string, any> = {}): void {
9
+ const { state } = editor
10
+ const { doc, tr } = state
11
+ const thisMark = checkMark
12
+
13
+ doc.descendants((node, pos) => {
14
+ const from = tr.mapping.map(pos)
15
+ const to = tr.mapping.map(pos) + node.nodeSize
16
+ let foundMark: Mark | null = null
17
+
18
+ // find the mark on the current node
19
+ node.marks.forEach(mark => {
20
+ if (mark !== thisMark) {
21
+ return false
22
+ }
23
+
24
+ foundMark = mark
25
+ })
26
+
27
+ if (!foundMark) {
28
+ return
29
+ }
30
+
31
+ // check if we need to update given the attributes
32
+ let needsUpdate = false
33
+ Object.keys(attrs).forEach(k => {
34
+ if (attrs[k] !== foundMark!.attrs[k]) {
35
+ needsUpdate = true
36
+ }
37
+ })
38
+
39
+ if (needsUpdate) {
40
+ const updatedMark = checkMark.type.create({
41
+ ...checkMark.attrs,
42
+ ...attrs,
43
+ })
44
+
45
+ tr.removeMark(from, to, checkMark.type)
46
+ tr.addMark(from, to, updatedMark)
47
+ }
48
+ })
49
+
50
+ if (tr.docChanged) {
51
+ editor.view.dispatch(tr)
52
+ }
53
+ }
54
+
7
55
  export class MarkView<Component, Options extends MarkViewRendererOptions = MarkViewRendererOptions> {
8
56
  component: Component
9
57
  editor: Editor
@@ -27,6 +75,14 @@ export class MarkView<Component, Options extends MarkViewRendererOptions = MarkV
27
75
  return null
28
76
  }
29
77
 
78
+ /**
79
+ * Update the attributes of the mark in the document.
80
+ * @param attrs The attributes to update.
81
+ */
82
+ updateAttributes(attrs: Record<string, any>, checkMark?: Mark): void {
83
+ updateMarkViewAttributes(checkMark || this.mark, this.editor, attrs)
84
+ }
85
+
30
86
  ignoreMutation(mutation: ViewMutationRecord): boolean {
31
87
  if (!this.dom || !this.contentDOM) {
32
88
  return true
package/src/Node.ts CHANGED
@@ -340,8 +340,14 @@ export interface NodeConfig<Options = any, Storage = any>
340
340
  export class Node<Options = any, Storage = any> extends Extendable<Options, Storage, NodeConfig<Options, Storage>> {
341
341
  type = 'node'
342
342
 
343
- static create<O = any, S = any>(config: Partial<NodeConfig<O, S>> = {}) {
344
- return new Node<O, S>(config)
343
+ /**
344
+ * Create a new Node instance
345
+ * @param config - Node configuration object or a function that returns a configuration object
346
+ */
347
+ static create<O = any, S = any>(config: Partial<NodeConfig<O, S>> | (() => Partial<NodeConfig<O, S>>) = {}) {
348
+ // If the config is a function, execute it to get the configuration object
349
+ const resolvedConfig = typeof config === 'function' ? config() : config
350
+ return new Node<O, S>(resolvedConfig)
345
351
  }
346
352
 
347
353
  configure(options?: Partial<Options>) {
@@ -352,7 +358,9 @@ export class Node<Options = any, Storage = any> extends Extendable<Options, Stor
352
358
  ExtendedOptions = Options,
353
359
  ExtendedStorage = Storage,
354
360
  ExtendedConfig = NodeConfig<ExtendedOptions, ExtendedStorage>,
355
- >(extendedConfig?: Partial<ExtendedConfig>) {
356
- return super.extend(extendedConfig) as Node<ExtendedOptions, ExtendedStorage>
361
+ >(extendedConfig?: Partial<ExtendedConfig> | (() => Partial<ExtendedConfig>)) {
362
+ // If the extended config is a function, execute it to get the configuration object
363
+ const resolvedConfig = typeof extendedConfig === 'function' ? extendedConfig() : extendedConfig
364
+ return super.extend(resolvedConfig) as Node<ExtendedOptions, ExtendedStorage>
357
365
  }
358
366
  }
package/src/NodePos.ts CHANGED
@@ -136,6 +136,12 @@ export class NodePos {
136
136
  const isNonTextAtom = node.isAtom && !node.isText
137
137
 
138
138
  const targetPos = this.pos + offset + (isNonTextAtom ? 0 : 1)
139
+
140
+ // Check if targetPos is within valid document range
141
+ if (targetPos < 0 || targetPos > this.resolvedPos.doc.nodeSize - 2) {
142
+ return
143
+ }
144
+
139
145
  const $pos = this.resolvedPos.doc.resolve(targetPos)
140
146
 
141
147
  if (!isBlock && $pos.depth <= this.depth) {
package/src/PasteRule.ts CHANGED
@@ -267,7 +267,7 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
267
267
  if (!isDroppedFromProseMirror) {
268
268
  const dragFromOtherEditor = tiptapDragFromOtherEditor
269
269
 
270
- if (dragFromOtherEditor) {
270
+ if (dragFromOtherEditor?.isEditable) {
271
271
  // setTimeout to avoid the wrong content after drop, timeout arg can't be empty or 0
272
272
  setTimeout(() => {
273
273
  const selection = dragFromOtherEditor.state.selection
@@ -30,7 +30,7 @@ export const cut: RawCommands['cut'] =
30
30
 
31
31
  tr.insert(newPos, contentSlice.content)
32
32
 
33
- tr.setSelection(new TextSelection(tr.doc.resolve(newPos - 1)))
33
+ tr.setSelection(new TextSelection(tr.doc.resolve(Math.max(newPos - 1, 0))))
34
34
 
35
35
  return true
36
36
  }
@@ -76,18 +76,10 @@ export const insertContentAt: RawCommands['insertContentAt'] =
76
76
  let content: Fragment | ProseMirrorNode
77
77
  const { selection } = editor.state
78
78
 
79
- try {
80
- content = createNodeFromContent(value, editor.schema, {
81
- parseOptions: {
82
- preserveWhitespace: 'full',
83
- ...options.parseOptions,
84
- },
85
- errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
86
- })
87
- } catch (e) {
79
+ const emitContentError = (error: Error) => {
88
80
  editor.emit('contentError', {
89
81
  editor,
90
- error: e as Error,
82
+ error,
91
83
  disableCollaboration: () => {
92
84
  if (
93
85
  'collaboration' in editor.storage &&
@@ -98,6 +90,33 @@ export const insertContentAt: RawCommands['insertContentAt'] =
98
90
  }
99
91
  },
100
92
  })
93
+ }
94
+
95
+ const parseOptions: ParseOptions = {
96
+ preserveWhitespace: 'full',
97
+ ...options.parseOptions,
98
+ }
99
+
100
+ // If `emitContentError` is enabled, we want to check the content for errors
101
+ // but ignore them (do not remove the invalid content from the document)
102
+ if (!options.errorOnInvalidContent && !editor.options.enableContentCheck && editor.options.emitContentError) {
103
+ try {
104
+ createNodeFromContent(value, editor.schema, {
105
+ parseOptions,
106
+ errorOnInvalidContent: true,
107
+ })
108
+ } catch (e) {
109
+ emitContentError(e as Error)
110
+ }
111
+ }
112
+
113
+ try {
114
+ content = createNodeFromContent(value, editor.schema, {
115
+ parseOptions,
116
+ errorOnInvalidContent: options.errorOnInvalidContent ?? editor.options.enableContentCheck,
117
+ })
118
+ } catch (e) {
119
+ emitContentError(e as Error)
101
120
  return false
102
121
  }
103
122
 
@@ -163,8 +182,9 @@ export const insertContentAt: RawCommands['insertContentAt'] =
163
182
 
164
183
  const fromSelectionAtStart = selection.$from.parentOffset === 0
165
184
  const isTextSelection = selection.$from.node().isText || selection.$from.node().isTextblock
185
+ const hasContent = selection.$from.node().content.size > 0
166
186
 
167
- if (fromSelectionAtStart && isTextSelection) {
187
+ if (fromSelectionAtStart && isTextSelection && hasContent) {
168
188
  from = Math.max(0, from - 1)
169
189
  }
170
190
 
package/src/types.ts CHANGED
@@ -354,6 +354,15 @@ export interface EditorOptions {
354
354
  * @default false
355
355
  */
356
356
  enableContentCheck: boolean
357
+ /**
358
+ * If `true`, the editor will emit the `contentError` event if invalid content is
359
+ * encountered but `enableContentCheck` is `false`. This lets you preserve the
360
+ * invalid editor content while still showing a warning or error message to
361
+ * the user.
362
+ *
363
+ * @default false
364
+ */
365
+ emitContentError: boolean
357
366
  /**
358
367
  * Called before the editor is constructed.
359
368
  */
@@ -653,9 +662,11 @@ export interface MarkViewRendererProps {
653
662
  * The HTML attributes that should be added to the mark's DOM element.
654
663
  */
655
664
  HTMLAttributes: Record<string, any>
665
+
666
+ updateAttributes: (attrs: Record<string, any>) => void
656
667
  }
657
668
 
658
- export type MarkViewRenderer = (props: MarkViewRendererProps) => MarkView
669
+ export type MarkViewRenderer<Props = MarkViewRendererProps> = (props: Props) => MarkView
659
670
 
660
671
  export interface MarkViewRendererOptions {
661
672
  ignoreMutation: ((props: { mutation: ViewMutationRecord }) => boolean) | null
@@ -0,0 +1,30 @@
1
+ import type { NodeType } from '@tiptap/pm/model'
2
+ import { type EditorState, NodeSelection } from '@tiptap/pm/state'
3
+
4
+ export function canInsertNode(state: EditorState, nodeType: NodeType): boolean {
5
+ const { selection } = state
6
+ const { $from } = selection
7
+
8
+ // Special handling for NodeSelection
9
+ if (selection instanceof NodeSelection) {
10
+ const index = $from.index()
11
+ const parent = $from.parent
12
+
13
+ // Can we replace the selected node with the horizontal rule?
14
+ return parent.canReplaceWith(index, index + 1, nodeType)
15
+ }
16
+
17
+ // Default: check if we can insert at the current position
18
+ let depth = $from.depth
19
+
20
+ while (depth >= 0) {
21
+ const index = $from.index(depth)
22
+ const parent = $from.node(depth)
23
+ const match = parent.contentMatchAt(index)
24
+ if (match.matchType(nodeType)) {
25
+ return true
26
+ }
27
+ depth -= 1
28
+ }
29
+ return false
30
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './callOrReturn.js'
2
+ export * from './canInsertNode.js'
2
3
  export * from './createStyleTag.js'
3
4
  export * from './deleteProps.js'
4
5
  export * from './elementFromString.js'