prosemirror-view 1.41.4 → 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 +10 -0
- package/dist/index.cjs +58 -9
- package/dist/index.js +65 -19
- package/package.json +1 -1
- package/src/domchange.ts +1 -1
- package/src/domobserver.ts +45 -6
- package/src/index.ts +1 -1
- package/src/input.ts +3 -1
- package/src/viewdesc.ts +18 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
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
|
+
|
|
1
11
|
## 1.41.4 (2025-12-01)
|
|
2
12
|
|
|
3
13
|
### 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
|
-
|
|
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
|
-
}))
|
|
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) {
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 => {
|
|
@@ -4760,6 +4771,10 @@ class DOMObserver {
|
|
|
4760
4771
|
view.docView.markDirty(from, to);
|
|
4761
4772
|
checkCSS(view);
|
|
4762
4773
|
}
|
|
4774
|
+
if (view.input.badSafariComposition) {
|
|
4775
|
+
view.input.badSafariComposition = false;
|
|
4776
|
+
fixUpBadSafariComposition(view, added);
|
|
4777
|
+
}
|
|
4763
4778
|
this.handleDOMChange(from, to, typeOver, added);
|
|
4764
4779
|
if (view.docView && view.docView.dirty)
|
|
4765
4780
|
view.updateState(view.state);
|
|
@@ -4883,6 +4898,37 @@ function blockParent(view, node) {
|
|
|
4883
4898
|
}
|
|
4884
4899
|
return null;
|
|
4885
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
|
+
}
|
|
4886
4932
|
|
|
4887
4933
|
// Note that all referencing and parsing is done with the
|
|
4888
4934
|
// start-of-operation selection and document, since that's the one
|
|
@@ -5455,7 +5501,7 @@ class EditorView {
|
|
|
5455
5501
|
this.docView.destroy();
|
|
5456
5502
|
this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this);
|
|
5457
5503
|
}
|
|
5458
|
-
if (chromeKludge && !this.trackWrites)
|
|
5504
|
+
if (chromeKludge && (!this.trackWrites || !this.dom.contains(this.trackWrites)))
|
|
5459
5505
|
forceSelUpdate = true;
|
|
5460
5506
|
}
|
|
5461
5507
|
// Work around for an issue where an update arriving right between
|
package/package.json
CHANGED
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)
|
package/src/domobserver.ts
CHANGED
|
@@ -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 => {
|
|
@@ -226,6 +234,10 @@ export class DOMObserver {
|
|
|
226
234
|
view.docView.markDirty(from, to)
|
|
227
235
|
checkCSS(view)
|
|
228
236
|
}
|
|
237
|
+
if (view.input.badSafariComposition) {
|
|
238
|
+
view.input.badSafariComposition = false
|
|
239
|
+
fixUpBadSafariComposition(view, added)
|
|
240
|
+
}
|
|
229
241
|
this.handleDOMChange(from, to, typeOver, added)
|
|
230
242
|
if (view.docView && view.docView.dirty) view.updateState(view.state)
|
|
231
243
|
else if (!this.currentSelection.eq(sel)) selectionToDOM(view)
|
|
@@ -347,3 +359,30 @@ function blockParent(view: EditorView, node: DOMNode): Node | null {
|
|
|
347
359
|
}
|
|
348
360
|
return null
|
|
349
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
|
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.
|
|
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
|
-
|
|
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.
|
|
1408
|
-
|
|
1409
|
-
// fragment
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
}
|