@codemirror/view 6.26.0 → 6.26.2
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 +94 -85
- package/dist/index.js +94 -85
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
## 6.26.2 (2024-04-09)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
Improve behavior of `scrollPastEnd` in a scaled editor.
|
|
6
|
+
|
|
7
|
+
When available, use `Selection.getComposedRanges` on Safari to find the selection inside a shadow DOM.
|
|
8
|
+
|
|
9
|
+
Remove the workaround that avoided inappropriate styling on composed text after a decoration again, since it breaks the stock Android virtual keyboard.
|
|
10
|
+
|
|
11
|
+
## 6.26.1 (2024-03-28)
|
|
12
|
+
|
|
13
|
+
### Bug fixes
|
|
14
|
+
|
|
15
|
+
Fix the editor getting stuck in composition when Safari fails to fire a compositionend event for a dead key composition.
|
|
16
|
+
|
|
17
|
+
Fix an issue where, with IME systems that kept the cursor at the start of the composed text, the editor misidentified the target node and disrupted composition.
|
|
18
|
+
|
|
19
|
+
Fix a bug where in a line-wrapped editor, with some content, the initial scroll position would be off from the top of the document.
|
|
20
|
+
|
|
1
21
|
## 6.26.0 (2024-03-14)
|
|
2
22
|
|
|
3
23
|
### Bug fixes
|
package/dist/index.cjs
CHANGED
|
@@ -61,6 +61,9 @@ function domIndex(node) {
|
|
|
61
61
|
return index;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
+
function isBlockElement(node) {
|
|
65
|
+
return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
|
|
66
|
+
}
|
|
64
67
|
function scanFor(node, off, targetNode, targetOff, dir) {
|
|
65
68
|
for (;;) {
|
|
66
69
|
if (node == targetNode && off == targetOff)
|
|
@@ -342,6 +345,46 @@ function atElementStart(doc, selection) {
|
|
|
342
345
|
function isScrolledToBottom(elt) {
|
|
343
346
|
return elt.scrollTop > Math.max(1, elt.scrollHeight - elt.clientHeight - 4);
|
|
344
347
|
}
|
|
348
|
+
function textNodeBefore(startNode, startOffset) {
|
|
349
|
+
for (let node = startNode, offset = startOffset;;) {
|
|
350
|
+
if (node.nodeType == 3 && offset > 0) {
|
|
351
|
+
return { node: node, offset: offset };
|
|
352
|
+
}
|
|
353
|
+
else if (node.nodeType == 1 && offset > 0) {
|
|
354
|
+
if (node.contentEditable == "false")
|
|
355
|
+
return null;
|
|
356
|
+
node = node.childNodes[offset - 1];
|
|
357
|
+
offset = maxOffset(node);
|
|
358
|
+
}
|
|
359
|
+
else if (node.parentNode && !isBlockElement(node)) {
|
|
360
|
+
offset = domIndex(node);
|
|
361
|
+
node = node.parentNode;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function textNodeAfter(startNode, startOffset) {
|
|
369
|
+
for (let node = startNode, offset = startOffset;;) {
|
|
370
|
+
if (node.nodeType == 3 && offset < node.nodeValue.length) {
|
|
371
|
+
return { node: node, offset: offset };
|
|
372
|
+
}
|
|
373
|
+
else if (node.nodeType == 1 && offset < node.childNodes.length) {
|
|
374
|
+
if (node.contentEditable == "false")
|
|
375
|
+
return null;
|
|
376
|
+
node = node.childNodes[offset];
|
|
377
|
+
offset = 0;
|
|
378
|
+
}
|
|
379
|
+
else if (node.parentNode && !isBlockElement(node)) {
|
|
380
|
+
offset = domIndex(node) + 1;
|
|
381
|
+
node = node.parentNode;
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
345
388
|
|
|
346
389
|
class DOMPos {
|
|
347
390
|
constructor(node, offset, precise = true) {
|
|
@@ -2661,11 +2704,11 @@ class DocView extends ContentView {
|
|
|
2661
2704
|
super();
|
|
2662
2705
|
this.view = view;
|
|
2663
2706
|
this.decorations = [];
|
|
2664
|
-
this.dynamicDecorationMap = [
|
|
2707
|
+
this.dynamicDecorationMap = [];
|
|
2665
2708
|
this.domChanged = null;
|
|
2666
2709
|
this.hasComposition = null;
|
|
2667
2710
|
this.markedForComposition = new Set;
|
|
2668
|
-
this.
|
|
2711
|
+
this.lastCompositionAfterCursor = false;
|
|
2669
2712
|
// Track a minimum width for the editor. When measuring sizes in
|
|
2670
2713
|
// measureVisibleLineHeights, this is updated to point at the width
|
|
2671
2714
|
// of a given element and its extent in the document. When a change
|
|
@@ -2882,7 +2925,7 @@ class DocView extends ContentView {
|
|
|
2882
2925
|
if (browser.gecko) {
|
|
2883
2926
|
let nextTo = nextToUneditable(anchor.node, anchor.offset);
|
|
2884
2927
|
if (nextTo && nextTo != (1 /* NextTo.Before */ | 2 /* NextTo.After */)) {
|
|
2885
|
-
let text =
|
|
2928
|
+
let text = (nextTo == 1 /* NextTo.Before */ ? textNodeBefore : textNodeAfter)(anchor.node, anchor.offset);
|
|
2886
2929
|
if (text)
|
|
2887
2930
|
anchor = new DOMPos(text.node, text.offset);
|
|
2888
2931
|
}
|
|
@@ -2929,7 +2972,7 @@ class DocView extends ContentView {
|
|
|
2929
2972
|
// composition, avoid moving it across it and disrupting the
|
|
2930
2973
|
// composition.
|
|
2931
2974
|
suppressWidgetCursorChange(sel, cursor) {
|
|
2932
|
-
return this.hasComposition && cursor.empty &&
|
|
2975
|
+
return this.hasComposition && cursor.empty &&
|
|
2933
2976
|
isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset) &&
|
|
2934
2977
|
this.posFromDOM(sel.focusNode, sel.focusOffset) == cursor.head;
|
|
2935
2978
|
}
|
|
@@ -3137,7 +3180,7 @@ class DocView extends ContentView {
|
|
|
3137
3180
|
return Decoration.set(deco);
|
|
3138
3181
|
}
|
|
3139
3182
|
updateDeco() {
|
|
3140
|
-
let i =
|
|
3183
|
+
let i = 0;
|
|
3141
3184
|
let allDeco = this.view.state.facet(decorations).map(d => {
|
|
3142
3185
|
let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
|
|
3143
3186
|
return dynamic ? d(this.view) : d;
|
|
@@ -3153,7 +3196,6 @@ class DocView extends ContentView {
|
|
|
3153
3196
|
allDeco.push(state.RangeSet.join(outerDeco));
|
|
3154
3197
|
}
|
|
3155
3198
|
this.decorations = [
|
|
3156
|
-
this.compositionBarrier,
|
|
3157
3199
|
...allDeco,
|
|
3158
3200
|
this.computeBlockGapDeco(),
|
|
3159
3201
|
this.view.viewState.lineGapDeco
|
|
@@ -3162,34 +3204,6 @@ class DocView extends ContentView {
|
|
|
3162
3204
|
this.dynamicDecorationMap[i++] = false;
|
|
3163
3205
|
return this.decorations;
|
|
3164
3206
|
}
|
|
3165
|
-
// Starting a composition will style the inserted text with the
|
|
3166
|
-
// style of the text before it, and this is only cleared when the
|
|
3167
|
-
// composition ends, because touching it before that will abort it.
|
|
3168
|
-
// This (called from compositionstart handler) tries to notice when
|
|
3169
|
-
// the cursor is after a non-inclusive mark, where the styling could
|
|
3170
|
-
// be jarring, and insert an ad-hoc widget before the cursor to
|
|
3171
|
-
// isolate it from the style before it.
|
|
3172
|
-
maybeCreateCompositionBarrier() {
|
|
3173
|
-
let { main: { head, empty } } = this.view.state.selection;
|
|
3174
|
-
if (!empty)
|
|
3175
|
-
return false;
|
|
3176
|
-
let found = null;
|
|
3177
|
-
for (let set of this.decorations) {
|
|
3178
|
-
set.between(head, head, (from, to, value) => {
|
|
3179
|
-
if (value.point)
|
|
3180
|
-
found = false;
|
|
3181
|
-
else if (value.endSide < 0 && from < head && to == head)
|
|
3182
|
-
found = true;
|
|
3183
|
-
});
|
|
3184
|
-
if (found === false)
|
|
3185
|
-
break;
|
|
3186
|
-
}
|
|
3187
|
-
this.compositionBarrier = found ? Decoration.set(compositionBarrierWidget.range(head)) : Decoration.none;
|
|
3188
|
-
return !!found;
|
|
3189
|
-
}
|
|
3190
|
-
clearCompositionBarrier() {
|
|
3191
|
-
this.compositionBarrier = Decoration.none;
|
|
3192
|
-
}
|
|
3193
3207
|
scrollIntoView(target) {
|
|
3194
3208
|
if (target.isSnapshot) {
|
|
3195
3209
|
let ref = this.view.viewState.lineBlockAt(target.range.head);
|
|
@@ -3222,7 +3236,6 @@ class DocView extends ContentView {
|
|
|
3222
3236
|
scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == exports.Direction.LTR);
|
|
3223
3237
|
}
|
|
3224
3238
|
}
|
|
3225
|
-
const compositionBarrierWidget = Decoration.widget({ side: -1, widget: NullWidget.inline });
|
|
3226
3239
|
function betweenUneditable(pos) {
|
|
3227
3240
|
return pos.node.nodeType == 1 && pos.node.firstChild &&
|
|
3228
3241
|
(pos.offset == 0 || pos.node.childNodes[pos.offset - 1].contentEditable == "false") &&
|
|
@@ -3250,7 +3263,23 @@ class BlockGapWidget extends WidgetType {
|
|
|
3250
3263
|
}
|
|
3251
3264
|
function findCompositionNode(view, headPos) {
|
|
3252
3265
|
let sel = view.observer.selectionRange;
|
|
3253
|
-
|
|
3266
|
+
if (!sel.focusNode)
|
|
3267
|
+
return null;
|
|
3268
|
+
let textBefore = textNodeBefore(sel.focusNode, sel.focusOffset);
|
|
3269
|
+
let textAfter = textNodeAfter(sel.focusNode, sel.focusOffset);
|
|
3270
|
+
let textNode = textBefore || textAfter;
|
|
3271
|
+
if (textAfter && textBefore && textAfter.node != textBefore.node) {
|
|
3272
|
+
let descAfter = ContentView.get(textAfter.node);
|
|
3273
|
+
if (!descAfter || descAfter instanceof TextView && descAfter.text != textAfter.node.nodeValue) {
|
|
3274
|
+
textNode = textAfter;
|
|
3275
|
+
}
|
|
3276
|
+
else if (view.docView.lastCompositionAfterCursor) {
|
|
3277
|
+
let descBefore = ContentView.get(textBefore.node);
|
|
3278
|
+
if (!(!descBefore || descBefore instanceof TextView && descBefore.text != textBefore.node.nodeValue))
|
|
3279
|
+
textNode = textAfter;
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
view.docView.lastCompositionAfterCursor = textNode != textBefore;
|
|
3254
3283
|
if (!textNode)
|
|
3255
3284
|
return null;
|
|
3256
3285
|
let from = headPos - textNode.offset;
|
|
@@ -3285,33 +3314,6 @@ function findCompositionRange(view, changes, headPos) {
|
|
|
3285
3314
|
return null;
|
|
3286
3315
|
}
|
|
3287
3316
|
}
|
|
3288
|
-
function nearbyTextNode(startNode, startOffset, side) {
|
|
3289
|
-
if (side <= 0)
|
|
3290
|
-
for (let node = startNode, offset = startOffset;;) {
|
|
3291
|
-
if (node.nodeType == 3)
|
|
3292
|
-
return { node: node, offset: offset };
|
|
3293
|
-
if (node.nodeType == 1 && offset > 0) {
|
|
3294
|
-
node = node.childNodes[offset - 1];
|
|
3295
|
-
offset = maxOffset(node);
|
|
3296
|
-
}
|
|
3297
|
-
else {
|
|
3298
|
-
break;
|
|
3299
|
-
}
|
|
3300
|
-
}
|
|
3301
|
-
if (side >= 0)
|
|
3302
|
-
for (let node = startNode, offset = startOffset;;) {
|
|
3303
|
-
if (node.nodeType == 3)
|
|
3304
|
-
return { node: node, offset: offset };
|
|
3305
|
-
if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
|
|
3306
|
-
node = node.childNodes[offset];
|
|
3307
|
-
offset = 0;
|
|
3308
|
-
}
|
|
3309
|
-
else {
|
|
3310
|
-
break;
|
|
3311
|
-
}
|
|
3312
|
-
}
|
|
3313
|
-
return null;
|
|
3314
|
-
}
|
|
3315
3317
|
function nextToUneditable(node, offset) {
|
|
3316
3318
|
if (node.nodeType != 1)
|
|
3317
3319
|
return 0;
|
|
@@ -4475,10 +4477,6 @@ observers.compositionstart = observers.compositionupdate = view => {
|
|
|
4475
4477
|
if (view.inputState.composing < 0) {
|
|
4476
4478
|
// FIXME possibly set a timeout to clear it again on Android
|
|
4477
4479
|
view.inputState.composing = 0;
|
|
4478
|
-
if (view.docView.maybeCreateCompositionBarrier()) {
|
|
4479
|
-
view.update([]);
|
|
4480
|
-
view.docView.clearCompositionBarrier();
|
|
4481
|
-
}
|
|
4482
4480
|
}
|
|
4483
4481
|
};
|
|
4484
4482
|
observers.compositionend = view => {
|
|
@@ -4540,6 +4538,10 @@ handlers.beforeinput = (view, event) => {
|
|
|
4540
4538
|
// keyboard.
|
|
4541
4539
|
view.observer.flushSoon();
|
|
4542
4540
|
}
|
|
4541
|
+
// Safari will occasionally forget to fire compositionend at the end of a dead-key composition
|
|
4542
|
+
if (browser.safari && event.inputType == "insertText" && view.inputState.composing >= 0) {
|
|
4543
|
+
setTimeout(() => observers.compositionend(view, event), 20);
|
|
4544
|
+
}
|
|
4543
4545
|
return false;
|
|
4544
4546
|
};
|
|
4545
4547
|
const appliedFirefoxHack = new Set;
|
|
@@ -4902,7 +4904,8 @@ class HeightMapGap extends HeightMap {
|
|
|
4902
4904
|
blockAt(height, oracle, top, offset) {
|
|
4903
4905
|
let { firstLine, lastLine, perLine, perChar } = this.heightMetrics(oracle, offset);
|
|
4904
4906
|
if (oracle.lineWrapping) {
|
|
4905
|
-
let guess = offset +
|
|
4907
|
+
let guess = offset + (height < oracle.lineHeight ? 0
|
|
4908
|
+
: Math.round(Math.max(0, Math.min(1, (height - top) / this.height)) * this.length));
|
|
4906
4909
|
let line = oracle.doc.lineAt(guess), lineHeight = perLine + line.length * perChar;
|
|
4907
4910
|
let lineTop = Math.max(top, height - lineHeight / 2);
|
|
4908
4911
|
return new BlockInfo(line.from, line.length, lineTop, lineHeight, 0);
|
|
@@ -6238,9 +6241,6 @@ function isAtEnd(parent, node, offset) {
|
|
|
6238
6241
|
node = node.parentNode;
|
|
6239
6242
|
}
|
|
6240
6243
|
}
|
|
6241
|
-
function isBlockElement(node) {
|
|
6242
|
-
return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
|
|
6243
|
-
}
|
|
6244
6244
|
class DOMPoint {
|
|
6245
6245
|
constructor(node, offset) {
|
|
6246
6246
|
this.node = node;
|
|
@@ -6676,9 +6676,12 @@ class DOMObserver {
|
|
|
6676
6676
|
let { view } = this;
|
|
6677
6677
|
// The Selection object is broken in shadow roots in Safari. See
|
|
6678
6678
|
// https://github.com/codemirror/dev/issues/414
|
|
6679
|
+
let selection = getSelection(view.root);
|
|
6680
|
+
if (!selection)
|
|
6681
|
+
return false;
|
|
6679
6682
|
let range = browser.safari && view.root.nodeType == 11 &&
|
|
6680
6683
|
deepActiveElement(this.dom.ownerDocument) == this.dom &&
|
|
6681
|
-
safariSelectionRangeHack(this.view) ||
|
|
6684
|
+
safariSelectionRangeHack(this.view, selection) || selection;
|
|
6682
6685
|
if (!range || this.selectionRange.eq(range))
|
|
6683
6686
|
return false;
|
|
6684
6687
|
let local = hasSelection(this.dom, range);
|
|
@@ -6950,8 +6953,24 @@ function findChild(cView, dom, dir) {
|
|
|
6950
6953
|
}
|
|
6951
6954
|
return null;
|
|
6952
6955
|
}
|
|
6956
|
+
function buildSelectionRangeFromRange(view, range) {
|
|
6957
|
+
let anchorNode = range.startContainer, anchorOffset = range.startOffset;
|
|
6958
|
+
let focusNode = range.endContainer, focusOffset = range.endOffset;
|
|
6959
|
+
let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
|
|
6960
|
+
// Since such a range doesn't distinguish between anchor and head,
|
|
6961
|
+
// use a heuristic that flips it around if its end matches the
|
|
6962
|
+
// current anchor.
|
|
6963
|
+
if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
|
|
6964
|
+
[anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
|
|
6965
|
+
return { anchorNode, anchorOffset, focusNode, focusOffset };
|
|
6966
|
+
}
|
|
6953
6967
|
// Used to work around a Safari Selection/shadow DOM bug (#414)
|
|
6954
|
-
function safariSelectionRangeHack(view) {
|
|
6968
|
+
function safariSelectionRangeHack(view, selection) {
|
|
6969
|
+
if (selection.getComposedRanges) {
|
|
6970
|
+
let range = selection.getComposedRanges(view.root)[0];
|
|
6971
|
+
if (range)
|
|
6972
|
+
return buildSelectionRangeFromRange(view, range);
|
|
6973
|
+
}
|
|
6955
6974
|
let found = null;
|
|
6956
6975
|
// Because Safari (at least in 2018-2021) doesn't provide regular
|
|
6957
6976
|
// access to the selection inside a shadowroot, we have to perform a
|
|
@@ -6966,17 +6985,7 @@ function safariSelectionRangeHack(view) {
|
|
|
6966
6985
|
view.contentDOM.addEventListener("beforeinput", read, true);
|
|
6967
6986
|
view.dom.ownerDocument.execCommand("indent");
|
|
6968
6987
|
view.contentDOM.removeEventListener("beforeinput", read, true);
|
|
6969
|
-
|
|
6970
|
-
return null;
|
|
6971
|
-
let anchorNode = found.startContainer, anchorOffset = found.startOffset;
|
|
6972
|
-
let focusNode = found.endContainer, focusOffset = found.endOffset;
|
|
6973
|
-
let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
|
|
6974
|
-
// Since such a range doesn't distinguish between anchor and head,
|
|
6975
|
-
// use a heuristic that flips it around if its end matches the
|
|
6976
|
-
// current anchor.
|
|
6977
|
-
if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
|
|
6978
|
-
[anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
|
|
6979
|
-
return { anchorNode, anchorOffset, focusNode, focusOffset };
|
|
6988
|
+
return found ? buildSelectionRangeFromRange(view, found) : null;
|
|
6980
6989
|
}
|
|
6981
6990
|
|
|
6982
6991
|
// The editor's update state machine looks something like this:
|
|
@@ -9067,7 +9076,7 @@ const plugin = ViewPlugin.fromClass(class {
|
|
|
9067
9076
|
}
|
|
9068
9077
|
update(update) {
|
|
9069
9078
|
let { view } = update;
|
|
9070
|
-
let height = view.viewState.editorHeight
|
|
9079
|
+
let height = view.viewState.editorHeight -
|
|
9071
9080
|
view.defaultLineHeight - view.documentPadding.top - 0.5;
|
|
9072
9081
|
if (height >= 0 && height != this.height) {
|
|
9073
9082
|
this.height = height;
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,9 @@ function domIndex(node) {
|
|
|
59
59
|
return index;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
function isBlockElement(node) {
|
|
63
|
+
return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
|
|
64
|
+
}
|
|
62
65
|
function scanFor(node, off, targetNode, targetOff, dir) {
|
|
63
66
|
for (;;) {
|
|
64
67
|
if (node == targetNode && off == targetOff)
|
|
@@ -340,6 +343,46 @@ function atElementStart(doc, selection) {
|
|
|
340
343
|
function isScrolledToBottom(elt) {
|
|
341
344
|
return elt.scrollTop > Math.max(1, elt.scrollHeight - elt.clientHeight - 4);
|
|
342
345
|
}
|
|
346
|
+
function textNodeBefore(startNode, startOffset) {
|
|
347
|
+
for (let node = startNode, offset = startOffset;;) {
|
|
348
|
+
if (node.nodeType == 3 && offset > 0) {
|
|
349
|
+
return { node: node, offset: offset };
|
|
350
|
+
}
|
|
351
|
+
else if (node.nodeType == 1 && offset > 0) {
|
|
352
|
+
if (node.contentEditable == "false")
|
|
353
|
+
return null;
|
|
354
|
+
node = node.childNodes[offset - 1];
|
|
355
|
+
offset = maxOffset(node);
|
|
356
|
+
}
|
|
357
|
+
else if (node.parentNode && !isBlockElement(node)) {
|
|
358
|
+
offset = domIndex(node);
|
|
359
|
+
node = node.parentNode;
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function textNodeAfter(startNode, startOffset) {
|
|
367
|
+
for (let node = startNode, offset = startOffset;;) {
|
|
368
|
+
if (node.nodeType == 3 && offset < node.nodeValue.length) {
|
|
369
|
+
return { node: node, offset: offset };
|
|
370
|
+
}
|
|
371
|
+
else if (node.nodeType == 1 && offset < node.childNodes.length) {
|
|
372
|
+
if (node.contentEditable == "false")
|
|
373
|
+
return null;
|
|
374
|
+
node = node.childNodes[offset];
|
|
375
|
+
offset = 0;
|
|
376
|
+
}
|
|
377
|
+
else if (node.parentNode && !isBlockElement(node)) {
|
|
378
|
+
offset = domIndex(node) + 1;
|
|
379
|
+
node = node.parentNode;
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
343
386
|
|
|
344
387
|
class DOMPos {
|
|
345
388
|
constructor(node, offset, precise = true) {
|
|
@@ -2657,11 +2700,11 @@ class DocView extends ContentView {
|
|
|
2657
2700
|
super();
|
|
2658
2701
|
this.view = view;
|
|
2659
2702
|
this.decorations = [];
|
|
2660
|
-
this.dynamicDecorationMap = [
|
|
2703
|
+
this.dynamicDecorationMap = [];
|
|
2661
2704
|
this.domChanged = null;
|
|
2662
2705
|
this.hasComposition = null;
|
|
2663
2706
|
this.markedForComposition = new Set;
|
|
2664
|
-
this.
|
|
2707
|
+
this.lastCompositionAfterCursor = false;
|
|
2665
2708
|
// Track a minimum width for the editor. When measuring sizes in
|
|
2666
2709
|
// measureVisibleLineHeights, this is updated to point at the width
|
|
2667
2710
|
// of a given element and its extent in the document. When a change
|
|
@@ -2878,7 +2921,7 @@ class DocView extends ContentView {
|
|
|
2878
2921
|
if (browser.gecko) {
|
|
2879
2922
|
let nextTo = nextToUneditable(anchor.node, anchor.offset);
|
|
2880
2923
|
if (nextTo && nextTo != (1 /* NextTo.Before */ | 2 /* NextTo.After */)) {
|
|
2881
|
-
let text =
|
|
2924
|
+
let text = (nextTo == 1 /* NextTo.Before */ ? textNodeBefore : textNodeAfter)(anchor.node, anchor.offset);
|
|
2882
2925
|
if (text)
|
|
2883
2926
|
anchor = new DOMPos(text.node, text.offset);
|
|
2884
2927
|
}
|
|
@@ -2925,7 +2968,7 @@ class DocView extends ContentView {
|
|
|
2925
2968
|
// composition, avoid moving it across it and disrupting the
|
|
2926
2969
|
// composition.
|
|
2927
2970
|
suppressWidgetCursorChange(sel, cursor) {
|
|
2928
|
-
return this.hasComposition && cursor.empty &&
|
|
2971
|
+
return this.hasComposition && cursor.empty &&
|
|
2929
2972
|
isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset) &&
|
|
2930
2973
|
this.posFromDOM(sel.focusNode, sel.focusOffset) == cursor.head;
|
|
2931
2974
|
}
|
|
@@ -3133,7 +3176,7 @@ class DocView extends ContentView {
|
|
|
3133
3176
|
return Decoration.set(deco);
|
|
3134
3177
|
}
|
|
3135
3178
|
updateDeco() {
|
|
3136
|
-
let i =
|
|
3179
|
+
let i = 0;
|
|
3137
3180
|
let allDeco = this.view.state.facet(decorations).map(d => {
|
|
3138
3181
|
let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
|
|
3139
3182
|
return dynamic ? d(this.view) : d;
|
|
@@ -3149,7 +3192,6 @@ class DocView extends ContentView {
|
|
|
3149
3192
|
allDeco.push(RangeSet.join(outerDeco));
|
|
3150
3193
|
}
|
|
3151
3194
|
this.decorations = [
|
|
3152
|
-
this.compositionBarrier,
|
|
3153
3195
|
...allDeco,
|
|
3154
3196
|
this.computeBlockGapDeco(),
|
|
3155
3197
|
this.view.viewState.lineGapDeco
|
|
@@ -3158,34 +3200,6 @@ class DocView extends ContentView {
|
|
|
3158
3200
|
this.dynamicDecorationMap[i++] = false;
|
|
3159
3201
|
return this.decorations;
|
|
3160
3202
|
}
|
|
3161
|
-
// Starting a composition will style the inserted text with the
|
|
3162
|
-
// style of the text before it, and this is only cleared when the
|
|
3163
|
-
// composition ends, because touching it before that will abort it.
|
|
3164
|
-
// This (called from compositionstart handler) tries to notice when
|
|
3165
|
-
// the cursor is after a non-inclusive mark, where the styling could
|
|
3166
|
-
// be jarring, and insert an ad-hoc widget before the cursor to
|
|
3167
|
-
// isolate it from the style before it.
|
|
3168
|
-
maybeCreateCompositionBarrier() {
|
|
3169
|
-
let { main: { head, empty } } = this.view.state.selection;
|
|
3170
|
-
if (!empty)
|
|
3171
|
-
return false;
|
|
3172
|
-
let found = null;
|
|
3173
|
-
for (let set of this.decorations) {
|
|
3174
|
-
set.between(head, head, (from, to, value) => {
|
|
3175
|
-
if (value.point)
|
|
3176
|
-
found = false;
|
|
3177
|
-
else if (value.endSide < 0 && from < head && to == head)
|
|
3178
|
-
found = true;
|
|
3179
|
-
});
|
|
3180
|
-
if (found === false)
|
|
3181
|
-
break;
|
|
3182
|
-
}
|
|
3183
|
-
this.compositionBarrier = found ? Decoration.set(compositionBarrierWidget.range(head)) : Decoration.none;
|
|
3184
|
-
return !!found;
|
|
3185
|
-
}
|
|
3186
|
-
clearCompositionBarrier() {
|
|
3187
|
-
this.compositionBarrier = Decoration.none;
|
|
3188
|
-
}
|
|
3189
3203
|
scrollIntoView(target) {
|
|
3190
3204
|
if (target.isSnapshot) {
|
|
3191
3205
|
let ref = this.view.viewState.lineBlockAt(target.range.head);
|
|
@@ -3218,7 +3232,6 @@ class DocView extends ContentView {
|
|
|
3218
3232
|
scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == Direction.LTR);
|
|
3219
3233
|
}
|
|
3220
3234
|
}
|
|
3221
|
-
const compositionBarrierWidget = /*@__PURE__*/Decoration.widget({ side: -1, widget: NullWidget.inline });
|
|
3222
3235
|
function betweenUneditable(pos) {
|
|
3223
3236
|
return pos.node.nodeType == 1 && pos.node.firstChild &&
|
|
3224
3237
|
(pos.offset == 0 || pos.node.childNodes[pos.offset - 1].contentEditable == "false") &&
|
|
@@ -3246,7 +3259,23 @@ class BlockGapWidget extends WidgetType {
|
|
|
3246
3259
|
}
|
|
3247
3260
|
function findCompositionNode(view, headPos) {
|
|
3248
3261
|
let sel = view.observer.selectionRange;
|
|
3249
|
-
|
|
3262
|
+
if (!sel.focusNode)
|
|
3263
|
+
return null;
|
|
3264
|
+
let textBefore = textNodeBefore(sel.focusNode, sel.focusOffset);
|
|
3265
|
+
let textAfter = textNodeAfter(sel.focusNode, sel.focusOffset);
|
|
3266
|
+
let textNode = textBefore || textAfter;
|
|
3267
|
+
if (textAfter && textBefore && textAfter.node != textBefore.node) {
|
|
3268
|
+
let descAfter = ContentView.get(textAfter.node);
|
|
3269
|
+
if (!descAfter || descAfter instanceof TextView && descAfter.text != textAfter.node.nodeValue) {
|
|
3270
|
+
textNode = textAfter;
|
|
3271
|
+
}
|
|
3272
|
+
else if (view.docView.lastCompositionAfterCursor) {
|
|
3273
|
+
let descBefore = ContentView.get(textBefore.node);
|
|
3274
|
+
if (!(!descBefore || descBefore instanceof TextView && descBefore.text != textBefore.node.nodeValue))
|
|
3275
|
+
textNode = textAfter;
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
view.docView.lastCompositionAfterCursor = textNode != textBefore;
|
|
3250
3279
|
if (!textNode)
|
|
3251
3280
|
return null;
|
|
3252
3281
|
let from = headPos - textNode.offset;
|
|
@@ -3281,33 +3310,6 @@ function findCompositionRange(view, changes, headPos) {
|
|
|
3281
3310
|
return null;
|
|
3282
3311
|
}
|
|
3283
3312
|
}
|
|
3284
|
-
function nearbyTextNode(startNode, startOffset, side) {
|
|
3285
|
-
if (side <= 0)
|
|
3286
|
-
for (let node = startNode, offset = startOffset;;) {
|
|
3287
|
-
if (node.nodeType == 3)
|
|
3288
|
-
return { node: node, offset: offset };
|
|
3289
|
-
if (node.nodeType == 1 && offset > 0) {
|
|
3290
|
-
node = node.childNodes[offset - 1];
|
|
3291
|
-
offset = maxOffset(node);
|
|
3292
|
-
}
|
|
3293
|
-
else {
|
|
3294
|
-
break;
|
|
3295
|
-
}
|
|
3296
|
-
}
|
|
3297
|
-
if (side >= 0)
|
|
3298
|
-
for (let node = startNode, offset = startOffset;;) {
|
|
3299
|
-
if (node.nodeType == 3)
|
|
3300
|
-
return { node: node, offset: offset };
|
|
3301
|
-
if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
|
|
3302
|
-
node = node.childNodes[offset];
|
|
3303
|
-
offset = 0;
|
|
3304
|
-
}
|
|
3305
|
-
else {
|
|
3306
|
-
break;
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
return null;
|
|
3310
|
-
}
|
|
3311
3313
|
function nextToUneditable(node, offset) {
|
|
3312
3314
|
if (node.nodeType != 1)
|
|
3313
3315
|
return 0;
|
|
@@ -4471,10 +4473,6 @@ observers.compositionstart = observers.compositionupdate = view => {
|
|
|
4471
4473
|
if (view.inputState.composing < 0) {
|
|
4472
4474
|
// FIXME possibly set a timeout to clear it again on Android
|
|
4473
4475
|
view.inputState.composing = 0;
|
|
4474
|
-
if (view.docView.maybeCreateCompositionBarrier()) {
|
|
4475
|
-
view.update([]);
|
|
4476
|
-
view.docView.clearCompositionBarrier();
|
|
4477
|
-
}
|
|
4478
4476
|
}
|
|
4479
4477
|
};
|
|
4480
4478
|
observers.compositionend = view => {
|
|
@@ -4536,6 +4534,10 @@ handlers.beforeinput = (view, event) => {
|
|
|
4536
4534
|
// keyboard.
|
|
4537
4535
|
view.observer.flushSoon();
|
|
4538
4536
|
}
|
|
4537
|
+
// Safari will occasionally forget to fire compositionend at the end of a dead-key composition
|
|
4538
|
+
if (browser.safari && event.inputType == "insertText" && view.inputState.composing >= 0) {
|
|
4539
|
+
setTimeout(() => observers.compositionend(view, event), 20);
|
|
4540
|
+
}
|
|
4539
4541
|
return false;
|
|
4540
4542
|
};
|
|
4541
4543
|
const appliedFirefoxHack = /*@__PURE__*/new Set;
|
|
@@ -4897,7 +4899,8 @@ class HeightMapGap extends HeightMap {
|
|
|
4897
4899
|
blockAt(height, oracle, top, offset) {
|
|
4898
4900
|
let { firstLine, lastLine, perLine, perChar } = this.heightMetrics(oracle, offset);
|
|
4899
4901
|
if (oracle.lineWrapping) {
|
|
4900
|
-
let guess = offset +
|
|
4902
|
+
let guess = offset + (height < oracle.lineHeight ? 0
|
|
4903
|
+
: Math.round(Math.max(0, Math.min(1, (height - top) / this.height)) * this.length));
|
|
4901
4904
|
let line = oracle.doc.lineAt(guess), lineHeight = perLine + line.length * perChar;
|
|
4902
4905
|
let lineTop = Math.max(top, height - lineHeight / 2);
|
|
4903
4906
|
return new BlockInfo(line.from, line.length, lineTop, lineHeight, 0);
|
|
@@ -6233,9 +6236,6 @@ function isAtEnd(parent, node, offset) {
|
|
|
6233
6236
|
node = node.parentNode;
|
|
6234
6237
|
}
|
|
6235
6238
|
}
|
|
6236
|
-
function isBlockElement(node) {
|
|
6237
|
-
return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
|
|
6238
|
-
}
|
|
6239
6239
|
class DOMPoint {
|
|
6240
6240
|
constructor(node, offset) {
|
|
6241
6241
|
this.node = node;
|
|
@@ -6671,9 +6671,12 @@ class DOMObserver {
|
|
|
6671
6671
|
let { view } = this;
|
|
6672
6672
|
// The Selection object is broken in shadow roots in Safari. See
|
|
6673
6673
|
// https://github.com/codemirror/dev/issues/414
|
|
6674
|
+
let selection = getSelection(view.root);
|
|
6675
|
+
if (!selection)
|
|
6676
|
+
return false;
|
|
6674
6677
|
let range = browser.safari && view.root.nodeType == 11 &&
|
|
6675
6678
|
deepActiveElement(this.dom.ownerDocument) == this.dom &&
|
|
6676
|
-
safariSelectionRangeHack(this.view) ||
|
|
6679
|
+
safariSelectionRangeHack(this.view, selection) || selection;
|
|
6677
6680
|
if (!range || this.selectionRange.eq(range))
|
|
6678
6681
|
return false;
|
|
6679
6682
|
let local = hasSelection(this.dom, range);
|
|
@@ -6945,8 +6948,24 @@ function findChild(cView, dom, dir) {
|
|
|
6945
6948
|
}
|
|
6946
6949
|
return null;
|
|
6947
6950
|
}
|
|
6951
|
+
function buildSelectionRangeFromRange(view, range) {
|
|
6952
|
+
let anchorNode = range.startContainer, anchorOffset = range.startOffset;
|
|
6953
|
+
let focusNode = range.endContainer, focusOffset = range.endOffset;
|
|
6954
|
+
let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
|
|
6955
|
+
// Since such a range doesn't distinguish between anchor and head,
|
|
6956
|
+
// use a heuristic that flips it around if its end matches the
|
|
6957
|
+
// current anchor.
|
|
6958
|
+
if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
|
|
6959
|
+
[anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
|
|
6960
|
+
return { anchorNode, anchorOffset, focusNode, focusOffset };
|
|
6961
|
+
}
|
|
6948
6962
|
// Used to work around a Safari Selection/shadow DOM bug (#414)
|
|
6949
|
-
function safariSelectionRangeHack(view) {
|
|
6963
|
+
function safariSelectionRangeHack(view, selection) {
|
|
6964
|
+
if (selection.getComposedRanges) {
|
|
6965
|
+
let range = selection.getComposedRanges(view.root)[0];
|
|
6966
|
+
if (range)
|
|
6967
|
+
return buildSelectionRangeFromRange(view, range);
|
|
6968
|
+
}
|
|
6950
6969
|
let found = null;
|
|
6951
6970
|
// Because Safari (at least in 2018-2021) doesn't provide regular
|
|
6952
6971
|
// access to the selection inside a shadowroot, we have to perform a
|
|
@@ -6961,17 +6980,7 @@ function safariSelectionRangeHack(view) {
|
|
|
6961
6980
|
view.contentDOM.addEventListener("beforeinput", read, true);
|
|
6962
6981
|
view.dom.ownerDocument.execCommand("indent");
|
|
6963
6982
|
view.contentDOM.removeEventListener("beforeinput", read, true);
|
|
6964
|
-
|
|
6965
|
-
return null;
|
|
6966
|
-
let anchorNode = found.startContainer, anchorOffset = found.startOffset;
|
|
6967
|
-
let focusNode = found.endContainer, focusOffset = found.endOffset;
|
|
6968
|
-
let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
|
|
6969
|
-
// Since such a range doesn't distinguish between anchor and head,
|
|
6970
|
-
// use a heuristic that flips it around if its end matches the
|
|
6971
|
-
// current anchor.
|
|
6972
|
-
if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
|
|
6973
|
-
[anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
|
|
6974
|
-
return { anchorNode, anchorOffset, focusNode, focusOffset };
|
|
6983
|
+
return found ? buildSelectionRangeFromRange(view, found) : null;
|
|
6975
6984
|
}
|
|
6976
6985
|
|
|
6977
6986
|
// The editor's update state machine looks something like this:
|
|
@@ -9062,7 +9071,7 @@ const plugin = /*@__PURE__*/ViewPlugin.fromClass(class {
|
|
|
9062
9071
|
}
|
|
9063
9072
|
update(update) {
|
|
9064
9073
|
let { view } = update;
|
|
9065
|
-
let height = view.viewState.editorHeight
|
|
9074
|
+
let height = view.viewState.editorHeight -
|
|
9066
9075
|
view.defaultLineHeight - view.documentPadding.top - 0.5;
|
|
9067
9076
|
if (height >= 0 && height != this.height) {
|
|
9068
9077
|
this.height = height;
|