@tiptap/core 2.2.0-rc.7 → 2.2.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.
@@ -4,6 +4,7 @@ import { EditorView } from '@tiptap/pm/view';
4
4
  import { EventEmitter } from './EventEmitter.js';
5
5
  import { ExtensionManager } from './ExtensionManager.js';
6
6
  import * as extensions from './extensions/index.js';
7
+ import { NodePos } from './NodePos.js';
7
8
  import { CanCommands, ChainedCommands, EditorEvents, EditorOptions, JSONContent, SingleCommands, TextSerializer } from './types.js';
8
9
  export { extensions };
9
10
  export interface HTMLElement {
@@ -148,4 +149,12 @@ export declare class Editor extends EventEmitter<EditorEvents> {
148
149
  * Check if the editor is already destroyed.
149
150
  */
150
151
  get isDestroyed(): boolean;
152
+ $node(selector: string, attributes?: {
153
+ [key: string]: any;
154
+ }): NodePos | null;
155
+ $nodes(selector: string, attributes?: {
156
+ [key: string]: any;
157
+ }): NodePos[] | null;
158
+ $pos(pos: number): NodePos;
159
+ get $doc(): NodePos;
151
160
  }
@@ -0,0 +1,40 @@
1
+ import { Fragment, Node, ResolvedPos } from '@tiptap/pm/model';
2
+ import { Editor } from './Editor.js';
3
+ import { Content, Range } from './types.js';
4
+ export declare class NodePos {
5
+ private resolvedPos;
6
+ private editor;
7
+ constructor(pos: ResolvedPos, editor: Editor);
8
+ get node(): Node;
9
+ get element(): HTMLElement;
10
+ get depth(): number;
11
+ get pos(): number;
12
+ get content(): Fragment;
13
+ set content(content: Content);
14
+ get attributes(): {
15
+ [key: string]: any;
16
+ };
17
+ get textContent(): string;
18
+ get size(): number;
19
+ get from(): number;
20
+ get range(): Range;
21
+ get to(): number;
22
+ get parent(): NodePos | null;
23
+ get before(): NodePos | null;
24
+ get after(): NodePos | null;
25
+ get children(): NodePos[];
26
+ get firstChild(): NodePos | null;
27
+ get lastChild(): NodePos | null;
28
+ closest(selector: string, attributes?: {
29
+ [key: string]: any;
30
+ }): NodePos | null;
31
+ querySelector(selector: string, attributes?: {
32
+ [key: string]: any;
33
+ }): NodePos | null;
34
+ querySelectorAll(selector: string, attributes?: {
35
+ [key: string]: any;
36
+ }, firstItemOnly?: boolean): NodePos[];
37
+ setAttribute(attributes: {
38
+ [key: string]: any;
39
+ }): void;
40
+ }
@@ -8,7 +8,7 @@ export declare type PasteRuleMatch = {
8
8
  match?: RegExpMatchArray;
9
9
  data?: Record<string, any>;
10
10
  };
11
- export declare type PasteRuleFinder = RegExp | ((text: string) => PasteRuleMatch[] | null | undefined);
11
+ export declare type PasteRuleFinder = RegExp | ((text: string, event?: ClipboardEvent) => PasteRuleMatch[] | null | undefined);
12
12
  export declare class PasteRule {
13
13
  find: PasteRuleFinder;
14
14
  handler: (props: {
@@ -19,6 +19,8 @@ export * from './insertContentAt.js';
19
19
  export * from './join.js';
20
20
  export * from './joinItemBackward.js';
21
21
  export * from './joinItemForward.js';
22
+ export * from './joinTextblockBackward.js';
23
+ export * from './joinTextblockForward.js';
22
24
  export * from './keyboardShortcut.js';
23
25
  export * from './lift.js';
24
26
  export * from './liftEmptyBlock.js';
@@ -0,0 +1,12 @@
1
+ import { RawCommands } from '../types.js';
2
+ declare module '@tiptap/core' {
3
+ interface Commands<ReturnType> {
4
+ joinTextblockBackward: {
5
+ /**
6
+ * A more limited form of joinBackward that only tries to join the current textblock to the one before it, if the cursor is at the start of a textblock.
7
+ */
8
+ joinTextblockBackward: () => ReturnType;
9
+ };
10
+ }
11
+ }
12
+ export declare const joinTextblockBackward: RawCommands['joinTextblockBackward'];
@@ -0,0 +1,12 @@
1
+ import { RawCommands } from '../types.js';
2
+ declare module '@tiptap/core' {
3
+ interface Commands<ReturnType> {
4
+ joinTextblockForward: {
5
+ /**
6
+ * A more limited form of joinForward that only tries to join the current textblock to the one after it, if the cursor is at the end of a textblock.
7
+ */
8
+ joinTextblockForward: () => ReturnType;
9
+ };
10
+ }
11
+ }
12
+ export declare const joinTextblockForward: RawCommands['joinTextblockForward'];
@@ -7,6 +7,7 @@ export * from './InputRule.js';
7
7
  export * from './inputRules/index.js';
8
8
  export * from './Mark.js';
9
9
  export * from './Node.js';
10
+ export * from './NodePos.js';
10
11
  export * from './NodeView.js';
11
12
  export * from './PasteRule.js';
12
13
  export * from './pasteRules/index.js';
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.0-rc.7",
4
+ "version": "2.2.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.0-rc.7"
35
+ "@tiptap/pm": "^2.2.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@tiptap/pm": "^2.0.0"
package/src/Editor.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { MarkType, NodeType, Schema } from '@tiptap/pm/model'
1
+ import {
2
+ MarkType, NodeType, Schema,
3
+ } from '@tiptap/pm/model'
2
4
  import {
3
5
  EditorState, Plugin, PluginKey, Transaction,
4
6
  } from '@tiptap/pm/state'
@@ -16,6 +18,7 @@ import { getTextSerializersFromSchema } from './helpers/getTextSerializersFromSc
16
18
  import { isActive } from './helpers/isActive.js'
17
19
  import { isNodeEmpty } from './helpers/isNodeEmpty.js'
18
20
  import { resolveFocusPosition } from './helpers/resolveFocusPosition.js'
21
+ import { NodePos } from './NodePos.js'
19
22
  import { style } from './style.js'
20
23
  import {
21
24
  CanCommands,
@@ -486,4 +489,22 @@ export class Editor extends EventEmitter<EditorEvents> {
486
489
  // @ts-ignore
487
490
  return !this.view?.docView
488
491
  }
492
+
493
+ public $node(selector: string, attributes?: { [key: string]: any }): NodePos | null {
494
+ return this.$doc?.querySelector(selector, attributes) || null
495
+ }
496
+
497
+ public $nodes(selector: string, attributes?: { [key: string]: any }): NodePos[] | null {
498
+ return this.$doc?.querySelectorAll(selector, attributes) || null
499
+ }
500
+
501
+ public $pos(pos: number) {
502
+ const $pos = this.state.doc.resolve(pos)
503
+
504
+ return new NodePos($pos, this)
505
+ }
506
+
507
+ get $doc() {
508
+ return this.$pos(0)
509
+ }
489
510
  }
package/src/Extension.ts CHANGED
@@ -297,7 +297,7 @@ export class Extension<Options = any, Storage = any> {
297
297
 
298
298
  config: ExtensionConfig = {
299
299
  name: this.name,
300
- defaultOptions: undefined,
300
+ defaultOptions: {},
301
301
  }
302
302
 
303
303
  constructor(config: Partial<ExtensionConfig<Options, Storage>> = {}) {
@@ -308,7 +308,7 @@ export class Extension<Options = any, Storage = any> {
308
308
 
309
309
  this.name = this.config.name
310
310
 
311
- if (config.defaultOptions) {
311
+ if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) {
312
312
  console.warn(
313
313
  `[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`,
314
314
  )
package/src/Mark.ts CHANGED
@@ -429,7 +429,7 @@ export class Mark<Options = any, Storage = any> {
429
429
 
430
430
  config: MarkConfig = {
431
431
  name: this.name,
432
- defaultOptions: undefined,
432
+ defaultOptions: {},
433
433
  }
434
434
 
435
435
  constructor(config: Partial<MarkConfig<Options, Storage>> = {}) {
@@ -440,7 +440,7 @@ export class Mark<Options = any, Storage = any> {
440
440
 
441
441
  this.name = this.config.name
442
442
 
443
- if (config.defaultOptions) {
443
+ if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) {
444
444
  console.warn(
445
445
  `[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`,
446
446
  )
package/src/Node.ts CHANGED
@@ -538,7 +538,7 @@ export class Node<Options = any, Storage = any> {
538
538
 
539
539
  config: NodeConfig = {
540
540
  name: this.name,
541
- defaultOptions: undefined,
541
+ defaultOptions: {},
542
542
  }
543
543
 
544
544
  constructor(config: Partial<NodeConfig<Options, Storage>> = {}) {
@@ -549,7 +549,7 @@ export class Node<Options = any, Storage = any> {
549
549
 
550
550
  this.name = this.config.name
551
551
 
552
- if (config.defaultOptions) {
552
+ if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) {
553
553
  console.warn(
554
554
  `[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`,
555
555
  )
package/src/NodePos.ts ADDED
@@ -0,0 +1,201 @@
1
+ import {
2
+ Fragment, Node, ResolvedPos,
3
+ } from '@tiptap/pm/model'
4
+
5
+ import { Editor } from './Editor.js'
6
+ import { Content, Range } from './types.js'
7
+
8
+ export class NodePos {
9
+ private resolvedPos: ResolvedPos
10
+
11
+ private editor: Editor
12
+
13
+ constructor(pos: ResolvedPos, editor: Editor) {
14
+ this.resolvedPos = pos
15
+ this.editor = editor
16
+ }
17
+
18
+ get node(): Node {
19
+ return this.resolvedPos.node()
20
+ }
21
+
22
+ get element(): HTMLElement {
23
+ return this.editor.view.domAtPos(this.pos).node as HTMLElement
24
+ }
25
+
26
+ get depth(): number {
27
+ return this.resolvedPos.depth
28
+ }
29
+
30
+ get pos(): number {
31
+ return this.resolvedPos.pos
32
+ }
33
+
34
+ get content(): Fragment {
35
+ return this.node.content
36
+ }
37
+
38
+ set content(content: Content) {
39
+ this.editor.commands.insertContentAt({ from: this.from, to: this.to }, content)
40
+ }
41
+
42
+ get attributes() : { [key: string]: any } {
43
+ return this.node.attrs
44
+ }
45
+
46
+ get textContent(): string {
47
+ return this.node.textContent
48
+ }
49
+
50
+ get size(): number {
51
+ return this.node.nodeSize
52
+ }
53
+
54
+ get from(): number {
55
+ return this.resolvedPos.start(this.resolvedPos.depth)
56
+ }
57
+
58
+ get range(): Range {
59
+ return {
60
+ from: this.from,
61
+ to: this.to,
62
+ }
63
+ }
64
+
65
+ get to(): number {
66
+ return this.resolvedPos.end(this.resolvedPos.depth) + (this.node.isText ? 0 : 1)
67
+ }
68
+
69
+ get parent(): NodePos | null {
70
+ if (this.depth === 0) {
71
+ return null
72
+ }
73
+
74
+ const parentPos = this.resolvedPos.start(this.resolvedPos.depth - 1)
75
+ const $pos = this.resolvedPos.doc.resolve(parentPos)
76
+
77
+ return new NodePos($pos, this.editor)
78
+ }
79
+
80
+ get before(): NodePos | null {
81
+ let $pos = this.resolvedPos.doc.resolve(this.from - 2)
82
+
83
+ if ($pos.depth !== this.depth) {
84
+ $pos = this.resolvedPos.doc.resolve(this.from - 3)
85
+ }
86
+
87
+ return new NodePos($pos, this.editor)
88
+ }
89
+
90
+ get after(): NodePos | null {
91
+ let $pos = this.resolvedPos.doc.resolve(this.to + 2)
92
+
93
+ if ($pos.depth !== this.depth) {
94
+ $pos = this.resolvedPos.doc.resolve(this.to + 3)
95
+ }
96
+
97
+ return new NodePos($pos, this.editor)
98
+ }
99
+
100
+ get children(): NodePos[] {
101
+ const children: NodePos[] = []
102
+
103
+ this.node.content.forEach((node, offset) => {
104
+ const targetPos = this.pos + offset + 1
105
+ const $pos = this.resolvedPos.doc.resolve(targetPos)
106
+
107
+ if ($pos.depth === this.depth) {
108
+ return
109
+ }
110
+
111
+ children.push(new NodePos($pos, this.editor))
112
+ })
113
+
114
+ return children
115
+ }
116
+
117
+ get firstChild(): NodePos | null {
118
+ return this.children[0] || null
119
+ }
120
+
121
+ get lastChild(): NodePos | null {
122
+ const children = this.children
123
+
124
+ return children[children.length - 1] || null
125
+ }
126
+
127
+ closest(selector: string, attributes: { [key: string]: any } = {}): NodePos | null {
128
+ let node: NodePos | null = null
129
+ let currentNode = this.parent
130
+
131
+ while (currentNode && !node) {
132
+ if (currentNode.node.type.name === selector) {
133
+ if (Object.keys(attributes).length > 0) {
134
+ const nodeAttributes = currentNode.node.attrs
135
+ const attrKeys = Object.keys(attributes)
136
+
137
+ for (let index = 0; index < attrKeys.length; index += 1) {
138
+ const key = attrKeys[index]
139
+
140
+ if (nodeAttributes[key] !== attributes[key]) {
141
+ break
142
+ }
143
+ }
144
+ } else {
145
+ node = currentNode
146
+ }
147
+ }
148
+
149
+ currentNode = currentNode.parent
150
+ }
151
+
152
+ return node
153
+ }
154
+
155
+ querySelector(selector: string, attributes: { [key: string]: any } = {}): NodePos | null {
156
+ return this.querySelectorAll(selector, attributes, true)[0] || null
157
+ }
158
+
159
+ querySelectorAll(selector: string, attributes: { [key: string]: any } = {}, firstItemOnly = false): NodePos[] {
160
+ let nodes: NodePos[] = []
161
+
162
+ // iterate through children recursively finding all nodes which match the selector with the node name
163
+ if (!this.children || this.children.length === 0) {
164
+ return nodes
165
+ }
166
+
167
+ this.children.forEach(node => {
168
+ if (node.node.type.name === selector) {
169
+ if (Object.keys(attributes).length > 0) {
170
+ const nodeAttributes = node.node.attrs
171
+ const attrKeys = Object.keys(attributes)
172
+
173
+ for (let index = 0; index < attrKeys.length; index += 1) {
174
+ const key = attrKeys[index]
175
+
176
+ if (nodeAttributes[key] !== attributes[key]) {
177
+ return
178
+ }
179
+ }
180
+ }
181
+
182
+ nodes.push(node)
183
+
184
+ if (firstItemOnly) {
185
+ return
186
+ }
187
+ }
188
+
189
+ nodes = nodes.concat(node.querySelectorAll(selector))
190
+ })
191
+
192
+ return nodes
193
+ }
194
+
195
+ setAttribute(attributes: { [key: string]: any }) {
196
+ const oldSelection = this.editor.state.selection
197
+
198
+ this.editor.chain().setTextSelection(this.from).updateAttributes(this.node.type.name, attributes).setTextSelection(oldSelection.from)
199
+ .run()
200
+ }
201
+ }
package/src/PasteRule.ts CHANGED
@@ -21,7 +21,7 @@ export type PasteRuleMatch = {
21
21
  data?: Record<string, any>
22
22
  }
23
23
 
24
- export type PasteRuleFinder = RegExp | ((text: string) => PasteRuleMatch[] | null | undefined)
24
+ export type PasteRuleFinder = RegExp | ((text: string, event?: ClipboardEvent) => PasteRuleMatch[] | null | undefined)
25
25
 
26
26
  export class PasteRule {
27
27
  find: PasteRuleFinder
@@ -58,12 +58,13 @@ export class PasteRule {
58
58
  const pasteRuleMatcherHandler = (
59
59
  text: string,
60
60
  find: PasteRuleFinder,
61
+ event?: ClipboardEvent,
61
62
  ): ExtendedRegExpMatchArray[] => {
62
63
  if (isRegExp(find)) {
63
64
  return [...text.matchAll(find)]
64
65
  }
65
66
 
66
- const matches = find(text)
67
+ const matches = find(text, event)
67
68
 
68
69
  if (!matches) {
69
70
  return []
@@ -119,7 +120,7 @@ function run(config: {
119
120
  const resolvedTo = Math.min(to, pos + node.content.size)
120
121
  const textToMatch = node.textBetween(resolvedFrom - pos, resolvedTo - pos, undefined, '\ufffc')
121
122
 
122
- const matches = pasteRuleMatcherHandler(textToMatch, rule.find)
123
+ const matches = pasteRuleMatcherHandler(textToMatch, rule.find, pasteEvent)
123
124
 
124
125
  matches.forEach(match => {
125
126
  if (match.index === undefined) {
@@ -19,6 +19,8 @@ export * from './insertContentAt.js'
19
19
  export * from './join.js'
20
20
  export * from './joinItemBackward.js'
21
21
  export * from './joinItemForward.js'
22
+ export * from './joinTextblockBackward.js'
23
+ export * from './joinTextblockForward.js'
22
24
  export * from './keyboardShortcut.js'
23
25
  export * from './lift.js'
24
26
  export * from './liftEmptyBlock.js'
@@ -0,0 +1,18 @@
1
+ import { joinTextblockBackward as originalCommand } from '@tiptap/pm/commands'
2
+
3
+ import { RawCommands } from '../types.js'
4
+
5
+ declare module '@tiptap/core' {
6
+ interface Commands<ReturnType> {
7
+ joinTextblockBackward: {
8
+ /**
9
+ * A more limited form of joinBackward that only tries to join the current textblock to the one before it, if the cursor is at the start of a textblock.
10
+ */
11
+ joinTextblockBackward: () => ReturnType
12
+ }
13
+ }
14
+ }
15
+
16
+ export const joinTextblockBackward: RawCommands['joinTextblockBackward'] = () => ({ state, dispatch }) => {
17
+ return originalCommand(state, dispatch)
18
+ }
@@ -0,0 +1,18 @@
1
+ import { joinTextblockForward as originalCommand } from '@tiptap/pm/commands'
2
+
3
+ import { RawCommands } from '../types.js'
4
+
5
+ declare module '@tiptap/core' {
6
+ interface Commands<ReturnType> {
7
+ joinTextblockForward: {
8
+ /**
9
+ * A more limited form of joinForward that only tries to join the current textblock to the one after it, if the cursor is at the end of a textblock.
10
+ */
11
+ joinTextblockForward: () => ReturnType
12
+ }
13
+ }
14
+ }
15
+
16
+ export const joinTextblockForward: RawCommands['joinTextblockForward'] = () => ({ state, dispatch }) => {
17
+ return originalCommand(state, dispatch)
18
+ }
@@ -13,7 +13,6 @@ export function createChainableState(config: {
13
13
  ...state,
14
14
  apply: state.apply.bind(state),
15
15
  applyTransaction: state.applyTransaction.bind(state),
16
- filterTransaction: state.filterTransaction,
17
16
  plugins: state.plugins,
18
17
  schema: state.schema,
19
18
  reconfigure: state.reconfigure.bind(state),
@@ -26,6 +26,10 @@ export function getMarksBetween(from: number, to: number, doc: ProseMirrorNode):
26
26
  })
27
27
  } else {
28
28
  doc.nodesBetween(from, to, (node, pos) => {
29
+ if (!node || node?.nodeSize === undefined) {
30
+ return
31
+ }
32
+
29
33
  marks.push(
30
34
  ...node.marks.map(mark => ({
31
35
  from: pos,
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from './InputRule.js'
7
7
  export * from './inputRules/index.js'
8
8
  export * from './Mark.js'
9
9
  export * from './Node.js'
10
+ export * from './NodePos.js'
10
11
  export * from './NodeView.js'
11
12
  export * from './PasteRule.js'
12
13
  export * from './pasteRules/index.js'
@@ -1,6 +1,24 @@
1
+ const removeWhitespaces = (node: HTMLElement) => {
2
+ const children = node.childNodes
3
+
4
+ for (let i = children.length - 1; i >= 0; i -= 1) {
5
+ const child = children[i]
6
+
7
+ if (child.nodeType === 3 && child.nodeValue && /^(\n\s\s|\n)$/.test(child.nodeValue)) {
8
+ node.removeChild(child)
9
+ } else if (child.nodeType === 1) {
10
+ removeWhitespaces(child as HTMLElement)
11
+ }
12
+ }
13
+
14
+ return node
15
+ }
16
+
1
17
  export function elementFromString(value: string): HTMLElement {
2
18
  // add a wrapper to preserve leading and trailing whitespace
3
19
  const wrappedValue = `<body>${value}</body>`
4
20
 
5
- return new window.DOMParser().parseFromString(wrappedValue, 'text/html').body
21
+ const html = new window.DOMParser().parseFromString(wrappedValue, 'text/html').body
22
+
23
+ return removeWhitespaces(html)
6
24
  }