prosemirror-view 1.39.3 → 1.40.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 1.40.0 (2025-06-02)
2
+
3
+ ### Bug fixes
4
+
5
+ Suppress lint warnings about dereferencing methods by making `EditorView.dispatch` a property instead of a method.
6
+
7
+ ### New features
8
+
9
+ The `"handleTextInput"` prop now takes an additional argument that can be used to generate the default transaction for that text change.
10
+
11
+ Widget decorations now support a `relaxedSide` option which can be used to allow the DOM cursor to appear at either side.
12
+
1
13
  ## 1.39.3 (2025-05-14)
2
14
 
3
15
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -50,6 +50,7 @@ var isEquivalentPosition = function isEquivalentPosition(node, off, targetNode,
50
50
  };
51
51
  var atomElements = /^(img|br|input|textarea|hr)$/i;
52
52
  function scanFor(node, off, targetNode, targetOff, dir) {
53
+ var _a;
53
54
  for (;;) {
54
55
  if (node == targetNode && off == targetOff) return true;
55
56
  if (off == (dir < 0 ? 0 : nodeSize(node))) {
@@ -58,9 +59,13 @@ function scanFor(node, off, targetNode, targetOff, dir) {
58
59
  off = domIndex(node) + (dir < 0 ? 0 : 1);
59
60
  node = parent;
60
61
  } else if (node.nodeType == 1) {
61
- node = node.childNodes[off + (dir < 0 ? -1 : 0)];
62
- if (node.contentEditable == "false") return false;
63
- off = dir < 0 ? nodeSize(node) : 0;
62
+ var child = node.childNodes[off + (dir < 0 ? -1 : 0)];
63
+ if (child.nodeType == 1 && child.contentEditable == "false") {
64
+ if ((_a = child.pmViewDesc) === null || _a === void 0 ? void 0 : _a.ignoreForSelection) off += dir;else return false;
65
+ } else {
66
+ node = child;
67
+ off = dir < 0 ? nodeSize(node) : 0;
68
+ }
64
69
  } else {
65
70
  return false;
66
71
  }
@@ -1063,6 +1068,11 @@ var ViewDesc = function () {
1063
1068
  get: function get() {
1064
1069
  return false;
1065
1070
  }
1071
+ }, {
1072
+ key: "ignoreForSelection",
1073
+ get: function get() {
1074
+ return false;
1075
+ }
1066
1076
  }, {
1067
1077
  key: "isText",
1068
1078
  value: function isText(text) {
@@ -1132,6 +1142,11 @@ var WidgetViewDesc = function (_ViewDesc) {
1132
1142
  get: function get() {
1133
1143
  return true;
1134
1144
  }
1145
+ }, {
1146
+ key: "ignoreForSelection",
1147
+ get: function get() {
1148
+ return !!this.widget.type.spec.relaxedSide;
1149
+ }
1135
1150
  }, {
1136
1151
  key: "side",
1137
1152
  get: function get() {
@@ -2991,9 +3006,12 @@ editHandlers.keypress = function (view, _event) {
2991
3006
  var sel = view.state.selection;
2992
3007
  if (!(sel instanceof prosemirrorState.TextSelection) || !sel.$from.sameParent(sel.$to)) {
2993
3008
  var text = String.fromCharCode(event.charCode);
3009
+ var deflt = function deflt() {
3010
+ return view.state.tr.insertText(text).scrollIntoView();
3011
+ };
2994
3012
  if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", function (f) {
2995
- return f(view, sel.$from.pos, sel.$to.pos, text);
2996
- })) view.dispatch(view.state.tr.insertText(text).scrollIntoView());
3013
+ return f(view, sel.$from.pos, sel.$to.pos, text, deflt);
3014
+ })) view.dispatch(deflt());
2997
3015
  event.preventDefault();
2998
3016
  }
2999
3017
  };
@@ -4598,10 +4616,10 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4598
4616
  if (chrome && android && view.input.lastKeyCode === 13 && Date.now() - 100 < view.input.lastKeyCodeTime && view.someProp("handleKeyDown", function (f) {
4599
4617
  return f(view, keyEvent(13, "Enter"));
4600
4618
  })) return;
4601
- var _tr = view.state.tr.setSelection(newSel);
4602
- if (origin == "pointer") _tr.setMeta("pointer", true);else if (origin == "key") _tr.scrollIntoView();
4603
- if (compositionID) _tr.setMeta("composition", compositionID);
4604
- view.dispatch(_tr);
4619
+ var tr = view.state.tr.setSelection(newSel);
4620
+ if (origin == "pointer") tr.setMeta("pointer", true);else if (origin == "key") tr.scrollIntoView();
4621
+ if (compositionID) tr.setMeta("composition", compositionID);
4622
+ view.dispatch(tr);
4605
4623
  }
4606
4624
  return;
4607
4625
  }
@@ -4643,9 +4661,9 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4643
4661
  if (parse.sel) {
4644
4662
  var _sel2 = resolveSelection(view, view.state.doc, parse.sel);
4645
4663
  if (_sel2 && !_sel2.eq(view.state.selection)) {
4646
- var _tr2 = view.state.tr.setSelection(_sel2);
4647
- if (compositionID) _tr2.setMeta("composition", compositionID);
4648
- view.dispatch(_tr2);
4664
+ var _tr = view.state.tr.setSelection(_sel2);
4665
+ if (compositionID) _tr.setMeta("composition", compositionID);
4666
+ view.dispatch(_tr);
4649
4667
  }
4650
4668
  }
4651
4669
  return;
@@ -4695,7 +4713,16 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4695
4713
  }
4696
4714
  var chFrom = change.start,
4697
4715
  chTo = change.endA;
4698
- var tr, storedMarks, markChange;
4716
+ var mkTr = function mkTr(base) {
4717
+ var tr = base || view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from));
4718
+ if (parse.sel) {
4719
+ var _sel3 = resolveSelection(view, tr.doc, parse.sel);
4720
+ if (_sel3 && !(chrome && view.composing && _sel3.empty && (change.start != change.endB || view.input.lastChromeDelete < Date.now() - 100) && (_sel3.head == chFrom || _sel3.head == tr.mapping.map(chTo) - 1) || ie && _sel3.empty && _sel3.head == chFrom)) tr.setSelection(_sel3);
4721
+ }
4722
+ if (compositionID) tr.setMeta("composition", compositionID);
4723
+ return tr.scrollIntoView();
4724
+ };
4725
+ var markChange;
4699
4726
  if (inlineChange) {
4700
4727
  if ($from.pos == $to.pos) {
4701
4728
  if (ie && ie_version <= 11 && $from.parentOffset == 0) {
@@ -4704,27 +4731,26 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4704
4731
  return selectionToDOM(view);
4705
4732
  }, 20);
4706
4733
  }
4707
- tr = view.state.tr["delete"](chFrom, chTo);
4708
- storedMarks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA));
4734
+ var _tr2 = mkTr(view.state.tr["delete"](chFrom, chTo));
4735
+ var marks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA));
4736
+ if (marks) _tr2.ensureMarks(marks);
4737
+ view.dispatch(_tr2);
4709
4738
  } else if (change.endA == change.endB && (markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset), $fromA.parent.content.cut($fromA.parentOffset, change.endA - $fromA.start())))) {
4710
- tr = view.state.tr;
4711
- if (markChange.type == "add") tr.addMark(chFrom, chTo, markChange.mark);else tr.removeMark(chFrom, chTo, markChange.mark);
4739
+ var _tr3 = mkTr(view.state.tr);
4740
+ if (markChange.type == "add") _tr3.addMark(chFrom, chTo, markChange.mark);else _tr3.removeMark(chFrom, chTo, markChange.mark);
4741
+ view.dispatch(_tr3);
4712
4742
  } else if ($from.parent.child($from.index()).isText && $from.index() == $to.index() - ($to.textOffset ? 0 : 1)) {
4713
4743
  var text = $from.parent.textBetween($from.parentOffset, $to.parentOffset);
4714
- if (view.someProp("handleTextInput", function (f) {
4715
- return f(view, chFrom, chTo, text);
4716
- })) return;
4717
- tr = view.state.tr.insertText(text, chFrom, chTo);
4744
+ var deflt = function deflt() {
4745
+ return mkTr(view.state.tr.insertText(text, chFrom, chTo));
4746
+ };
4747
+ if (!view.someProp("handleTextInput", function (f) {
4748
+ return f(view, chFrom, chTo, text, deflt);
4749
+ })) view.dispatch(deflt());
4718
4750
  }
4751
+ } else {
4752
+ view.dispatch(mkTr());
4719
4753
  }
4720
- if (!tr) tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from));
4721
- if (parse.sel) {
4722
- var _sel3 = resolveSelection(view, tr.doc, parse.sel);
4723
- if (_sel3 && !(chrome && view.composing && _sel3.empty && (change.start != change.endB || view.input.lastChromeDelete < Date.now() - 100) && (_sel3.head == chFrom || _sel3.head == tr.mapping.map(chTo) - 1) || ie && _sel3.empty && _sel3.head == chFrom)) tr.setSelection(_sel3);
4724
- }
4725
- if (storedMarks) tr.ensureMarks(storedMarks);
4726
- if (compositionID) tr.setMeta("composition", compositionID);
4727
- view.dispatch(tr.scrollIntoView());
4728
4754
  }
4729
4755
  function resolveSelection(view, doc, parsedSel) {
4730
4756
  if (Math.max(parsedSel.anchor, parsedSel.head) > doc.content.size) return null;
@@ -5168,12 +5194,6 @@ var EditorView = function () {
5168
5194
  value: function dispatchEvent(event) {
5169
5195
  return _dispatchEvent(this, event);
5170
5196
  }
5171
- }, {
5172
- key: "dispatch",
5173
- value: function dispatch(tr) {
5174
- var dispatchTransaction = this._props.dispatchTransaction;
5175
- if (dispatchTransaction) dispatchTransaction.call(this, tr);else this.updateState(this.state.apply(tr));
5176
- }
5177
5197
  }, {
5178
5198
  key: "domSelectionRange",
5179
5199
  value: function domSelectionRange() {
@@ -5194,6 +5214,10 @@ var EditorView = function () {
5194
5214
  }]);
5195
5215
  return EditorView;
5196
5216
  }();
5217
+ EditorView.prototype.dispatch = function (tr) {
5218
+ var dispatchTransaction = this._props.dispatchTransaction;
5219
+ if (dispatchTransaction) dispatchTransaction.call(this, tr);else this.updateState(this.state.apply(tr));
5220
+ };
5197
5221
  function computeDocDeco(view) {
5198
5222
  var attrs = Object.create(null);
5199
5223
  attrs["class"] = "ProseMirror";
package/dist/index.d.cts CHANGED
@@ -48,6 +48,20 @@ declare class Decoration {
48
48
  */
49
49
  side?: number;
50
50
  /**
51
+ By default, the cursor, when at the position of the widget,
52
+ will be strictly kept on the side indicated by
53
+ [`side`](https://prosemirror.net/docs/ref/#view.Decoration^widget^spec.side). Set this to true
54
+ to allow the DOM selection to stay on the other side if the
55
+ client sets it there.
56
+
57
+ **Note**: Mapping of this decoration, which decides on which
58
+ side insertions at its position appear, will still happen
59
+ according to `side`, and keyboard cursor motion will not,
60
+ without further custom handling, visit both sides of the
61
+ widget.
62
+ */
63
+ relaxedSide?: boolean;
64
+ /**
51
65
  The precise set of marks to draw around the widget.
52
66
  */
53
67
  marks?: readonly Mark[];
@@ -367,7 +381,6 @@ editable document. Its state and behavior are determined by its
367
381
  [props](https://prosemirror.net/docs/ref/#view.DirectEditorProps).
368
382
  */
369
383
  declare class EditorView {
370
- private _props;
371
384
  private directPlugins;
372
385
  private _root;
373
386
  private mounted;
@@ -587,7 +600,7 @@ declare class EditorView {
587
600
  This method is bound to the view instance, so that it can be
588
601
  easily passed around.
589
602
  */
590
- dispatch(tr: Transaction): void;
603
+ dispatch: (tr: Transaction) => void;
591
604
  }
592
605
  /**
593
606
  The type of function [provided](https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews) to
@@ -652,7 +665,7 @@ interface EditorProps<P = any> {
652
665
  before the input is applied. If it returns `true`, the default
653
666
  behavior of actually inserting the text is suppressed.
654
667
  */
655
- handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string) => boolean | void;
668
+ handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string, deflt: () => Transaction) => boolean | void;
656
669
  /**
657
670
  Called for each node around a click, from the inside out. The
658
671
  `direct` flag will be true for the inner node.
package/dist/index.d.ts CHANGED
@@ -48,6 +48,20 @@ declare class Decoration {
48
48
  */
49
49
  side?: number;
50
50
  /**
51
+ By default, the cursor, when at the position of the widget,
52
+ will be strictly kept on the side indicated by
53
+ [`side`](https://prosemirror.net/docs/ref/#view.Decoration^widget^spec.side). Set this to true
54
+ to allow the DOM selection to stay on the other side if the
55
+ client sets it there.
56
+
57
+ **Note**: Mapping of this decoration, which decides on which
58
+ side insertions at its position appear, will still happen
59
+ according to `side`, and keyboard cursor motion will not,
60
+ without further custom handling, visit both sides of the
61
+ widget.
62
+ */
63
+ relaxedSide?: boolean;
64
+ /**
51
65
  The precise set of marks to draw around the widget.
52
66
  */
53
67
  marks?: readonly Mark[];
@@ -367,7 +381,6 @@ editable document. Its state and behavior are determined by its
367
381
  [props](https://prosemirror.net/docs/ref/#view.DirectEditorProps).
368
382
  */
369
383
  declare class EditorView {
370
- private _props;
371
384
  private directPlugins;
372
385
  private _root;
373
386
  private mounted;
@@ -587,7 +600,7 @@ declare class EditorView {
587
600
  This method is bound to the view instance, so that it can be
588
601
  easily passed around.
589
602
  */
590
- dispatch(tr: Transaction): void;
603
+ dispatch: (tr: Transaction) => void;
591
604
  }
592
605
  /**
593
606
  The type of function [provided](https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews) to
@@ -652,7 +665,7 @@ interface EditorProps<P = any> {
652
665
  before the input is applied. If it returns `true`, the default
653
666
  behavior of actually inserting the text is suppressed.
654
667
  */
655
- handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string) => boolean | void;
668
+ handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string, deflt: () => Transaction) => boolean | void;
656
669
  /**
657
670
  Called for each node around a click, from the inside out. The
658
671
  `direct` flag will be true for the inner node.
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ const isEquivalentPosition = function (node, off, targetNode, targetOff) {
35
35
  };
36
36
  const atomElements = /^(img|br|input|textarea|hr)$/i;
37
37
  function scanFor(node, off, targetNode, targetOff, dir) {
38
+ var _a;
38
39
  for (;;) {
39
40
  if (node == targetNode && off == targetOff)
40
41
  return true;
@@ -47,10 +48,17 @@ function scanFor(node, off, targetNode, targetOff, dir) {
47
48
  node = parent;
48
49
  }
49
50
  else if (node.nodeType == 1) {
50
- node = node.childNodes[off + (dir < 0 ? -1 : 0)];
51
- if (node.contentEditable == "false")
52
- return false;
53
- off = dir < 0 ? nodeSize(node) : 0;
51
+ let child = node.childNodes[off + (dir < 0 ? -1 : 0)];
52
+ if (child.nodeType == 1 && child.contentEditable == "false") {
53
+ if ((_a = child.pmViewDesc) === null || _a === void 0 ? void 0 : _a.ignoreForSelection)
54
+ off += dir;
55
+ else
56
+ return false;
57
+ }
58
+ else {
59
+ node = child;
60
+ off = dir < 0 ? nodeSize(node) : 0;
61
+ }
54
62
  }
55
63
  else {
56
64
  return false;
@@ -1134,6 +1142,7 @@ class ViewDesc {
1134
1142
  }
1135
1143
  get domAtom() { return false; }
1136
1144
  get ignoreForCoords() { return false; }
1145
+ get ignoreForSelection() { return false; }
1137
1146
  isText(text) { return false; }
1138
1147
  }
1139
1148
  // A widget desc represents a widget decoration, which is a DOM node
@@ -1178,6 +1187,7 @@ class WidgetViewDesc extends ViewDesc {
1178
1187
  super.destroy();
1179
1188
  }
1180
1189
  get domAtom() { return true; }
1190
+ get ignoreForSelection() { return !!this.widget.type.spec.relaxedSide; }
1181
1191
  get side() { return this.widget.type.side; }
1182
1192
  }
1183
1193
  class CompositionViewDesc extends ViewDesc {
@@ -3175,8 +3185,9 @@ editHandlers.keypress = (view, _event) => {
3175
3185
  let sel = view.state.selection;
3176
3186
  if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) {
3177
3187
  let text = String.fromCharCode(event.charCode);
3178
- if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text)))
3179
- view.dispatch(view.state.tr.insertText(text).scrollIntoView());
3188
+ let deflt = () => view.state.tr.insertText(text).scrollIntoView();
3189
+ if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text, deflt)))
3190
+ view.dispatch(deflt());
3180
3191
  event.preventDefault();
3181
3192
  }
3182
3193
  };
@@ -5061,7 +5072,26 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
5061
5072
  }, 20);
5062
5073
  }
5063
5074
  let chFrom = change.start, chTo = change.endA;
5064
- let tr, storedMarks, markChange;
5075
+ let mkTr = (base) => {
5076
+ let tr = base || view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from));
5077
+ if (parse.sel) {
5078
+ let sel = resolveSelection(view, tr.doc, parse.sel);
5079
+ // Chrome will sometimes, during composition, report the
5080
+ // selection in the wrong place. If it looks like that is
5081
+ // happening, don't update the selection.
5082
+ // Edge just doesn't move the cursor forward when you start typing
5083
+ // in an empty block or between br nodes.
5084
+ if (sel && !(chrome && view.composing && sel.empty &&
5085
+ (change.start != change.endB || view.input.lastChromeDelete < Date.now() - 100) &&
5086
+ (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
5087
+ ie && sel.empty && sel.head == chFrom))
5088
+ tr.setSelection(sel);
5089
+ }
5090
+ if (compositionID)
5091
+ tr.setMeta("composition", compositionID);
5092
+ return tr.scrollIntoView();
5093
+ };
5094
+ let markChange;
5065
5095
  if (inlineChange) {
5066
5096
  if ($from.pos == $to.pos) { // Deletion
5067
5097
  // IE11 sometimes weirdly moves the DOM selection around after
@@ -5070,46 +5100,33 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
5070
5100
  view.domObserver.suppressSelectionUpdates();
5071
5101
  setTimeout(() => selectionToDOM(view), 20);
5072
5102
  }
5073
- tr = view.state.tr.delete(chFrom, chTo);
5074
- storedMarks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA));
5103
+ let tr = mkTr(view.state.tr.delete(chFrom, chTo));
5104
+ let marks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA));
5105
+ if (marks)
5106
+ tr.ensureMarks(marks);
5107
+ view.dispatch(tr);
5075
5108
  }
5076
5109
  else if ( // Adding or removing a mark
5077
5110
  change.endA == change.endB &&
5078
5111
  (markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset), $fromA.parent.content.cut($fromA.parentOffset, change.endA - $fromA.start())))) {
5079
- tr = view.state.tr;
5112
+ let tr = mkTr(view.state.tr);
5080
5113
  if (markChange.type == "add")
5081
5114
  tr.addMark(chFrom, chTo, markChange.mark);
5082
5115
  else
5083
5116
  tr.removeMark(chFrom, chTo, markChange.mark);
5117
+ view.dispatch(tr);
5084
5118
  }
5085
5119
  else if ($from.parent.child($from.index()).isText && $from.index() == $to.index() - ($to.textOffset ? 0 : 1)) {
5086
5120
  // Both positions in the same text node -- simply insert text
5087
5121
  let text = $from.parent.textBetween($from.parentOffset, $to.parentOffset);
5088
- if (view.someProp("handleTextInput", f => f(view, chFrom, chTo, text)))
5089
- return;
5090
- tr = view.state.tr.insertText(text, chFrom, chTo);
5091
- }
5092
- }
5093
- if (!tr)
5094
- tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from));
5095
- if (parse.sel) {
5096
- let sel = resolveSelection(view, tr.doc, parse.sel);
5097
- // Chrome will sometimes, during composition, report the
5098
- // selection in the wrong place. If it looks like that is
5099
- // happening, don't update the selection.
5100
- // Edge just doesn't move the cursor forward when you start typing
5101
- // in an empty block or between br nodes.
5102
- if (sel && !(chrome && view.composing && sel.empty &&
5103
- (change.start != change.endB || view.input.lastChromeDelete < Date.now() - 100) &&
5104
- (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
5105
- ie && sel.empty && sel.head == chFrom))
5106
- tr.setSelection(sel);
5107
- }
5108
- if (storedMarks)
5109
- tr.ensureMarks(storedMarks);
5110
- if (compositionID)
5111
- tr.setMeta("composition", compositionID);
5112
- view.dispatch(tr.scrollIntoView());
5122
+ let deflt = () => mkTr(view.state.tr.insertText(text, chFrom, chTo));
5123
+ if (!view.someProp("handleTextInput", f => f(view, chFrom, chTo, text, deflt)))
5124
+ view.dispatch(deflt());
5125
+ }
5126
+ }
5127
+ else {
5128
+ view.dispatch(mkTr());
5129
+ }
5113
5130
  }
5114
5131
  function resolveSelection(view, doc, parsedSel) {
5115
5132
  if (Math.max(parsedSel.anchor, parsedSel.head) > doc.content.size)
@@ -5712,22 +5729,6 @@ class EditorView {
5712
5729
  return dispatchEvent(this, event);
5713
5730
  }
5714
5731
  /**
5715
- Dispatch a transaction. Will call
5716
- [`dispatchTransaction`](https://prosemirror.net/docs/ref/#view.DirectEditorProps.dispatchTransaction)
5717
- when given, and otherwise defaults to applying the transaction to
5718
- the current state and calling
5719
- [`updateState`](https://prosemirror.net/docs/ref/#view.EditorView.updateState) with the result.
5720
- This method is bound to the view instance, so that it can be
5721
- easily passed around.
5722
- */
5723
- dispatch(tr) {
5724
- let dispatchTransaction = this._props.dispatchTransaction;
5725
- if (dispatchTransaction)
5726
- dispatchTransaction.call(this, tr);
5727
- else
5728
- this.updateState(this.state.apply(tr));
5729
- }
5730
- /**
5731
5732
  @internal
5732
5733
  */
5733
5734
  domSelectionRange() {
@@ -5744,6 +5745,13 @@ class EditorView {
5744
5745
  return this.root.getSelection();
5745
5746
  }
5746
5747
  }
5748
+ EditorView.prototype.dispatch = function (tr) {
5749
+ let dispatchTransaction = this._props.dispatchTransaction;
5750
+ if (dispatchTransaction)
5751
+ dispatchTransaction.call(this, tr);
5752
+ else
5753
+ this.updateState(this.state.apply(tr));
5754
+ };
5747
5755
  function computeDocDeco(view) {
5748
5756
  let attrs = Object.create(null);
5749
5757
  attrs.class = "ProseMirror";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.39.3",
3
+ "version": "1.40.0",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/decoration.ts CHANGED
@@ -156,6 +156,19 @@ export class Decoration {
156
156
  /// negative, those of the node after when positive.
157
157
  side?: number
158
158
 
159
+ /// By default, the cursor, when at the position of the widget,
160
+ /// will be strictly kept on the side indicated by
161
+ /// [`side`](#view.Decoration^widget^spec.side). Set this to true
162
+ /// to allow the DOM selection to stay on the other side if the
163
+ /// client sets it there.
164
+ ///
165
+ /// **Note**: Mapping of this decoration, which decides on which
166
+ /// side insertions at its position appear, will still happen
167
+ /// according to `side`, and keyboard cursor motion will not,
168
+ /// without further custom handling, visit both sides of the
169
+ /// widget.
170
+ relaxedSide?: boolean
171
+
159
172
  /// The precise set of marks to draw around the widget.
160
173
  marks?: readonly Mark[]
161
174
 
package/src/dom.ts CHANGED
@@ -54,9 +54,14 @@ function scanFor(node: Node, off: number, targetNode: Node, targetOff: number, d
54
54
  off = domIndex(node) + (dir < 0 ? 0 : 1)
55
55
  node = parent
56
56
  } else if (node.nodeType == 1) {
57
- node = node.childNodes[off + (dir < 0 ? -1 : 0)]
58
- if ((node as HTMLElement).contentEditable == "false") return false
59
- off = dir < 0 ? nodeSize(node) : 0
57
+ let child = node.childNodes[off + (dir < 0 ? -1 : 0)]
58
+ if (child.nodeType == 1 && (child as HTMLElement).contentEditable == "false") {
59
+ if (child.pmViewDesc?.ignoreForSelection) off += dir
60
+ else return false
61
+ } else {
62
+ node = child
63
+ off = dir < 0 ? nodeSize(node) : 0
64
+ }
60
65
  } else {
61
66
  return false
62
67
  }
package/src/domchange.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import {Fragment, DOMParser, TagParseRule, Node, Mark, ResolvedPos} from "prosemirror-model"
2
- import {Selection, TextSelection} from "prosemirror-state"
2
+ import {Selection, TextSelection, Transaction} from "prosemirror-state"
3
3
 
4
4
  import {selectionBetween, selectionFromDOM, selectionToDOM} from "./selection"
5
5
  import {selectionCollapsed, keyEvent, DOMNode} from "./dom"
@@ -223,7 +223,27 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
223
223
 
224
224
  let chFrom = change.start, chTo = change.endA
225
225
 
226
- let tr, storedMarks, markChange
226
+ let mkTr = (base?: Transaction) => {
227
+ let tr = base || view.state.tr.replace(chFrom, chTo, parse.doc.slice(change!.start - parse.from,
228
+ change!.endB - parse.from))
229
+ if (parse.sel) {
230
+ let sel = resolveSelection(view, tr.doc, parse.sel)
231
+ // Chrome will sometimes, during composition, report the
232
+ // selection in the wrong place. If it looks like that is
233
+ // happening, don't update the selection.
234
+ // Edge just doesn't move the cursor forward when you start typing
235
+ // in an empty block or between br nodes.
236
+ if (sel && !(browser.chrome && view.composing && sel.empty &&
237
+ (change!.start != change!.endB || view.input.lastChromeDelete < Date.now() - 100) &&
238
+ (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
239
+ browser.ie && sel.empty && sel.head == chFrom))
240
+ tr.setSelection(sel)
241
+ }
242
+ if (compositionID) tr.setMeta("composition", compositionID)
243
+ return tr.scrollIntoView()
244
+ }
245
+
246
+ let markChange
227
247
  if (inlineChange) {
228
248
  if ($from.pos == $to.pos) { // Deletion
229
249
  // IE11 sometimes weirdly moves the DOM selection around after
@@ -232,42 +252,29 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
232
252
  view.domObserver.suppressSelectionUpdates()
233
253
  setTimeout(() => selectionToDOM(view), 20)
234
254
  }
235
- tr = view.state.tr.delete(chFrom, chTo)
236
- storedMarks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA))
255
+ let tr = mkTr(view.state.tr.delete(chFrom, chTo))
256
+ let marks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA))
257
+ if (marks) tr.ensureMarks(marks)
258
+ view.dispatch(tr)
237
259
  } else if ( // Adding or removing a mark
238
260
  change.endA == change.endB &&
239
261
  (markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset),
240
262
  $fromA.parent.content.cut($fromA.parentOffset, change.endA - $fromA.start())))
241
263
  ) {
242
- tr = view.state.tr
264
+ let tr = mkTr(view.state.tr)
243
265
  if (markChange.type == "add") tr.addMark(chFrom, chTo, markChange.mark)
244
266
  else tr.removeMark(chFrom, chTo, markChange.mark)
267
+ view.dispatch(tr)
245
268
  } else if ($from.parent.child($from.index()).isText && $from.index() == $to.index() - ($to.textOffset ? 0 : 1)) {
246
269
  // Both positions in the same text node -- simply insert text
247
270
  let text = $from.parent.textBetween($from.parentOffset, $to.parentOffset)
248
- if (view.someProp("handleTextInput", f => f(view, chFrom, chTo, text))) return
249
- tr = view.state.tr.insertText(text, chFrom, chTo)
271
+ let deflt = () => mkTr(view.state.tr.insertText(text, chFrom, chTo))
272
+ if (!view.someProp("handleTextInput", f => f(view, chFrom, chTo, text, deflt)))
273
+ view.dispatch(deflt())
250
274
  }
275
+ } else {
276
+ view.dispatch(mkTr())
251
277
  }
252
-
253
- if (!tr)
254
- tr = view.state.tr.replace(chFrom, chTo, parse.doc.slice(change.start - parse.from, change.endB - parse.from))
255
- if (parse.sel) {
256
- let sel = resolveSelection(view, tr.doc, parse.sel)
257
- // Chrome will sometimes, during composition, report the
258
- // selection in the wrong place. If it looks like that is
259
- // happening, don't update the selection.
260
- // Edge just doesn't move the cursor forward when you start typing
261
- // in an empty block or between br nodes.
262
- if (sel && !(browser.chrome && view.composing && sel.empty &&
263
- (change.start != change.endB || view.input.lastChromeDelete < Date.now() - 100) &&
264
- (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) ||
265
- browser.ie && sel.empty && sel.head == chFrom))
266
- tr.setSelection(sel)
267
- }
268
- if (storedMarks) tr.ensureMarks(storedMarks)
269
- if (compositionID) tr.setMeta("composition", compositionID)
270
- view.dispatch(tr.scrollIntoView())
271
278
  }
272
279
 
273
280
  function resolveSelection(view: EditorView, doc: Node, parsedSel: {anchor: number, head: number}) {
package/src/index.ts CHANGED
@@ -28,7 +28,8 @@ export const __endComposition = endComposition
28
28
  /// editable document. Its state and behavior are determined by its
29
29
  /// [props](#view.DirectEditorProps).
30
30
  export class EditorView {
31
- private _props: DirectEditorProps
31
+ /// @internal
32
+ _props: DirectEditorProps
32
33
  private directPlugins: readonly Plugin[]
33
34
  private _root: Document | ShadowRoot | null = null
34
35
  /// @internal
@@ -490,11 +491,7 @@ export class EditorView {
490
491
  /// [`updateState`](#view.EditorView.updateState) with the result.
491
492
  /// This method is bound to the view instance, so that it can be
492
493
  /// easily passed around.
493
- dispatch(tr: Transaction) {
494
- let dispatchTransaction = this._props.dispatchTransaction
495
- if (dispatchTransaction) dispatchTransaction.call(this, tr)
496
- else this.updateState(this.state.apply(tr))
497
- }
494
+ declare dispatch: (tr: Transaction) => void
498
495
 
499
496
  /// @internal
500
497
  domSelectionRange(): DOMSelectionRange {
@@ -510,6 +507,12 @@ export class EditorView {
510
507
  }
511
508
  }
512
509
 
510
+ EditorView.prototype.dispatch = function(tr: Transaction) {
511
+ let dispatchTransaction = this._props.dispatchTransaction
512
+ if (dispatchTransaction) dispatchTransaction.call(this, tr)
513
+ else this.updateState(this.state.apply(tr))
514
+ }
515
+
513
516
  function computeDocDeco(view: EditorView) {
514
517
  let attrs = Object.create(null)
515
518
  attrs.class = "ProseMirror"
@@ -635,7 +638,7 @@ export interface EditorProps<P = any> {
635
638
  /// Whenever the user directly input text, this handler is called
636
639
  /// before the input is applied. If it returns `true`, the default
637
640
  /// behavior of actually inserting the text is suppressed.
638
- handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string) => boolean | void
641
+ handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string, deflt: () => Transaction) => boolean | void
639
642
 
640
643
  /// Called for each node around a click, from the inside out. The
641
644
  /// `direct` flag will be true for the inner node.
package/src/input.ts CHANGED
@@ -151,8 +151,9 @@ editHandlers.keypress = (view, _event) => {
151
151
  let sel = view.state.selection
152
152
  if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) {
153
153
  let text = String.fromCharCode(event.charCode)
154
- if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text)))
155
- view.dispatch(view.state.tr.insertText(text).scrollIntoView())
154
+ let deflt = () => view.state.tr.insertText(text).scrollIntoView()
155
+ if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text, deflt)))
156
+ view.dispatch(deflt())
156
157
  event.preventDefault()
157
158
  }
158
159
  }
package/src/viewdesc.ts CHANGED
@@ -528,6 +528,8 @@ export class ViewDesc {
528
528
 
529
529
  get ignoreForCoords() { return false }
530
530
 
531
+ get ignoreForSelection() { return false }
532
+
531
533
  isText(text: string) { return false }
532
534
  }
533
535
 
@@ -576,6 +578,8 @@ class WidgetViewDesc extends ViewDesc {
576
578
 
577
579
  get domAtom() { return true }
578
580
 
581
+ get ignoreForSelection() { return !!this.widget.type.spec.relaxedSide }
582
+
579
583
  get side() { return (this.widget.type as any).side as number }
580
584
  }
581
585