prosemirror-view 1.41.4 → 1.41.6

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,19 @@
1
+ ## 1.41.6 (2026-02-06)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix an issue where backspacing content before a flexbox or grid styled widget would insert a bogus line break on Firefox.
6
+
7
+ ## 1.41.5 (2026-01-14)
8
+
9
+ ### Bug fixes
10
+
11
+ Work around an issue where, after some kinds of changes, Chrome misreports the cursor position and breaks composition.
12
+
13
+ Improve DOM update behavior when deleting three or more nodes right before a mark, which would unnecessarily re-render the entire mark.
14
+
15
+ Work around a newly introduced bug in Safari where composition in an empty table cell causes it to move the text out of the cell into the row element.
16
+
1
17
  ## 1.41.4 (2025-12-01)
2
18
 
3
19
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -1338,17 +1338,17 @@ var NodeViewDesc = function (_ViewDesc4) {
1338
1338
  var compositionInChild = composition && composition.pos < 0;
1339
1339
  var updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view);
1340
1340
  iterDeco(this.node, this.innerDeco, function (widget, i, insideNode) {
1341
- if (widget.spec.marks) updater.syncToMarks(widget.spec.marks, inline, view);else if (widget.type.side >= 0 && !insideNode) updater.syncToMarks(i == _this6.node.childCount ? prosemirrorModel.Mark.none : _this6.node.child(i).marks, inline, view);
1341
+ if (widget.spec.marks) updater.syncToMarks(widget.spec.marks, inline, view, i);else if (widget.type.side >= 0 && !insideNode) updater.syncToMarks(i == _this6.node.childCount ? prosemirrorModel.Mark.none : _this6.node.child(i).marks, inline, view, i);
1342
1342
  updater.placeWidget(widget, view, off);
1343
1343
  }, function (child, outerDeco, innerDeco, i) {
1344
- updater.syncToMarks(child.marks, inline, view);
1344
+ updater.syncToMarks(child.marks, inline, view, i);
1345
1345
  var compIndex;
1346
1346
  if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) ;else if (compositionInChild && view.state.selection.from > off && view.state.selection.to < off + child.nodeSize && (compIndex = updater.findIndexWithChild(composition.node)) > -1 && updater.updateNodeAt(child, outerDeco, innerDeco, compIndex, view)) ;else if (updater.updateNextNode(child, outerDeco, innerDeco, view, i, off)) ;else {
1347
1347
  updater.addNode(child, outerDeco, innerDeco, view, off);
1348
1348
  }
1349
1349
  off += child.nodeSize;
1350
1350
  });
1351
- updater.syncToMarks([], inline, view);
1351
+ updater.syncToMarks([], inline, view, 0);
1352
1352
  if (this.node.isTextblock) updater.addTextblockHacks();
1353
1353
  updater.destroyRest();
1354
1354
  if (updater.changed || this.dirty == CONTENT_DIRTY) {
@@ -1786,7 +1786,7 @@ var ViewTreeUpdater = function () {
1786
1786
  }
1787
1787
  }, {
1788
1788
  key: "syncToMarks",
1789
- value: function syncToMarks(marks, inline, view) {
1789
+ value: function syncToMarks(marks, inline, view, parentIndex) {
1790
1790
  var keep = 0,
1791
1791
  depth = this.stack.length >> 1;
1792
1792
  var maxKeep = Math.min(depth, marks.length);
@@ -1800,8 +1800,10 @@ var ViewTreeUpdater = function () {
1800
1800
  }
1801
1801
  while (depth < marks.length) {
1802
1802
  this.stack.push(this.top, this.index + 1);
1803
- var found = -1;
1804
- for (var i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
1803
+ var found = -1,
1804
+ scanTo = this.top.children.length;
1805
+ if (parentIndex < this.preMatch.index) scanTo = Math.min(this.index + 3, scanTo);
1806
+ for (var i = this.index; i < scanTo; i++) {
1805
1807
  var next = this.top.children[i];
1806
1808
  if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) {
1807
1809
  found = i;
@@ -2919,6 +2921,7 @@ var InputState = _createClass(function InputState() {
2919
2921
  this.compositionNodes = [];
2920
2922
  this.compositionEndedAt = -2e8;
2921
2923
  this.compositionID = 1;
2924
+ this.badSafariComposition = false;
2922
2925
  this.compositionPendingChanges = 0;
2923
2926
  this.domChangeCount = 0;
2924
2927
  this.eventHandlers = Object.create(null);
@@ -3317,7 +3320,7 @@ editHandlers.compositionend = function (view, event) {
3317
3320
  view.input.compositionEndedAt = event.timeStamp;
3318
3321
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
3319
3322
  view.input.compositionNode = null;
3320
- if (view.input.compositionPendingChanges) Promise.resolve().then(function () {
3323
+ if (view.input.badSafariComposition) view.domObserver.forceFlush();else if (view.input.compositionPendingChanges) Promise.resolve().then(function () {
3321
3324
  return view.domObserver.flush();
3322
3325
  });
3323
3326
  view.input.compositionID++;
@@ -4235,7 +4238,16 @@ var DOMObserver = function () {
4235
4238
  for (var i = 0; i < mutations.length; i++) _this11.queue.push(mutations[i]);
4236
4239
  if (ie && ie_version <= 11 && mutations.some(function (m) {
4237
4240
  return m.type == "childList" && m.removedNodes.length || m.type == "characterData" && m.oldValue.length > m.target.nodeValue.length;
4238
- })) _this11.flushSoon();else _this11.flush();
4241
+ })) {
4242
+ _this11.flushSoon();
4243
+ } else if (safari && view.composing && mutations.some(function (m) {
4244
+ return m.type == "childList" && m.target.nodeName == "TR";
4245
+ })) {
4246
+ view.input.badSafariComposition = true;
4247
+ _this11.flushSoon();
4248
+ } else {
4249
+ _this11.flush();
4250
+ }
4239
4251
  });
4240
4252
  if (useCharData) {
4241
4253
  this.onCharData = function (e) {
@@ -4391,7 +4403,25 @@ var DOMObserver = function () {
4391
4403
  }
4392
4404
  }
4393
4405
  }
4394
- if (gecko && added.length) {
4406
+ if (added.some(function (n) {
4407
+ return n.nodeName == "BR";
4408
+ }) && (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
4409
+ var _iterator3 = _createForOfIteratorHelper(added),
4410
+ _step3;
4411
+ try {
4412
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
4413
+ var node = _step3.value;
4414
+ if (node.nodeName == "BR" && node.parentNode) {
4415
+ var after = node.nextSibling;
4416
+ if (after && after.nodeType == 1 && after.contentEditable == "false") node.parentNode.removeChild(node);
4417
+ }
4418
+ }
4419
+ } catch (err) {
4420
+ _iterator3.e(err);
4421
+ } finally {
4422
+ _iterator3.f();
4423
+ }
4424
+ } else if (gecko && added.length) {
4395
4425
  var brs = added.filter(function (n) {
4396
4426
  return n.nodeName == "BR";
4397
4427
  });
@@ -4402,38 +4432,20 @@ var DOMObserver = function () {
4402
4432
  if (a.parentNode && a.parentNode.parentNode == b.parentNode) b.remove();else a.remove();
4403
4433
  } else {
4404
4434
  var focusNode = this.currentSelection.focusNode;
4405
- var _iterator3 = _createForOfIteratorHelper(brs),
4406
- _step3;
4435
+ var _iterator4 = _createForOfIteratorHelper(brs),
4436
+ _step4;
4407
4437
  try {
4408
- for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
4409
- var br = _step3.value;
4438
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
4439
+ var br = _step4.value;
4410
4440
  var parent = br.parentNode;
4411
4441
  if (parent && parent.nodeName == "LI" && (!focusNode || blockParent(view, focusNode) != parent)) br.remove();
4412
4442
  }
4413
4443
  } catch (err) {
4414
- _iterator3.e(err);
4444
+ _iterator4.e(err);
4415
4445
  } finally {
4416
- _iterator3.f();
4446
+ _iterator4.f();
4417
4447
  }
4418
4448
  }
4419
- } else if ((chrome || safari) && added.some(function (n) {
4420
- return n.nodeName == "BR";
4421
- }) && (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
4422
- var _iterator4 = _createForOfIteratorHelper(added),
4423
- _step4;
4424
- try {
4425
- for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
4426
- var node = _step4.value;
4427
- if (node.nodeName == "BR" && node.parentNode) {
4428
- var after = node.nextSibling;
4429
- if (after && after.nodeType == 1 && after.contentEditable == "false") node.parentNode.removeChild(node);
4430
- }
4431
- }
4432
- } catch (err) {
4433
- _iterator4.e(err);
4434
- } finally {
4435
- _iterator4.f();
4436
- }
4437
4449
  }
4438
4450
  var readSel = null;
4439
4451
  if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 && Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 && selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) && readSel.eq(prosemirrorState.Selection.near(view.state.doc.resolve(0), 1))) {
@@ -4446,6 +4458,10 @@ var DOMObserver = function () {
4446
4458
  view.docView.markDirty(from, to);
4447
4459
  checkCSS(view);
4448
4460
  }
4461
+ if (view.input.badSafariComposition) {
4462
+ view.input.badSafariComposition = false;
4463
+ fixUpBadSafariComposition(view, added);
4464
+ }
4449
4465
  this.handleDOMChange(from, to, typeOver, added);
4450
4466
  if (view.docView && view.docView.dirty) view.updateState(view.state);else if (!this.currentSelection.eq(sel)) selectionToDOM(view);
4451
4467
  this.currentSelection.set(sel);
@@ -4559,6 +4575,39 @@ function blockParent(view, node) {
4559
4575
  }
4560
4576
  return null;
4561
4577
  }
4578
+ function fixUpBadSafariComposition(view, addedNodes) {
4579
+ var _a;
4580
+ var _view$domSelectionRan5 = view.domSelectionRange(),
4581
+ focusNode = _view$domSelectionRan5.focusNode,
4582
+ focusOffset = _view$domSelectionRan5.focusOffset;
4583
+ var _iterator5 = _createForOfIteratorHelper(addedNodes),
4584
+ _step5;
4585
+ try {
4586
+ for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
4587
+ var node = _step5.value;
4588
+ if (((_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.nodeName) == "TR") {
4589
+ var nextCell = node.nextSibling;
4590
+ while (nextCell && nextCell.nodeName != "TD" && nextCell.nodeName != "TH") nextCell = nextCell.nextSibling;
4591
+ if (nextCell) {
4592
+ var parent = nextCell;
4593
+ for (;;) {
4594
+ var first = parent.firstChild;
4595
+ if (!first || first.nodeType != 1 || first.contentEditable == "false" || /^(BR|IMG)$/.test(first.nodeName)) break;
4596
+ parent = first;
4597
+ }
4598
+ parent.insertBefore(node, parent.firstChild);
4599
+ if (focusNode == node) view.domSelection().collapse(node, focusOffset);
4600
+ } else {
4601
+ node.parentNode.removeChild(node);
4602
+ }
4603
+ }
4604
+ }
4605
+ } catch (err) {
4606
+ _iterator5.e(err);
4607
+ } finally {
4608
+ _iterator5.f();
4609
+ }
4610
+ }
4562
4611
  function parseBetween(view, from_, to_) {
4563
4612
  var _view$docView$parseRa = view.docView.parseRange(from_, to_),
4564
4613
  parent = _view$docView$parseRa.node,
@@ -5014,7 +5063,7 @@ var EditorView = function () {
5014
5063
  this.docView.destroy();
5015
5064
  this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this);
5016
5065
  }
5017
- if (chromeKludge && !this.trackWrites) forceSelUpdate = true;
5066
+ if (chromeKludge && (!this.trackWrites || !this.dom.contains(this.trackWrites))) forceSelUpdate = true;
5018
5067
  }
5019
5068
  if (forceSelUpdate || !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) && anchorInRightPlace(this))) {
5020
5069
  selectionToDOM(this, forceSelUpdate);
package/dist/index.js CHANGED
@@ -1376,15 +1376,15 @@ class NodeViewDesc extends ViewDesc {
1376
1376
  let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view);
1377
1377
  iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => {
1378
1378
  if (widget.spec.marks)
1379
- updater.syncToMarks(widget.spec.marks, inline, view);
1379
+ updater.syncToMarks(widget.spec.marks, inline, view, i);
1380
1380
  else if (widget.type.side >= 0 && !insideNode)
1381
- updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view);
1381
+ updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view, i);
1382
1382
  // If the next node is a desc matching this widget, reuse it,
1383
1383
  // otherwise insert the widget as a new view desc.
1384
1384
  updater.placeWidget(widget, view, off);
1385
1385
  }, (child, outerDeco, innerDeco, i) => {
1386
1386
  // Make sure the wrapping mark descs match the node's marks.
1387
- updater.syncToMarks(child.marks, inline, view);
1387
+ updater.syncToMarks(child.marks, inline, view, i);
1388
1388
  // Try several strategies for drawing this node
1389
1389
  let compIndex;
1390
1390
  if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) ;
@@ -1400,7 +1400,7 @@ class NodeViewDesc extends ViewDesc {
1400
1400
  off += child.nodeSize;
1401
1401
  });
1402
1402
  // Drop all remaining descs after the current position.
1403
- updater.syncToMarks([], inline, view);
1403
+ updater.syncToMarks([], inline, view, 0);
1404
1404
  if (this.node.isTextblock)
1405
1405
  updater.addTextblockHacks();
1406
1406
  updater.destroyRest();
@@ -1790,7 +1790,7 @@ class ViewTreeUpdater {
1790
1790
  }
1791
1791
  // Sync the current stack of mark descs with the given array of
1792
1792
  // marks, reusing existing mark descs when possible.
1793
- syncToMarks(marks, inline, view) {
1793
+ syncToMarks(marks, inline, view, parentIndex) {
1794
1794
  let keep = 0, depth = this.stack.length >> 1;
1795
1795
  let maxKeep = Math.min(depth, marks.length);
1796
1796
  while (keep < maxKeep &&
@@ -1806,8 +1806,10 @@ class ViewTreeUpdater {
1806
1806
  }
1807
1807
  while (depth < marks.length) {
1808
1808
  this.stack.push(this.top, this.index + 1);
1809
- let found = -1;
1810
- for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
1809
+ let found = -1, scanTo = this.top.children.length;
1810
+ if (parentIndex < this.preMatch.index)
1811
+ scanTo = Math.min(this.index + 3, scanTo);
1812
+ for (let i = this.index; i < scanTo; i++) {
1811
1813
  let next = this.top.children[i];
1812
1814
  if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) {
1813
1815
  found = i;
@@ -2003,9 +2005,7 @@ class ViewTreeUpdater {
2003
2005
  }
2004
2006
  // Iterate from the end of the fragment and array of descs to find
2005
2007
  // directly matching ones, in order to avoid overeagerly reusing those
2006
- // for other nodes. Returns the fragment index of the first node that
2007
- // is part of the sequence of matched nodes at the end of the
2008
- // fragment.
2008
+ // for other nodes.
2009
2009
  function preMatch(frag, parentDesc) {
2010
2010
  let curDesc = parentDesc, descI = curDesc.children.length;
2011
2011
  let fI = frag.childCount, matched = new Map, matches = [];
@@ -2028,7 +2028,6 @@ function preMatch(frag, parentDesc) {
2028
2028
  break outer;
2029
2029
  }
2030
2030
  else {
2031
- // FIXME
2032
2031
  descI = curDesc.parent.children.indexOf(curDesc);
2033
2032
  curDesc = curDesc.parent;
2034
2033
  }
@@ -3075,6 +3074,7 @@ class InputState {
3075
3074
  this.compositionNodes = [];
3076
3075
  this.compositionEndedAt = -2e8;
3077
3076
  this.compositionID = 1;
3077
+ this.badSafariComposition = false;
3078
3078
  // Set to a composition ID when there are pending changes at compositionend
3079
3079
  this.compositionPendingChanges = 0;
3080
3080
  this.domChangeCount = 0;
@@ -3522,7 +3522,9 @@ editHandlers.compositionend = (view, event) => {
3522
3522
  view.input.compositionEndedAt = event.timeStamp;
3523
3523
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
3524
3524
  view.input.compositionNode = null;
3525
- if (view.input.compositionPendingChanges)
3525
+ if (view.input.badSafariComposition)
3526
+ view.domObserver.forceFlush();
3527
+ else if (view.input.compositionPendingChanges)
3526
3528
  Promise.resolve().then(() => view.domObserver.flush());
3527
3529
  view.input.compositionID++;
3528
3530
  scheduleComposeEnd(view, 20);
@@ -4585,15 +4587,24 @@ class DOMObserver {
4585
4587
  new window.MutationObserver(mutations => {
4586
4588
  for (let i = 0; i < mutations.length; i++)
4587
4589
  this.queue.push(mutations[i]);
4588
- // IE11 will sometimes (on backspacing out a single character
4589
- // text node after a BR node) call the observer callback
4590
- // before actually updating the DOM, which will cause
4591
- // ProseMirror to miss the change (see #930)
4592
4590
  if (ie && ie_version <= 11 && mutations.some(m => m.type == "childList" && m.removedNodes.length ||
4593
- m.type == "characterData" && m.oldValue.length > m.target.nodeValue.length))
4591
+ m.type == "characterData" && m.oldValue.length > m.target.nodeValue.length)) {
4592
+ // IE11 will sometimes (on backspacing out a single character
4593
+ // text node after a BR node) call the observer callback
4594
+ // before actually updating the DOM, which will cause
4595
+ // ProseMirror to miss the change (see #930)
4594
4596
  this.flushSoon();
4595
- else
4597
+ }
4598
+ else if (safari && view.composing && mutations.some(m => m.type == "childList" && m.target.nodeName == "TR")) {
4599
+ // Safari does weird stuff when finishing a composition in a
4600
+ // table cell, which tends to involve inserting inappropriate
4601
+ // nodes in the table row.
4602
+ view.input.badSafariComposition = true;
4603
+ this.flushSoon();
4604
+ }
4605
+ else {
4596
4606
  this.flush();
4607
+ }
4597
4608
  });
4598
4609
  if (useCharData) {
4599
4610
  this.onCharData = e => {
@@ -4713,7 +4724,17 @@ class DOMObserver {
4713
4724
  }
4714
4725
  }
4715
4726
  }
4716
- if (gecko && added.length) {
4727
+ if (added.some(n => n.nodeName == "BR") && (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
4728
+ // Browsers sometimes insert a bogus break node if you
4729
+ // backspace out the last bit of text before an inline-flex node (#1552)
4730
+ for (let node of added)
4731
+ if (node.nodeName == "BR" && node.parentNode) {
4732
+ let after = node.nextSibling;
4733
+ if (after && after.nodeType == 1 && after.contentEditable == "false")
4734
+ node.parentNode.removeChild(node);
4735
+ }
4736
+ }
4737
+ else if (gecko && added.length) {
4717
4738
  let brs = added.filter(n => n.nodeName == "BR");
4718
4739
  if (brs.length == 2) {
4719
4740
  let [a, b] = brs;
@@ -4731,17 +4752,6 @@ class DOMObserver {
4731
4752
  }
4732
4753
  }
4733
4754
  }
4734
- else if ((chrome || safari) && added.some(n => n.nodeName == "BR") &&
4735
- (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
4736
- // Chrome/Safari sometimes insert a bogus break node if you
4737
- // backspace out the last bit of text before an inline-flex node (#1552)
4738
- for (let node of added)
4739
- if (node.nodeName == "BR" && node.parentNode) {
4740
- let after = node.nextSibling;
4741
- if (after && after.nodeType == 1 && after.contentEditable == "false")
4742
- node.parentNode.removeChild(node);
4743
- }
4744
- }
4745
4755
  let readSel = null;
4746
4756
  // If it looks like the browser has reset the selection to the
4747
4757
  // start of the document after focus, restore the selection from
@@ -4760,6 +4770,10 @@ class DOMObserver {
4760
4770
  view.docView.markDirty(from, to);
4761
4771
  checkCSS(view);
4762
4772
  }
4773
+ if (view.input.badSafariComposition) {
4774
+ view.input.badSafariComposition = false;
4775
+ fixUpBadSafariComposition(view, added);
4776
+ }
4763
4777
  this.handleDOMChange(from, to, typeOver, added);
4764
4778
  if (view.docView && view.docView.dirty)
4765
4779
  view.updateState(view.state);
@@ -4883,6 +4897,37 @@ function blockParent(view, node) {
4883
4897
  }
4884
4898
  return null;
4885
4899
  }
4900
+ // Kludge for a Safari bug where, on ending a composition in an
4901
+ // otherwise empty table cell, it randomly moves the composed text
4902
+ // into the table row around that cell, greatly confusing everything
4903
+ // (#188).
4904
+ function fixUpBadSafariComposition(view, addedNodes) {
4905
+ var _a;
4906
+ let { focusNode, focusOffset } = view.domSelectionRange();
4907
+ for (let node of addedNodes) {
4908
+ if (((_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.nodeName) == "TR") {
4909
+ let nextCell = node.nextSibling;
4910
+ while (nextCell && (nextCell.nodeName != "TD" && nextCell.nodeName != "TH"))
4911
+ nextCell = nextCell.nextSibling;
4912
+ if (nextCell) {
4913
+ let parent = nextCell;
4914
+ for (;;) {
4915
+ let first = parent.firstChild;
4916
+ if (!first || first.nodeType != 1 || first.contentEditable == "false" ||
4917
+ /^(BR|IMG)$/.test(first.nodeName))
4918
+ break;
4919
+ parent = first;
4920
+ }
4921
+ parent.insertBefore(node, parent.firstChild);
4922
+ if (focusNode == node)
4923
+ view.domSelection().collapse(node, focusOffset);
4924
+ }
4925
+ else {
4926
+ node.parentNode.removeChild(node);
4927
+ }
4928
+ }
4929
+ }
4930
+ }
4886
4931
 
4887
4932
  // Note that all referencing and parsing is done with the
4888
4933
  // start-of-operation selection and document, since that's the one
@@ -5455,7 +5500,7 @@ class EditorView {
5455
5500
  this.docView.destroy();
5456
5501
  this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this);
5457
5502
  }
5458
- if (chromeKludge && !this.trackWrites)
5503
+ if (chromeKludge && (!this.trackWrites || !this.dom.contains(this.trackWrites)))
5459
5504
  forceSelUpdate = true;
5460
5505
  }
5461
5506
  // Work around for an issue where an update arriving right between
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prosemirror-view",
3
- "version": "1.41.4",
3
+ "version": "1.41.6",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/domchange.ts CHANGED
@@ -81,7 +81,7 @@ const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|
81
81
  export function readDOMChange(view: EditorView, from: number, to: number, typeOver: boolean, addedNodes: readonly DOMNode[]) {
82
82
  let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0)
83
83
  view.input.compositionPendingChanges = 0
84
-
84
+
85
85
  if (from < 0) {
86
86
  let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null
87
87
  let newSel = selectionFromDOM(view, origin)
@@ -52,16 +52,24 @@ export class DOMObserver {
52
52
  this.observer = window.MutationObserver &&
53
53
  new window.MutationObserver(mutations => {
54
54
  for (let i = 0; i < mutations.length; i++) this.queue.push(mutations[i])
55
- // IE11 will sometimes (on backspacing out a single character
56
- // text node after a BR node) call the observer callback
57
- // before actually updating the DOM, which will cause
58
- // ProseMirror to miss the change (see #930)
59
55
  if (browser.ie && browser.ie_version <= 11 && mutations.some(
60
56
  m => m.type == "childList" && m.removedNodes.length ||
61
- m.type == "characterData" && m.oldValue!.length > m.target.nodeValue!.length))
57
+ m.type == "characterData" && m.oldValue!.length > m.target.nodeValue!.length)) {
58
+ // IE11 will sometimes (on backspacing out a single character
59
+ // text node after a BR node) call the observer callback
60
+ // before actually updating the DOM, which will cause
61
+ // ProseMirror to miss the change (see #930)
62
62
  this.flushSoon()
63
- else
63
+ } else if (browser.safari && view.composing && mutations.some(
64
+ m => m.type == "childList" && m.target.nodeName == "TR")) {
65
+ // Safari does weird stuff when finishing a composition in a
66
+ // table cell, which tends to involve inserting inappropriate
67
+ // nodes in the table row.
68
+ view.input.badSafariComposition = true
69
+ this.flushSoon()
70
+ } else {
64
71
  this.flush()
72
+ }
65
73
  })
66
74
  if (useCharData) {
67
75
  this.onCharData = e => {
@@ -184,7 +192,15 @@ export class DOMObserver {
184
192
  }
185
193
  }
186
194
 
187
- if (browser.gecko && added.length) {
195
+ if (added.some(n => n.nodeName == "BR") && (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
196
+ // Browsers sometimes insert a bogus break node if you
197
+ // backspace out the last bit of text before an inline-flex node (#1552)
198
+ for (let node of added) if (node.nodeName == "BR" && node.parentNode) {
199
+ let after = node.nextSibling
200
+ if (after && after.nodeType == 1 && (after as HTMLElement).contentEditable == "false")
201
+ node.parentNode.removeChild(node)
202
+ }
203
+ } else if (browser.gecko && added.length) {
188
204
  let brs = added.filter(n => n.nodeName == "BR") as HTMLElement[]
189
205
  if (brs.length == 2) {
190
206
  let [a, b] = brs
@@ -198,15 +214,6 @@ export class DOMObserver {
198
214
  br.remove()
199
215
  }
200
216
  }
201
- } else if ((browser.chrome || browser.safari) && added.some(n => n.nodeName == "BR") &&
202
- (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
203
- // Chrome/Safari sometimes insert a bogus break node if you
204
- // backspace out the last bit of text before an inline-flex node (#1552)
205
- for (let node of added) if (node.nodeName == "BR" && node.parentNode) {
206
- let after = node.nextSibling
207
- if (after && after.nodeType == 1 && (after as HTMLElement).contentEditable == "false")
208
- node.parentNode.removeChild(node)
209
- }
210
217
  }
211
218
 
212
219
  let readSel: Selection | null = null
@@ -226,6 +233,10 @@ export class DOMObserver {
226
233
  view.docView.markDirty(from, to)
227
234
  checkCSS(view)
228
235
  }
236
+ if (view.input.badSafariComposition) {
237
+ view.input.badSafariComposition = false
238
+ fixUpBadSafariComposition(view, added)
239
+ }
229
240
  this.handleDOMChange(from, to, typeOver, added)
230
241
  if (view.docView && view.docView.dirty) view.updateState(view.state)
231
242
  else if (!this.currentSelection.eq(sel)) selectionToDOM(view)
@@ -347,3 +358,30 @@ function blockParent(view: EditorView, node: DOMNode): Node | null {
347
358
  }
348
359
  return null
349
360
  }
361
+
362
+ // Kludge for a Safari bug where, on ending a composition in an
363
+ // otherwise empty table cell, it randomly moves the composed text
364
+ // into the table row around that cell, greatly confusing everything
365
+ // (#188).
366
+ function fixUpBadSafariComposition(view: EditorView, addedNodes: readonly DOMNode[]) {
367
+ let {focusNode, focusOffset} = view.domSelectionRange()
368
+ for (let node of addedNodes) {
369
+ if (node.parentNode?.nodeName == "TR") {
370
+ let nextCell = node.nextSibling
371
+ while (nextCell && (nextCell.nodeName != "TD" && nextCell.nodeName != "TH")) nextCell = nextCell.nextSibling
372
+ if (nextCell) {
373
+ let parent = nextCell
374
+ for (;;) {
375
+ let first = parent.firstChild
376
+ if (!first || first.nodeType != 1 || (first as HTMLElement).contentEditable == "false" ||
377
+ /^(BR|IMG)$/.test(first.nodeName)) break
378
+ parent = first
379
+ }
380
+ parent.insertBefore(node, parent.firstChild)
381
+ if (focusNode == node) view.domSelection()!.collapse(node, focusOffset)
382
+ } else {
383
+ node.parentNode.removeChild(node)
384
+ }
385
+ }
386
+ }
387
+ }
package/src/index.ts CHANGED
@@ -202,7 +202,7 @@ export class EditorView {
202
202
  this.docView.destroy()
203
203
  this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this)
204
204
  }
205
- if (chromeKludge && !this.trackWrites) forceSelUpdate = true
205
+ if (chromeKludge && (!this.trackWrites || !this.dom.contains(this.trackWrites))) forceSelUpdate = true
206
206
  }
207
207
  // Work around for an issue where an update arriving right between
208
208
  // a DOM selection change and the "selectionchange" event for it
package/src/input.ts CHANGED
@@ -35,6 +35,7 @@ export class InputState {
35
35
  compositionNodes: ViewDesc[] = []
36
36
  compositionEndedAt = -2e8
37
37
  compositionID = 1
38
+ badSafariComposition = false
38
39
  // Set to a composition ID when there are pending changes at compositionend
39
40
  compositionPendingChanges = 0
40
41
  domChangeCount = 0
@@ -504,7 +505,8 @@ editHandlers.compositionend = (view, event) => {
504
505
  view.input.compositionEndedAt = event.timeStamp
505
506
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0
506
507
  view.input.compositionNode = null
507
- if (view.input.compositionPendingChanges) Promise.resolve().then(() => view.domObserver.flush())
508
+ if (view.input.badSafariComposition) view.domObserver.forceFlush()
509
+ else if (view.input.compositionPendingChanges) Promise.resolve().then(() => view.domObserver.flush())
508
510
  view.input.compositionID++
509
511
  scheduleComposeEnd(view, 20)
510
512
  }
package/src/viewdesc.ts CHANGED
@@ -772,15 +772,15 @@ export class NodeViewDesc extends ViewDesc {
772
772
  let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view)
773
773
  iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => {
774
774
  if (widget.spec.marks)
775
- updater.syncToMarks(widget.spec.marks, inline, view)
775
+ updater.syncToMarks(widget.spec.marks, inline, view, i)
776
776
  else if ((widget.type as WidgetType).side >= 0 && !insideNode)
777
- updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view)
777
+ updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view, i)
778
778
  // If the next node is a desc matching this widget, reuse it,
779
779
  // otherwise insert the widget as a new view desc.
780
780
  updater.placeWidget(widget, view, off)
781
781
  }, (child, outerDeco, innerDeco, i) => {
782
782
  // Make sure the wrapping mark descs match the node's marks.
783
- updater.syncToMarks(child.marks, inline, view)
783
+ updater.syncToMarks(child.marks, inline, view, i)
784
784
  // Try several strategies for drawing this node
785
785
  let compIndex
786
786
  if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) {
@@ -799,7 +799,7 @@ export class NodeViewDesc extends ViewDesc {
799
799
  off += child.nodeSize
800
800
  })
801
801
  // Drop all remaining descs after the current position.
802
- updater.syncToMarks([], inline, view)
802
+ updater.syncToMarks([], inline, view, 0)
803
803
  if (this.node.isTextblock) updater.addTextblockHacks()
804
804
  updater.destroyRest()
805
805
 
@@ -1197,7 +1197,7 @@ class ViewTreeUpdater {
1197
1197
 
1198
1198
  // Sync the current stack of mark descs with the given array of
1199
1199
  // marks, reusing existing mark descs when possible.
1200
- syncToMarks(marks: readonly Mark[], inline: boolean, view: EditorView) {
1200
+ syncToMarks(marks: readonly Mark[], inline: boolean, view: EditorView, parentIndex: number) {
1201
1201
  let keep = 0, depth = this.stack.length >> 1
1202
1202
  let maxKeep = Math.min(depth, marks.length)
1203
1203
  while (keep < maxKeep &&
@@ -1214,8 +1214,9 @@ class ViewTreeUpdater {
1214
1214
  }
1215
1215
  while (depth < marks.length) {
1216
1216
  this.stack.push(this.top, this.index + 1)
1217
- let found = -1
1218
- for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) {
1217
+ let found = -1, scanTo = this.top.children.length
1218
+ if (parentIndex < this.preMatch.index) scanTo = Math.min(this.index + 3, scanTo)
1219
+ for (let i = this.index; i < scanTo; i++) {
1219
1220
  let next = this.top.children[i]
1220
1221
  if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) { found = i; break }
1221
1222
  }
@@ -1404,12 +1405,16 @@ class ViewTreeUpdater {
1404
1405
 
1405
1406
  // Iterate from the end of the fragment and array of descs to find
1406
1407
  // directly matching ones, in order to avoid overeagerly reusing those
1407
- // for other nodes. Returns the fragment index of the first node that
1408
- // is part of the sequence of matched nodes at the end of the
1409
- // fragment.
1410
- function preMatch(
1411
- frag: Fragment, parentDesc: ViewDesc
1412
- ): {index: number, matched: Map<ViewDesc, number>, matches: readonly ViewDesc[]} {
1408
+ // for other nodes.
1409
+ function preMatch(frag: Fragment, parentDesc: ViewDesc): {
1410
+ // The fragment index of the first node that is part of the sequence
1411
+ // of matched nodes at the end of the fragment.
1412
+ index: number,
1413
+ // A map from matched descs to fragment indices.
1414
+ matched: Map<ViewDesc, number>,
1415
+ // The matched descs.
1416
+ matches: readonly ViewDesc[]
1417
+ } {
1413
1418
  let curDesc = parentDesc, descI = curDesc.children.length
1414
1419
  let fI = frag.childCount, matched = new Map, matches = []
1415
1420
  outer: while (fI > 0) {
@@ -1428,7 +1433,6 @@ function preMatch(
1428
1433
  } else if (curDesc == parentDesc) {
1429
1434
  break outer
1430
1435
  } else {
1431
- // FIXME
1432
1436
  descI = curDesc.parent!.children.indexOf(curDesc)
1433
1437
  curDesc = curDesc.parent!
1434
1438
  }