@tiptap/core 2.0.0-beta.180 → 2.0.0-beta.183

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.
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.0.0-beta.180",
4
+ "version": "2.0.0-beta.183",
5
5
  "homepage": "https://tiptap.dev",
6
6
  "keywords": [
7
7
  "tiptap",
@@ -24,13 +24,13 @@
24
24
  "dist"
25
25
  ],
26
26
  "dependencies": {
27
- "prosemirror-commands": "^1.3.0",
28
- "prosemirror-keymap": "^1.2.0",
29
- "prosemirror-model": "^1.17.0",
30
- "prosemirror-schema-list": "^1.2.0",
31
- "prosemirror-state": "^1.4.0",
32
- "prosemirror-transform": "^1.6.0",
33
- "prosemirror-view": "^1.25.0"
27
+ "prosemirror-commands": "1.3.0",
28
+ "prosemirror-keymap": "1.2.0",
29
+ "prosemirror-model": "1.18.1",
30
+ "prosemirror-schema-list": "1.2.0",
31
+ "prosemirror-state": "1.4.1",
32
+ "prosemirror-transform": "1.6.0",
33
+ "prosemirror-view": "1.26.2"
34
34
  },
35
35
  "repository": {
36
36
  "type": "git",
@@ -38,5 +38,5 @@
38
38
  "directory": "packages/core"
39
39
  },
40
40
  "sideEffects": false,
41
- "gitHead": "a1e612bf897a14065b4f9ba6d48925c97d136811"
41
+ "gitHead": "5bcfb322ceb30575430bd8797f2787d65de80f2c"
42
42
  }
@@ -107,13 +107,13 @@ export class CommandManager {
107
107
 
108
108
  public createCan(startTr?: Transaction): CanCommands {
109
109
  const { rawCommands, state } = this
110
- const dispatch = undefined
110
+ const dispatch = false
111
111
  const tr = startTr || state.tr
112
112
  const props = this.buildProps(tr, dispatch)
113
113
  const formattedCommands = Object.fromEntries(Object
114
114
  .entries(rawCommands)
115
115
  .map(([name, command]) => {
116
- return [name, (...args: never[]) => command(...args)({ ...props, dispatch })]
116
+ return [name, (...args: never[]) => command(...args)({ ...props, dispatch: undefined })]
117
117
  })) as unknown as SingleCommands
118
118
 
119
119
  return {
package/src/Editor.ts CHANGED
@@ -169,6 +169,7 @@ export class Editor extends EventEmitter<EditorEvents> {
169
169
  */
170
170
  public setEditable(editable: boolean): void {
171
171
  this.setOptions({ editable })
172
+ this.emit('update', { editor: this, transaction: this.state.tr })
172
173
  }
173
174
 
174
175
  /**
@@ -3,7 +3,7 @@ import { Node as ProsemirrorNode, Schema } from 'prosemirror-model'
3
3
  import { Plugin } from 'prosemirror-state'
4
4
  import { Decoration, EditorView } from 'prosemirror-view'
5
5
 
6
- import { NodeConfig } from '.'
6
+ import { Mark, NodeConfig } from '.'
7
7
  import { Editor } from './Editor'
8
8
  import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions'
9
9
  import { getExtensionField } from './helpers/getExtensionField'
@@ -252,6 +252,13 @@ export class ExtensionManager {
252
252
  context,
253
253
  )
254
254
 
255
+ let defaultBindings: Record<string, () => boolean> = {}
256
+
257
+ // bind exit handling
258
+ if (extension.type === 'mark' && extension.config.exitable) {
259
+ defaultBindings.ArrowRight = () => Mark.handleExit({ editor, mark: (extension as Mark) })
260
+ }
261
+
255
262
  if (addKeyboardShortcuts) {
256
263
  const bindings = Object.fromEntries(
257
264
  Object
@@ -261,11 +268,13 @@ export class ExtensionManager {
261
268
  }),
262
269
  )
263
270
 
264
- const keyMapPlugin = keymap(bindings)
265
-
266
- plugins.push(keyMapPlugin)
271
+ defaultBindings = { ...defaultBindings, ...bindings }
267
272
  }
268
273
 
274
+ const keyMapPlugin = keymap(defaultBindings)
275
+
276
+ plugins.push(keyMapPlugin)
277
+
269
278
  const addInputRules = getExtensionField<AnyConfig['addInputRules']>(
270
279
  extension,
271
280
  'addInputRules',
package/src/Mark.ts CHANGED
@@ -304,6 +304,11 @@ declare module '@tiptap/core' {
304
304
  parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'],
305
305
  }) => MarkSpec['excludes']),
306
306
 
307
+ /**
308
+ * Marks this Mark as exitable
309
+ */
310
+ exitable?: boolean | (() => boolean),
311
+
307
312
  /**
308
313
  * Group
309
314
  */
@@ -486,4 +491,38 @@ export class Mark<Options = any, Storage = any> {
486
491
 
487
492
  return extension
488
493
  }
494
+
495
+ static handleExit({
496
+ editor,
497
+ mark,
498
+ }: {
499
+ editor: Editor
500
+ mark: Mark
501
+ }) {
502
+ const { tr } = editor.state
503
+ const currentPos = editor.state.selection.$from
504
+ const isAtEnd = currentPos.pos === currentPos.end()
505
+
506
+ if (isAtEnd) {
507
+ const currentMarks = currentPos.marks()
508
+ const isInMark = !!currentMarks.find(m => m?.type.name === mark.name)
509
+
510
+ if (!isInMark) {
511
+ return false
512
+ }
513
+
514
+ const removeMark = currentMarks.find(m => m?.type.name === mark.name)
515
+
516
+ if (removeMark) {
517
+ tr.removeStoredMark(removeMark)
518
+ }
519
+ tr.insertText(' ', currentPos.pos)
520
+
521
+ editor.view.dispatch(tr)
522
+
523
+ return true
524
+ }
525
+
526
+ return false
527
+ }
489
528
  }
package/src/PasteRule.ts CHANGED
@@ -190,7 +190,7 @@ export function pasteRulesPlugin(props: { editor: Editor, rules: PasteRule[] }):
190
190
  return false
191
191
  },
192
192
 
193
- paste: (view, event) => {
193
+ paste: (view, event: Event) => {
194
194
  const html = (event as ClipboardEvent).clipboardData?.getData('text/html')
195
195
 
196
196
  isPastedFromProseMirror = !!html?.includes('data-pm-slice')
@@ -229,7 +229,7 @@ export function pasteRulesPlugin(props: { editor: Editor, rules: PasteRule[] }):
229
229
  editor,
230
230
  state: chainableState,
231
231
  from: Math.max(from - 1, 0),
232
- to: to.b,
232
+ to: to.b - 1,
233
233
  rule,
234
234
  })
235
235
 
@@ -60,7 +60,9 @@ export const focus: RawCommands['focus'] = (position = null, options = {}) => ({
60
60
  return true
61
61
  }
62
62
 
63
- const selection = resolveFocusPosition(editor.state.doc, position) || editor.state.selection
63
+ // pass through tr.doc instead of editor.state.doc
64
+ // since transactions could change the editors state before this command has been run
65
+ const selection = resolveFocusPosition(tr.doc, position) || editor.state.selection
64
66
  const isSameSelection = editor.state.selection.eq(selection)
65
67
 
66
68
  if (dispatch) {
@@ -1,5 +1,4 @@
1
1
  import { ParseOptions } from 'prosemirror-model'
2
- import { TextSelection } from 'prosemirror-state'
3
2
 
4
3
  import { createDocument } from '../helpers/createDocument'
5
4
  import { Content, RawCommands } from '../types'
@@ -22,11 +21,9 @@ declare module '@tiptap/core' {
22
21
  export const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}) => ({ tr, editor, dispatch }) => {
23
22
  const { doc } = tr
24
23
  const document = createDocument(content, editor.schema, parseOptions)
25
- const selection = TextSelection.create(doc, 0, doc.content.size)
26
24
 
27
25
  if (dispatch) {
28
- tr.setSelection(selection)
29
- .replaceSelectionWith(document, false)
26
+ tr.replaceWith(0, doc.content.size, document)
30
27
  .setMeta('preventUpdate', !emitUpdate)
31
28
  }
32
29
 
@@ -1,4 +1,4 @@
1
- import { NodeSelection, Selection } from 'prosemirror-state'
1
+ import { NodeSelection } from 'prosemirror-state'
2
2
 
3
3
  import { RawCommands } from '../types'
4
4
  import { minMax } from '../utilities/minMax'
@@ -17,10 +17,8 @@ declare module '@tiptap/core' {
17
17
  export const setNodeSelection: RawCommands['setNodeSelection'] = position => ({ tr, dispatch }) => {
18
18
  if (dispatch) {
19
19
  const { doc } = tr
20
- const minPos = Selection.atStart(doc).from
21
- const maxPos = Selection.atEnd(doc).to
22
- const resolvedPos = minMax(position, minPos, maxPos)
23
- const selection = NodeSelection.create(doc, resolvedPos)
20
+ const from = minMax(position, 0, doc.content.size)
21
+ const selection = NodeSelection.create(doc, from)
24
22
 
25
23
  tr.setSelection(selection)
26
24
  }
@@ -1,21 +1,10 @@
1
- import { ContentMatch } from 'prosemirror-model'
2
1
  import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state'
3
2
  import { canSplit } from 'prosemirror-transform'
4
3
 
4
+ import { defaultBlockAt } from '../helpers/defaultBlockAt'
5
5
  import { getSplittedAttributes } from '../helpers/getSplittedAttributes'
6
6
  import { RawCommands } from '../types'
7
7
 
8
- function defaultBlockAt(match: ContentMatch) {
9
- for (let i = 0; i < match.edgeCount; i += 1) {
10
- const { type } = match.edge(i)
11
-
12
- if (type.isTextblock && !type.hasRequiredAttrs()) {
13
- return type
14
- }
15
- }
16
- return null
17
- }
18
-
19
8
  function ensureMarks(state: EditorState, splittableMarks?: string[]) {
20
9
  const marks = state.storedMarks
21
10
  || (state.selection.$to.parentOffset && state.selection.$from.marks())
@@ -13,7 +13,7 @@ export const FocusEvents = Extension.create({
13
13
  key: new PluginKey('focusEvents'),
14
14
  props: {
15
15
  handleDOMEvents: {
16
- focus: (view, event) => {
16
+ focus: (view, event: Event) => {
17
17
  editor.isFocused = true
18
18
 
19
19
  const transaction = editor.state.tr
@@ -24,7 +24,7 @@ export const FocusEvents = Extension.create({
24
24
 
25
25
  return false
26
26
  },
27
- blur: (view, event) => {
27
+ blur: (view, event: Event) => {
28
28
  editor.isFocused = false
29
29
 
30
30
  const transaction = editor.state.tr
@@ -9,7 +9,7 @@ export const getTextContentFromNodes = ($from: ResolvedPos, maxMatch = 500) => {
9
9
  (node, pos, parent, index) => {
10
10
  textBefore += node.type.spec.toText?.({
11
11
  node, pos, parent, index,
12
- }) || node.textContent || '%leaf%'
12
+ }) || $from.nodeBefore?.text || '%leaf%'
13
13
  },
14
14
  )
15
15
 
@@ -1,7 +1,5 @@
1
1
  import { NodeSelection } from 'prosemirror-state'
2
2
 
3
- import { isObject } from '../utilities/isObject'
4
-
5
3
  export function isNodeSelection(value: unknown): value is NodeSelection {
6
- return isObject(value) && value instanceof NodeSelection
4
+ return value instanceof NodeSelection
7
5
  }
@@ -1,7 +1,5 @@
1
1
  import { TextSelection } from 'prosemirror-state'
2
2
 
3
- import { isObject } from '../utilities/isObject'
4
-
5
3
  export function isTextSelection(value: unknown): value is TextSelection {
6
- return isObject(value) && value instanceof TextSelection
4
+ return value instanceof TextSelection
7
5
  }
@@ -1,2 +1,3 @@
1
1
  export * from './markPasteRule'
2
+ export * from './nodePasteRule'
2
3
  export * from './textPasteRule'
@@ -0,0 +1,39 @@
1
+ import { NodeType } from 'prosemirror-model'
2
+
3
+ import { PasteRule } from '../PasteRule'
4
+ import { ExtendedRegExpMatchArray } from '../types'
5
+ import { callOrReturn } from '../utilities'
6
+
7
+ /**
8
+ * Build an paste rule that adds a node when the
9
+ * matched text is pasted into it.
10
+ */
11
+ export function nodePasteRule(config: {
12
+ find: RegExp,
13
+ type: NodeType,
14
+ getAttributes?:
15
+ | Record<string, any>
16
+ | ((match: ExtendedRegExpMatchArray) => Record<string, any>)
17
+ | false
18
+ | null,
19
+ }) {
20
+ return new PasteRule({
21
+ find: config.find,
22
+ handler({ match, chain, range }) {
23
+ const attributes = callOrReturn(config.getAttributes, undefined, match)
24
+
25
+ if (attributes === false || attributes === null) {
26
+ return null
27
+ }
28
+
29
+ if (match.input) {
30
+ chain()
31
+ .deleteRange(range)
32
+ .insertContent({
33
+ type: config.type.name,
34
+ attrs: attributes,
35
+ })
36
+ }
37
+ },
38
+ })
39
+ }
@@ -1 +0,0 @@
1
- export declare function isClass(value: any): boolean;
@@ -1 +0,0 @@
1
- export declare function isObject(value: any): boolean;
@@ -1,7 +0,0 @@
1
- export function isClass(value: any): boolean {
2
- if (value.constructor?.toString().substring(0, 5) !== 'class') {
3
- return false
4
- }
5
-
6
- return true
7
- }
@@ -1,10 +0,0 @@
1
- import { isClass } from './isClass'
2
-
3
- export function isObject(value: any): boolean {
4
- return (
5
- value
6
- && typeof value === 'object'
7
- && !Array.isArray(value)
8
- && !isClass(value)
9
- )
10
- }