@tiptap/core 2.9.1 → 2.10.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.
Files changed (66) hide show
  1. package/dist/Editor.d.ts.map +1 -1
  2. package/dist/EventEmitter.d.ts +1 -0
  3. package/dist/EventEmitter.d.ts.map +1 -1
  4. package/dist/Extension.d.ts +2 -2
  5. package/dist/Extension.d.ts.map +1 -1
  6. package/dist/InputRule.d.ts.map +1 -1
  7. package/dist/Mark.d.ts +2 -2
  8. package/dist/Mark.d.ts.map +1 -1
  9. package/dist/Node.d.ts +18 -2
  10. package/dist/Node.d.ts.map +1 -1
  11. package/dist/PasteRule.d.ts +1 -1
  12. package/dist/PasteRule.d.ts.map +1 -1
  13. package/dist/commands/insertContent.d.ts +2 -2
  14. package/dist/commands/insertContent.d.ts.map +1 -1
  15. package/dist/commands/insertContentAt.d.ts +2 -2
  16. package/dist/commands/insertContentAt.d.ts.map +1 -1
  17. package/dist/commands/setContent.d.ts +2 -2
  18. package/dist/commands/setContent.d.ts.map +1 -1
  19. package/dist/commands/setNode.d.ts.map +1 -1
  20. package/dist/commands/updateAttributes.d.ts.map +1 -1
  21. package/dist/helpers/createDocument.d.ts +2 -2
  22. package/dist/helpers/createDocument.d.ts.map +1 -1
  23. package/dist/helpers/createNodeFromContent.d.ts +1 -1
  24. package/dist/helpers/createNodeFromContent.d.ts.map +1 -1
  25. package/dist/helpers/getMarkRange.d.ts +17 -1
  26. package/dist/helpers/getMarkRange.d.ts.map +1 -1
  27. package/dist/helpers/getSchemaByResolvedExtensions.d.ts.map +1 -1
  28. package/dist/index.cjs +158 -44
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +159 -45
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.umd.js +158 -44
  33. package/dist/index.umd.js.map +1 -1
  34. package/dist/inputRules/markInputRule.d.ts +1 -1
  35. package/dist/inputRules/nodeInputRule.d.ts +1 -1
  36. package/dist/inputRules/textInputRule.d.ts +1 -1
  37. package/dist/inputRules/textblockTypeInputRule.d.ts +1 -1
  38. package/dist/inputRules/wrappingInputRule.d.ts +1 -1
  39. package/dist/pasteRules/markPasteRule.d.ts +1 -1
  40. package/dist/pasteRules/nodePasteRule.d.ts +1 -1
  41. package/dist/pasteRules/textPasteRule.d.ts +1 -1
  42. package/package.json +2 -2
  43. package/src/Editor.ts +5 -8
  44. package/src/EventEmitter.ts +9 -0
  45. package/src/Extension.ts +2 -2
  46. package/src/InputRule.ts +45 -30
  47. package/src/Mark.ts +2 -2
  48. package/src/Node.ts +21 -2
  49. package/src/PasteRule.ts +67 -42
  50. package/src/commands/insertContent.ts +9 -9
  51. package/src/commands/insertContentAt.ts +11 -1
  52. package/src/commands/setContent.ts +10 -14
  53. package/src/commands/setNode.ts +9 -2
  54. package/src/commands/updateAttributes.ts +72 -12
  55. package/src/helpers/createDocument.ts +4 -2
  56. package/src/helpers/createNodeFromContent.ts +4 -1
  57. package/src/helpers/getMarkRange.ts +29 -5
  58. package/src/helpers/getSchemaByResolvedExtensions.ts +1 -0
  59. package/src/inputRules/markInputRule.ts +1 -1
  60. package/src/inputRules/nodeInputRule.ts +1 -1
  61. package/src/inputRules/textInputRule.ts +1 -1
  62. package/src/inputRules/textblockTypeInputRule.ts +1 -1
  63. package/src/inputRules/wrappingInputRule.ts +1 -1
  64. package/src/pasteRules/markPasteRule.ts +1 -1
  65. package/src/pasteRules/nodePasteRule.ts +1 -1
  66. package/src/pasteRules/textPasteRule.ts +1 -1
package/dist/index.umd.js CHANGED
@@ -170,6 +170,13 @@
170
170
  }
171
171
  return this;
172
172
  }
173
+ once(event, fn) {
174
+ const onceFn = (...args) => {
175
+ this.off(event, onceFn);
176
+ fn.apply(this, args);
177
+ };
178
+ return this.on(event, onceFn);
179
+ }
173
180
  removeAllListeners() {
174
181
  this.callbacks = {};
175
182
  }
@@ -475,6 +482,7 @@
475
482
  draggable: callOrReturn(getExtensionField(extension, 'draggable', context)),
476
483
  code: callOrReturn(getExtensionField(extension, 'code', context)),
477
484
  whitespace: callOrReturn(getExtensionField(extension, 'whitespace', context)),
485
+ linebreakReplacement: callOrReturn(getExtensionField(extension, 'linebreakReplacement', context)),
478
486
  defining: callOrReturn(getExtensionField(extension, 'defining', context)),
479
487
  isolating: callOrReturn(getExtensionField(extension, 'isolating', context)),
480
488
  attrs: Object.fromEntries(extensionAttributes.map(extensionAttribute => {
@@ -568,6 +576,14 @@
568
576
  return enabled;
569
577
  }
570
578
 
579
+ function getHTMLFromFragment(fragment, schema) {
580
+ const documentFragment = model.DOMSerializer.fromSchema(schema).serializeFragment(fragment);
581
+ const temporaryDocument = document.implementation.createHTMLDocument();
582
+ const container = temporaryDocument.createElement('div');
583
+ container.appendChild(documentFragment);
584
+ return container.innerHTML;
585
+ }
586
+
571
587
  /**
572
588
  * Returns the text content of a resolved prosemirror position
573
589
  * @param $from The resolved position to get the text content from
@@ -697,7 +713,7 @@
697
713
  init() {
698
714
  return null;
699
715
  },
700
- apply(tr, prev) {
716
+ apply(tr, prev, state) {
701
717
  const stored = tr.getMeta(plugin);
702
718
  if (stored) {
703
719
  return stored;
@@ -707,7 +723,14 @@
707
723
  const isSimulatedInput = !!simulatedInputMeta;
708
724
  if (isSimulatedInput) {
709
725
  setTimeout(() => {
710
- const { from, text } = simulatedInputMeta;
726
+ let { text } = simulatedInputMeta;
727
+ if (typeof text === 'string') {
728
+ text = text;
729
+ }
730
+ else {
731
+ text = getHTMLFromFragment(model.Fragment.from(text), state.schema);
732
+ }
733
+ const { from } = simulatedInputMeta;
711
734
  const to = from + text.length;
712
735
  run$1({
713
736
  editor,
@@ -900,7 +923,7 @@
900
923
 
901
924
  /**
902
925
  * Paste rules are used to react to pasted content.
903
- * @see https://tiptap.dev/guide/custom-extensions/#paste-rules
926
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#paste-rules
904
927
  */
905
928
  class PasteRule {
906
929
  constructor(config) {
@@ -990,7 +1013,13 @@
990
1013
  let isPastedFromProseMirror = false;
991
1014
  let isDroppedFromProseMirror = false;
992
1015
  let pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null;
993
- let dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null;
1016
+ let dropEvent;
1017
+ try {
1018
+ dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null;
1019
+ }
1020
+ catch (e) {
1021
+ dropEvent = null;
1022
+ }
994
1023
  const processEvent = ({ state, from, to, rule, pasteEvt, }) => {
995
1024
  const tr = state.tr;
996
1025
  const chainableState = createChainableState({
@@ -1009,7 +1038,12 @@
1009
1038
  if (!handler || !tr.steps.length) {
1010
1039
  return;
1011
1040
  }
1012
- dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null;
1041
+ try {
1042
+ dropEvent = typeof DragEvent !== 'undefined' ? new DragEvent('drop') : null;
1043
+ }
1044
+ catch (e) {
1045
+ dropEvent = null;
1046
+ }
1013
1047
  pasteEvent = typeof ClipboardEvent !== 'undefined' ? new ClipboardEvent('paste') : null;
1014
1048
  return tr;
1015
1049
  };
@@ -1058,7 +1092,14 @@
1058
1092
  }
1059
1093
  // Handle simulated paste
1060
1094
  if (isSimulatedPaste) {
1061
- const { from, text } = simulatedPasteMeta;
1095
+ let { text } = simulatedPasteMeta;
1096
+ if (typeof text === 'string') {
1097
+ text = text;
1098
+ }
1099
+ else {
1100
+ text = getHTMLFromFragment(model.Fragment.from(text), state.schema);
1101
+ }
1102
+ const { from } = simulatedPasteMeta;
1062
1103
  const to = from + text.length;
1063
1104
  const pasteEvt = createClipboardPasteEvent(text);
1064
1105
  return processEvent({
@@ -1655,13 +1696,33 @@
1655
1696
 
1656
1697
  function findMarkInSet(marks, type, attributes = {}) {
1657
1698
  return marks.find(item => {
1658
- return item.type === type && objectIncludes(item.attrs, attributes);
1699
+ return (item.type === type
1700
+ && objectIncludes(
1701
+ // Only check equality for the attributes that are provided
1702
+ Object.fromEntries(Object.keys(attributes).map(k => [k, item.attrs[k]])), attributes));
1659
1703
  });
1660
1704
  }
1661
1705
  function isMarkInSet(marks, type, attributes = {}) {
1662
1706
  return !!findMarkInSet(marks, type, attributes);
1663
1707
  }
1664
- function getMarkRange($pos, type, attributes = {}) {
1708
+ /**
1709
+ * Get the range of a mark at a resolved position.
1710
+ */
1711
+ function getMarkRange(
1712
+ /**
1713
+ * The position to get the mark range for.
1714
+ */
1715
+ $pos,
1716
+ /**
1717
+ * The mark type to get the range for.
1718
+ */
1719
+ type,
1720
+ /**
1721
+ * The attributes to match against.
1722
+ * If not provided, only the first mark at the position will be matched.
1723
+ */
1724
+ attributes) {
1725
+ var _a;
1665
1726
  if (!$pos || !type) {
1666
1727
  return;
1667
1728
  }
@@ -1674,6 +1735,8 @@
1674
1735
  if (!start.node || !start.node.marks.some(mark => mark.type === type)) {
1675
1736
  return;
1676
1737
  }
1738
+ // Default to only matching against the first mark's attributes
1739
+ attributes = attributes || ((_a = start.node.marks[0]) === null || _a === void 0 ? void 0 : _a.attrs);
1677
1740
  // We now know that the cursor is either at the start, middle or end of a text node with the specified mark
1678
1741
  // so we can look it up on the targeted mark
1679
1742
  const mark = findMarkInSet([...start.node.marks], type, attributes);
@@ -1684,8 +1747,8 @@
1684
1747
  let startPos = $pos.start() + start.offset;
1685
1748
  let endIndex = startIndex + 1;
1686
1749
  let endPos = startPos + start.node.nodeSize;
1687
- findMarkInSet([...start.node.marks], type, attributes);
1688
- while (startIndex > 0 && mark.isInSet($pos.parent.child(startIndex - 1).marks)) {
1750
+ while (startIndex > 0
1751
+ && isMarkInSet([...$pos.parent.child(startIndex - 1).marks], type, attributes)) {
1689
1752
  startIndex -= 1;
1690
1753
  startPos -= $pos.parent.child(startIndex).nodeSize;
1691
1754
  }
@@ -1861,6 +1924,9 @@
1861
1924
  * @returns The created Prosemirror node or fragment
1862
1925
  */
1863
1926
  function createNodeFromContent(content, schema, options) {
1927
+ if (content instanceof model.Node || content instanceof model.Fragment) {
1928
+ return content;
1929
+ }
1864
1930
  options = {
1865
1931
  slice: true,
1866
1932
  parseOptions: {},
@@ -2025,6 +2091,15 @@
2025
2091
  if (Array.isArray(value)) {
2026
2092
  newContent = value.map(v => v.text || '').join('');
2027
2093
  }
2094
+ else if (value instanceof model.Fragment) {
2095
+ let text = '';
2096
+ value.forEach(node => {
2097
+ if (node.text) {
2098
+ text += node.text;
2099
+ }
2100
+ });
2101
+ newContent = text;
2102
+ }
2028
2103
  else if (typeof value === 'object' && !!value && !!value.text) {
2029
2104
  newContent = value.text;
2030
2105
  }
@@ -2521,14 +2596,6 @@
2521
2596
  return (selection) => findParentNodeClosestToPos(selection.$from, predicate);
2522
2597
  }
2523
2598
 
2524
- function getHTMLFromFragment(fragment, schema) {
2525
- const documentFragment = model.DOMSerializer.fromSchema(schema).serializeFragment(fragment);
2526
- const temporaryDocument = document.implementation.createHTMLDocument();
2527
- const container = temporaryDocument.createElement('div');
2528
- container.appendChild(documentFragment);
2529
- return container.innerHTML;
2530
- }
2531
-
2532
2599
  function getSchema(extensions, editor) {
2533
2600
  const resolvedExtensions = ExtensionManager.resolve(extensions);
2534
2601
  return getSchemaByResolvedExtensions(resolvedExtensions, editor);
@@ -3099,6 +3166,11 @@
3099
3166
 
3100
3167
  const setNode = (typeOrName, attributes = {}) => ({ state, dispatch, chain }) => {
3101
3168
  const type = getNodeType(typeOrName, state.schema);
3169
+ let attributesToCopy;
3170
+ if (state.selection.$anchor.sameParent(state.selection.$head)) {
3171
+ // only copy attributes if the selection is pointing to a node of the same type
3172
+ attributesToCopy = state.selection.$anchor.parent.attrs;
3173
+ }
3102
3174
  // TODO: use a fallback like insertContent?
3103
3175
  if (!type.isTextblock) {
3104
3176
  console.warn('[tiptap warn]: Currently "setNode()" only supports text block nodes.');
@@ -3107,14 +3179,14 @@
3107
3179
  return (chain()
3108
3180
  // try to convert node to default node if needed
3109
3181
  .command(({ commands }) => {
3110
- const canSetBlock = commands$1.setBlockType(type, attributes)(state);
3182
+ const canSetBlock = commands$1.setBlockType(type, { ...attributesToCopy, ...attributes })(state);
3111
3183
  if (canSetBlock) {
3112
3184
  return true;
3113
3185
  }
3114
3186
  return commands.clearNodes();
3115
3187
  })
3116
3188
  .command(({ state: updatedState }) => {
3117
- return commands$1.setBlockType(type, attributes)(updatedState, dispatch);
3189
+ return commands$1.setBlockType(type, { ...attributesToCopy, ...attributes })(updatedState, dispatch);
3118
3190
  })
3119
3191
  .run());
3120
3192
  };
@@ -3533,21 +3605,63 @@
3533
3605
  markType = getMarkType(typeOrName, state.schema);
3534
3606
  }
3535
3607
  if (dispatch) {
3536
- tr.selection.ranges.forEach(range => {
3608
+ tr.selection.ranges.forEach((range) => {
3537
3609
  const from = range.$from.pos;
3538
3610
  const to = range.$to.pos;
3539
- state.doc.nodesBetween(from, to, (node, pos) => {
3540
- if (nodeType && nodeType === node.type) {
3541
- tr.setNodeMarkup(pos, undefined, {
3542
- ...node.attrs,
3611
+ let lastPos;
3612
+ let lastNode;
3613
+ let trimmedFrom;
3614
+ let trimmedTo;
3615
+ if (tr.selection.empty) {
3616
+ state.doc.nodesBetween(from, to, (node, pos) => {
3617
+ if (nodeType && nodeType === node.type) {
3618
+ trimmedFrom = Math.max(pos, from);
3619
+ trimmedTo = Math.min(pos + node.nodeSize, to);
3620
+ lastPos = pos;
3621
+ lastNode = node;
3622
+ }
3623
+ });
3624
+ }
3625
+ else {
3626
+ state.doc.nodesBetween(from, to, (node, pos) => {
3627
+ if (pos < from && nodeType && nodeType === node.type) {
3628
+ trimmedFrom = Math.max(pos, from);
3629
+ trimmedTo = Math.min(pos + node.nodeSize, to);
3630
+ lastPos = pos;
3631
+ lastNode = node;
3632
+ }
3633
+ if (pos >= from && pos <= to) {
3634
+ if (nodeType && nodeType === node.type) {
3635
+ tr.setNodeMarkup(pos, undefined, {
3636
+ ...node.attrs,
3637
+ ...attributes,
3638
+ });
3639
+ }
3640
+ if (markType && node.marks.length) {
3641
+ node.marks.forEach((mark) => {
3642
+ if (markType === mark.type) {
3643
+ const trimmedFrom2 = Math.max(pos, from);
3644
+ const trimmedTo2 = Math.min(pos + node.nodeSize, to);
3645
+ tr.addMark(trimmedFrom2, trimmedTo2, markType.create({
3646
+ ...mark.attrs,
3647
+ ...attributes,
3648
+ }));
3649
+ }
3650
+ });
3651
+ }
3652
+ }
3653
+ });
3654
+ }
3655
+ if (lastNode) {
3656
+ if (lastPos !== undefined) {
3657
+ tr.setNodeMarkup(lastPos, undefined, {
3658
+ ...lastNode.attrs,
3543
3659
  ...attributes,
3544
3660
  });
3545
3661
  }
3546
- if (markType && node.marks.length) {
3547
- node.marks.forEach(mark => {
3662
+ if (markType && lastNode.marks.length) {
3663
+ lastNode.marks.forEach((mark) => {
3548
3664
  if (markType === mark.type) {
3549
- const trimmedFrom = Math.max(pos, from);
3550
- const trimmedTo = Math.min(pos + node.nodeSize, to);
3551
3665
  tr.addMark(trimmedFrom, trimmedTo, markType.create({
3552
3666
  ...mark.attrs,
3553
3667
  ...attributes,
@@ -3555,7 +3669,7 @@
3555
3669
  }
3556
3670
  });
3557
3671
  }
3558
- });
3672
+ }
3559
3673
  });
3560
3674
  }
3561
3675
  return true;
@@ -4371,6 +4485,7 @@ img.ProseMirror-separator {
4371
4485
  * Creates a ProseMirror view.
4372
4486
  */
4373
4487
  createView() {
4488
+ var _a;
4374
4489
  let doc;
4375
4490
  try {
4376
4491
  doc = createDocument(this.options.content, this.schema, this.options.parseOptions, { errorOnInvalidContent: this.options.enableContentCheck });
@@ -4399,18 +4514,17 @@ img.ProseMirror-separator {
4399
4514
  const selection = resolveFocusPosition(doc, this.options.autofocus);
4400
4515
  this.view = new view.EditorView(this.options.element, {
4401
4516
  ...this.options.editorProps,
4517
+ attributes: {
4518
+ // add `role="textbox"` to the editor element
4519
+ role: 'textbox',
4520
+ ...(_a = this.options.editorProps) === null || _a === void 0 ? void 0 : _a.attributes,
4521
+ },
4402
4522
  dispatchTransaction: this.dispatchTransaction.bind(this),
4403
4523
  state: state.EditorState.create({
4404
4524
  doc,
4405
4525
  selection: selection || undefined,
4406
4526
  }),
4407
4527
  });
4408
- // add `role="textbox"` to the editor element
4409
- this.view.dom.setAttribute('role', 'textbox');
4410
- // add aria-label to the editor element
4411
- if (!this.view.dom.getAttribute('aria-label')) {
4412
- this.view.dom.setAttribute('aria-label', 'Rich-Text Editor');
4413
- }
4414
4528
  // `editor.view` is not yet available at this time.
4415
4529
  // Therefore we will add all plugins and node views directly afterwards.
4416
4530
  const newState = this.state.reconfigure({
@@ -4606,7 +4720,7 @@ img.ProseMirror-separator {
4606
4720
  /**
4607
4721
  * Build an input rule that adds a mark when the
4608
4722
  * matched text is typed into it.
4609
- * @see https://tiptap.dev/guide/custom-extensions/#input-rules
4723
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#input-rules
4610
4724
  */
4611
4725
  function markInputRule(config) {
4612
4726
  return new InputRule({
@@ -4650,7 +4764,7 @@ img.ProseMirror-separator {
4650
4764
  /**
4651
4765
  * Build an input rule that adds a node when the
4652
4766
  * matched text is typed into it.
4653
- * @see https://tiptap.dev/guide/custom-extensions/#input-rules
4767
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#input-rules
4654
4768
  */
4655
4769
  function nodeInputRule(config) {
4656
4770
  return new InputRule({
@@ -4690,7 +4804,7 @@ img.ProseMirror-separator {
4690
4804
  * matched text is typed into it. When using a regular expresion you’ll
4691
4805
  * probably want the regexp to start with `^`, so that the pattern can
4692
4806
  * only occur at the start of a textblock.
4693
- * @see https://tiptap.dev/guide/custom-extensions/#input-rules
4807
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#input-rules
4694
4808
  */
4695
4809
  function textblockTypeInputRule(config) {
4696
4810
  return new InputRule({
@@ -4711,7 +4825,7 @@ img.ProseMirror-separator {
4711
4825
  /**
4712
4826
  * Build an input rule that replaces text when the
4713
4827
  * matched text is typed into it.
4714
- * @see https://tiptap.dev/guide/custom-extensions/#input-rules
4828
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#input-rules
4715
4829
  */
4716
4830
  function textInputRule(config) {
4717
4831
  return new InputRule({
@@ -4748,7 +4862,7 @@ img.ProseMirror-separator {
4748
4862
  * two nodes. You can pass a join predicate, which takes a regular
4749
4863
  * expression match and the node before the wrapped node, and can
4750
4864
  * return a boolean to indicate whether a join should happen.
4751
- * @see https://tiptap.dev/guide/custom-extensions/#input-rules
4865
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#input-rules
4752
4866
  */
4753
4867
  function wrappingInputRule(config) {
4754
4868
  return new InputRule({
@@ -5081,7 +5195,7 @@ img.ProseMirror-separator {
5081
5195
  /**
5082
5196
  * Build an paste rule that adds a mark when the
5083
5197
  * matched text is pasted into it.
5084
- * @see https://tiptap.dev/guide/custom-extensions/#paste-rules
5198
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#paste-rules
5085
5199
  */
5086
5200
  function markPasteRule(config) {
5087
5201
  return new PasteRule({
@@ -5135,7 +5249,7 @@ img.ProseMirror-separator {
5135
5249
  /**
5136
5250
  * Build an paste rule that adds a node when the
5137
5251
  * matched text is pasted into it.
5138
- * @see https://tiptap.dev/guide/custom-extensions/#paste-rules
5252
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#paste-rules
5139
5253
  */
5140
5254
  function nodePasteRule(config) {
5141
5255
  return new PasteRule({
@@ -5160,7 +5274,7 @@ img.ProseMirror-separator {
5160
5274
  /**
5161
5275
  * Build an paste rule that replaces text when the
5162
5276
  * matched text is pasted into it.
5163
- * @see https://tiptap.dev/guide/custom-extensions/#paste-rules
5277
+ * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#paste-rules
5164
5278
  */
5165
5279
  function textPasteRule(config) {
5166
5280
  return new PasteRule({