prosemirror-view 1.33.1 → 1.33.3

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,23 @@
1
+ ## 1.33.3 (2024-03-22)
2
+
3
+ ### Bug fixes
4
+
5
+ Further improve preserving of compositions for IME systems that keep the cursor before the composed text.
6
+
7
+ ## 1.33.2 (2024-03-21)
8
+
9
+ ### Bug fixes
10
+
11
+ Improve Android backspace detection when the native effect is to delete an entire block element.
12
+
13
+ Fix a bug where reconfiguring node views caused the editor to lose focus.
14
+
15
+ Fix an issue where some commands didn't properly scroll the cursor into view on Mobile Safari.
16
+
17
+ Fix a bug where starting a composition with stored marks or after a noninclusive mark, with an IME that keeps the cursor at the start of the composed text (like many Chinese IMEs) disrupted composition on Chrome.
18
+
19
+ Fix a memory leak where a (single) destroyed editor could be kept alive through a reused `Range` object.
20
+
1
21
  ## 1.33.1 (2024-02-15)
2
22
 
3
23
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -38,6 +38,9 @@ var textRange = function textRange(node, from, to) {
38
38
  range.setStart(node, from || 0);
39
39
  return range;
40
40
  };
41
+ var clearReusedRange = function clearReusedRange() {
42
+ reusedRange = null;
43
+ };
41
44
  var isEquivalentPosition = function isEquivalentPosition(node, off, targetNode, targetOff) {
42
45
  return targetNode && (scanFor(node, off, targetNode, targetOff, -1) || scanFor(node, off, targetNode, targetOff, 1));
43
46
  };
@@ -62,6 +65,36 @@ function scanFor(node, off, targetNode, targetOff, dir) {
62
65
  function nodeSize(node) {
63
66
  return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length;
64
67
  }
68
+ function textNodeBefore$1(node, offset) {
69
+ for (;;) {
70
+ if (node.nodeType == 3 && offset) return node;
71
+ if (node.nodeType == 1 && offset > 0) {
72
+ if (node.contentEditable == "false") return null;
73
+ node = node.childNodes[offset - 1];
74
+ offset = nodeSize(node);
75
+ } else if (node.parentNode && !hasBlockDesc(node)) {
76
+ offset = domIndex(node);
77
+ node = node.parentNode;
78
+ } else {
79
+ return null;
80
+ }
81
+ }
82
+ }
83
+ function textNodeAfter$1(node, offset) {
84
+ for (;;) {
85
+ if (node.nodeType == 3 && offset < node.nodeValue.length) return node;
86
+ if (node.nodeType == 1 && offset < node.childNodes.length) {
87
+ if (node.contentEditable == "false") return null;
88
+ node = node.childNodes[offset];
89
+ offset = 0;
90
+ } else if (node.parentNode && !hasBlockDesc(node)) {
91
+ offset = domIndex(node) + 1;
92
+ node = node.parentNode;
93
+ } else {
94
+ return null;
95
+ }
96
+ }
97
+ }
65
98
  function isOnEdge(node, offset, parent) {
66
99
  for (var atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;) {
67
100
  if (node == parent) return true;
@@ -131,6 +164,13 @@ var android = /Android \d/.test(agent);
131
164
  var webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style;
132
165
  var webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0;
133
166
  function windowRect(doc) {
167
+ var vp = doc.defaultView && doc.defaultView.visualViewport;
168
+ if (vp) return {
169
+ left: 0,
170
+ right: vp.width,
171
+ top: 0,
172
+ bottom: vp.height
173
+ };
134
174
  return {
135
175
  left: 0,
136
176
  right: doc.documentElement.clientWidth,
@@ -1004,6 +1044,11 @@ var ViewDesc = function () {
1004
1044
  get: function get() {
1005
1045
  return false;
1006
1046
  }
1047
+ }, {
1048
+ key: "isText",
1049
+ value: function isText(text) {
1050
+ return false;
1051
+ }
1007
1052
  }]);
1008
1053
  return ViewDesc;
1009
1054
  }();
@@ -1268,8 +1313,7 @@ var NodeViewDesc = function (_ViewDesc4) {
1268
1313
  from = _view$state$selection.from,
1269
1314
  to = _view$state$selection.to;
1270
1315
  if (!(view.state.selection instanceof prosemirrorState.TextSelection) || from < pos || to > pos + this.node.content.size) return null;
1271
- var sel = view.domSelectionRange();
1272
- var textNode = nearbyTextNode(sel.focusNode, sel.focusOffset);
1316
+ var textNode = view.input.compositionNode;
1273
1317
  if (!textNode || !this.dom.contains(textNode.parentNode)) return null;
1274
1318
  if (this.node.inlineContent) {
1275
1319
  var text = textNode.nodeValue;
@@ -1459,6 +1503,11 @@ var TextViewDesc = function (_NodeViewDesc) {
1459
1503
  get: function get() {
1460
1504
  return false;
1461
1505
  }
1506
+ }, {
1507
+ key: "isText",
1508
+ value: function isText(text) {
1509
+ return this.node.text == text;
1510
+ }
1462
1511
  }]);
1463
1512
  return TextViewDesc;
1464
1513
  }(NodeViewDesc);
@@ -1998,21 +2047,6 @@ function iosHacks(dom) {
1998
2047
  dom.style.cssText = oldCSS;
1999
2048
  }
2000
2049
  }
2001
- function nearbyTextNode(node, offset) {
2002
- for (;;) {
2003
- if (node.nodeType == 3) return node;
2004
- if (node.nodeType == 1 && offset > 0) {
2005
- if (node.childNodes.length > offset && node.childNodes[offset].nodeType == 3) return node.childNodes[offset];
2006
- node = node.childNodes[offset - 1];
2007
- offset = nodeSize(node);
2008
- } else if (node.nodeType == 1 && offset < node.childNodes.length) {
2009
- node = node.childNodes[offset];
2010
- offset = 0;
2011
- } else {
2012
- return null;
2013
- }
2014
- }
2015
- }
2016
2050
  function findTextInFragment(frag, text, from, to) {
2017
2051
  for (var i = 0, pos = 0; i < frag.childCount && pos <= to;) {
2018
2052
  var child = frag.child(i++),
@@ -2791,6 +2825,7 @@ var InputState = _createClass(function InputState() {
2791
2825
  this.lastTouch = 0;
2792
2826
  this.lastAndroidDelete = 0;
2793
2827
  this.composing = false;
2828
+ this.compositionNode = null;
2794
2829
  this.composingTimeout = -1;
2795
2830
  this.compositionNodes = [];
2796
2831
  this.compositionEndedAt = -2e8;
@@ -3178,6 +3213,7 @@ editHandlers.compositionend = function (view, event) {
3178
3213
  view.input.composing = false;
3179
3214
  view.input.compositionEndedAt = event.timeStamp;
3180
3215
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
3216
+ view.input.compositionNode = null;
3181
3217
  if (view.input.compositionPendingChanges) Promise.resolve().then(function () {
3182
3218
  return view.domObserver.flush();
3183
3219
  });
@@ -3198,6 +3234,22 @@ function clearComposition(view) {
3198
3234
  }
3199
3235
  while (view.input.compositionNodes.length > 0) view.input.compositionNodes.pop().markParentsDirty();
3200
3236
  }
3237
+ function findCompositionNode(view) {
3238
+ var sel = view.domSelectionRange();
3239
+ if (!sel.focusNode) return null;
3240
+ var textBefore = textNodeBefore$1(sel.focusNode, sel.focusOffset);
3241
+ var textAfter = textNodeAfter$1(sel.focusNode, sel.focusOffset);
3242
+ if (textBefore && textAfter && textBefore != textAfter) {
3243
+ var descAfter = textAfter.pmViewDesc;
3244
+ if (!descAfter || !descAfter.isText(textAfter.nodeValue)) {
3245
+ return textAfter;
3246
+ } else if (view.input.compositionNode == textAfter) {
3247
+ var descBefore = textBefore.pmViewDesc;
3248
+ if (!(!descBefore || !descBefore.isText(textBefore.nodeValue))) return textAfter;
3249
+ }
3250
+ }
3251
+ return textBefore;
3252
+ }
3201
3253
  function timestampFromCustomEvent() {
3202
3254
  var event = document.createEvent("Event");
3203
3255
  event.initEvent("event", true, true);
@@ -4474,13 +4526,6 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4474
4526
  return;
4475
4527
  }
4476
4528
  }
4477
- if (chrome && view.cursorWrapper && parse.sel && parse.sel.anchor == view.cursorWrapper.deco.from && parse.sel.head == parse.sel.anchor) {
4478
- var size = change.endB - change.start;
4479
- parse.sel = {
4480
- anchor: parse.sel.anchor + size,
4481
- head: parse.sel.anchor + size
4482
- };
4483
- }
4484
4529
  view.input.domChangeCount++;
4485
4530
  if (view.state.selection.from < view.state.selection.to && change.start == change.endB && view.state.selection instanceof prosemirrorState.TextSelection) {
4486
4531
  if (change.start > view.state.selection.from && change.start <= view.state.selection.from + 2 && view.state.selection.from >= parse.from) {
@@ -4508,7 +4553,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4508
4553
  view.input.lastIOSEnter = 0;
4509
4554
  return;
4510
4555
  }
4511
- if (view.state.selection.anchor > change.start && looksLikeJoin(doc, change.start, change.endA, $from, $to) && view.someProp("handleKeyDown", function (f) {
4556
+ if (view.state.selection.anchor > change.start && looksLikeBackspace(doc, change.start, change.endA, $from, $to) && view.someProp("handleKeyDown", function (f) {
4512
4557
  return f(view, keyEvent(8, "Backspace"));
4513
4558
  })) {
4514
4559
  if (android && chrome) view.domObserver.suppressSelectionUpdates();
@@ -4593,9 +4638,13 @@ function isMarkChange(cur, prev) {
4593
4638
  type: type
4594
4639
  };
4595
4640
  }
4596
- function looksLikeJoin(old, start, end, $newStart, $newEnd) {
4597
- if (!$newStart.parent.isTextblock || end - start <= $newEnd.pos - $newStart.pos || skipClosingAndOpening($newStart, true, false) < $newEnd.pos) return false;
4641
+ function looksLikeBackspace(old, start, end, $newStart, $newEnd) {
4642
+ if (end - start <= $newEnd.pos - $newStart.pos || skipClosingAndOpening($newStart, true, false) < $newEnd.pos) return false;
4598
4643
  var $start = old.resolve(start);
4644
+ if (!$newStart.parent.isTextblock) {
4645
+ var after = $start.nodeAfter;
4646
+ return after != null && end == start + after.nodeSize;
4647
+ }
4599
4648
  if ($start.parentOffset < $start.parent.content.size || !$start.parent.isTextblock) return false;
4600
4649
  var $next = old.resolve(skipClosingAndOpening($start, true, true));
4601
4650
  if (!$next.parent.isTextblock || $next.pos > end || skipClosingAndOpening($next, true, false) < end) return false;
@@ -4770,8 +4819,9 @@ var EditorView = function () {
4770
4819
  var forceSelUpdate = updateDoc && (ie || chrome) && !this.composing && !prev.selection.empty && !state.selection.empty && selectionContextChanged(prev.selection, state.selection);
4771
4820
  if (updateDoc) {
4772
4821
  var chromeKludge = chrome ? this.trackWrites = this.domSelectionRange().focusNode : null;
4822
+ if (this.composing) this.input.compositionNode = findCompositionNode(this);
4773
4823
  if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
4774
- this.docView.updateOuterDeco([]);
4824
+ this.docView.updateOuterDeco(outerDeco);
4775
4825
  this.docView.destroy();
4776
4826
  this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this);
4777
4827
  }
@@ -4978,6 +5028,7 @@ var EditorView = function () {
4978
5028
  }
4979
5029
  this.docView.destroy();
4980
5030
  this.docView = null;
5031
+ clearReusedRange();
4981
5032
  }
4982
5033
  }, {
4983
5034
  key: "isDestroyed",
package/dist/index.d.cts CHANGED
@@ -361,6 +361,7 @@ declare class ViewDesc {
361
361
  markParentsDirty(): void;
362
362
  get domAtom(): boolean;
363
363
  get ignoreForCoords(): boolean;
364
+ isText(text: string): boolean;
364
365
  }
365
366
  declare class NodeViewDesc extends ViewDesc {
366
367
  node: Node;
@@ -405,6 +406,7 @@ declare class TextViewDesc extends NodeViewDesc {
405
406
  slice(from: number, to: number, view: EditorView): TextViewDesc;
406
407
  markDirty(from: number, to: number): void;
407
408
  get domAtom(): boolean;
409
+ isText(text: string): boolean;
408
410
  }
409
411
 
410
412
  /**
package/dist/index.d.ts CHANGED
@@ -361,6 +361,7 @@ declare class ViewDesc {
361
361
  markParentsDirty(): void;
362
362
  get domAtom(): boolean;
363
363
  get ignoreForCoords(): boolean;
364
+ isText(text: string): boolean;
364
365
  }
365
366
  declare class NodeViewDesc extends ViewDesc {
366
367
  node: Node;
@@ -405,6 +406,7 @@ declare class TextViewDesc extends NodeViewDesc {
405
406
  slice(from: number, to: number, view: EditorView): TextViewDesc;
406
407
  markDirty(from: number, to: number): void;
407
408
  get domAtom(): boolean;
409
+ isText(text: string): boolean;
408
410
  }
409
411
 
410
412
  /**
package/dist/index.js CHANGED
@@ -23,6 +23,9 @@ const textRange = function (node, from, to) {
23
23
  range.setStart(node, from || 0);
24
24
  return range;
25
25
  };
26
+ const clearReusedRange = function () {
27
+ reusedRange = null;
28
+ };
26
29
  // Scans forward and backward through DOM positions equivalent to the
27
30
  // given one to see if the two are in the same place (i.e. after a
28
31
  // text node vs at the end of that text node)
@@ -57,6 +60,44 @@ function scanFor(node, off, targetNode, targetOff, dir) {
57
60
  function nodeSize(node) {
58
61
  return node.nodeType == 3 ? node.nodeValue.length : node.childNodes.length;
59
62
  }
63
+ function textNodeBefore$1(node, offset) {
64
+ for (;;) {
65
+ if (node.nodeType == 3 && offset)
66
+ return node;
67
+ if (node.nodeType == 1 && offset > 0) {
68
+ if (node.contentEditable == "false")
69
+ return null;
70
+ node = node.childNodes[offset - 1];
71
+ offset = nodeSize(node);
72
+ }
73
+ else if (node.parentNode && !hasBlockDesc(node)) {
74
+ offset = domIndex(node);
75
+ node = node.parentNode;
76
+ }
77
+ else {
78
+ return null;
79
+ }
80
+ }
81
+ }
82
+ function textNodeAfter$1(node, offset) {
83
+ for (;;) {
84
+ if (node.nodeType == 3 && offset < node.nodeValue.length)
85
+ return node;
86
+ if (node.nodeType == 1 && offset < node.childNodes.length) {
87
+ if (node.contentEditable == "false")
88
+ return null;
89
+ node = node.childNodes[offset];
90
+ offset = 0;
91
+ }
92
+ else if (node.parentNode && !hasBlockDesc(node)) {
93
+ offset = domIndex(node) + 1;
94
+ node = node.parentNode;
95
+ }
96
+ else {
97
+ return null;
98
+ }
99
+ }
100
+ }
60
101
  function isOnEdge(node, offset, parent) {
61
102
  for (let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;) {
62
103
  if (node == parent)
@@ -133,6 +174,12 @@ const webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style;
133
174
  const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0;
134
175
 
135
176
  function windowRect(doc) {
177
+ let vp = doc.defaultView && doc.defaultView.visualViewport;
178
+ if (vp)
179
+ return {
180
+ left: 0, right: vp.width,
181
+ top: 0, bottom: vp.height
182
+ };
136
183
  return { left: 0, right: doc.documentElement.clientWidth,
137
184
  top: 0, bottom: doc.documentElement.clientHeight };
138
185
  }
@@ -1068,6 +1115,7 @@ class ViewDesc {
1068
1115
  }
1069
1116
  get domAtom() { return false; }
1070
1117
  get ignoreForCoords() { return false; }
1118
+ isText(text) { return false; }
1071
1119
  }
1072
1120
  // A widget desc represents a widget decoration, which is a DOM node
1073
1121
  // drawn between the document nodes.
@@ -1330,8 +1378,7 @@ class NodeViewDesc extends ViewDesc {
1330
1378
  let { from, to } = view.state.selection;
1331
1379
  if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size)
1332
1380
  return null;
1333
- let sel = view.domSelectionRange();
1334
- let textNode = nearbyTextNode(sel.focusNode, sel.focusOffset);
1381
+ let textNode = view.input.compositionNode;
1335
1382
  if (!textNode || !this.dom.contains(textNode.parentNode))
1336
1383
  return null;
1337
1384
  if (this.node.inlineContent) {
@@ -1473,6 +1520,7 @@ class TextViewDesc extends NodeViewDesc {
1473
1520
  this.dirty = NODE_DIRTY;
1474
1521
  }
1475
1522
  get domAtom() { return false; }
1523
+ isText(text) { return this.node.text == text; }
1476
1524
  }
1477
1525
  // A dummy desc used to tag trailing BR or IMG nodes created to work
1478
1526
  // around contentEditable terribleness.
@@ -2042,25 +2090,6 @@ function iosHacks(dom) {
2042
2090
  dom.style.cssText = oldCSS;
2043
2091
  }
2044
2092
  }
2045
- function nearbyTextNode(node, offset) {
2046
- for (;;) {
2047
- if (node.nodeType == 3)
2048
- return node;
2049
- if (node.nodeType == 1 && offset > 0) {
2050
- if (node.childNodes.length > offset && node.childNodes[offset].nodeType == 3)
2051
- return node.childNodes[offset];
2052
- node = node.childNodes[offset - 1];
2053
- offset = nodeSize(node);
2054
- }
2055
- else if (node.nodeType == 1 && offset < node.childNodes.length) {
2056
- node = node.childNodes[offset];
2057
- offset = 0;
2058
- }
2059
- else {
2060
- return null;
2061
- }
2062
- }
2063
- }
2064
2093
  // Find a piece of text in an inline fragment, overlapping from-to
2065
2094
  function findTextInFragment(frag, text, from, to) {
2066
2095
  for (let i = 0, pos = 0; i < frag.childCount && pos <= to;) {
@@ -2965,6 +2994,7 @@ class InputState {
2965
2994
  this.lastTouch = 0;
2966
2995
  this.lastAndroidDelete = 0;
2967
2996
  this.composing = false;
2997
+ this.compositionNode = null;
2968
2998
  this.composingTimeout = -1;
2969
2999
  this.compositionNodes = [];
2970
3000
  this.compositionEndedAt = -2e8;
@@ -3401,6 +3431,7 @@ editHandlers.compositionend = (view, event) => {
3401
3431
  view.input.composing = false;
3402
3432
  view.input.compositionEndedAt = event.timeStamp;
3403
3433
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
3434
+ view.input.compositionNode = null;
3404
3435
  if (view.input.compositionPendingChanges)
3405
3436
  Promise.resolve().then(() => view.domObserver.flush());
3406
3437
  view.input.compositionID++;
@@ -3420,6 +3451,25 @@ function clearComposition(view) {
3420
3451
  while (view.input.compositionNodes.length > 0)
3421
3452
  view.input.compositionNodes.pop().markParentsDirty();
3422
3453
  }
3454
+ function findCompositionNode(view) {
3455
+ let sel = view.domSelectionRange();
3456
+ if (!sel.focusNode)
3457
+ return null;
3458
+ let textBefore = textNodeBefore$1(sel.focusNode, sel.focusOffset);
3459
+ let textAfter = textNodeAfter$1(sel.focusNode, sel.focusOffset);
3460
+ if (textBefore && textAfter && textBefore != textAfter) {
3461
+ let descAfter = textAfter.pmViewDesc;
3462
+ if (!descAfter || !descAfter.isText(textAfter.nodeValue)) {
3463
+ return textAfter;
3464
+ }
3465
+ else if (view.input.compositionNode == textAfter) {
3466
+ let descBefore = textBefore.pmViewDesc;
3467
+ if (!(!descBefore || !descBefore.isText(textBefore.nodeValue)))
3468
+ return textAfter;
3469
+ }
3470
+ }
3471
+ return textBefore;
3472
+ }
3423
3473
  function timestampFromCustomEvent() {
3424
3474
  let event = document.createEvent("Event");
3425
3475
  event.initEvent("event", true, true);
@@ -4822,13 +4872,6 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4822
4872
  return;
4823
4873
  }
4824
4874
  }
4825
- // Chrome sometimes leaves the cursor before the inserted text when
4826
- // composing after a cursor wrapper. This moves it forward.
4827
- if (chrome && view.cursorWrapper && parse.sel && parse.sel.anchor == view.cursorWrapper.deco.from &&
4828
- parse.sel.head == parse.sel.anchor) {
4829
- let size = change.endB - change.start;
4830
- parse.sel = { anchor: parse.sel.anchor + size, head: parse.sel.anchor + size };
4831
- }
4832
4875
  view.input.domChangeCount++;
4833
4876
  // Handle the case where overwriting a selection by typing matches
4834
4877
  // the start or end of the selected content, creating a change
@@ -4874,7 +4917,7 @@ function readDOMChange(view, from, to, typeOver, addedNodes) {
4874
4917
  }
4875
4918
  // Same for backspace
4876
4919
  if (view.state.selection.anchor > change.start &&
4877
- looksLikeJoin(doc, change.start, change.endA, $from, $to) &&
4920
+ looksLikeBackspace(doc, change.start, change.endA, $from, $to) &&
4878
4921
  view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) {
4879
4922
  if (android && chrome)
4880
4923
  view.domObserver.suppressSelectionUpdates(); // #820
@@ -4986,14 +5029,18 @@ function isMarkChange(cur, prev) {
4986
5029
  if (Fragment.from(updated).eq(cur))
4987
5030
  return { mark, type };
4988
5031
  }
4989
- function looksLikeJoin(old, start, end, $newStart, $newEnd) {
4990
- if (!$newStart.parent.isTextblock ||
4991
- // The content must have shrunk
4992
- end - start <= $newEnd.pos - $newStart.pos ||
5032
+ function looksLikeBackspace(old, start, end, $newStart, $newEnd) {
5033
+ if ( // The content must have shrunk
5034
+ end - start <= $newEnd.pos - $newStart.pos ||
4993
5035
  // newEnd must point directly at or after the end of the block that newStart points into
4994
5036
  skipClosingAndOpening($newStart, true, false) < $newEnd.pos)
4995
5037
  return false;
4996
5038
  let $start = old.resolve(start);
5039
+ // Handle the case where, rather than joining blocks, the change just removed an entire block
5040
+ if (!$newStart.parent.isTextblock) {
5041
+ let after = $start.nodeAfter;
5042
+ return after != null && end == start + after.nodeSize;
5043
+ }
4997
5044
  // Start must be at the end of a block
4998
5045
  if ($start.parentOffset < $start.parent.content.size || !$start.parent.isTextblock)
4999
5046
  return false;
@@ -5244,8 +5291,10 @@ class EditorView {
5244
5291
  // tracks that and forces a selection reset when our update
5245
5292
  // did write to the node.
5246
5293
  let chromeKludge = chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null;
5294
+ if (this.composing)
5295
+ this.input.compositionNode = findCompositionNode(this);
5247
5296
  if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
5248
- this.docView.updateOuterDeco([]);
5297
+ this.docView.updateOuterDeco(outerDeco);
5249
5298
  this.docView.destroy();
5250
5299
  this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this);
5251
5300
  }
@@ -5522,6 +5571,7 @@ class EditorView {
5522
5571
  }
5523
5572
  this.docView.destroy();
5524
5573
  this.docView = null;
5574
+ clearReusedRange();
5525
5575
  }
5526
5576
  /**
5527
5577
  This is true when the view has been
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.33.1",
3
+ "version": "1.33.3",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/dom.ts CHANGED
@@ -29,6 +29,10 @@ export const textRange = function(node: Text, from?: number, to?: number) {
29
29
  return range
30
30
  }
31
31
 
32
+ export const clearReusedRange = function() {
33
+ reusedRange = null;
34
+ }
35
+
32
36
  // Scans forward and backward through DOM positions equivalent to the
33
37
  // given one to see if the two are in the same place (i.e. after a
34
38
  // text node vs at the end of that text node)
@@ -63,6 +67,38 @@ export function nodeSize(node: Node) {
63
67
  return node.nodeType == 3 ? node.nodeValue!.length : node.childNodes.length
64
68
  }
65
69
 
70
+ export function textNodeBefore(node: Node, offset: number) {
71
+ for (;;) {
72
+ if (node.nodeType == 3 && offset) return node as Text
73
+ if (node.nodeType == 1 && offset > 0) {
74
+ if ((node as HTMLElement).contentEditable == "false") return null
75
+ node = node.childNodes[offset - 1]
76
+ offset = nodeSize(node)
77
+ } else if (node.parentNode && !hasBlockDesc(node)) {
78
+ offset = domIndex(node)
79
+ node = node.parentNode
80
+ } else {
81
+ return null
82
+ }
83
+ }
84
+ }
85
+
86
+ export function textNodeAfter(node: Node, offset: number) {
87
+ for (;;) {
88
+ if (node.nodeType == 3 && offset < node.nodeValue!.length) return node as Text
89
+ if (node.nodeType == 1 && offset < node.childNodes.length) {
90
+ if ((node as HTMLElement).contentEditable == "false") return null
91
+ node = node.childNodes[offset]
92
+ offset = 0
93
+ } else if (node.parentNode && !hasBlockDesc(node)) {
94
+ offset = domIndex(node) + 1
95
+ node = node.parentNode
96
+ } else {
97
+ return null
98
+ }
99
+ }
100
+ }
101
+
66
102
  export function isOnEdge(node: Node, offset: number, parent: Node) {
67
103
  for (let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;) {
68
104
  if (node == parent) return true
package/src/domchange.ts CHANGED
@@ -143,13 +143,6 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
143
143
  return
144
144
  }
145
145
  }
146
- // Chrome sometimes leaves the cursor before the inserted text when
147
- // composing after a cursor wrapper. This moves it forward.
148
- if (browser.chrome && view.cursorWrapper && parse.sel && parse.sel.anchor == view.cursorWrapper.deco.from &&
149
- parse.sel.head == parse.sel.anchor) {
150
- let size = change.endB - change.start
151
- parse.sel = {anchor: parse.sel.anchor + size, head: parse.sel.anchor + size}
152
- }
153
146
 
154
147
  view.input.domChangeCount++
155
148
  // Handle the case where overwriting a selection by typing matches
@@ -197,7 +190,7 @@ export function readDOMChange(view: EditorView, from: number, to: number, typeOv
197
190
  }
198
191
  // Same for backspace
199
192
  if (view.state.selection.anchor > change.start &&
200
- looksLikeJoin(doc, change.start, change.endA, $from, $to) &&
193
+ looksLikeBackspace(doc, change.start, change.endA, $from, $to) &&
201
194
  view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) {
202
195
  if (browser.android && browser.chrome) view.domObserver.suppressSelectionUpdates() // #820
203
196
  return
@@ -304,15 +297,21 @@ function isMarkChange(cur: Fragment, prev: Fragment) {
304
297
  if (Fragment.from(updated).eq(cur)) return {mark, type}
305
298
  }
306
299
 
307
- function looksLikeJoin(old: Node, start: number, end: number, $newStart: ResolvedPos, $newEnd: ResolvedPos) {
308
- if (!$newStart.parent.isTextblock ||
309
- // The content must have shrunk
300
+ function looksLikeBackspace(old: Node, start: number, end: number, $newStart: ResolvedPos, $newEnd: ResolvedPos) {
301
+ if (// The content must have shrunk
310
302
  end - start <= $newEnd.pos - $newStart.pos ||
311
303
  // newEnd must point directly at or after the end of the block that newStart points into
312
304
  skipClosingAndOpening($newStart, true, false) < $newEnd.pos)
313
305
  return false
314
306
 
315
307
  let $start = old.resolve(start)
308
+
309
+ // Handle the case where, rather than joining blocks, the change just removed an entire block
310
+ if (!$newStart.parent.isTextblock) {
311
+ let after = $start.nodeAfter
312
+ return after != null && end == start + after.nodeSize
313
+ }
314
+
316
315
  // Start must be at the end of a block
317
316
  if ($start.parentOffset < $start.parent.content.size || !$start.parent.isTextblock)
318
317
  return false
package/src/domcoords.ts CHANGED
@@ -6,6 +6,11 @@ import {EditorView} from "./index"
6
6
  export type Rect = {left: number, right: number, top: number, bottom: number}
7
7
 
8
8
  function windowRect(doc: Document): Rect {
9
+ let vp = doc.defaultView && doc.defaultView.visualViewport
10
+ if (vp) return {
11
+ left: 0, right: vp.width,
12
+ top: 0, bottom: vp.height
13
+ }
9
14
  return {left: 0, right: doc.documentElement.clientWidth,
10
15
  top: 0, bottom: doc.documentElement.clientHeight}
11
16
  }
package/src/index.ts CHANGED
@@ -5,12 +5,12 @@ import {scrollRectIntoView, posAtCoords, coordsAtPos, endOfTextblock, storeScrol
5
5
  resetScrollPos, focusPreventScroll} from "./domcoords"
6
6
  import {docViewDesc, ViewDesc, NodeView, NodeViewDesc} from "./viewdesc"
7
7
  import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition,
8
- InputState, doPaste, Dragging} from "./input"
8
+ InputState, doPaste, Dragging, findCompositionNode} from "./input"
9
9
  import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection"
10
10
  import {Decoration, viewDecorations, DecorationSource} from "./decoration"
11
11
  import {DOMObserver, safariShadowSelectionRange} from "./domobserver"
12
12
  import {readDOMChange} from "./domchange"
13
- import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement} from "./dom"
13
+ import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement, clearReusedRange} from "./dom"
14
14
  import * as browser from "./browser"
15
15
 
16
16
  export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration"
@@ -197,8 +197,9 @@ export class EditorView {
197
197
  // tracks that and forces a selection reset when our update
198
198
  // did write to the node.
199
199
  let chromeKludge = browser.chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null
200
+ if (this.composing) this.input.compositionNode = findCompositionNode(this)
200
201
  if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) {
201
- this.docView.updateOuterDeco([])
202
+ this.docView.updateOuterDeco(outerDeco)
202
203
  this.docView.destroy()
203
204
  this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this)
204
205
  }
@@ -457,6 +458,7 @@ export class EditorView {
457
458
  }
458
459
  this.docView.destroy()
459
460
  ;(this as any).docView = null
461
+ clearReusedRange();
460
462
  }
461
463
 
462
464
  /// This is true when the view has been
package/src/input.ts CHANGED
@@ -6,7 +6,7 @@ import * as browser from "./browser"
6
6
  import {captureKeyDown} from "./capturekeys"
7
7
  import {parseFromClipboard, serializeForClipboard} from "./clipboard"
8
8
  import {selectionBetween, selectionToDOM, selectionFromDOM} from "./selection"
9
- import {keyEvent, DOMNode} from "./dom"
9
+ import {keyEvent, DOMNode, textNodeBefore, textNodeAfter} from "./dom"
10
10
  import {EditorView} from "./index"
11
11
  import {ViewDesc} from "./viewdesc"
12
12
 
@@ -30,6 +30,7 @@ export class InputState {
30
30
  lastTouch = 0
31
31
  lastAndroidDelete = 0
32
32
  composing = false
33
+ compositionNode: Text | null = null
33
34
  composingTimeout = -1
34
35
  compositionNodes: ViewDesc[] = []
35
36
  compositionEndedAt = -2e8
@@ -490,6 +491,7 @@ editHandlers.compositionend = (view, event) => {
490
491
  view.input.composing = false
491
492
  view.input.compositionEndedAt = event.timeStamp
492
493
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0
494
+ view.input.compositionNode = null
493
495
  if (view.input.compositionPendingChanges) Promise.resolve().then(() => view.domObserver.flush())
494
496
  view.input.compositionID++
495
497
  scheduleComposeEnd(view, 20)
@@ -509,6 +511,24 @@ export function clearComposition(view: EditorView) {
509
511
  while (view.input.compositionNodes.length > 0) view.input.compositionNodes.pop()!.markParentsDirty()
510
512
  }
511
513
 
514
+ export function findCompositionNode(view: EditorView) {
515
+ let sel = view.domSelectionRange()
516
+ if (!sel.focusNode) return null
517
+ let textBefore = textNodeBefore(sel.focusNode, sel.focusOffset)
518
+ let textAfter = textNodeAfter(sel.focusNode, sel.focusOffset)
519
+ if (textBefore && textAfter && textBefore != textAfter) {
520
+ let descAfter = textAfter.pmViewDesc
521
+ if (!descAfter || !descAfter.isText(textAfter.nodeValue!)) {
522
+ return textAfter
523
+ } else if (view.input.compositionNode == textAfter) {
524
+ let descBefore = textBefore.pmViewDesc
525
+ if (!(!descBefore || !descBefore.isText(textBefore.nodeValue!)))
526
+ return textAfter
527
+ }
528
+ }
529
+ return textBefore
530
+ }
531
+
512
532
  function timestampFromCustomEvent() {
513
533
  let event = document.createEvent("Event")
514
534
  event.initEvent("event", true, true)
package/src/viewdesc.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {DOMSerializer, Fragment, Mark, Node, ParseRule} from "prosemirror-model"
2
2
  import {TextSelection} from "prosemirror-state"
3
3
 
4
- import {domIndex, isEquivalentPosition, nodeSize, DOMNode} from "./dom"
4
+ import {domIndex, isEquivalentPosition, DOMNode} from "./dom"
5
5
  import * as browser from "./browser"
6
6
  import {Decoration, DecorationSource, WidgetConstructor, WidgetType, NodeType} from "./decoration"
7
7
  import {EditorView} from "./index"
@@ -485,6 +485,8 @@ export class ViewDesc {
485
485
  get domAtom() { return false }
486
486
 
487
487
  get ignoreForCoords() { return false }
488
+
489
+ isText(text: string) { return false }
488
490
  }
489
491
 
490
492
  // A widget desc represents a widget decoration, which is a DOM node
@@ -759,8 +761,7 @@ export class NodeViewDesc extends ViewDesc {
759
761
  // are inside of this node
760
762
  let {from, to} = view.state.selection
761
763
  if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size) return null
762
- let sel = view.domSelectionRange()
763
- let textNode = nearbyTextNode(sel.focusNode!, sel.focusOffset)
764
+ let textNode = view.input.compositionNode
764
765
  if (!textNode || !this.dom.contains(textNode.parentNode)) return null
765
766
 
766
767
  if (this.node.inlineContent) {
@@ -906,6 +907,8 @@ class TextViewDesc extends NodeViewDesc {
906
907
  }
907
908
 
908
909
  get domAtom() { return false }
910
+
911
+ isText(text: string) { return this.node.text == text }
909
912
  }
910
913
 
911
914
  // A dummy desc used to tag trailing BR or IMG nodes created to work
@@ -1470,23 +1473,6 @@ function iosHacks(dom: HTMLElement) {
1470
1473
  }
1471
1474
  }
1472
1475
 
1473
- function nearbyTextNode(node: DOMNode, offset: number): Text | null {
1474
- for (;;) {
1475
- if (node.nodeType == 3) return node as Text
1476
- if (node.nodeType == 1 && offset > 0) {
1477
- if (node.childNodes.length > offset && node.childNodes[offset].nodeType == 3)
1478
- return node.childNodes[offset] as Text
1479
- node = node.childNodes[offset - 1]
1480
- offset = nodeSize(node)
1481
- } else if (node.nodeType == 1 && offset < node.childNodes.length) {
1482
- node = node.childNodes[offset]
1483
- offset = 0
1484
- } else {
1485
- return null
1486
- }
1487
- }
1488
- }
1489
-
1490
1476
  // Find a piece of text in an inline fragment, overlapping from-to
1491
1477
  function findTextInFragment(frag: Fragment, text: string, from: number, to: number) {
1492
1478
  for (let i = 0, pos = 0; i < frag.childCount && pos <= to;) {