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