prosemirror-view 1.41.3 → 1.41.5

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.41.5 (2026-01-14)
2
+
3
+ ### Bug fixes
4
+
5
+ Work around an issue where, after some kinds of changes, Chrome misreports the cursor position and breaks composition.
6
+
7
+ Improve DOM update behavior when deleting three or more nodes right before a mark, which would unnecessarily re-render the entire mark.
8
+
9
+ 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.
10
+
11
+ ## 1.41.4 (2025-12-01)
12
+
13
+ ### Bug fixes
14
+
15
+ `EditorView.dragging` is now only cleared after the drop event has entirely been handled.
16
+
17
+ Add a workaround for an issue where Chrome and Safari will, if you backspace out the last text before an inline-flex node, insert a bogus hard break.
18
+
19
+ Fix a composition problem on Chrome Windows when starting a composition before an absolutely positioned widget.
20
+
1
21
  ## 1.41.3 (2025-10-09)
2
22
 
3
23
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -360,18 +360,23 @@ function findOffsetInNode(node, coords) {
360
360
  }
361
361
  function findOffsetInText(node, coords) {
362
362
  var len = node.nodeValue.length;
363
- var range = document.createRange();
363
+ var range = document.createRange(),
364
+ result;
364
365
  for (var i = 0; i < len; i++) {
365
366
  range.setEnd(node, i + 1);
366
367
  range.setStart(node, i);
367
368
  var rect = singleRect(range, 1);
368
369
  if (rect.top == rect.bottom) continue;
369
- if (inRect(coords, rect)) return {
370
- node: node,
371
- offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)
372
- };
370
+ if (inRect(coords, rect)) {
371
+ result = {
372
+ node: node,
373
+ offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)
374
+ };
375
+ break;
376
+ }
373
377
  }
374
- return {
378
+ range.detach();
379
+ return result || {
375
380
  node: node,
376
381
  offset: 0
377
382
  };
@@ -1333,17 +1338,17 @@ var NodeViewDesc = function (_ViewDesc4) {
1333
1338
  var compositionInChild = composition && composition.pos < 0;
1334
1339
  var updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view);
1335
1340
  iterDeco(this.node, this.innerDeco, function (widget, i, insideNode) {
1336
- 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);
1337
1342
  updater.placeWidget(widget, view, off);
1338
1343
  }, function (child, outerDeco, innerDeco, i) {
1339
- updater.syncToMarks(child.marks, inline, view);
1344
+ updater.syncToMarks(child.marks, inline, view, i);
1340
1345
  var compIndex;
1341
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 {
1342
1347
  updater.addNode(child, outerDeco, innerDeco, view, off);
1343
1348
  }
1344
1349
  off += child.nodeSize;
1345
1350
  });
1346
- updater.syncToMarks([], inline, view);
1351
+ updater.syncToMarks([], inline, view, 0);
1347
1352
  if (this.node.isTextblock) updater.addTextblockHacks();
1348
1353
  updater.destroyRest();
1349
1354
  if (updater.changed || this.dirty == CONTENT_DIRTY) {
@@ -1781,7 +1786,7 @@ var ViewTreeUpdater = function () {
1781
1786
  }
1782
1787
  }, {
1783
1788
  key: "syncToMarks",
1784
- value: function syncToMarks(marks, inline, view) {
1789
+ value: function syncToMarks(marks, inline, view, parentIndex) {
1785
1790
  var keep = 0,
1786
1791
  depth = this.stack.length >> 1;
1787
1792
  var maxKeep = Math.min(depth, marks.length);
@@ -1795,8 +1800,10 @@ var ViewTreeUpdater = function () {
1795
1800
  }
1796
1801
  while (depth < marks.length) {
1797
1802
  this.stack.push(this.top, this.index + 1);
1798
- var found = -1;
1799
- 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++) {
1800
1807
  var next = this.top.children[i];
1801
1808
  if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) {
1802
1809
  found = i;
@@ -2914,6 +2921,7 @@ var InputState = _createClass(function InputState() {
2914
2921
  this.compositionNodes = [];
2915
2922
  this.compositionEndedAt = -2e8;
2916
2923
  this.compositionID = 1;
2924
+ this.badSafariComposition = false;
2917
2925
  this.compositionPendingChanges = 0;
2918
2926
  this.domChangeCount = 0;
2919
2927
  this.eventHandlers = Object.create(null);
@@ -3272,7 +3280,7 @@ editHandlers.compositionstart = editHandlers.compositionupdate = function (view)
3272
3280
  $pos = state.selection.$to;
3273
3281
  if (state.selection instanceof prosemirrorState.TextSelection && (state.storedMarks || !$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(function (m) {
3274
3282
  return m.type.spec.inclusive === false;
3275
- }))) {
3283
+ }) || chrome && windows && selectionBeforeUneditable(view))) {
3276
3284
  view.markCursor = view.state.storedMarks || $pos.marks();
3277
3285
  endComposition(view, true);
3278
3286
  view.markCursor = null;
@@ -3298,13 +3306,21 @@ editHandlers.compositionstart = editHandlers.compositionupdate = function (view)
3298
3306
  }
3299
3307
  scheduleComposeEnd(view, timeoutComposition);
3300
3308
  };
3309
+ function selectionBeforeUneditable(view) {
3310
+ var _view$domSelectionRan4 = view.domSelectionRange(),
3311
+ focusNode = _view$domSelectionRan4.focusNode,
3312
+ focusOffset = _view$domSelectionRan4.focusOffset;
3313
+ if (!focusNode || focusNode.nodeType != 1 || focusOffset >= focusNode.childNodes.length) return false;
3314
+ var next = focusNode.childNodes[focusOffset];
3315
+ return next.nodeType == 1 && next.contentEditable == "false";
3316
+ }
3301
3317
  editHandlers.compositionend = function (view, event) {
3302
3318
  if (view.composing) {
3303
3319
  view.input.composing = false;
3304
3320
  view.input.compositionEndedAt = event.timeStamp;
3305
3321
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
3306
3322
  view.input.compositionNode = null;
3307
- 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 () {
3308
3324
  return view.domObserver.flush();
3309
3325
  });
3310
3326
  view.input.compositionID++;
@@ -3485,10 +3501,14 @@ handlers.dragend = function (view) {
3485
3501
  editHandlers.dragover = editHandlers.dragenter = function (_, e) {
3486
3502
  return e.preventDefault();
3487
3503
  };
3488
- editHandlers.drop = function (view, _event) {
3489
- var event = _event;
3490
- var dragging = view.dragging;
3491
- view.dragging = null;
3504
+ editHandlers.drop = function (view, event) {
3505
+ try {
3506
+ handleDrop(view, event, view.dragging);
3507
+ } finally {
3508
+ view.dragging = null;
3509
+ }
3510
+ };
3511
+ function handleDrop(view, event, dragging) {
3492
3512
  if (!event.dataTransfer) return;
3493
3513
  var eventPos = view.posAtCoords(eventCoords(event));
3494
3514
  if (!eventPos) return;
@@ -3534,7 +3554,7 @@ editHandlers.drop = function (view, _event) {
3534
3554
  }
3535
3555
  view.focus();
3536
3556
  view.dispatch(tr.setMeta("uiEvent", "drop"));
3537
- };
3557
+ }
3538
3558
  handlers.focus = function (view) {
3539
3559
  view.input.lastFocus = Date.now();
3540
3560
  if (!view.focused) {
@@ -4218,7 +4238,16 @@ var DOMObserver = function () {
4218
4238
  for (var i = 0; i < mutations.length; i++) _this11.queue.push(mutations[i]);
4219
4239
  if (ie && ie_version <= 11 && mutations.some(function (m) {
4220
4240
  return m.type == "childList" && m.removedNodes.length || m.type == "characterData" && m.oldValue.length > m.target.nodeValue.length;
4221
- })) _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
+ }
4222
4251
  });
4223
4252
  if (useCharData) {
4224
4253
  this.onCharData = function (e) {
@@ -4399,6 +4428,24 @@ var DOMObserver = function () {
4399
4428
  _iterator3.f();
4400
4429
  }
4401
4430
  }
4431
+ } else if ((chrome || safari) && added.some(function (n) {
4432
+ return n.nodeName == "BR";
4433
+ }) && (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
4434
+ var _iterator4 = _createForOfIteratorHelper(added),
4435
+ _step4;
4436
+ try {
4437
+ for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
4438
+ var node = _step4.value;
4439
+ if (node.nodeName == "BR" && node.parentNode) {
4440
+ var after = node.nextSibling;
4441
+ if (after && after.nodeType == 1 && after.contentEditable == "false") node.parentNode.removeChild(node);
4442
+ }
4443
+ }
4444
+ } catch (err) {
4445
+ _iterator4.e(err);
4446
+ } finally {
4447
+ _iterator4.f();
4448
+ }
4402
4449
  }
4403
4450
  var readSel = null;
4404
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))) {
@@ -4411,6 +4458,10 @@ var DOMObserver = function () {
4411
4458
  view.docView.markDirty(from, to);
4412
4459
  checkCSS(view);
4413
4460
  }
4461
+ if (view.input.badSafariComposition) {
4462
+ view.input.badSafariComposition = false;
4463
+ fixUpBadSafariComposition(view, added);
4464
+ }
4414
4465
  this.handleDOMChange(from, to, typeOver, added);
4415
4466
  if (view.docView && view.docView.dirty) view.updateState(view.state);else if (!this.currentSelection.eq(sel)) selectionToDOM(view);
4416
4467
  this.currentSelection.set(sel);
@@ -4524,6 +4575,39 @@ function blockParent(view, node) {
4524
4575
  }
4525
4576
  return null;
4526
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
+ }
4527
4611
  function parseBetween(view, from_, to_) {
4528
4612
  var _view$docView$parseRa = view.docView.parseRange(from_, to_),
4529
4613
  parent = _view$docView$parseRa.node,
@@ -4979,7 +5063,7 @@ var EditorView = function () {
4979
5063
  this.docView.destroy();
4980
5064
  this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this);
4981
5065
  }
4982
- if (chromeKludge && !this.trackWrites) forceSelUpdate = true;
5066
+ if (chromeKludge && (!this.trackWrites || !this.dom.contains(this.trackWrites))) forceSelUpdate = true;
4983
5067
  }
4984
5068
  if (forceSelUpdate || !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) && anchorInRightPlace(this))) {
4985
5069
  selectionToDOM(this, forceSelUpdate);
package/dist/index.js CHANGED
@@ -367,17 +367,20 @@ function findOffsetInNode(node, coords) {
367
367
  }
368
368
  function findOffsetInText(node, coords) {
369
369
  let len = node.nodeValue.length;
370
- let range = document.createRange();
370
+ let range = document.createRange(), result;
371
371
  for (let i = 0; i < len; i++) {
372
372
  range.setEnd(node, i + 1);
373
373
  range.setStart(node, i);
374
374
  let rect = singleRect(range, 1);
375
375
  if (rect.top == rect.bottom)
376
376
  continue;
377
- if (inRect(coords, rect))
378
- return { node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0) };
377
+ if (inRect(coords, rect)) {
378
+ result = { node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0) };
379
+ break;
380
+ }
379
381
  }
380
- return { node, offset: 0 };
382
+ range.detach();
383
+ return result || { node, offset: 0 };
381
384
  }
382
385
  function inRect(coords, rect) {
383
386
  return coords.left >= rect.left - 1 && coords.left <= rect.right + 1 &&
@@ -1373,15 +1376,15 @@ class NodeViewDesc extends ViewDesc {
1373
1376
  let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view);
1374
1377
  iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => {
1375
1378
  if (widget.spec.marks)
1376
- updater.syncToMarks(widget.spec.marks, inline, view);
1379
+ updater.syncToMarks(widget.spec.marks, inline, view, i);
1377
1380
  else if (widget.type.side >= 0 && !insideNode)
1378
- 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);
1379
1382
  // If the next node is a desc matching this widget, reuse it,
1380
1383
  // otherwise insert the widget as a new view desc.
1381
1384
  updater.placeWidget(widget, view, off);
1382
1385
  }, (child, outerDeco, innerDeco, i) => {
1383
1386
  // Make sure the wrapping mark descs match the node's marks.
1384
- updater.syncToMarks(child.marks, inline, view);
1387
+ updater.syncToMarks(child.marks, inline, view, i);
1385
1388
  // Try several strategies for drawing this node
1386
1389
  let compIndex;
1387
1390
  if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) ;
@@ -1397,7 +1400,7 @@ class NodeViewDesc extends ViewDesc {
1397
1400
  off += child.nodeSize;
1398
1401
  });
1399
1402
  // Drop all remaining descs after the current position.
1400
- updater.syncToMarks([], inline, view);
1403
+ updater.syncToMarks([], inline, view, 0);
1401
1404
  if (this.node.isTextblock)
1402
1405
  updater.addTextblockHacks();
1403
1406
  updater.destroyRest();
@@ -1787,7 +1790,7 @@ class ViewTreeUpdater {
1787
1790
  }
1788
1791
  // Sync the current stack of mark descs with the given array of
1789
1792
  // marks, reusing existing mark descs when possible.
1790
- syncToMarks(marks, inline, view) {
1793
+ syncToMarks(marks, inline, view, parentIndex) {
1791
1794
  let keep = 0, depth = this.stack.length >> 1;
1792
1795
  let maxKeep = Math.min(depth, marks.length);
1793
1796
  while (keep < maxKeep &&
@@ -1803,8 +1806,10 @@ class ViewTreeUpdater {
1803
1806
  }
1804
1807
  while (depth < marks.length) {
1805
1808
  this.stack.push(this.top, this.index + 1);
1806
- let found = -1;
1807
- 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++) {
1808
1813
  let next = this.top.children[i];
1809
1814
  if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) {
1810
1815
  found = i;
@@ -2000,9 +2005,7 @@ class ViewTreeUpdater {
2000
2005
  }
2001
2006
  // Iterate from the end of the fragment and array of descs to find
2002
2007
  // directly matching ones, in order to avoid overeagerly reusing those
2003
- // for other nodes. Returns the fragment index of the first node that
2004
- // is part of the sequence of matched nodes at the end of the
2005
- // fragment.
2008
+ // for other nodes.
2006
2009
  function preMatch(frag, parentDesc) {
2007
2010
  let curDesc = parentDesc, descI = curDesc.children.length;
2008
2011
  let fI = frag.childCount, matched = new Map, matches = [];
@@ -2025,7 +2028,6 @@ function preMatch(frag, parentDesc) {
2025
2028
  break outer;
2026
2029
  }
2027
2030
  else {
2028
- // FIXME
2029
2031
  descI = curDesc.parent.children.indexOf(curDesc);
2030
2032
  curDesc = curDesc.parent;
2031
2033
  }
@@ -3072,6 +3074,7 @@ class InputState {
3072
3074
  this.compositionNodes = [];
3073
3075
  this.compositionEndedAt = -2e8;
3074
3076
  this.compositionID = 1;
3077
+ this.badSafariComposition = false;
3075
3078
  // Set to a composition ID when there are pending changes at compositionend
3076
3079
  this.compositionPendingChanges = 0;
3077
3080
  this.domChangeCount = 0;
@@ -3471,7 +3474,8 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
3471
3474
  let { state } = view, $pos = state.selection.$to;
3472
3475
  if (state.selection instanceof TextSelection &&
3473
3476
  (state.storedMarks ||
3474
- (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(m => m.type.spec.inclusive === false)))) {
3477
+ (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore.marks.some(m => m.type.spec.inclusive === false)) ||
3478
+ chrome && windows && selectionBeforeUneditable(view))) { // Issue #1500
3475
3479
  // Need to wrap the cursor in mark nodes different from the ones in the DOM context
3476
3480
  view.markCursor = view.state.storedMarks || $pos.marks();
3477
3481
  endComposition(view, true);
@@ -3505,13 +3509,22 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
3505
3509
  }
3506
3510
  scheduleComposeEnd(view, timeoutComposition);
3507
3511
  };
3512
+ function selectionBeforeUneditable(view) {
3513
+ let { focusNode, focusOffset } = view.domSelectionRange();
3514
+ if (!focusNode || focusNode.nodeType != 1 || focusOffset >= focusNode.childNodes.length)
3515
+ return false;
3516
+ let next = focusNode.childNodes[focusOffset];
3517
+ return next.nodeType == 1 && next.contentEditable == "false";
3518
+ }
3508
3519
  editHandlers.compositionend = (view, event) => {
3509
3520
  if (view.composing) {
3510
3521
  view.input.composing = false;
3511
3522
  view.input.compositionEndedAt = event.timeStamp;
3512
3523
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0;
3513
3524
  view.input.compositionNode = null;
3514
- if (view.input.compositionPendingChanges)
3525
+ if (view.input.badSafariComposition)
3526
+ view.domObserver.forceFlush();
3527
+ else if (view.input.compositionPendingChanges)
3515
3528
  Promise.resolve().then(() => view.domObserver.flush());
3516
3529
  view.input.compositionID++;
3517
3530
  scheduleComposeEnd(view, 20);
@@ -3732,10 +3745,15 @@ handlers.dragend = view => {
3732
3745
  }, 50);
3733
3746
  };
3734
3747
  editHandlers.dragover = editHandlers.dragenter = (_, e) => e.preventDefault();
3735
- editHandlers.drop = (view, _event) => {
3736
- let event = _event;
3737
- let dragging = view.dragging;
3738
- view.dragging = null;
3748
+ editHandlers.drop = (view, event) => {
3749
+ try {
3750
+ handleDrop(view, event, view.dragging);
3751
+ }
3752
+ finally {
3753
+ view.dragging = null;
3754
+ }
3755
+ };
3756
+ function handleDrop(view, event, dragging) {
3739
3757
  if (!event.dataTransfer)
3740
3758
  return;
3741
3759
  let eventPos = view.posAtCoords(eventCoords(event));
@@ -3789,7 +3807,7 @@ editHandlers.drop = (view, _event) => {
3789
3807
  }
3790
3808
  view.focus();
3791
3809
  view.dispatch(tr.setMeta("uiEvent", "drop"));
3792
- };
3810
+ }
3793
3811
  handlers.focus = view => {
3794
3812
  view.input.lastFocus = Date.now();
3795
3813
  if (!view.focused) {
@@ -4569,15 +4587,24 @@ class DOMObserver {
4569
4587
  new window.MutationObserver(mutations => {
4570
4588
  for (let i = 0; i < mutations.length; i++)
4571
4589
  this.queue.push(mutations[i]);
4572
- // IE11 will sometimes (on backspacing out a single character
4573
- // text node after a BR node) call the observer callback
4574
- // before actually updating the DOM, which will cause
4575
- // ProseMirror to miss the change (see #930)
4576
4590
  if (ie && ie_version <= 11 && mutations.some(m => m.type == "childList" && m.removedNodes.length ||
4577
- 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)
4578
4596
  this.flushSoon();
4579
- 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 {
4580
4606
  this.flush();
4607
+ }
4581
4608
  });
4582
4609
  if (useCharData) {
4583
4610
  this.onCharData = e => {
@@ -4715,6 +4742,17 @@ class DOMObserver {
4715
4742
  }
4716
4743
  }
4717
4744
  }
4745
+ else if ((chrome || safari) && added.some(n => n.nodeName == "BR") &&
4746
+ (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
4747
+ // Chrome/Safari sometimes insert a bogus break node if you
4748
+ // backspace out the last bit of text before an inline-flex node (#1552)
4749
+ for (let node of added)
4750
+ if (node.nodeName == "BR" && node.parentNode) {
4751
+ let after = node.nextSibling;
4752
+ if (after && after.nodeType == 1 && after.contentEditable == "false")
4753
+ node.parentNode.removeChild(node);
4754
+ }
4755
+ }
4718
4756
  let readSel = null;
4719
4757
  // If it looks like the browser has reset the selection to the
4720
4758
  // start of the document after focus, restore the selection from
@@ -4733,6 +4771,10 @@ class DOMObserver {
4733
4771
  view.docView.markDirty(from, to);
4734
4772
  checkCSS(view);
4735
4773
  }
4774
+ if (view.input.badSafariComposition) {
4775
+ view.input.badSafariComposition = false;
4776
+ fixUpBadSafariComposition(view, added);
4777
+ }
4736
4778
  this.handleDOMChange(from, to, typeOver, added);
4737
4779
  if (view.docView && view.docView.dirty)
4738
4780
  view.updateState(view.state);
@@ -4856,6 +4898,37 @@ function blockParent(view, node) {
4856
4898
  }
4857
4899
  return null;
4858
4900
  }
4901
+ // Kludge for a Safari bug where, on ending a composition in an
4902
+ // otherwise empty table cell, it randomly moves the composed text
4903
+ // into the table row around that cell, greatly confusing everything
4904
+ // (#188).
4905
+ function fixUpBadSafariComposition(view, addedNodes) {
4906
+ var _a;
4907
+ let { focusNode, focusOffset } = view.domSelectionRange();
4908
+ for (let node of addedNodes) {
4909
+ if (((_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.nodeName) == "TR") {
4910
+ let nextCell = node.nextSibling;
4911
+ while (nextCell && (nextCell.nodeName != "TD" && nextCell.nodeName != "TH"))
4912
+ nextCell = nextCell.nextSibling;
4913
+ if (nextCell) {
4914
+ let parent = nextCell;
4915
+ for (;;) {
4916
+ let first = parent.firstChild;
4917
+ if (!first || first.nodeType != 1 || first.contentEditable == "false" ||
4918
+ /^(BR|IMG)$/.test(first.nodeName))
4919
+ break;
4920
+ parent = first;
4921
+ }
4922
+ parent.insertBefore(node, parent.firstChild);
4923
+ if (focusNode == node)
4924
+ view.domSelection().collapse(node, focusOffset);
4925
+ }
4926
+ else {
4927
+ node.parentNode.removeChild(node);
4928
+ }
4929
+ }
4930
+ }
4931
+ }
4859
4932
 
4860
4933
  // Note that all referencing and parsing is done with the
4861
4934
  // start-of-operation selection and document, since that's the one
@@ -5428,7 +5501,7 @@ class EditorView {
5428
5501
  this.docView.destroy();
5429
5502
  this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this);
5430
5503
  }
5431
- if (chromeKludge && !this.trackWrites)
5504
+ if (chromeKludge && (!this.trackWrites || !this.dom.contains(this.trackWrites)))
5432
5505
  forceSelUpdate = true;
5433
5506
  }
5434
5507
  // 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.3",
3
+ "version": "1.41.5",
4
4
  "description": "ProseMirror's view component",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/dom.ts CHANGED
@@ -30,7 +30,7 @@ export const textRange = function(node: Text, from?: number, to?: number) {
30
30
  }
31
31
 
32
32
  export const clearReusedRange = function() {
33
- reusedRange = null;
33
+ reusedRange = null
34
34
  }
35
35
 
36
36
  // Scans forward and backward through DOM positions equivalent to the
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, Transaction} from "prosemirror-state"
2
+ import {TextSelection, Transaction} from "prosemirror-state"
3
3
 
4
4
  import {selectionBetween, selectionFromDOM, selectionToDOM} from "./selection"
5
5
  import {selectionCollapsed, keyEvent, DOMNode} from "./dom"
@@ -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)
package/src/domcoords.ts CHANGED
@@ -184,16 +184,19 @@ function findOffsetInNode(node: HTMLElement, coords: {top: number, left: number}
184
184
 
185
185
  function findOffsetInText(node: Text, coords: {top: number, left: number}) {
186
186
  let len = node.nodeValue!.length
187
- let range = document.createRange()
187
+ let range = document.createRange(), result: {node: Node, offset: number} | undefined
188
188
  for (let i = 0; i < len; i++) {
189
189
  range.setEnd(node, i + 1)
190
190
  range.setStart(node, i)
191
191
  let rect = singleRect(range, 1)
192
192
  if (rect.top == rect.bottom) continue
193
- if (inRect(coords, rect))
194
- return {node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)}
193
+ if (inRect(coords, rect)) {
194
+ result = {node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)}
195
+ break
196
+ }
195
197
  }
196
- return {node, offset: 0}
198
+ range.detach()
199
+ return result || {node, offset: 0}
197
200
  }
198
201
 
199
202
  function inRect(coords: {top: number, left: number}, rect: Rect) {
@@ -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 => {
@@ -198,6 +206,15 @@ export class DOMObserver {
198
206
  br.remove()
199
207
  }
200
208
  }
209
+ } else if ((browser.chrome || browser.safari) && added.some(n => n.nodeName == "BR") &&
210
+ (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) {
211
+ // Chrome/Safari sometimes insert a bogus break node if you
212
+ // backspace out the last bit of text before an inline-flex node (#1552)
213
+ for (let node of added) if (node.nodeName == "BR" && node.parentNode) {
214
+ let after = node.nextSibling
215
+ if (after && after.nodeType == 1 && (after as HTMLElement).contentEditable == "false")
216
+ node.parentNode.removeChild(node)
217
+ }
201
218
  }
202
219
 
203
220
  let readSel: Selection | null = null
@@ -217,6 +234,10 @@ export class DOMObserver {
217
234
  view.docView.markDirty(from, to)
218
235
  checkCSS(view)
219
236
  }
237
+ if (view.input.badSafariComposition) {
238
+ view.input.badSafariComposition = false
239
+ fixUpBadSafariComposition(view, added)
240
+ }
220
241
  this.handleDOMChange(from, to, typeOver, added)
221
242
  if (view.docView && view.docView.dirty) view.updateState(view.state)
222
243
  else if (!this.currentSelection.eq(sel)) selectionToDOM(view)
@@ -338,3 +359,30 @@ function blockParent(view: EditorView, node: DOMNode): Node | null {
338
359
  }
339
360
  return null
340
361
  }
362
+
363
+ // Kludge for a Safari bug where, on ending a composition in an
364
+ // otherwise empty table cell, it randomly moves the composed text
365
+ // into the table row around that cell, greatly confusing everything
366
+ // (#188).
367
+ function fixUpBadSafariComposition(view: EditorView, addedNodes: readonly DOMNode[]) {
368
+ let {focusNode, focusOffset} = view.domSelectionRange()
369
+ for (let node of addedNodes) {
370
+ if (node.parentNode?.nodeName == "TR") {
371
+ let nextCell = node.nextSibling
372
+ while (nextCell && (nextCell.nodeName != "TD" && nextCell.nodeName != "TH")) nextCell = nextCell.nextSibling
373
+ if (nextCell) {
374
+ let parent = nextCell
375
+ for (;;) {
376
+ let first = parent.firstChild
377
+ if (!first || first.nodeType != 1 || (first as HTMLElement).contentEditable == "false" ||
378
+ /^(BR|IMG)$/.test(first.nodeName)) break
379
+ parent = first
380
+ }
381
+ parent.insertBefore(node, parent.firstChild)
382
+ if (focusNode == node) view.domSelection()!.collapse(node, focusOffset)
383
+ } else {
384
+ node.parentNode.removeChild(node)
385
+ }
386
+ }
387
+ }
388
+ }
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
@@ -469,7 +469,7 @@ export class EditorView {
469
469
  }
470
470
  this.docView.destroy()
471
471
  ;(this as any).docView = null
472
- clearReusedRange();
472
+ clearReusedRange()
473
473
  }
474
474
 
475
475
  /// This is true when the view has been
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
@@ -459,7 +460,8 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
459
460
  let {state} = view, $pos = state.selection.$to
460
461
  if (state.selection instanceof TextSelection &&
461
462
  (state.storedMarks ||
462
- (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore!.marks.some(m => m.type.spec.inclusive === false)))) {
463
+ (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore!.marks.some(m => m.type.spec.inclusive === false)) ||
464
+ browser.chrome && browser.windows && selectionBeforeUneditable(view))) { // Issue #1500
463
465
  // Need to wrap the cursor in mark nodes different from the ones in the DOM context
464
466
  view.markCursor = view.state.storedMarks || $pos.marks()
465
467
  endComposition(view, true)
@@ -490,13 +492,21 @@ editHandlers.compositionstart = editHandlers.compositionupdate = view => {
490
492
  scheduleComposeEnd(view, timeoutComposition)
491
493
  }
492
494
 
495
+ function selectionBeforeUneditable(view: EditorView) {
496
+ let {focusNode, focusOffset} = view.domSelectionRange()
497
+ if (!focusNode || focusNode.nodeType != 1 || focusOffset >= focusNode.childNodes.length) return false
498
+ let next = focusNode.childNodes[focusOffset]
499
+ return next.nodeType == 1 && (next as HTMLElement).contentEditable == "false"
500
+ }
501
+
493
502
  editHandlers.compositionend = (view, event) => {
494
503
  if (view.composing) {
495
504
  view.input.composing = false
496
505
  view.input.compositionEndedAt = event.timeStamp
497
506
  view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0
498
507
  view.input.compositionNode = null
499
- 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())
500
510
  view.input.compositionID++
501
511
  scheduleComposeEnd(view, 20)
502
512
  }
@@ -706,11 +716,15 @@ handlers.dragend = view => {
706
716
 
707
717
  editHandlers.dragover = editHandlers.dragenter = (_, e) => e.preventDefault()
708
718
 
709
- editHandlers.drop = (view, _event) => {
710
- let event = _event as DragEvent
711
- let dragging = view.dragging
712
- view.dragging = null
719
+ editHandlers.drop = (view, event) => {
720
+ try {
721
+ handleDrop(view, event as DragEvent, view.dragging)
722
+ } finally {
723
+ view.dragging = null
724
+ }
725
+ }
713
726
 
727
+ function handleDrop(view: EditorView, event: DragEvent, dragging: Dragging | null) {
714
728
  if (!event.dataTransfer) return
715
729
 
716
730
  let eventPos = view.posAtCoords(eventCoords(event))
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
  }