@tiptap/core 2.2.6 → 2.3.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.
@@ -3,10 +3,9 @@ import { EditorState, Plugin, PluginKey, Transaction } from '@tiptap/pm/state';
3
3
  import { EditorView } from '@tiptap/pm/view';
4
4
  import { EventEmitter } from './EventEmitter.js';
5
5
  import { ExtensionManager } from './ExtensionManager.js';
6
- import * as extensions from './extensions/index.js';
7
6
  import { NodePos } from './NodePos.js';
8
7
  import { CanCommands, ChainedCommands, EditorEvents, EditorOptions, JSONContent, SingleCommands, TextSerializer } from './types.js';
9
- export { extensions };
8
+ export * as extensions from './extensions/index.js';
10
9
  export interface HTMLElement {
11
10
  editor?: Editor;
12
11
  }
@@ -9,6 +9,8 @@ declare module '@tiptap/core' {
9
9
  insertContent: (value: Content, options?: {
10
10
  parseOptions?: ParseOptions;
11
11
  updateSelection?: boolean;
12
+ applyInputRules?: boolean;
13
+ applyPasteRules?: boolean;
12
14
  }) => ReturnType;
13
15
  };
14
16
  }
@@ -9,6 +9,8 @@ declare module '@tiptap/core' {
9
9
  insertContentAt: (position: number | Range, value: Content, options?: {
10
10
  parseOptions?: ParseOptions;
11
11
  updateSelection?: boolean;
12
+ applyInputRules?: boolean;
13
+ applyPasteRules?: boolean;
12
14
  }) => ReturnType;
13
15
  };
14
16
  }
@@ -1,2 +1,5 @@
1
1
  import { Extension } from '../Extension.js';
2
- export declare const ClipboardTextSerializer: Extension<any, any>;
2
+ export declare type ClipboardTextSerializerOptions = {
3
+ blockSeparator?: string;
4
+ };
5
+ export declare const ClipboardTextSerializer: Extension<ClipboardTextSerializerOptions, any>;
@@ -58,6 +58,11 @@ export interface EditorOptions {
58
58
  editable: boolean;
59
59
  editorProps: EditorProps;
60
60
  parseOptions: ParseOptions;
61
+ coreExtensionOptions?: {
62
+ clipboardTextSerializer?: {
63
+ blockSeparator?: string;
64
+ };
65
+ };
61
66
  enableInputRules: EnableRules;
62
67
  enablePasteRules: EnableRules;
63
68
  enableCoreExtensions: boolean;
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.2.6",
4
+ "version": "2.3.0",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -32,7 +32,7 @@
32
32
  "dist"
33
33
  ],
34
34
  "devDependencies": {
35
- "@tiptap/pm": "^2.2.6"
35
+ "@tiptap/pm": "^2.3.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@tiptap/pm": "^2.0.0"
package/src/Editor.ts CHANGED
@@ -9,7 +9,9 @@ import { EditorView } from '@tiptap/pm/view'
9
9
  import { CommandManager } from './CommandManager.js'
10
10
  import { EventEmitter } from './EventEmitter.js'
11
11
  import { ExtensionManager } from './ExtensionManager.js'
12
- import * as extensions from './extensions/index.js'
12
+ import {
13
+ ClipboardTextSerializer, Commands, Editable, FocusEvents, Keymap, Tabindex,
14
+ } from './extensions/index.js'
13
15
  import { createDocument } from './helpers/createDocument.js'
14
16
  import { getAttributes } from './helpers/getAttributes.js'
15
17
  import { getHTMLFromFragment } from './helpers/getHTMLFromFragment.js'
@@ -32,7 +34,7 @@ import {
32
34
  import { createStyleTag } from './utilities/createStyleTag.js'
33
35
  import { isFunction } from './utilities/isFunction.js'
34
36
 
35
- export { extensions }
37
+ export * as extensions from './extensions/index.js'
36
38
 
37
39
  export interface HTMLElement {
38
40
  editor?: Editor
@@ -63,6 +65,7 @@ export class Editor extends EventEmitter<EditorEvents> {
63
65
  editable: true,
64
66
  editorProps: {},
65
67
  parseOptions: {},
68
+ coreExtensionOptions: {},
66
69
  enableInputRules: true,
67
70
  enablePasteRules: true,
68
71
  enableCoreExtensions: true,
@@ -235,7 +238,17 @@ export class Editor extends EventEmitter<EditorEvents> {
235
238
  * Creates an extension manager.
236
239
  */
237
240
  private createExtensionManager(): void {
238
- const coreExtensions = this.options.enableCoreExtensions ? Object.values(extensions) : []
241
+
242
+ const coreExtensions = this.options.enableCoreExtensions ? [
243
+ Editable,
244
+ ClipboardTextSerializer.configure({
245
+ blockSeparator: this.options.coreExtensionOptions?.clipboardTextSerializer?.blockSeparator,
246
+ }),
247
+ Commands,
248
+ FocusEvents,
249
+ Keymap,
250
+ Tabindex,
251
+ ] : []
239
252
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter(extension => {
240
253
  return ['extension', 'node', 'mark'].includes(extension?.type)
241
254
  })
package/src/InputRule.ts CHANGED
@@ -191,6 +191,26 @@ export function inputRulesPlugin(props: { editor: Editor; rules: InputRule[] }):
191
191
  return stored
192
192
  }
193
193
 
194
+ // if InputRule is triggered by insertContent()
195
+ const simulatedInputMeta = tr.getMeta('applyInputRules')
196
+ const isSimulatedInput = !!simulatedInputMeta
197
+
198
+ if (isSimulatedInput) {
199
+ setTimeout(() => {
200
+ const { from, text } = simulatedInputMeta
201
+ const to = from + text.length
202
+
203
+ run({
204
+ editor,
205
+ from,
206
+ to,
207
+ text,
208
+ rules,
209
+ plugin,
210
+ })
211
+ })
212
+ }
213
+
194
214
  return tr.selectionSet || tr.docChanged ? null : prev
195
215
  },
196
216
  },
package/src/NodePos.ts CHANGED
@@ -136,7 +136,7 @@ export class NodePos {
136
136
  this.node.content.forEach((node, offset) => {
137
137
  const isBlock = node.isBlock && !node.isTextblock
138
138
 
139
- const targetPos = this.pos + offset + (isBlock ? 0 : 1)
139
+ const targetPos = this.pos + offset + 1
140
140
  const $pos = this.resolvedPos.doc.resolve(targetPos)
141
141
 
142
142
  if (!isBlock && $pos.depth <= this.depth) {
@@ -201,7 +201,7 @@ export class NodePos {
201
201
  let nodes: NodePos[] = []
202
202
 
203
203
  // iterate through children recursively finding all nodes which match the selector with the node name
204
- if (this.isBlock || !this.children || this.children.length === 0) {
204
+ if (!this.children || this.children.length === 0) {
205
205
  return nodes
206
206
  }
207
207
 
package/src/PasteRule.ts CHANGED
@@ -154,6 +154,16 @@ function run(config: {
154
154
  return success
155
155
  }
156
156
 
157
+ const createClipboardPasteEvent = (text: string) => {
158
+ const event = new ClipboardEvent('paste', {
159
+ clipboardData: new DataTransfer(),
160
+ })
161
+
162
+ event.clipboardData?.setData('text/html', text)
163
+
164
+ return event
165
+ }
166
+
157
167
  /**
158
168
  * Create an paste rules plugin. When enabled, it will cause pasted
159
169
  * text that matches any of the given rules to trigger the rule’s
@@ -167,6 +177,45 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
167
177
  let pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
168
178
  let dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
169
179
 
180
+ const processEvent = ({
181
+ state,
182
+ from,
183
+ to,
184
+ rule,
185
+ pasteEvt,
186
+ }: {
187
+ state: EditorState
188
+ from: number
189
+ to: { b: number }
190
+ rule: PasteRule
191
+ pasteEvt: ClipboardEvent | null
192
+ }) => {
193
+ const tr = state.tr
194
+ const chainableState = createChainableState({
195
+ state,
196
+ transaction: tr,
197
+ })
198
+
199
+ const handler = run({
200
+ editor,
201
+ state: chainableState,
202
+ from: Math.max(from - 1, 0),
203
+ to: to.b - 1,
204
+ rule,
205
+ pasteEvent: pasteEvt,
206
+ dropEvent,
207
+ })
208
+
209
+ if (!handler || !tr.steps.length) {
210
+ return
211
+ }
212
+
213
+ dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
214
+ pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
215
+
216
+ return tr
217
+ }
218
+
170
219
  const plugins = rules.map(rule => {
171
220
  return new Plugin({
172
221
  // we register a global drag handler to track the current drag source element
@@ -212,45 +261,45 @@ export function pasteRulesPlugin(props: { editor: Editor; rules: PasteRule[] }):
212
261
  const isPaste = transaction.getMeta('uiEvent') === 'paste' && !isPastedFromProseMirror
213
262
  const isDrop = transaction.getMeta('uiEvent') === 'drop' && !isDroppedFromProseMirror
214
263
 
215
- if (!isPaste && !isDrop) {
264
+ // if PasteRule is triggered by insertContent()
265
+ const simulatedPasteMeta = transaction.getMeta('applyPasteRules')
266
+ const isSimulatedPaste = !!simulatedPasteMeta
267
+
268
+ if (!isPaste && !isDrop && !isSimulatedPaste) {
216
269
  return
217
270
  }
218
271
 
219
- // stop if there is no changed range
272
+ // Handle simulated paste
273
+ if (isSimulatedPaste) {
274
+ const { from, text } = simulatedPasteMeta
275
+ const to = from + text.length
276
+ const pasteEvt = createClipboardPasteEvent(text)
277
+
278
+ return processEvent({
279
+ rule,
280
+ state,
281
+ from,
282
+ to: { b: to },
283
+ pasteEvt,
284
+ })
285
+ }
286
+
287
+ // handle actual paste/drop
220
288
  const from = oldState.doc.content.findDiffStart(state.doc.content)
221
289
  const to = oldState.doc.content.findDiffEnd(state.doc.content)
222
290
 
291
+ // stop if there is no changed range
223
292
  if (!isNumber(from) || !to || from === to.b) {
224
293
  return
225
294
  }
226
295
 
227
- // build a chainable state
228
- // so we can use a single transaction for all paste rules
229
- const tr = state.tr
230
- const chainableState = createChainableState({
231
- state,
232
- transaction: tr,
233
- })
234
-
235
- const handler = run({
236
- editor,
237
- state: chainableState,
238
- from: Math.max(from - 1, 0),
239
- to: to.b - 1,
296
+ return processEvent({
240
297
  rule,
241
- pasteEvent,
242
- dropEvent,
298
+ state,
299
+ from,
300
+ to,
301
+ pasteEvt: pasteEvent,
243
302
  })
244
-
245
- // stop if there are no changes
246
- if (!handler || !tr.steps.length) {
247
- return
248
- }
249
-
250
- dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null
251
- pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null
252
-
253
- return tr
254
303
  },
255
304
  })
256
305
  })
@@ -13,6 +13,8 @@ declare module '@tiptap/core' {
13
13
  options?: {
14
14
  parseOptions?: ParseOptions
15
15
  updateSelection?: boolean
16
+ applyInputRules?: boolean
17
+ applyPasteRules?: boolean
16
18
  },
17
19
  ) => ReturnType
18
20
  }
@@ -16,6 +16,8 @@ declare module '@tiptap/core' {
16
16
  options?: {
17
17
  parseOptions?: ParseOptions
18
18
  updateSelection?: boolean
19
+ applyInputRules?: boolean
20
+ applyPasteRules?: boolean
19
21
  },
20
22
  ) => ReturnType
21
23
  }
@@ -31,6 +33,8 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
31
33
  options = {
32
34
  parseOptions: {},
33
35
  updateSelection: true,
36
+ applyInputRules: false,
37
+ applyPasteRules: false,
34
38
  ...options,
35
39
  }
36
40
 
@@ -76,26 +80,40 @@ export const insertContentAt: RawCommands['insertContentAt'] = (position, value,
76
80
  }
77
81
  }
78
82
 
83
+ let newContent
84
+
79
85
  // if there is only plain text we have to use `insertText`
80
86
  // because this will keep the current marks
81
87
  if (isOnlyTextContent) {
82
88
  // if value is string, we can use it directly
83
89
  // otherwise if it is an array, we have to join it
84
90
  if (Array.isArray(value)) {
85
- tr.insertText(value.map(v => v.text || '').join(''), from, to)
91
+ newContent = value.map(v => v.text || '').join('')
86
92
  } else if (typeof value === 'object' && !!value && !!value.text) {
87
- tr.insertText(value.text, from, to)
93
+ newContent = value.text
88
94
  } else {
89
- tr.insertText(value as string, from, to)
95
+ newContent = value as string
90
96
  }
97
+
98
+ tr.insertText(newContent, from, to)
91
99
  } else {
92
- tr.replaceWith(from, to, content)
100
+ newContent = content
101
+
102
+ tr.replaceWith(from, to, newContent)
93
103
  }
94
104
 
95
105
  // set cursor at end of inserted content
96
106
  if (options.updateSelection) {
97
107
  selectionToInsertionEnd(tr, tr.steps.length - 1, -1)
98
108
  }
109
+
110
+ if (options.applyInputRules) {
111
+ tr.setMeta('applyInputRules', { from, text: newContent })
112
+ }
113
+
114
+ if (options.applyPasteRules) {
115
+ tr.setMeta('applyPasteRules', { from, text: newContent })
116
+ }
99
117
  }
100
118
 
101
119
  return true
@@ -4,9 +4,19 @@ import { Extension } from '../Extension.js'
4
4
  import { getTextBetween } from '../helpers/getTextBetween.js'
5
5
  import { getTextSerializersFromSchema } from '../helpers/getTextSerializersFromSchema.js'
6
6
 
7
- export const ClipboardTextSerializer = Extension.create({
7
+ export type ClipboardTextSerializerOptions = {
8
+ blockSeparator?: string,
9
+ }
10
+
11
+ export const ClipboardTextSerializer = Extension.create<ClipboardTextSerializerOptions>({
8
12
  name: 'clipboardTextSerializer',
9
13
 
14
+ addOptions() {
15
+ return {
16
+ blockSeparator: undefined,
17
+ }
18
+ },
19
+
10
20
  addProseMirrorPlugins() {
11
21
  return [
12
22
  new Plugin({
@@ -23,6 +33,9 @@ export const ClipboardTextSerializer = Extension.create({
23
33
  const range = { from, to }
24
34
 
25
35
  return getTextBetween(doc, range, {
36
+ ...(this.options.blockSeparator !== undefined
37
+ ? { blockSeparator: this.options.blockSeparator }
38
+ : {}),
26
39
  textSerializers,
27
40
  })
28
41
  },
package/src/types.ts CHANGED
@@ -59,6 +59,11 @@ export interface EditorOptions {
59
59
  editable: boolean
60
60
  editorProps: EditorProps
61
61
  parseOptions: ParseOptions
62
+ coreExtensionOptions?: {
63
+ clipboardTextSerializer?: {
64
+ blockSeparator?: string
65
+ }
66
+ }
62
67
  enableInputRules: EnableRules
63
68
  enablePasteRules: EnableRules
64
69
  enableCoreExtensions: boolean