@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 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 = 0;
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() < this.lastEscPress + 2000)
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.view.inputState.lastEscPress = 0;
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.lastEscPress = Date.now();
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
- for (let line of this.viewportLines) {
5690
- if (line.length < doubleMargin)
5691
- continue;
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
- continue;
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
- if (browser.ios && view.inputState.flushIOSKey(change))
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 = 0;
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() < this.lastEscPress + 2000)
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.view.inputState.lastEscPress = 0;
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.lastEscPress = Date.now();
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
- for (let line of this.viewportLines) {
5685
- if (line.length < doubleMargin)
5686
- continue;
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
- continue;
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
- if (browser.ios && view.inputState.flushIOSKey(change))
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.26.4",
3
+ "version": "6.28.0",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",