@codemirror/view 0.19.29 → 0.19.30
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 +14 -0
- package/dist/index.cjs +70 -57
- package/dist/index.d.ts +9 -0
- package/dist/index.js +70 -57
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 0.19.30 (2021-12-13)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
Refine Android key event handling to work properly in a GBoard corner case where pressing Enter fires a bunch of spurious deleteContentBackward events.
|
|
6
|
+
|
|
7
|
+
Fix a crash in `drawSelection` for some kinds of selections.
|
|
8
|
+
|
|
9
|
+
Prevent a possibility where some content updates causes duplicate text to remain in DOM.
|
|
10
|
+
|
|
11
|
+
### New features
|
|
12
|
+
|
|
13
|
+
Support a `maxLength` option to `MatchDecorator` that allows user code to control how far it scans into hidden parts of viewport lines.
|
|
14
|
+
|
|
1
15
|
## 0.19.29 (2021-12-09)
|
|
2
16
|
|
|
3
17
|
### Bug fixes
|
package/dist/index.cjs
CHANGED
|
@@ -658,6 +658,7 @@ class TextView extends ContentView {
|
|
|
658
658
|
split(from) {
|
|
659
659
|
let result = new TextView(this.text.slice(from));
|
|
660
660
|
this.text = this.text.slice(0, from);
|
|
661
|
+
this.markDirty();
|
|
661
662
|
return result;
|
|
662
663
|
}
|
|
663
664
|
localPosFromDOM(node, offset) {
|
|
@@ -2398,16 +2399,6 @@ class DocView extends ContentView {
|
|
|
2398
2399
|
return true;
|
|
2399
2400
|
}
|
|
2400
2401
|
}
|
|
2401
|
-
reset(sel) {
|
|
2402
|
-
if (this.dirty) {
|
|
2403
|
-
this.view.observer.ignore(() => this.view.docView.sync());
|
|
2404
|
-
this.dirty = 0 /* Not */;
|
|
2405
|
-
this.updateSelection(true);
|
|
2406
|
-
}
|
|
2407
|
-
else {
|
|
2408
|
-
this.updateSelection();
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
2402
|
// Used by update and the constructor do perform the actual DOM
|
|
2412
2403
|
// update
|
|
2413
2404
|
updateInner(changes, deco, oldLength) {
|
|
@@ -2483,7 +2474,8 @@ class DocView extends ContentView {
|
|
|
2483
2474
|
// inside an uneditable node, and not bring it back when we
|
|
2484
2475
|
// move the cursor to its proper position. This tries to
|
|
2485
2476
|
// restore the keyboard by cycling focus.
|
|
2486
|
-
if (browser.android && browser.chrome && this.dom.contains(domSel.focusNode) &&
|
|
2477
|
+
if (browser.android && browser.chrome && this.dom.contains(domSel.focusNode) &&
|
|
2478
|
+
inUneditable(domSel.focusNode, this.dom)) {
|
|
2487
2479
|
this.dom.blur();
|
|
2488
2480
|
this.dom.focus({ preventScroll: true });
|
|
2489
2481
|
}
|
|
@@ -3136,14 +3128,6 @@ class InputState {
|
|
|
3136
3128
|
constructor(view) {
|
|
3137
3129
|
this.lastKeyCode = 0;
|
|
3138
3130
|
this.lastKeyTime = 0;
|
|
3139
|
-
// On Chrome Android, backspace near widgets is just completely
|
|
3140
|
-
// broken, and there are no key events, so we need to handle the
|
|
3141
|
-
// beforeinput event. Deleting stuff will often create a flurry of
|
|
3142
|
-
// events, and interrupting it before it is done just makes
|
|
3143
|
-
// subsequent events even more broken, so again, we hold off doing
|
|
3144
|
-
// anything until the browser is finished with whatever it is trying
|
|
3145
|
-
// to do.
|
|
3146
|
-
this.pendingAndroidKey = undefined;
|
|
3147
3131
|
// On iOS, some keys need to have their default behavior happen
|
|
3148
3132
|
// (after which we retroactively handle them and reset the DOM) to
|
|
3149
3133
|
// avoid messing up the virtual keyboard state.
|
|
@@ -3212,22 +3196,15 @@ class InputState {
|
|
|
3212
3196
|
}
|
|
3213
3197
|
runCustomHandlers(type, view, event) {
|
|
3214
3198
|
for (let set of this.customHandlers) {
|
|
3215
|
-
let handler = set.handlers[type]
|
|
3199
|
+
let handler = set.handlers[type];
|
|
3216
3200
|
if (handler) {
|
|
3217
3201
|
try {
|
|
3218
|
-
|
|
3202
|
+
if (handler.call(set.plugin, event, view))
|
|
3203
|
+
return true;
|
|
3219
3204
|
}
|
|
3220
3205
|
catch (e) {
|
|
3221
3206
|
logException(view.state, e);
|
|
3222
3207
|
}
|
|
3223
|
-
if (handled || event.defaultPrevented) {
|
|
3224
|
-
// Chrome for Android often applies a bunch of nonsensical
|
|
3225
|
-
// DOM changes after an enter press, even when
|
|
3226
|
-
// preventDefault-ed. This tries to ignore those.
|
|
3227
|
-
if (browser.android && type == "keydown" && event.keyCode == 13)
|
|
3228
|
-
view.observer.flushSoon();
|
|
3229
|
-
return true;
|
|
3230
|
-
}
|
|
3231
3208
|
}
|
|
3232
3209
|
}
|
|
3233
3210
|
return false;
|
|
@@ -3251,6 +3228,16 @@ class InputState {
|
|
|
3251
3228
|
this.lastKeyTime = Date.now();
|
|
3252
3229
|
if (this.screenKeyEvent(view, event))
|
|
3253
3230
|
return true;
|
|
3231
|
+
// Chrome for Android usually doesn't fire proper key events, but
|
|
3232
|
+
// occasionally does, usually surrounded by a bunch of complicated
|
|
3233
|
+
// composition changes. When an enter or backspace key event is
|
|
3234
|
+
// seen, hold off on handling DOM events for a bit, and then
|
|
3235
|
+
// dispatch it.
|
|
3236
|
+
if (browser.android && browser.chrome && !event.synthetic &&
|
|
3237
|
+
(event.keyCode == 13 || event.keyCode == 8)) {
|
|
3238
|
+
view.observer.delayAndroidKey(event.key, event.keyCode);
|
|
3239
|
+
return true;
|
|
3240
|
+
}
|
|
3254
3241
|
// Prevent the default behavior of Enter on iOS makes the
|
|
3255
3242
|
// virtual keyboard get stuck in the wrong (lowercase)
|
|
3256
3243
|
// state. So we let it go through, and then, in
|
|
@@ -3272,24 +3259,6 @@ class InputState {
|
|
|
3272
3259
|
this.pendingIOSKey = undefined;
|
|
3273
3260
|
return dispatchKey(view.contentDOM, key.key, key.keyCode);
|
|
3274
3261
|
}
|
|
3275
|
-
// This causes the DOM observer to pause for a bit, and sets an
|
|
3276
|
-
// animation frame (which seems the most reliable way to detect
|
|
3277
|
-
// 'Chrome is done flailing about messing with the DOM') to fire a
|
|
3278
|
-
// fake key event and re-sync the view again.
|
|
3279
|
-
setPendingAndroidKey(view, pending) {
|
|
3280
|
-
this.pendingAndroidKey = pending;
|
|
3281
|
-
requestAnimationFrame(() => {
|
|
3282
|
-
let key = this.pendingAndroidKey;
|
|
3283
|
-
if (!key)
|
|
3284
|
-
return;
|
|
3285
|
-
this.pendingAndroidKey = undefined;
|
|
3286
|
-
view.observer.processRecords();
|
|
3287
|
-
let startState = view.state;
|
|
3288
|
-
dispatchKey(view.contentDOM, key.key, key.keyCode);
|
|
3289
|
-
if (view.state == startState)
|
|
3290
|
-
view.docView.reset(true);
|
|
3291
|
-
});
|
|
3292
|
-
}
|
|
3293
3262
|
ignoreDuringComposition(event) {
|
|
3294
3263
|
if (!/^key/.test(event.type))
|
|
3295
3264
|
return false;
|
|
@@ -3768,12 +3737,12 @@ handlers.compositionstart = handlers.compositionupdate = view => {
|
|
|
3768
3737
|
if (view.inputState.compositionFirstChange == null)
|
|
3769
3738
|
view.inputState.compositionFirstChange = true;
|
|
3770
3739
|
if (view.inputState.composing < 0) {
|
|
3740
|
+
// FIXME possibly set a timeout to clear it again on Android
|
|
3741
|
+
view.inputState.composing = 0;
|
|
3771
3742
|
if (view.docView.compositionDeco.size) {
|
|
3772
3743
|
view.observer.flush();
|
|
3773
3744
|
forceClearComposition(view, true);
|
|
3774
3745
|
}
|
|
3775
|
-
// FIXME possibly set a timeout to clear it again on Android
|
|
3776
|
-
view.inputState.composing = 0;
|
|
3777
3746
|
}
|
|
3778
3747
|
};
|
|
3779
3748
|
handlers.compositionend = view => {
|
|
@@ -3799,7 +3768,7 @@ handlers.beforeinput = (view, event) => {
|
|
|
3799
3768
|
// seems to do nothing at all on Chrome).
|
|
3800
3769
|
let pending;
|
|
3801
3770
|
if (browser.chrome && browser.android && (pending = PendingKeys.find(key => key.inputType == event.inputType))) {
|
|
3802
|
-
view.
|
|
3771
|
+
view.observer.delayAndroidKey(pending.key, pending.keyCode);
|
|
3803
3772
|
if (pending.key == "Backspace" || pending.key == "Delete") {
|
|
3804
3773
|
let startViewHeight = ((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0;
|
|
3805
3774
|
setTimeout(() => {
|
|
@@ -5188,6 +5157,7 @@ class DOMObserver {
|
|
|
5188
5157
|
this.delayedFlush = -1;
|
|
5189
5158
|
this.resizeTimeout = -1;
|
|
5190
5159
|
this.queue = [];
|
|
5160
|
+
this.delayedAndroidKey = null;
|
|
5191
5161
|
this.scrollTargets = [];
|
|
5192
5162
|
this.intersection = null;
|
|
5193
5163
|
this.resize = null;
|
|
@@ -5271,7 +5241,7 @@ class DOMObserver {
|
|
|
5271
5241
|
}
|
|
5272
5242
|
}
|
|
5273
5243
|
onSelectionChange(event) {
|
|
5274
|
-
if (!this.readSelectionRange())
|
|
5244
|
+
if (!this.readSelectionRange() || this.delayedAndroidKey)
|
|
5275
5245
|
return;
|
|
5276
5246
|
let { view } = this, sel = this.selectionRange;
|
|
5277
5247
|
if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(view.dom, sel))
|
|
@@ -5367,6 +5337,32 @@ class DOMObserver {
|
|
|
5367
5337
|
this.queue.length = 0;
|
|
5368
5338
|
this.selectionChanged = false;
|
|
5369
5339
|
}
|
|
5340
|
+
// Chrome Android, especially in combination with GBoard, not only
|
|
5341
|
+
// doesn't reliably fire regular key events, but also often
|
|
5342
|
+
// surrounds the effect of enter or backspace with a bunch of
|
|
5343
|
+
// composition events that, when interrupted, cause text duplication
|
|
5344
|
+
// or other kinds of corruption. This hack makes the editor back off
|
|
5345
|
+
// from handling DOM changes for a moment when such a key is
|
|
5346
|
+
// detected (via beforeinput or keydown), and then dispatches the
|
|
5347
|
+
// key event, throwing away the DOM changes if it gets handled.
|
|
5348
|
+
delayAndroidKey(key, keyCode) {
|
|
5349
|
+
if (!this.delayedAndroidKey)
|
|
5350
|
+
requestAnimationFrame(() => {
|
|
5351
|
+
let key = this.delayedAndroidKey;
|
|
5352
|
+
this.delayedAndroidKey = null;
|
|
5353
|
+
let startState = this.view.state;
|
|
5354
|
+
if (dispatchKey(this.view.contentDOM, key.key, key.keyCode))
|
|
5355
|
+
this.processRecords();
|
|
5356
|
+
else
|
|
5357
|
+
this.flush();
|
|
5358
|
+
if (this.view.state == startState)
|
|
5359
|
+
this.view.update([]);
|
|
5360
|
+
});
|
|
5361
|
+
// Since backspace beforeinput is sometimes signalled spuriously,
|
|
5362
|
+
// Enter always takes precedence.
|
|
5363
|
+
if (!this.delayedAndroidKey || key == "Enter")
|
|
5364
|
+
this.delayedAndroidKey = { key, keyCode };
|
|
5365
|
+
}
|
|
5370
5366
|
flushSoon() {
|
|
5371
5367
|
if (this.delayedFlush < 0)
|
|
5372
5368
|
this.delayedFlush = window.setTimeout(() => { this.delayedFlush = -1; this.flush(); }, 20);
|
|
@@ -5403,13 +5399,13 @@ class DOMObserver {
|
|
|
5403
5399
|
}
|
|
5404
5400
|
// Apply pending changes, if any
|
|
5405
5401
|
flush(readSelection = true) {
|
|
5406
|
-
if (readSelection)
|
|
5407
|
-
this.readSelectionRange();
|
|
5408
5402
|
// Completely hold off flushing when pending keys are set—the code
|
|
5409
5403
|
// managing those will make sure processRecords is called and the
|
|
5410
5404
|
// view is resynchronized after
|
|
5411
|
-
if (this.delayedFlush >= 0 || this.
|
|
5405
|
+
if (this.delayedFlush >= 0 || this.delayedAndroidKey)
|
|
5412
5406
|
return;
|
|
5407
|
+
if (readSelection)
|
|
5408
|
+
this.readSelectionRange();
|
|
5413
5409
|
let { from, to, typeOver } = this.processRecords();
|
|
5414
5410
|
let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
|
|
5415
5411
|
if (from < 0 && !newSel)
|
|
@@ -6929,7 +6925,7 @@ function measureRange(view, range) {
|
|
|
6929
6925
|
let between = [];
|
|
6930
6926
|
if ((visualStart || startBlock).to < (visualEnd || endBlock).from - 1)
|
|
6931
6927
|
between.push(piece(leftSide, top.bottom, rightSide, bottom.top));
|
|
6932
|
-
else if (top.bottom < bottom.top &&
|
|
6928
|
+
else if (top.bottom < bottom.top && view.elementAtHeight((top.bottom + bottom.top) / 2).type == exports.BlockType.Text)
|
|
6933
6929
|
top.bottom = bottom.top = (top.bottom + bottom.top) / 2;
|
|
6934
6930
|
return pieces(top).concat(between).concat(pieces(bottom));
|
|
6935
6931
|
}
|
|
@@ -7002,6 +6998,22 @@ function iterMatches(doc, re, from, to, f) {
|
|
|
7002
6998
|
f(pos + m.index, pos + m.index + m[0].length, m);
|
|
7003
6999
|
}
|
|
7004
7000
|
}
|
|
7001
|
+
function matchRanges(view, maxLength) {
|
|
7002
|
+
let visible = view.visibleRanges;
|
|
7003
|
+
if (visible.length == 1 && visible[0].from == view.viewport.from &&
|
|
7004
|
+
visible[0].to == view.viewport.to)
|
|
7005
|
+
return visible;
|
|
7006
|
+
let result = [];
|
|
7007
|
+
for (let { from, to } of visible) {
|
|
7008
|
+
from = Math.max(view.state.doc.lineAt(from).from, from - maxLength);
|
|
7009
|
+
to = Math.min(view.state.doc.lineAt(to).to, to + maxLength);
|
|
7010
|
+
if (result.length && result[result.length - 1].to >= from)
|
|
7011
|
+
result[result.length - 1].to = to;
|
|
7012
|
+
else
|
|
7013
|
+
result.push({ from, to });
|
|
7014
|
+
}
|
|
7015
|
+
return result;
|
|
7016
|
+
}
|
|
7005
7017
|
/**
|
|
7006
7018
|
Helper class used to make it easier to maintain decorations on
|
|
7007
7019
|
visible code that matches a given regular expression. To be used
|
|
@@ -7013,12 +7025,13 @@ class MatchDecorator {
|
|
|
7013
7025
|
Create a decorator.
|
|
7014
7026
|
*/
|
|
7015
7027
|
constructor(config) {
|
|
7016
|
-
let { regexp, decoration, boundary } = config;
|
|
7028
|
+
let { regexp, decoration, boundary, maxLength = 1000 } = config;
|
|
7017
7029
|
if (!regexp.global)
|
|
7018
7030
|
throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");
|
|
7019
7031
|
this.regexp = regexp;
|
|
7020
7032
|
this.getDeco = typeof decoration == "function" ? decoration : () => decoration;
|
|
7021
7033
|
this.boundary = boundary;
|
|
7034
|
+
this.maxLength = maxLength;
|
|
7022
7035
|
}
|
|
7023
7036
|
/**
|
|
7024
7037
|
Compute the full set of decorations for matches in the given
|
|
@@ -7027,7 +7040,7 @@ class MatchDecorator {
|
|
|
7027
7040
|
*/
|
|
7028
7041
|
createDeco(view) {
|
|
7029
7042
|
let build = new rangeset.RangeSetBuilder();
|
|
7030
|
-
for (let { from, to } of view.
|
|
7043
|
+
for (let { from, to } of matchRanges(view, this.maxLength))
|
|
7031
7044
|
iterMatches(view.state.doc, this.regexp, from, to, (a, b, m) => build.add(a, b, this.getDeco(m, view, a)));
|
|
7032
7045
|
return build.finish();
|
|
7033
7046
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1362,6 +1362,7 @@ declare class MatchDecorator {
|
|
|
1362
1362
|
private regexp;
|
|
1363
1363
|
private getDeco;
|
|
1364
1364
|
private boundary;
|
|
1365
|
+
private maxLength;
|
|
1365
1366
|
/**
|
|
1366
1367
|
Create a decorator.
|
|
1367
1368
|
*/
|
|
@@ -1384,6 +1385,14 @@ declare class MatchDecorator {
|
|
|
1384
1385
|
the amount of re-matching.
|
|
1385
1386
|
*/
|
|
1386
1387
|
boundary?: RegExp;
|
|
1388
|
+
/**
|
|
1389
|
+
Matching happens by line, by default, but when lines are
|
|
1390
|
+
folded or very long lines are only partially drawn, the
|
|
1391
|
+
decorator may avoid matching part of them for speed. This
|
|
1392
|
+
controls how much additional invisible content it should
|
|
1393
|
+
include in its matches. Defaults to 1000.
|
|
1394
|
+
*/
|
|
1395
|
+
maxLength?: number;
|
|
1387
1396
|
});
|
|
1388
1397
|
/**
|
|
1389
1398
|
Compute the full set of decorations for matches in the given
|
package/dist/index.js
CHANGED
|
@@ -655,6 +655,7 @@ class TextView extends ContentView {
|
|
|
655
655
|
split(from) {
|
|
656
656
|
let result = new TextView(this.text.slice(from));
|
|
657
657
|
this.text = this.text.slice(0, from);
|
|
658
|
+
this.markDirty();
|
|
658
659
|
return result;
|
|
659
660
|
}
|
|
660
661
|
localPosFromDOM(node, offset) {
|
|
@@ -2393,16 +2394,6 @@ class DocView extends ContentView {
|
|
|
2393
2394
|
return true;
|
|
2394
2395
|
}
|
|
2395
2396
|
}
|
|
2396
|
-
reset(sel) {
|
|
2397
|
-
if (this.dirty) {
|
|
2398
|
-
this.view.observer.ignore(() => this.view.docView.sync());
|
|
2399
|
-
this.dirty = 0 /* Not */;
|
|
2400
|
-
this.updateSelection(true);
|
|
2401
|
-
}
|
|
2402
|
-
else {
|
|
2403
|
-
this.updateSelection();
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
2397
|
// Used by update and the constructor do perform the actual DOM
|
|
2407
2398
|
// update
|
|
2408
2399
|
updateInner(changes, deco, oldLength) {
|
|
@@ -2478,7 +2469,8 @@ class DocView extends ContentView {
|
|
|
2478
2469
|
// inside an uneditable node, and not bring it back when we
|
|
2479
2470
|
// move the cursor to its proper position. This tries to
|
|
2480
2471
|
// restore the keyboard by cycling focus.
|
|
2481
|
-
if (browser.android && browser.chrome && this.dom.contains(domSel.focusNode) &&
|
|
2472
|
+
if (browser.android && browser.chrome && this.dom.contains(domSel.focusNode) &&
|
|
2473
|
+
inUneditable(domSel.focusNode, this.dom)) {
|
|
2482
2474
|
this.dom.blur();
|
|
2483
2475
|
this.dom.focus({ preventScroll: true });
|
|
2484
2476
|
}
|
|
@@ -3131,14 +3123,6 @@ class InputState {
|
|
|
3131
3123
|
constructor(view) {
|
|
3132
3124
|
this.lastKeyCode = 0;
|
|
3133
3125
|
this.lastKeyTime = 0;
|
|
3134
|
-
// On Chrome Android, backspace near widgets is just completely
|
|
3135
|
-
// broken, and there are no key events, so we need to handle the
|
|
3136
|
-
// beforeinput event. Deleting stuff will often create a flurry of
|
|
3137
|
-
// events, and interrupting it before it is done just makes
|
|
3138
|
-
// subsequent events even more broken, so again, we hold off doing
|
|
3139
|
-
// anything until the browser is finished with whatever it is trying
|
|
3140
|
-
// to do.
|
|
3141
|
-
this.pendingAndroidKey = undefined;
|
|
3142
3126
|
// On iOS, some keys need to have their default behavior happen
|
|
3143
3127
|
// (after which we retroactively handle them and reset the DOM) to
|
|
3144
3128
|
// avoid messing up the virtual keyboard state.
|
|
@@ -3207,22 +3191,15 @@ class InputState {
|
|
|
3207
3191
|
}
|
|
3208
3192
|
runCustomHandlers(type, view, event) {
|
|
3209
3193
|
for (let set of this.customHandlers) {
|
|
3210
|
-
let handler = set.handlers[type]
|
|
3194
|
+
let handler = set.handlers[type];
|
|
3211
3195
|
if (handler) {
|
|
3212
3196
|
try {
|
|
3213
|
-
|
|
3197
|
+
if (handler.call(set.plugin, event, view))
|
|
3198
|
+
return true;
|
|
3214
3199
|
}
|
|
3215
3200
|
catch (e) {
|
|
3216
3201
|
logException(view.state, e);
|
|
3217
3202
|
}
|
|
3218
|
-
if (handled || event.defaultPrevented) {
|
|
3219
|
-
// Chrome for Android often applies a bunch of nonsensical
|
|
3220
|
-
// DOM changes after an enter press, even when
|
|
3221
|
-
// preventDefault-ed. This tries to ignore those.
|
|
3222
|
-
if (browser.android && type == "keydown" && event.keyCode == 13)
|
|
3223
|
-
view.observer.flushSoon();
|
|
3224
|
-
return true;
|
|
3225
|
-
}
|
|
3226
3203
|
}
|
|
3227
3204
|
}
|
|
3228
3205
|
return false;
|
|
@@ -3246,6 +3223,16 @@ class InputState {
|
|
|
3246
3223
|
this.lastKeyTime = Date.now();
|
|
3247
3224
|
if (this.screenKeyEvent(view, event))
|
|
3248
3225
|
return true;
|
|
3226
|
+
// Chrome for Android usually doesn't fire proper key events, but
|
|
3227
|
+
// occasionally does, usually surrounded by a bunch of complicated
|
|
3228
|
+
// composition changes. When an enter or backspace key event is
|
|
3229
|
+
// seen, hold off on handling DOM events for a bit, and then
|
|
3230
|
+
// dispatch it.
|
|
3231
|
+
if (browser.android && browser.chrome && !event.synthetic &&
|
|
3232
|
+
(event.keyCode == 13 || event.keyCode == 8)) {
|
|
3233
|
+
view.observer.delayAndroidKey(event.key, event.keyCode);
|
|
3234
|
+
return true;
|
|
3235
|
+
}
|
|
3249
3236
|
// Prevent the default behavior of Enter on iOS makes the
|
|
3250
3237
|
// virtual keyboard get stuck in the wrong (lowercase)
|
|
3251
3238
|
// state. So we let it go through, and then, in
|
|
@@ -3267,24 +3254,6 @@ class InputState {
|
|
|
3267
3254
|
this.pendingIOSKey = undefined;
|
|
3268
3255
|
return dispatchKey(view.contentDOM, key.key, key.keyCode);
|
|
3269
3256
|
}
|
|
3270
|
-
// This causes the DOM observer to pause for a bit, and sets an
|
|
3271
|
-
// animation frame (which seems the most reliable way to detect
|
|
3272
|
-
// 'Chrome is done flailing about messing with the DOM') to fire a
|
|
3273
|
-
// fake key event and re-sync the view again.
|
|
3274
|
-
setPendingAndroidKey(view, pending) {
|
|
3275
|
-
this.pendingAndroidKey = pending;
|
|
3276
|
-
requestAnimationFrame(() => {
|
|
3277
|
-
let key = this.pendingAndroidKey;
|
|
3278
|
-
if (!key)
|
|
3279
|
-
return;
|
|
3280
|
-
this.pendingAndroidKey = undefined;
|
|
3281
|
-
view.observer.processRecords();
|
|
3282
|
-
let startState = view.state;
|
|
3283
|
-
dispatchKey(view.contentDOM, key.key, key.keyCode);
|
|
3284
|
-
if (view.state == startState)
|
|
3285
|
-
view.docView.reset(true);
|
|
3286
|
-
});
|
|
3287
|
-
}
|
|
3288
3257
|
ignoreDuringComposition(event) {
|
|
3289
3258
|
if (!/^key/.test(event.type))
|
|
3290
3259
|
return false;
|
|
@@ -3763,12 +3732,12 @@ handlers.compositionstart = handlers.compositionupdate = view => {
|
|
|
3763
3732
|
if (view.inputState.compositionFirstChange == null)
|
|
3764
3733
|
view.inputState.compositionFirstChange = true;
|
|
3765
3734
|
if (view.inputState.composing < 0) {
|
|
3735
|
+
// FIXME possibly set a timeout to clear it again on Android
|
|
3736
|
+
view.inputState.composing = 0;
|
|
3766
3737
|
if (view.docView.compositionDeco.size) {
|
|
3767
3738
|
view.observer.flush();
|
|
3768
3739
|
forceClearComposition(view, true);
|
|
3769
3740
|
}
|
|
3770
|
-
// FIXME possibly set a timeout to clear it again on Android
|
|
3771
|
-
view.inputState.composing = 0;
|
|
3772
3741
|
}
|
|
3773
3742
|
};
|
|
3774
3743
|
handlers.compositionend = view => {
|
|
@@ -3794,7 +3763,7 @@ handlers.beforeinput = (view, event) => {
|
|
|
3794
3763
|
// seems to do nothing at all on Chrome).
|
|
3795
3764
|
let pending;
|
|
3796
3765
|
if (browser.chrome && browser.android && (pending = PendingKeys.find(key => key.inputType == event.inputType))) {
|
|
3797
|
-
view.
|
|
3766
|
+
view.observer.delayAndroidKey(pending.key, pending.keyCode);
|
|
3798
3767
|
if (pending.key == "Backspace" || pending.key == "Delete") {
|
|
3799
3768
|
let startViewHeight = ((_a = window.visualViewport) === null || _a === void 0 ? void 0 : _a.height) || 0;
|
|
3800
3769
|
setTimeout(() => {
|
|
@@ -5182,6 +5151,7 @@ class DOMObserver {
|
|
|
5182
5151
|
this.delayedFlush = -1;
|
|
5183
5152
|
this.resizeTimeout = -1;
|
|
5184
5153
|
this.queue = [];
|
|
5154
|
+
this.delayedAndroidKey = null;
|
|
5185
5155
|
this.scrollTargets = [];
|
|
5186
5156
|
this.intersection = null;
|
|
5187
5157
|
this.resize = null;
|
|
@@ -5265,7 +5235,7 @@ class DOMObserver {
|
|
|
5265
5235
|
}
|
|
5266
5236
|
}
|
|
5267
5237
|
onSelectionChange(event) {
|
|
5268
|
-
if (!this.readSelectionRange())
|
|
5238
|
+
if (!this.readSelectionRange() || this.delayedAndroidKey)
|
|
5269
5239
|
return;
|
|
5270
5240
|
let { view } = this, sel = this.selectionRange;
|
|
5271
5241
|
if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(view.dom, sel))
|
|
@@ -5361,6 +5331,32 @@ class DOMObserver {
|
|
|
5361
5331
|
this.queue.length = 0;
|
|
5362
5332
|
this.selectionChanged = false;
|
|
5363
5333
|
}
|
|
5334
|
+
// Chrome Android, especially in combination with GBoard, not only
|
|
5335
|
+
// doesn't reliably fire regular key events, but also often
|
|
5336
|
+
// surrounds the effect of enter or backspace with a bunch of
|
|
5337
|
+
// composition events that, when interrupted, cause text duplication
|
|
5338
|
+
// or other kinds of corruption. This hack makes the editor back off
|
|
5339
|
+
// from handling DOM changes for a moment when such a key is
|
|
5340
|
+
// detected (via beforeinput or keydown), and then dispatches the
|
|
5341
|
+
// key event, throwing away the DOM changes if it gets handled.
|
|
5342
|
+
delayAndroidKey(key, keyCode) {
|
|
5343
|
+
if (!this.delayedAndroidKey)
|
|
5344
|
+
requestAnimationFrame(() => {
|
|
5345
|
+
let key = this.delayedAndroidKey;
|
|
5346
|
+
this.delayedAndroidKey = null;
|
|
5347
|
+
let startState = this.view.state;
|
|
5348
|
+
if (dispatchKey(this.view.contentDOM, key.key, key.keyCode))
|
|
5349
|
+
this.processRecords();
|
|
5350
|
+
else
|
|
5351
|
+
this.flush();
|
|
5352
|
+
if (this.view.state == startState)
|
|
5353
|
+
this.view.update([]);
|
|
5354
|
+
});
|
|
5355
|
+
// Since backspace beforeinput is sometimes signalled spuriously,
|
|
5356
|
+
// Enter always takes precedence.
|
|
5357
|
+
if (!this.delayedAndroidKey || key == "Enter")
|
|
5358
|
+
this.delayedAndroidKey = { key, keyCode };
|
|
5359
|
+
}
|
|
5364
5360
|
flushSoon() {
|
|
5365
5361
|
if (this.delayedFlush < 0)
|
|
5366
5362
|
this.delayedFlush = window.setTimeout(() => { this.delayedFlush = -1; this.flush(); }, 20);
|
|
@@ -5397,13 +5393,13 @@ class DOMObserver {
|
|
|
5397
5393
|
}
|
|
5398
5394
|
// Apply pending changes, if any
|
|
5399
5395
|
flush(readSelection = true) {
|
|
5400
|
-
if (readSelection)
|
|
5401
|
-
this.readSelectionRange();
|
|
5402
5396
|
// Completely hold off flushing when pending keys are set—the code
|
|
5403
5397
|
// managing those will make sure processRecords is called and the
|
|
5404
5398
|
// view is resynchronized after
|
|
5405
|
-
if (this.delayedFlush >= 0 || this.
|
|
5399
|
+
if (this.delayedFlush >= 0 || this.delayedAndroidKey)
|
|
5406
5400
|
return;
|
|
5401
|
+
if (readSelection)
|
|
5402
|
+
this.readSelectionRange();
|
|
5407
5403
|
let { from, to, typeOver } = this.processRecords();
|
|
5408
5404
|
let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
|
|
5409
5405
|
if (from < 0 && !newSel)
|
|
@@ -6923,7 +6919,7 @@ function measureRange(view, range) {
|
|
|
6923
6919
|
let between = [];
|
|
6924
6920
|
if ((visualStart || startBlock).to < (visualEnd || endBlock).from - 1)
|
|
6925
6921
|
between.push(piece(leftSide, top.bottom, rightSide, bottom.top));
|
|
6926
|
-
else if (top.bottom < bottom.top &&
|
|
6922
|
+
else if (top.bottom < bottom.top && view.elementAtHeight((top.bottom + bottom.top) / 2).type == BlockType.Text)
|
|
6927
6923
|
top.bottom = bottom.top = (top.bottom + bottom.top) / 2;
|
|
6928
6924
|
return pieces(top).concat(between).concat(pieces(bottom));
|
|
6929
6925
|
}
|
|
@@ -6996,6 +6992,22 @@ function iterMatches(doc, re, from, to, f) {
|
|
|
6996
6992
|
f(pos + m.index, pos + m.index + m[0].length, m);
|
|
6997
6993
|
}
|
|
6998
6994
|
}
|
|
6995
|
+
function matchRanges(view, maxLength) {
|
|
6996
|
+
let visible = view.visibleRanges;
|
|
6997
|
+
if (visible.length == 1 && visible[0].from == view.viewport.from &&
|
|
6998
|
+
visible[0].to == view.viewport.to)
|
|
6999
|
+
return visible;
|
|
7000
|
+
let result = [];
|
|
7001
|
+
for (let { from, to } of visible) {
|
|
7002
|
+
from = Math.max(view.state.doc.lineAt(from).from, from - maxLength);
|
|
7003
|
+
to = Math.min(view.state.doc.lineAt(to).to, to + maxLength);
|
|
7004
|
+
if (result.length && result[result.length - 1].to >= from)
|
|
7005
|
+
result[result.length - 1].to = to;
|
|
7006
|
+
else
|
|
7007
|
+
result.push({ from, to });
|
|
7008
|
+
}
|
|
7009
|
+
return result;
|
|
7010
|
+
}
|
|
6999
7011
|
/**
|
|
7000
7012
|
Helper class used to make it easier to maintain decorations on
|
|
7001
7013
|
visible code that matches a given regular expression. To be used
|
|
@@ -7007,12 +7019,13 @@ class MatchDecorator {
|
|
|
7007
7019
|
Create a decorator.
|
|
7008
7020
|
*/
|
|
7009
7021
|
constructor(config) {
|
|
7010
|
-
let { regexp, decoration, boundary } = config;
|
|
7022
|
+
let { regexp, decoration, boundary, maxLength = 1000 } = config;
|
|
7011
7023
|
if (!regexp.global)
|
|
7012
7024
|
throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");
|
|
7013
7025
|
this.regexp = regexp;
|
|
7014
7026
|
this.getDeco = typeof decoration == "function" ? decoration : () => decoration;
|
|
7015
7027
|
this.boundary = boundary;
|
|
7028
|
+
this.maxLength = maxLength;
|
|
7016
7029
|
}
|
|
7017
7030
|
/**
|
|
7018
7031
|
Compute the full set of decorations for matches in the given
|
|
@@ -7021,7 +7034,7 @@ class MatchDecorator {
|
|
|
7021
7034
|
*/
|
|
7022
7035
|
createDeco(view) {
|
|
7023
7036
|
let build = new RangeSetBuilder();
|
|
7024
|
-
for (let { from, to } of view.
|
|
7037
|
+
for (let { from, to } of matchRanges(view, this.maxLength))
|
|
7025
7038
|
iterMatches(view.state.doc, this.regexp, from, to, (a, b, m) => build.add(a, b, this.getDeco(m, view, a)));
|
|
7026
7039
|
return build.finish();
|
|
7027
7040
|
}
|