@codemirror/view 6.26.4 → 6.28.0
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 +16 -0
- package/dist/index.cjs +263 -42
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +263 -42
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 6.28.0 (2024-06-10)
|
|
2
|
+
|
|
3
|
+
### Bug fixes
|
|
4
|
+
|
|
5
|
+
Fix an issue where long lines broken up by block widgets were sometimes only partially rendered.
|
|
6
|
+
|
|
7
|
+
### New features
|
|
8
|
+
|
|
9
|
+
The editor will now, when available (which is only on Chrome for the foreseeable future) use the [`EditContext`](https://developer.mozilla.org/en-US/docs/Web/API/EditContext) API to capture text input.
|
|
10
|
+
|
|
11
|
+
## 6.27.0 (2024-06-04)
|
|
12
|
+
|
|
13
|
+
### New features
|
|
14
|
+
|
|
15
|
+
The new `setTabFocusMode` method can be used to control whether the editor disables key bindings for Tab and Shift-Tab.
|
|
16
|
+
|
|
1
17
|
## 6.26.4 (2024-06-04)
|
|
2
18
|
|
|
3
19
|
### Bug fixes
|
package/dist/index.cjs
CHANGED
|
@@ -2368,6 +2368,7 @@ class ScrollTarget {
|
|
|
2368
2368
|
}
|
|
2369
2369
|
}
|
|
2370
2370
|
const scrollIntoView = state.StateEffect.define({ map: (t, ch) => t.map(ch) });
|
|
2371
|
+
const setEditContextFormatting = state.StateEffect.define();
|
|
2371
2372
|
/**
|
|
2372
2373
|
Log or report an unhandled exception in client code. Should
|
|
2373
2374
|
probably only be used by extension code that allows client code to
|
|
@@ -2704,10 +2705,11 @@ class DocView extends ContentView {
|
|
|
2704
2705
|
super();
|
|
2705
2706
|
this.view = view;
|
|
2706
2707
|
this.decorations = [];
|
|
2707
|
-
this.dynamicDecorationMap = [];
|
|
2708
|
+
this.dynamicDecorationMap = [false];
|
|
2708
2709
|
this.domChanged = null;
|
|
2709
2710
|
this.hasComposition = null;
|
|
2710
2711
|
this.markedForComposition = new Set;
|
|
2712
|
+
this.editContextFormatting = Decoration.none;
|
|
2711
2713
|
this.lastCompositionAfterCursor = false;
|
|
2712
2714
|
// Track a minimum width for the editor. When measuring sizes in
|
|
2713
2715
|
// measureVisibleLineHeights, this is updated to point at the width
|
|
@@ -2746,8 +2748,9 @@ class DocView extends ContentView {
|
|
|
2746
2748
|
this.minWidthTo = update.changes.mapPos(this.minWidthTo, 1);
|
|
2747
2749
|
}
|
|
2748
2750
|
}
|
|
2751
|
+
this.updateEditContextFormatting(update);
|
|
2749
2752
|
let readCompositionAt = -1;
|
|
2750
|
-
if (this.view.inputState.composing >= 0) {
|
|
2753
|
+
if (this.view.inputState.composing >= 0 && !this.view.observer.editContext) {
|
|
2751
2754
|
if ((_a = this.domChanged) === null || _a === void 0 ? void 0 : _a.newSel)
|
|
2752
2755
|
readCompositionAt = this.domChanged.newSel.head;
|
|
2753
2756
|
else if (!touchesComposition(update.changes, this.hasComposition) && !update.selectionSet)
|
|
@@ -2855,6 +2858,14 @@ class DocView extends ContentView {
|
|
|
2855
2858
|
if (composition)
|
|
2856
2859
|
this.fixCompositionDOM(composition);
|
|
2857
2860
|
}
|
|
2861
|
+
updateEditContextFormatting(update) {
|
|
2862
|
+
this.editContextFormatting = this.editContextFormatting.map(update.changes);
|
|
2863
|
+
for (let tr of update.transactions)
|
|
2864
|
+
for (let effect of tr.effects)
|
|
2865
|
+
if (effect.is(setEditContextFormatting)) {
|
|
2866
|
+
this.editContextFormatting = effect.value;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2858
2869
|
compositionView(composition) {
|
|
2859
2870
|
let cur = new TextView(composition.text.nodeValue);
|
|
2860
2871
|
cur.flags |= 8 /* ViewFlag.Composition */;
|
|
@@ -3186,7 +3197,7 @@ class DocView extends ContentView {
|
|
|
3186
3197
|
return Decoration.set(deco);
|
|
3187
3198
|
}
|
|
3188
3199
|
updateDeco() {
|
|
3189
|
-
let i =
|
|
3200
|
+
let i = 1;
|
|
3190
3201
|
let allDeco = this.view.state.facet(decorations).map(d => {
|
|
3191
3202
|
let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
|
|
3192
3203
|
return dynamic ? d(this.view) : d;
|
|
@@ -3202,6 +3213,7 @@ class DocView extends ContentView {
|
|
|
3202
3213
|
allDeco.push(state.RangeSet.join(outerDeco));
|
|
3203
3214
|
}
|
|
3204
3215
|
this.decorations = [
|
|
3216
|
+
this.editContextFormatting,
|
|
3205
3217
|
...allDeco,
|
|
3206
3218
|
this.computeBlockGapDeco(),
|
|
3207
3219
|
this.view.viewState.lineGapDeco
|
|
@@ -3742,9 +3754,16 @@ class InputState {
|
|
|
3742
3754
|
// (after which we retroactively handle them and reset the DOM) to
|
|
3743
3755
|
// avoid messing up the virtual keyboard state.
|
|
3744
3756
|
this.pendingIOSKey = undefined;
|
|
3757
|
+
/**
|
|
3758
|
+
When enabled (>-1), tab presses are not given to key handlers,
|
|
3759
|
+
leaving the browser's default behavior. If >0, the mode expires
|
|
3760
|
+
at that timestamp, and any other keypress clears it.
|
|
3761
|
+
Esc enables temporary tab focus mode for two seconds when not
|
|
3762
|
+
otherwise handled.
|
|
3763
|
+
*/
|
|
3764
|
+
this.tabFocusMode = -1;
|
|
3745
3765
|
this.lastSelectionOrigin = null;
|
|
3746
3766
|
this.lastSelectionTime = 0;
|
|
3747
|
-
this.lastEscPress = 0;
|
|
3748
3767
|
this.lastContextMenu = 0;
|
|
3749
3768
|
this.scrollHandlers = [];
|
|
3750
3769
|
this.handlers = Object.create(null);
|
|
@@ -3824,10 +3843,10 @@ class InputState {
|
|
|
3824
3843
|
// Must always run, even if a custom handler handled the event
|
|
3825
3844
|
this.lastKeyCode = event.keyCode;
|
|
3826
3845
|
this.lastKeyTime = Date.now();
|
|
3827
|
-
if (event.keyCode == 9 && Date.now()
|
|
3846
|
+
if (event.keyCode == 9 && this.tabFocusMode > -1 && (!this.tabFocusMode || Date.now() <= this.tabFocusMode))
|
|
3828
3847
|
return true;
|
|
3829
|
-
if (event.keyCode != 27 && modifierCodes.indexOf(event.keyCode) < 0)
|
|
3830
|
-
this.
|
|
3848
|
+
if (this.tabFocusMode > 0 && event.keyCode != 27 && modifierCodes.indexOf(event.keyCode) < 0)
|
|
3849
|
+
this.tabFocusMode = -1;
|
|
3831
3850
|
// Chrome for Android usually doesn't fire proper key events, but
|
|
3832
3851
|
// occasionally does, usually surrounded by a bunch of complicated
|
|
3833
3852
|
// composition changes. When an enter or backspace key event is
|
|
@@ -3888,6 +3907,7 @@ class InputState {
|
|
|
3888
3907
|
this.mouseSelection = mouseSelection;
|
|
3889
3908
|
}
|
|
3890
3909
|
update(update) {
|
|
3910
|
+
this.view.observer.update(update);
|
|
3891
3911
|
if (this.mouseSelection)
|
|
3892
3912
|
this.mouseSelection.update(update);
|
|
3893
3913
|
if (this.draggedContent && update.docChanged)
|
|
@@ -4165,8 +4185,8 @@ observers.scroll = view => {
|
|
|
4165
4185
|
};
|
|
4166
4186
|
handlers.keydown = (view, event) => {
|
|
4167
4187
|
view.inputState.setSelectionOrigin("select");
|
|
4168
|
-
if (event.keyCode == 27)
|
|
4169
|
-
view.inputState.
|
|
4188
|
+
if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
|
|
4189
|
+
view.inputState.tabFocusMode = Date.now() + 2000;
|
|
4170
4190
|
return false;
|
|
4171
4191
|
};
|
|
4172
4192
|
observers.touchstart = (view, e) => {
|
|
@@ -4485,6 +4505,8 @@ observers.blur = view => {
|
|
|
4485
4505
|
updateForFocusChange(view);
|
|
4486
4506
|
};
|
|
4487
4507
|
observers.compositionstart = observers.compositionupdate = view => {
|
|
4508
|
+
if (view.observer.editContext)
|
|
4509
|
+
return; // Composition handled by edit context
|
|
4488
4510
|
if (view.inputState.compositionFirstChange == null)
|
|
4489
4511
|
view.inputState.compositionFirstChange = true;
|
|
4490
4512
|
if (view.inputState.composing < 0) {
|
|
@@ -4493,6 +4515,8 @@ observers.compositionstart = observers.compositionupdate = view => {
|
|
|
4493
4515
|
}
|
|
4494
4516
|
};
|
|
4495
4517
|
observers.compositionend = view => {
|
|
4518
|
+
if (view.observer.editContext)
|
|
4519
|
+
return; // Composition handled by edit context
|
|
4496
4520
|
view.inputState.composing = -1;
|
|
4497
4521
|
view.inputState.compositionEndedAt = Date.now();
|
|
4498
4522
|
view.inputState.compositionPendingKey = true;
|
|
@@ -5686,12 +5710,12 @@ class ViewState {
|
|
|
5686
5710
|
}
|
|
5687
5711
|
gaps.push(gap);
|
|
5688
5712
|
};
|
|
5689
|
-
|
|
5690
|
-
if (line.length < doubleMargin)
|
|
5691
|
-
|
|
5713
|
+
let checkLine = (line) => {
|
|
5714
|
+
if (line.length < doubleMargin || line.type != exports.BlockType.Text)
|
|
5715
|
+
return;
|
|
5692
5716
|
let structure = lineStructure(line.from, line.to, this.stateDeco);
|
|
5693
5717
|
if (structure.total < doubleMargin)
|
|
5694
|
-
|
|
5718
|
+
return;
|
|
5695
5719
|
let target = this.scrollTarget ? this.scrollTarget.range.head : null;
|
|
5696
5720
|
let viewFrom, viewTo;
|
|
5697
5721
|
if (wrapping) {
|
|
@@ -5731,6 +5755,12 @@ class ViewState {
|
|
|
5731
5755
|
addGap(line.from, viewFrom, line, structure);
|
|
5732
5756
|
if (viewTo < line.to)
|
|
5733
5757
|
addGap(viewTo, line.to, line, structure);
|
|
5758
|
+
};
|
|
5759
|
+
for (let line of this.viewportLines) {
|
|
5760
|
+
if (Array.isArray(line.type))
|
|
5761
|
+
line.type.forEach(checkLine);
|
|
5762
|
+
else
|
|
5763
|
+
checkLine(line);
|
|
5734
5764
|
}
|
|
5735
5765
|
return gaps;
|
|
5736
5766
|
}
|
|
@@ -6390,35 +6420,7 @@ function applyDOMChange(view, domChange) {
|
|
|
6390
6420
|
change = { from: sel.from, to: sel.to, insert: state.Text.of([" "]) };
|
|
6391
6421
|
}
|
|
6392
6422
|
if (change) {
|
|
6393
|
-
|
|
6394
|
-
return true;
|
|
6395
|
-
// Android browsers don't fire reasonable key events for enter,
|
|
6396
|
-
// backspace, or delete. So this detects changes that look like
|
|
6397
|
-
// they're caused by those keys, and reinterprets them as key
|
|
6398
|
-
// events. (Some of these keys are also handled by beforeinput
|
|
6399
|
-
// events and the pendingAndroidKey mechanism, but that's not
|
|
6400
|
-
// reliable in all situations.)
|
|
6401
|
-
if (browser.android &&
|
|
6402
|
-
((change.to == sel.to &&
|
|
6403
|
-
// GBoard will sometimes remove a space it just inserted
|
|
6404
|
-
// after a completion when you press enter
|
|
6405
|
-
(change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
|
|
6406
|
-
change.insert.length == 1 && change.insert.lines == 2 &&
|
|
6407
|
-
dispatchKey(view.contentDOM, "Enter", 13)) ||
|
|
6408
|
-
((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
|
|
6409
|
-
lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
|
|
6410
|
-
dispatchKey(view.contentDOM, "Backspace", 8)) ||
|
|
6411
|
-
(change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
|
|
6412
|
-
dispatchKey(view.contentDOM, "Delete", 46))))
|
|
6413
|
-
return true;
|
|
6414
|
-
let text = change.insert.toString();
|
|
6415
|
-
if (view.inputState.composing >= 0)
|
|
6416
|
-
view.inputState.composing++;
|
|
6417
|
-
let defaultTr;
|
|
6418
|
-
let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
|
|
6419
|
-
if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
|
|
6420
|
-
view.dispatch(defaultInsert());
|
|
6421
|
-
return true;
|
|
6423
|
+
return applyDOMChangeInner(view, change, newSel, lastKey);
|
|
6422
6424
|
}
|
|
6423
6425
|
else if (newSel && !newSel.main.eq(sel)) {
|
|
6424
6426
|
let scrollIntoView = false, userEvent = "select";
|
|
@@ -6434,6 +6436,38 @@ function applyDOMChange(view, domChange) {
|
|
|
6434
6436
|
return false;
|
|
6435
6437
|
}
|
|
6436
6438
|
}
|
|
6439
|
+
function applyDOMChangeInner(view, change, newSel, lastKey = -1) {
|
|
6440
|
+
if (browser.ios && view.inputState.flushIOSKey(change))
|
|
6441
|
+
return true;
|
|
6442
|
+
let sel = view.state.selection.main;
|
|
6443
|
+
// Android browsers don't fire reasonable key events for enter,
|
|
6444
|
+
// backspace, or delete. So this detects changes that look like
|
|
6445
|
+
// they're caused by those keys, and reinterprets them as key
|
|
6446
|
+
// events. (Some of these keys are also handled by beforeinput
|
|
6447
|
+
// events and the pendingAndroidKey mechanism, but that's not
|
|
6448
|
+
// reliable in all situations.)
|
|
6449
|
+
if (browser.android &&
|
|
6450
|
+
((change.to == sel.to &&
|
|
6451
|
+
// GBoard will sometimes remove a space it just inserted
|
|
6452
|
+
// after a completion when you press enter
|
|
6453
|
+
(change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
|
|
6454
|
+
change.insert.length == 1 && change.insert.lines == 2 &&
|
|
6455
|
+
dispatchKey(view.contentDOM, "Enter", 13)) ||
|
|
6456
|
+
((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
|
|
6457
|
+
lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
|
|
6458
|
+
dispatchKey(view.contentDOM, "Backspace", 8)) ||
|
|
6459
|
+
(change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
|
|
6460
|
+
dispatchKey(view.contentDOM, "Delete", 46))))
|
|
6461
|
+
return true;
|
|
6462
|
+
let text = change.insert.toString();
|
|
6463
|
+
if (view.inputState.composing >= 0)
|
|
6464
|
+
view.inputState.composing++;
|
|
6465
|
+
let defaultTr;
|
|
6466
|
+
let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
|
|
6467
|
+
if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
|
|
6468
|
+
view.dispatch(defaultInsert());
|
|
6469
|
+
return true;
|
|
6470
|
+
}
|
|
6437
6471
|
function applyDefaultInsert(view, change, newSel) {
|
|
6438
6472
|
let tr, startState = view.state, sel = startState.selection.main;
|
|
6439
6473
|
if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
|
|
@@ -6560,6 +6594,7 @@ class DOMObserver {
|
|
|
6560
6594
|
constructor(view) {
|
|
6561
6595
|
this.view = view;
|
|
6562
6596
|
this.active = false;
|
|
6597
|
+
this.editContext = null;
|
|
6563
6598
|
// The known selection. Kept in our own object, as opposed to just
|
|
6564
6599
|
// directly accessing the selection because:
|
|
6565
6600
|
// - Safari doesn't report the right selection in shadow DOM
|
|
@@ -6604,6 +6639,10 @@ class DOMObserver {
|
|
|
6604
6639
|
else
|
|
6605
6640
|
this.flush();
|
|
6606
6641
|
});
|
|
6642
|
+
if (window.EditContext && view.constructor.EDIT_CONTEXT !== false) {
|
|
6643
|
+
this.editContext = new EditContextManager(view);
|
|
6644
|
+
view.contentDOM.editContext = this.editContext.editContext;
|
|
6645
|
+
}
|
|
6607
6646
|
if (useCharData)
|
|
6608
6647
|
this.onCharData = (event) => {
|
|
6609
6648
|
this.queue.push({ target: event.target,
|
|
@@ -6654,6 +6693,8 @@ class DOMObserver {
|
|
|
6654
6693
|
onScroll(e) {
|
|
6655
6694
|
if (this.intersecting)
|
|
6656
6695
|
this.flush(false);
|
|
6696
|
+
if (this.editContext)
|
|
6697
|
+
this.view.requestMeasure(this.editContext.measureReq);
|
|
6657
6698
|
this.onScrollChanged(e);
|
|
6658
6699
|
}
|
|
6659
6700
|
onResize() {
|
|
@@ -6963,6 +7004,10 @@ class DOMObserver {
|
|
|
6963
7004
|
win.removeEventListener("beforeprint", this.onPrint);
|
|
6964
7005
|
win.document.removeEventListener("selectionchange", this.onSelectionChange);
|
|
6965
7006
|
}
|
|
7007
|
+
update(update) {
|
|
7008
|
+
if (this.editContext)
|
|
7009
|
+
this.editContext.update(update);
|
|
7010
|
+
}
|
|
6966
7011
|
destroy() {
|
|
6967
7012
|
var _a, _b, _c;
|
|
6968
7013
|
this.stop();
|
|
@@ -7022,6 +7067,161 @@ function safariSelectionRangeHack(view, selection) {
|
|
|
7022
7067
|
view.contentDOM.removeEventListener("beforeinput", read, true);
|
|
7023
7068
|
return found ? buildSelectionRangeFromRange(view, found) : null;
|
|
7024
7069
|
}
|
|
7070
|
+
class EditContextManager {
|
|
7071
|
+
constructor(view) {
|
|
7072
|
+
// The document window for which the text in the context is
|
|
7073
|
+
// maintained. For large documents, this may be smaller than the
|
|
7074
|
+
// editor document. This window always includes the selection head.
|
|
7075
|
+
this.from = 0;
|
|
7076
|
+
this.to = 0;
|
|
7077
|
+
// When applying a transaction, this is used to compare the change
|
|
7078
|
+
// made to the context content to the change in the transaction in
|
|
7079
|
+
// order to make the minimal changes to the context (since touching
|
|
7080
|
+
// that sometimes breaks series of multiple edits made for a single
|
|
7081
|
+
// user action on some Android keyboards)
|
|
7082
|
+
this.pendingContextChange = null;
|
|
7083
|
+
this.resetRange(view.state);
|
|
7084
|
+
let context = this.editContext = new window.EditContext({
|
|
7085
|
+
text: view.state.doc.sliceString(this.from, this.to),
|
|
7086
|
+
selectionStart: this.toContextPos(Math.max(this.from, Math.min(this.to, view.state.selection.main.anchor))),
|
|
7087
|
+
selectionEnd: this.toContextPos(view.state.selection.main.head)
|
|
7088
|
+
});
|
|
7089
|
+
context.addEventListener("textupdate", e => {
|
|
7090
|
+
let { anchor } = view.state.selection.main;
|
|
7091
|
+
let change = { from: this.toEditorPos(e.updateRangeStart),
|
|
7092
|
+
to: this.toEditorPos(e.updateRangeEnd),
|
|
7093
|
+
insert: state.Text.of(e.text.split("\n")) };
|
|
7094
|
+
// If the window doesn't include the anchor, assume changes
|
|
7095
|
+
// adjacent to a side go up to the anchor.
|
|
7096
|
+
if (change.from == this.from && anchor < this.from)
|
|
7097
|
+
change.from = anchor;
|
|
7098
|
+
else if (change.to == this.to && anchor > this.to)
|
|
7099
|
+
change.to = anchor;
|
|
7100
|
+
// Edit context sometimes fire empty changes
|
|
7101
|
+
if (change.from == change.to && !change.insert.length)
|
|
7102
|
+
return;
|
|
7103
|
+
this.pendingContextChange = change;
|
|
7104
|
+
applyDOMChangeInner(view, change, state.EditorSelection.single(this.toEditorPos(e.selectionStart), this.toEditorPos(e.selectionEnd)));
|
|
7105
|
+
// If the transaction didn't flush our change, revert it so
|
|
7106
|
+
// that the context is in sync with the editor state again.
|
|
7107
|
+
if (this.pendingContextChange)
|
|
7108
|
+
this.revertPending(view.state);
|
|
7109
|
+
});
|
|
7110
|
+
context.addEventListener("characterboundsupdate", e => {
|
|
7111
|
+
let rects = [], prev = null;
|
|
7112
|
+
for (let i = this.toEditorPos(e.rangeStart), end = this.toEditorPos(e.rangeEnd); i < end; i++) {
|
|
7113
|
+
let rect = view.coordsForChar(i);
|
|
7114
|
+
prev = (rect && new DOMRect(rect.left, rect.right, rect.right - rect.left, rect.bottom - rect.top))
|
|
7115
|
+
|| prev || new DOMRect;
|
|
7116
|
+
rects.push(prev);
|
|
7117
|
+
}
|
|
7118
|
+
context.updateCharacterBounds(e.rangeStart, rects);
|
|
7119
|
+
});
|
|
7120
|
+
context.addEventListener("textformatupdate", e => {
|
|
7121
|
+
let deco = [];
|
|
7122
|
+
for (let format of e.getTextFormats()) {
|
|
7123
|
+
let lineStyle = format.underlineStyle, thickness = format.underlineThickness;
|
|
7124
|
+
if (lineStyle != "None" && thickness != "None") {
|
|
7125
|
+
let style = `text-decoration: underline ${lineStyle == "Dashed" ? "dashed " : lineStyle == "Squiggle" ? "wavy " : ""}${thickness == "Thin" ? 1 : 2}px`;
|
|
7126
|
+
deco.push(Decoration.mark({ attributes: { style } })
|
|
7127
|
+
.range(this.toEditorPos(format.rangeStart), this.toEditorPos(format.rangeEnd)));
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
view.dispatch({ effects: setEditContextFormatting.of(Decoration.set(deco)) });
|
|
7131
|
+
});
|
|
7132
|
+
context.addEventListener("compositionstart", () => {
|
|
7133
|
+
if (view.inputState.composing < 0) {
|
|
7134
|
+
view.inputState.composing = 0;
|
|
7135
|
+
view.inputState.compositionFirstChange = true;
|
|
7136
|
+
}
|
|
7137
|
+
});
|
|
7138
|
+
context.addEventListener("compositionend", () => {
|
|
7139
|
+
view.inputState.composing = -1;
|
|
7140
|
+
view.inputState.compositionFirstChange = null;
|
|
7141
|
+
});
|
|
7142
|
+
this.measureReq = { read: view => {
|
|
7143
|
+
this.editContext.updateControlBounds(view.contentDOM.getBoundingClientRect());
|
|
7144
|
+
let sel = getSelection(view.root);
|
|
7145
|
+
if (sel && sel.rangeCount)
|
|
7146
|
+
this.editContext.updateSelectionBounds(sel.getRangeAt(0).getBoundingClientRect());
|
|
7147
|
+
} };
|
|
7148
|
+
}
|
|
7149
|
+
applyEdits(update) {
|
|
7150
|
+
let off = 0, abort = false, pending = this.pendingContextChange;
|
|
7151
|
+
update.changes.iterChanges((fromA, toA, _fromB, _toB, insert) => {
|
|
7152
|
+
if (abort)
|
|
7153
|
+
return;
|
|
7154
|
+
let dLen = insert.length - (toA - fromA);
|
|
7155
|
+
if (pending && toA >= pending.to) {
|
|
7156
|
+
if (pending.from == fromA && pending.to == toA && pending.insert.eq(insert)) {
|
|
7157
|
+
pending = this.pendingContextChange = null; // Match
|
|
7158
|
+
off += dLen;
|
|
7159
|
+
return;
|
|
7160
|
+
}
|
|
7161
|
+
else { // Mismatch, revert
|
|
7162
|
+
pending = null;
|
|
7163
|
+
this.revertPending(update.state);
|
|
7164
|
+
}
|
|
7165
|
+
}
|
|
7166
|
+
fromA += off;
|
|
7167
|
+
toA += off;
|
|
7168
|
+
if (toA <= this.from) { // Before the window
|
|
7169
|
+
this.from += dLen;
|
|
7170
|
+
this.to += dLen;
|
|
7171
|
+
}
|
|
7172
|
+
else if (fromA < this.to) { // Overlaps with window
|
|
7173
|
+
if (fromA < this.from || toA > this.to || (this.to - this.from) + insert.length > 30000 /* CxVp.MaxSize */) {
|
|
7174
|
+
abort = true;
|
|
7175
|
+
return;
|
|
7176
|
+
}
|
|
7177
|
+
this.editContext.updateText(this.toContextPos(fromA), this.toContextPos(toA), insert.toString());
|
|
7178
|
+
this.to += dLen;
|
|
7179
|
+
}
|
|
7180
|
+
off += dLen;
|
|
7181
|
+
});
|
|
7182
|
+
if (pending && !abort)
|
|
7183
|
+
this.revertPending(update.state);
|
|
7184
|
+
return !abort;
|
|
7185
|
+
}
|
|
7186
|
+
update(update) {
|
|
7187
|
+
if (!this.applyEdits(update) || !this.rangeIsValid(update.state)) {
|
|
7188
|
+
this.pendingContextChange = null;
|
|
7189
|
+
this.resetRange(update.state);
|
|
7190
|
+
this.editContext.updateText(0, this.editContext.text.length, update.state.doc.sliceString(this.from, this.to));
|
|
7191
|
+
this.setSelection(update.state);
|
|
7192
|
+
}
|
|
7193
|
+
else if (update.docChanged || update.selectionSet) {
|
|
7194
|
+
this.setSelection(update.state);
|
|
7195
|
+
}
|
|
7196
|
+
if (update.geometryChanged || update.docChanged || update.selectionSet)
|
|
7197
|
+
update.view.requestMeasure(this.measureReq);
|
|
7198
|
+
}
|
|
7199
|
+
resetRange(state) {
|
|
7200
|
+
let { head } = state.selection.main;
|
|
7201
|
+
this.from = Math.max(0, head - 10000 /* CxVp.Margin */);
|
|
7202
|
+
this.to = Math.min(state.doc.length, head + 10000 /* CxVp.Margin */);
|
|
7203
|
+
}
|
|
7204
|
+
revertPending(state) {
|
|
7205
|
+
let pending = this.pendingContextChange;
|
|
7206
|
+
this.pendingContextChange = null;
|
|
7207
|
+
this.editContext.updateText(this.toContextPos(pending.from), this.toContextPos(pending.to + pending.insert.length), state.doc.sliceString(pending.from, pending.to));
|
|
7208
|
+
}
|
|
7209
|
+
setSelection(state) {
|
|
7210
|
+
let { main } = state.selection;
|
|
7211
|
+
let start = this.toContextPos(Math.max(this.from, Math.min(this.to, main.anchor)));
|
|
7212
|
+
let end = this.toContextPos(main.head);
|
|
7213
|
+
if (this.editContext.selectionStart != start || this.editContext.selectionEnd != end)
|
|
7214
|
+
this.editContext.updateSelection(start, end);
|
|
7215
|
+
}
|
|
7216
|
+
rangeIsValid(state) {
|
|
7217
|
+
let { head } = state.selection.main;
|
|
7218
|
+
return !(this.from > 0 && head - this.from < 500 /* CxVp.MinMargin */ ||
|
|
7219
|
+
this.to < state.doc.length && this.to - head < 500 /* CxVp.MinMargin */ ||
|
|
7220
|
+
this.to - this.from > 10000 /* CxVp.Margin */ * 3);
|
|
7221
|
+
}
|
|
7222
|
+
toEditorPos(contextPos) { return contextPos + this.from; }
|
|
7223
|
+
toContextPos(editorPos) { return editorPos - this.from; }
|
|
7224
|
+
}
|
|
7025
7225
|
|
|
7026
7226
|
// The editor's update state machine looks something like this:
|
|
7027
7227
|
//
|
|
@@ -7844,6 +8044,8 @@ class EditorView {
|
|
|
7844
8044
|
calling this.
|
|
7845
8045
|
*/
|
|
7846
8046
|
destroy() {
|
|
8047
|
+
if (this.root.activeElement == this.contentDOM)
|
|
8048
|
+
this.contentDOM.blur();
|
|
7847
8049
|
for (let plugin of this.plugins)
|
|
7848
8050
|
plugin.destroy(this);
|
|
7849
8051
|
this.plugins = [];
|
|
@@ -7881,6 +8083,25 @@ class EditorView {
|
|
|
7881
8083
|
return scrollIntoView.of(new ScrollTarget(state.EditorSelection.cursor(ref.from), "start", "start", ref.top - scrollTop, scrollLeft, true));
|
|
7882
8084
|
}
|
|
7883
8085
|
/**
|
|
8086
|
+
Enable or disable tab-focus mode, which disables key bindings
|
|
8087
|
+
for Tab and Shift-Tab, letting the browser's default
|
|
8088
|
+
focus-changing behavior go through instead. This is useful to
|
|
8089
|
+
prevent trapping keyboard users in your editor.
|
|
8090
|
+
|
|
8091
|
+
Without argument, this toggles the mode. With a boolean, it
|
|
8092
|
+
enables (true) or disables it (false). Given a number, it
|
|
8093
|
+
temporarily enables the mode until that number of milliseconds
|
|
8094
|
+
have passed or another non-Tab key is pressed.
|
|
8095
|
+
*/
|
|
8096
|
+
setTabFocusMode(to) {
|
|
8097
|
+
if (to == null)
|
|
8098
|
+
this.inputState.tabFocusMode = this.inputState.tabFocusMode < 0 ? 0 : -1;
|
|
8099
|
+
else if (typeof to == "boolean")
|
|
8100
|
+
this.inputState.tabFocusMode = to ? 0 : -1;
|
|
8101
|
+
else if (this.inputState.tabFocusMode != 0)
|
|
8102
|
+
this.inputState.tabFocusMode = Date.now() + to;
|
|
8103
|
+
}
|
|
8104
|
+
/**
|
|
7884
8105
|
Returns an extension that can be used to add DOM event handlers.
|
|
7885
8106
|
The value should be an object mapping event names to handler
|
|
7886
8107
|
functions. For any given event, such functions are ordered by
|
package/dist/index.d.cts
CHANGED
|
@@ -1092,6 +1092,18 @@ declare class EditorView {
|
|
|
1092
1092
|
*/
|
|
1093
1093
|
scrollSnapshot(): StateEffect<ScrollTarget>;
|
|
1094
1094
|
/**
|
|
1095
|
+
Enable or disable tab-focus mode, which disables key bindings
|
|
1096
|
+
for Tab and Shift-Tab, letting the browser's default
|
|
1097
|
+
focus-changing behavior go through instead. This is useful to
|
|
1098
|
+
prevent trapping keyboard users in your editor.
|
|
1099
|
+
|
|
1100
|
+
Without argument, this toggles the mode. With a boolean, it
|
|
1101
|
+
enables (true) or disables it (false). Given a number, it
|
|
1102
|
+
temporarily enables the mode until that number of milliseconds
|
|
1103
|
+
have passed or another non-Tab key is pressed.
|
|
1104
|
+
*/
|
|
1105
|
+
setTabFocusMode(to?: boolean | number): void;
|
|
1106
|
+
/**
|
|
1095
1107
|
Facet to add a [style
|
|
1096
1108
|
module](https://github.com/marijnh/style-mod#documentation) to
|
|
1097
1109
|
an editor view. The view will ensure that the module is
|
package/dist/index.d.ts
CHANGED
|
@@ -1092,6 +1092,18 @@ declare class EditorView {
|
|
|
1092
1092
|
*/
|
|
1093
1093
|
scrollSnapshot(): StateEffect<ScrollTarget>;
|
|
1094
1094
|
/**
|
|
1095
|
+
Enable or disable tab-focus mode, which disables key bindings
|
|
1096
|
+
for Tab and Shift-Tab, letting the browser's default
|
|
1097
|
+
focus-changing behavior go through instead. This is useful to
|
|
1098
|
+
prevent trapping keyboard users in your editor.
|
|
1099
|
+
|
|
1100
|
+
Without argument, this toggles the mode. With a boolean, it
|
|
1101
|
+
enables (true) or disables it (false). Given a number, it
|
|
1102
|
+
temporarily enables the mode until that number of milliseconds
|
|
1103
|
+
have passed or another non-Tab key is pressed.
|
|
1104
|
+
*/
|
|
1105
|
+
setTabFocusMode(to?: boolean | number): void;
|
|
1106
|
+
/**
|
|
1095
1107
|
Facet to add a [style
|
|
1096
1108
|
module](https://github.com/marijnh/style-mod#documentation) to
|
|
1097
1109
|
an editor view. The view will ensure that the module is
|
package/dist/index.js
CHANGED
|
@@ -2364,6 +2364,7 @@ class ScrollTarget {
|
|
|
2364
2364
|
}
|
|
2365
2365
|
}
|
|
2366
2366
|
const scrollIntoView = /*@__PURE__*/StateEffect.define({ map: (t, ch) => t.map(ch) });
|
|
2367
|
+
const setEditContextFormatting = /*@__PURE__*/StateEffect.define();
|
|
2367
2368
|
/**
|
|
2368
2369
|
Log or report an unhandled exception in client code. Should
|
|
2369
2370
|
probably only be used by extension code that allows client code to
|
|
@@ -2700,10 +2701,11 @@ class DocView extends ContentView {
|
|
|
2700
2701
|
super();
|
|
2701
2702
|
this.view = view;
|
|
2702
2703
|
this.decorations = [];
|
|
2703
|
-
this.dynamicDecorationMap = [];
|
|
2704
|
+
this.dynamicDecorationMap = [false];
|
|
2704
2705
|
this.domChanged = null;
|
|
2705
2706
|
this.hasComposition = null;
|
|
2706
2707
|
this.markedForComposition = new Set;
|
|
2708
|
+
this.editContextFormatting = Decoration.none;
|
|
2707
2709
|
this.lastCompositionAfterCursor = false;
|
|
2708
2710
|
// Track a minimum width for the editor. When measuring sizes in
|
|
2709
2711
|
// measureVisibleLineHeights, this is updated to point at the width
|
|
@@ -2742,8 +2744,9 @@ class DocView extends ContentView {
|
|
|
2742
2744
|
this.minWidthTo = update.changes.mapPos(this.minWidthTo, 1);
|
|
2743
2745
|
}
|
|
2744
2746
|
}
|
|
2747
|
+
this.updateEditContextFormatting(update);
|
|
2745
2748
|
let readCompositionAt = -1;
|
|
2746
|
-
if (this.view.inputState.composing >= 0) {
|
|
2749
|
+
if (this.view.inputState.composing >= 0 && !this.view.observer.editContext) {
|
|
2747
2750
|
if ((_a = this.domChanged) === null || _a === void 0 ? void 0 : _a.newSel)
|
|
2748
2751
|
readCompositionAt = this.domChanged.newSel.head;
|
|
2749
2752
|
else if (!touchesComposition(update.changes, this.hasComposition) && !update.selectionSet)
|
|
@@ -2851,6 +2854,14 @@ class DocView extends ContentView {
|
|
|
2851
2854
|
if (composition)
|
|
2852
2855
|
this.fixCompositionDOM(composition);
|
|
2853
2856
|
}
|
|
2857
|
+
updateEditContextFormatting(update) {
|
|
2858
|
+
this.editContextFormatting = this.editContextFormatting.map(update.changes);
|
|
2859
|
+
for (let tr of update.transactions)
|
|
2860
|
+
for (let effect of tr.effects)
|
|
2861
|
+
if (effect.is(setEditContextFormatting)) {
|
|
2862
|
+
this.editContextFormatting = effect.value;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2854
2865
|
compositionView(composition) {
|
|
2855
2866
|
let cur = new TextView(composition.text.nodeValue);
|
|
2856
2867
|
cur.flags |= 8 /* ViewFlag.Composition */;
|
|
@@ -3182,7 +3193,7 @@ class DocView extends ContentView {
|
|
|
3182
3193
|
return Decoration.set(deco);
|
|
3183
3194
|
}
|
|
3184
3195
|
updateDeco() {
|
|
3185
|
-
let i =
|
|
3196
|
+
let i = 1;
|
|
3186
3197
|
let allDeco = this.view.state.facet(decorations).map(d => {
|
|
3187
3198
|
let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
|
|
3188
3199
|
return dynamic ? d(this.view) : d;
|
|
@@ -3198,6 +3209,7 @@ class DocView extends ContentView {
|
|
|
3198
3209
|
allDeco.push(RangeSet.join(outerDeco));
|
|
3199
3210
|
}
|
|
3200
3211
|
this.decorations = [
|
|
3212
|
+
this.editContextFormatting,
|
|
3201
3213
|
...allDeco,
|
|
3202
3214
|
this.computeBlockGapDeco(),
|
|
3203
3215
|
this.view.viewState.lineGapDeco
|
|
@@ -3738,9 +3750,16 @@ class InputState {
|
|
|
3738
3750
|
// (after which we retroactively handle them and reset the DOM) to
|
|
3739
3751
|
// avoid messing up the virtual keyboard state.
|
|
3740
3752
|
this.pendingIOSKey = undefined;
|
|
3753
|
+
/**
|
|
3754
|
+
When enabled (>-1), tab presses are not given to key handlers,
|
|
3755
|
+
leaving the browser's default behavior. If >0, the mode expires
|
|
3756
|
+
at that timestamp, and any other keypress clears it.
|
|
3757
|
+
Esc enables temporary tab focus mode for two seconds when not
|
|
3758
|
+
otherwise handled.
|
|
3759
|
+
*/
|
|
3760
|
+
this.tabFocusMode = -1;
|
|
3741
3761
|
this.lastSelectionOrigin = null;
|
|
3742
3762
|
this.lastSelectionTime = 0;
|
|
3743
|
-
this.lastEscPress = 0;
|
|
3744
3763
|
this.lastContextMenu = 0;
|
|
3745
3764
|
this.scrollHandlers = [];
|
|
3746
3765
|
this.handlers = Object.create(null);
|
|
@@ -3820,10 +3839,10 @@ class InputState {
|
|
|
3820
3839
|
// Must always run, even if a custom handler handled the event
|
|
3821
3840
|
this.lastKeyCode = event.keyCode;
|
|
3822
3841
|
this.lastKeyTime = Date.now();
|
|
3823
|
-
if (event.keyCode == 9 && Date.now()
|
|
3842
|
+
if (event.keyCode == 9 && this.tabFocusMode > -1 && (!this.tabFocusMode || Date.now() <= this.tabFocusMode))
|
|
3824
3843
|
return true;
|
|
3825
|
-
if (event.keyCode != 27 && modifierCodes.indexOf(event.keyCode) < 0)
|
|
3826
|
-
this.
|
|
3844
|
+
if (this.tabFocusMode > 0 && event.keyCode != 27 && modifierCodes.indexOf(event.keyCode) < 0)
|
|
3845
|
+
this.tabFocusMode = -1;
|
|
3827
3846
|
// Chrome for Android usually doesn't fire proper key events, but
|
|
3828
3847
|
// occasionally does, usually surrounded by a bunch of complicated
|
|
3829
3848
|
// composition changes. When an enter or backspace key event is
|
|
@@ -3884,6 +3903,7 @@ class InputState {
|
|
|
3884
3903
|
this.mouseSelection = mouseSelection;
|
|
3885
3904
|
}
|
|
3886
3905
|
update(update) {
|
|
3906
|
+
this.view.observer.update(update);
|
|
3887
3907
|
if (this.mouseSelection)
|
|
3888
3908
|
this.mouseSelection.update(update);
|
|
3889
3909
|
if (this.draggedContent && update.docChanged)
|
|
@@ -4161,8 +4181,8 @@ observers.scroll = view => {
|
|
|
4161
4181
|
};
|
|
4162
4182
|
handlers.keydown = (view, event) => {
|
|
4163
4183
|
view.inputState.setSelectionOrigin("select");
|
|
4164
|
-
if (event.keyCode == 27)
|
|
4165
|
-
view.inputState.
|
|
4184
|
+
if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
|
|
4185
|
+
view.inputState.tabFocusMode = Date.now() + 2000;
|
|
4166
4186
|
return false;
|
|
4167
4187
|
};
|
|
4168
4188
|
observers.touchstart = (view, e) => {
|
|
@@ -4481,6 +4501,8 @@ observers.blur = view => {
|
|
|
4481
4501
|
updateForFocusChange(view);
|
|
4482
4502
|
};
|
|
4483
4503
|
observers.compositionstart = observers.compositionupdate = view => {
|
|
4504
|
+
if (view.observer.editContext)
|
|
4505
|
+
return; // Composition handled by edit context
|
|
4484
4506
|
if (view.inputState.compositionFirstChange == null)
|
|
4485
4507
|
view.inputState.compositionFirstChange = true;
|
|
4486
4508
|
if (view.inputState.composing < 0) {
|
|
@@ -4489,6 +4511,8 @@ observers.compositionstart = observers.compositionupdate = view => {
|
|
|
4489
4511
|
}
|
|
4490
4512
|
};
|
|
4491
4513
|
observers.compositionend = view => {
|
|
4514
|
+
if (view.observer.editContext)
|
|
4515
|
+
return; // Composition handled by edit context
|
|
4492
4516
|
view.inputState.composing = -1;
|
|
4493
4517
|
view.inputState.compositionEndedAt = Date.now();
|
|
4494
4518
|
view.inputState.compositionPendingKey = true;
|
|
@@ -5681,12 +5705,12 @@ class ViewState {
|
|
|
5681
5705
|
}
|
|
5682
5706
|
gaps.push(gap);
|
|
5683
5707
|
};
|
|
5684
|
-
|
|
5685
|
-
if (line.length < doubleMargin)
|
|
5686
|
-
|
|
5708
|
+
let checkLine = (line) => {
|
|
5709
|
+
if (line.length < doubleMargin || line.type != BlockType.Text)
|
|
5710
|
+
return;
|
|
5687
5711
|
let structure = lineStructure(line.from, line.to, this.stateDeco);
|
|
5688
5712
|
if (structure.total < doubleMargin)
|
|
5689
|
-
|
|
5713
|
+
return;
|
|
5690
5714
|
let target = this.scrollTarget ? this.scrollTarget.range.head : null;
|
|
5691
5715
|
let viewFrom, viewTo;
|
|
5692
5716
|
if (wrapping) {
|
|
@@ -5726,6 +5750,12 @@ class ViewState {
|
|
|
5726
5750
|
addGap(line.from, viewFrom, line, structure);
|
|
5727
5751
|
if (viewTo < line.to)
|
|
5728
5752
|
addGap(viewTo, line.to, line, structure);
|
|
5753
|
+
};
|
|
5754
|
+
for (let line of this.viewportLines) {
|
|
5755
|
+
if (Array.isArray(line.type))
|
|
5756
|
+
line.type.forEach(checkLine);
|
|
5757
|
+
else
|
|
5758
|
+
checkLine(line);
|
|
5729
5759
|
}
|
|
5730
5760
|
return gaps;
|
|
5731
5761
|
}
|
|
@@ -6385,35 +6415,7 @@ function applyDOMChange(view, domChange) {
|
|
|
6385
6415
|
change = { from: sel.from, to: sel.to, insert: Text.of([" "]) };
|
|
6386
6416
|
}
|
|
6387
6417
|
if (change) {
|
|
6388
|
-
|
|
6389
|
-
return true;
|
|
6390
|
-
// Android browsers don't fire reasonable key events for enter,
|
|
6391
|
-
// backspace, or delete. So this detects changes that look like
|
|
6392
|
-
// they're caused by those keys, and reinterprets them as key
|
|
6393
|
-
// events. (Some of these keys are also handled by beforeinput
|
|
6394
|
-
// events and the pendingAndroidKey mechanism, but that's not
|
|
6395
|
-
// reliable in all situations.)
|
|
6396
|
-
if (browser.android &&
|
|
6397
|
-
((change.to == sel.to &&
|
|
6398
|
-
// GBoard will sometimes remove a space it just inserted
|
|
6399
|
-
// after a completion when you press enter
|
|
6400
|
-
(change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
|
|
6401
|
-
change.insert.length == 1 && change.insert.lines == 2 &&
|
|
6402
|
-
dispatchKey(view.contentDOM, "Enter", 13)) ||
|
|
6403
|
-
((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
|
|
6404
|
-
lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
|
|
6405
|
-
dispatchKey(view.contentDOM, "Backspace", 8)) ||
|
|
6406
|
-
(change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
|
|
6407
|
-
dispatchKey(view.contentDOM, "Delete", 46))))
|
|
6408
|
-
return true;
|
|
6409
|
-
let text = change.insert.toString();
|
|
6410
|
-
if (view.inputState.composing >= 0)
|
|
6411
|
-
view.inputState.composing++;
|
|
6412
|
-
let defaultTr;
|
|
6413
|
-
let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
|
|
6414
|
-
if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
|
|
6415
|
-
view.dispatch(defaultInsert());
|
|
6416
|
-
return true;
|
|
6418
|
+
return applyDOMChangeInner(view, change, newSel, lastKey);
|
|
6417
6419
|
}
|
|
6418
6420
|
else if (newSel && !newSel.main.eq(sel)) {
|
|
6419
6421
|
let scrollIntoView = false, userEvent = "select";
|
|
@@ -6429,6 +6431,38 @@ function applyDOMChange(view, domChange) {
|
|
|
6429
6431
|
return false;
|
|
6430
6432
|
}
|
|
6431
6433
|
}
|
|
6434
|
+
function applyDOMChangeInner(view, change, newSel, lastKey = -1) {
|
|
6435
|
+
if (browser.ios && view.inputState.flushIOSKey(change))
|
|
6436
|
+
return true;
|
|
6437
|
+
let sel = view.state.selection.main;
|
|
6438
|
+
// Android browsers don't fire reasonable key events for enter,
|
|
6439
|
+
// backspace, or delete. So this detects changes that look like
|
|
6440
|
+
// they're caused by those keys, and reinterprets them as key
|
|
6441
|
+
// events. (Some of these keys are also handled by beforeinput
|
|
6442
|
+
// events and the pendingAndroidKey mechanism, but that's not
|
|
6443
|
+
// reliable in all situations.)
|
|
6444
|
+
if (browser.android &&
|
|
6445
|
+
((change.to == sel.to &&
|
|
6446
|
+
// GBoard will sometimes remove a space it just inserted
|
|
6447
|
+
// after a completion when you press enter
|
|
6448
|
+
(change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
|
|
6449
|
+
change.insert.length == 1 && change.insert.lines == 2 &&
|
|
6450
|
+
dispatchKey(view.contentDOM, "Enter", 13)) ||
|
|
6451
|
+
((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
|
|
6452
|
+
lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
|
|
6453
|
+
dispatchKey(view.contentDOM, "Backspace", 8)) ||
|
|
6454
|
+
(change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
|
|
6455
|
+
dispatchKey(view.contentDOM, "Delete", 46))))
|
|
6456
|
+
return true;
|
|
6457
|
+
let text = change.insert.toString();
|
|
6458
|
+
if (view.inputState.composing >= 0)
|
|
6459
|
+
view.inputState.composing++;
|
|
6460
|
+
let defaultTr;
|
|
6461
|
+
let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
|
|
6462
|
+
if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
|
|
6463
|
+
view.dispatch(defaultInsert());
|
|
6464
|
+
return true;
|
|
6465
|
+
}
|
|
6432
6466
|
function applyDefaultInsert(view, change, newSel) {
|
|
6433
6467
|
let tr, startState = view.state, sel = startState.selection.main;
|
|
6434
6468
|
if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
|
|
@@ -6555,6 +6589,7 @@ class DOMObserver {
|
|
|
6555
6589
|
constructor(view) {
|
|
6556
6590
|
this.view = view;
|
|
6557
6591
|
this.active = false;
|
|
6592
|
+
this.editContext = null;
|
|
6558
6593
|
// The known selection. Kept in our own object, as opposed to just
|
|
6559
6594
|
// directly accessing the selection because:
|
|
6560
6595
|
// - Safari doesn't report the right selection in shadow DOM
|
|
@@ -6599,6 +6634,10 @@ class DOMObserver {
|
|
|
6599
6634
|
else
|
|
6600
6635
|
this.flush();
|
|
6601
6636
|
});
|
|
6637
|
+
if (window.EditContext && view.constructor.EDIT_CONTEXT !== false) {
|
|
6638
|
+
this.editContext = new EditContextManager(view);
|
|
6639
|
+
view.contentDOM.editContext = this.editContext.editContext;
|
|
6640
|
+
}
|
|
6602
6641
|
if (useCharData)
|
|
6603
6642
|
this.onCharData = (event) => {
|
|
6604
6643
|
this.queue.push({ target: event.target,
|
|
@@ -6649,6 +6688,8 @@ class DOMObserver {
|
|
|
6649
6688
|
onScroll(e) {
|
|
6650
6689
|
if (this.intersecting)
|
|
6651
6690
|
this.flush(false);
|
|
6691
|
+
if (this.editContext)
|
|
6692
|
+
this.view.requestMeasure(this.editContext.measureReq);
|
|
6652
6693
|
this.onScrollChanged(e);
|
|
6653
6694
|
}
|
|
6654
6695
|
onResize() {
|
|
@@ -6958,6 +6999,10 @@ class DOMObserver {
|
|
|
6958
6999
|
win.removeEventListener("beforeprint", this.onPrint);
|
|
6959
7000
|
win.document.removeEventListener("selectionchange", this.onSelectionChange);
|
|
6960
7001
|
}
|
|
7002
|
+
update(update) {
|
|
7003
|
+
if (this.editContext)
|
|
7004
|
+
this.editContext.update(update);
|
|
7005
|
+
}
|
|
6961
7006
|
destroy() {
|
|
6962
7007
|
var _a, _b, _c;
|
|
6963
7008
|
this.stop();
|
|
@@ -7017,6 +7062,161 @@ function safariSelectionRangeHack(view, selection) {
|
|
|
7017
7062
|
view.contentDOM.removeEventListener("beforeinput", read, true);
|
|
7018
7063
|
return found ? buildSelectionRangeFromRange(view, found) : null;
|
|
7019
7064
|
}
|
|
7065
|
+
class EditContextManager {
|
|
7066
|
+
constructor(view) {
|
|
7067
|
+
// The document window for which the text in the context is
|
|
7068
|
+
// maintained. For large documents, this may be smaller than the
|
|
7069
|
+
// editor document. This window always includes the selection head.
|
|
7070
|
+
this.from = 0;
|
|
7071
|
+
this.to = 0;
|
|
7072
|
+
// When applying a transaction, this is used to compare the change
|
|
7073
|
+
// made to the context content to the change in the transaction in
|
|
7074
|
+
// order to make the minimal changes to the context (since touching
|
|
7075
|
+
// that sometimes breaks series of multiple edits made for a single
|
|
7076
|
+
// user action on some Android keyboards)
|
|
7077
|
+
this.pendingContextChange = null;
|
|
7078
|
+
this.resetRange(view.state);
|
|
7079
|
+
let context = this.editContext = new window.EditContext({
|
|
7080
|
+
text: view.state.doc.sliceString(this.from, this.to),
|
|
7081
|
+
selectionStart: this.toContextPos(Math.max(this.from, Math.min(this.to, view.state.selection.main.anchor))),
|
|
7082
|
+
selectionEnd: this.toContextPos(view.state.selection.main.head)
|
|
7083
|
+
});
|
|
7084
|
+
context.addEventListener("textupdate", e => {
|
|
7085
|
+
let { anchor } = view.state.selection.main;
|
|
7086
|
+
let change = { from: this.toEditorPos(e.updateRangeStart),
|
|
7087
|
+
to: this.toEditorPos(e.updateRangeEnd),
|
|
7088
|
+
insert: Text.of(e.text.split("\n")) };
|
|
7089
|
+
// If the window doesn't include the anchor, assume changes
|
|
7090
|
+
// adjacent to a side go up to the anchor.
|
|
7091
|
+
if (change.from == this.from && anchor < this.from)
|
|
7092
|
+
change.from = anchor;
|
|
7093
|
+
else if (change.to == this.to && anchor > this.to)
|
|
7094
|
+
change.to = anchor;
|
|
7095
|
+
// Edit context sometimes fire empty changes
|
|
7096
|
+
if (change.from == change.to && !change.insert.length)
|
|
7097
|
+
return;
|
|
7098
|
+
this.pendingContextChange = change;
|
|
7099
|
+
applyDOMChangeInner(view, change, EditorSelection.single(this.toEditorPos(e.selectionStart), this.toEditorPos(e.selectionEnd)));
|
|
7100
|
+
// If the transaction didn't flush our change, revert it so
|
|
7101
|
+
// that the context is in sync with the editor state again.
|
|
7102
|
+
if (this.pendingContextChange)
|
|
7103
|
+
this.revertPending(view.state);
|
|
7104
|
+
});
|
|
7105
|
+
context.addEventListener("characterboundsupdate", e => {
|
|
7106
|
+
let rects = [], prev = null;
|
|
7107
|
+
for (let i = this.toEditorPos(e.rangeStart), end = this.toEditorPos(e.rangeEnd); i < end; i++) {
|
|
7108
|
+
let rect = view.coordsForChar(i);
|
|
7109
|
+
prev = (rect && new DOMRect(rect.left, rect.right, rect.right - rect.left, rect.bottom - rect.top))
|
|
7110
|
+
|| prev || new DOMRect;
|
|
7111
|
+
rects.push(prev);
|
|
7112
|
+
}
|
|
7113
|
+
context.updateCharacterBounds(e.rangeStart, rects);
|
|
7114
|
+
});
|
|
7115
|
+
context.addEventListener("textformatupdate", e => {
|
|
7116
|
+
let deco = [];
|
|
7117
|
+
for (let format of e.getTextFormats()) {
|
|
7118
|
+
let lineStyle = format.underlineStyle, thickness = format.underlineThickness;
|
|
7119
|
+
if (lineStyle != "None" && thickness != "None") {
|
|
7120
|
+
let style = `text-decoration: underline ${lineStyle == "Dashed" ? "dashed " : lineStyle == "Squiggle" ? "wavy " : ""}${thickness == "Thin" ? 1 : 2}px`;
|
|
7121
|
+
deco.push(Decoration.mark({ attributes: { style } })
|
|
7122
|
+
.range(this.toEditorPos(format.rangeStart), this.toEditorPos(format.rangeEnd)));
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
view.dispatch({ effects: setEditContextFormatting.of(Decoration.set(deco)) });
|
|
7126
|
+
});
|
|
7127
|
+
context.addEventListener("compositionstart", () => {
|
|
7128
|
+
if (view.inputState.composing < 0) {
|
|
7129
|
+
view.inputState.composing = 0;
|
|
7130
|
+
view.inputState.compositionFirstChange = true;
|
|
7131
|
+
}
|
|
7132
|
+
});
|
|
7133
|
+
context.addEventListener("compositionend", () => {
|
|
7134
|
+
view.inputState.composing = -1;
|
|
7135
|
+
view.inputState.compositionFirstChange = null;
|
|
7136
|
+
});
|
|
7137
|
+
this.measureReq = { read: view => {
|
|
7138
|
+
this.editContext.updateControlBounds(view.contentDOM.getBoundingClientRect());
|
|
7139
|
+
let sel = getSelection(view.root);
|
|
7140
|
+
if (sel && sel.rangeCount)
|
|
7141
|
+
this.editContext.updateSelectionBounds(sel.getRangeAt(0).getBoundingClientRect());
|
|
7142
|
+
} };
|
|
7143
|
+
}
|
|
7144
|
+
applyEdits(update) {
|
|
7145
|
+
let off = 0, abort = false, pending = this.pendingContextChange;
|
|
7146
|
+
update.changes.iterChanges((fromA, toA, _fromB, _toB, insert) => {
|
|
7147
|
+
if (abort)
|
|
7148
|
+
return;
|
|
7149
|
+
let dLen = insert.length - (toA - fromA);
|
|
7150
|
+
if (pending && toA >= pending.to) {
|
|
7151
|
+
if (pending.from == fromA && pending.to == toA && pending.insert.eq(insert)) {
|
|
7152
|
+
pending = this.pendingContextChange = null; // Match
|
|
7153
|
+
off += dLen;
|
|
7154
|
+
return;
|
|
7155
|
+
}
|
|
7156
|
+
else { // Mismatch, revert
|
|
7157
|
+
pending = null;
|
|
7158
|
+
this.revertPending(update.state);
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
fromA += off;
|
|
7162
|
+
toA += off;
|
|
7163
|
+
if (toA <= this.from) { // Before the window
|
|
7164
|
+
this.from += dLen;
|
|
7165
|
+
this.to += dLen;
|
|
7166
|
+
}
|
|
7167
|
+
else if (fromA < this.to) { // Overlaps with window
|
|
7168
|
+
if (fromA < this.from || toA > this.to || (this.to - this.from) + insert.length > 30000 /* CxVp.MaxSize */) {
|
|
7169
|
+
abort = true;
|
|
7170
|
+
return;
|
|
7171
|
+
}
|
|
7172
|
+
this.editContext.updateText(this.toContextPos(fromA), this.toContextPos(toA), insert.toString());
|
|
7173
|
+
this.to += dLen;
|
|
7174
|
+
}
|
|
7175
|
+
off += dLen;
|
|
7176
|
+
});
|
|
7177
|
+
if (pending && !abort)
|
|
7178
|
+
this.revertPending(update.state);
|
|
7179
|
+
return !abort;
|
|
7180
|
+
}
|
|
7181
|
+
update(update) {
|
|
7182
|
+
if (!this.applyEdits(update) || !this.rangeIsValid(update.state)) {
|
|
7183
|
+
this.pendingContextChange = null;
|
|
7184
|
+
this.resetRange(update.state);
|
|
7185
|
+
this.editContext.updateText(0, this.editContext.text.length, update.state.doc.sliceString(this.from, this.to));
|
|
7186
|
+
this.setSelection(update.state);
|
|
7187
|
+
}
|
|
7188
|
+
else if (update.docChanged || update.selectionSet) {
|
|
7189
|
+
this.setSelection(update.state);
|
|
7190
|
+
}
|
|
7191
|
+
if (update.geometryChanged || update.docChanged || update.selectionSet)
|
|
7192
|
+
update.view.requestMeasure(this.measureReq);
|
|
7193
|
+
}
|
|
7194
|
+
resetRange(state) {
|
|
7195
|
+
let { head } = state.selection.main;
|
|
7196
|
+
this.from = Math.max(0, head - 10000 /* CxVp.Margin */);
|
|
7197
|
+
this.to = Math.min(state.doc.length, head + 10000 /* CxVp.Margin */);
|
|
7198
|
+
}
|
|
7199
|
+
revertPending(state) {
|
|
7200
|
+
let pending = this.pendingContextChange;
|
|
7201
|
+
this.pendingContextChange = null;
|
|
7202
|
+
this.editContext.updateText(this.toContextPos(pending.from), this.toContextPos(pending.to + pending.insert.length), state.doc.sliceString(pending.from, pending.to));
|
|
7203
|
+
}
|
|
7204
|
+
setSelection(state) {
|
|
7205
|
+
let { main } = state.selection;
|
|
7206
|
+
let start = this.toContextPos(Math.max(this.from, Math.min(this.to, main.anchor)));
|
|
7207
|
+
let end = this.toContextPos(main.head);
|
|
7208
|
+
if (this.editContext.selectionStart != start || this.editContext.selectionEnd != end)
|
|
7209
|
+
this.editContext.updateSelection(start, end);
|
|
7210
|
+
}
|
|
7211
|
+
rangeIsValid(state) {
|
|
7212
|
+
let { head } = state.selection.main;
|
|
7213
|
+
return !(this.from > 0 && head - this.from < 500 /* CxVp.MinMargin */ ||
|
|
7214
|
+
this.to < state.doc.length && this.to - head < 500 /* CxVp.MinMargin */ ||
|
|
7215
|
+
this.to - this.from > 10000 /* CxVp.Margin */ * 3);
|
|
7216
|
+
}
|
|
7217
|
+
toEditorPos(contextPos) { return contextPos + this.from; }
|
|
7218
|
+
toContextPos(editorPos) { return editorPos - this.from; }
|
|
7219
|
+
}
|
|
7020
7220
|
|
|
7021
7221
|
// The editor's update state machine looks something like this:
|
|
7022
7222
|
//
|
|
@@ -7839,6 +8039,8 @@ class EditorView {
|
|
|
7839
8039
|
calling this.
|
|
7840
8040
|
*/
|
|
7841
8041
|
destroy() {
|
|
8042
|
+
if (this.root.activeElement == this.contentDOM)
|
|
8043
|
+
this.contentDOM.blur();
|
|
7842
8044
|
for (let plugin of this.plugins)
|
|
7843
8045
|
plugin.destroy(this);
|
|
7844
8046
|
this.plugins = [];
|
|
@@ -7876,6 +8078,25 @@ class EditorView {
|
|
|
7876
8078
|
return scrollIntoView.of(new ScrollTarget(EditorSelection.cursor(ref.from), "start", "start", ref.top - scrollTop, scrollLeft, true));
|
|
7877
8079
|
}
|
|
7878
8080
|
/**
|
|
8081
|
+
Enable or disable tab-focus mode, which disables key bindings
|
|
8082
|
+
for Tab and Shift-Tab, letting the browser's default
|
|
8083
|
+
focus-changing behavior go through instead. This is useful to
|
|
8084
|
+
prevent trapping keyboard users in your editor.
|
|
8085
|
+
|
|
8086
|
+
Without argument, this toggles the mode. With a boolean, it
|
|
8087
|
+
enables (true) or disables it (false). Given a number, it
|
|
8088
|
+
temporarily enables the mode until that number of milliseconds
|
|
8089
|
+
have passed or another non-Tab key is pressed.
|
|
8090
|
+
*/
|
|
8091
|
+
setTabFocusMode(to) {
|
|
8092
|
+
if (to == null)
|
|
8093
|
+
this.inputState.tabFocusMode = this.inputState.tabFocusMode < 0 ? 0 : -1;
|
|
8094
|
+
else if (typeof to == "boolean")
|
|
8095
|
+
this.inputState.tabFocusMode = to ? 0 : -1;
|
|
8096
|
+
else if (this.inputState.tabFocusMode != 0)
|
|
8097
|
+
this.inputState.tabFocusMode = Date.now() + to;
|
|
8098
|
+
}
|
|
8099
|
+
/**
|
|
7879
8100
|
Returns an extension that can be used to add DOM event handlers.
|
|
7880
8101
|
The value should be an object mapping event names to handler
|
|
7881
8102
|
functions. For any given event, such functions are ordered by
|