@codemirror/view 6.27.0 → 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,13 @@
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
+
1
11
  ## 6.27.0 (2024-06-04)
2
12
 
3
13
  ### New features
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
@@ -3895,6 +3907,7 @@ class InputState {
3895
3907
  this.mouseSelection = mouseSelection;
3896
3908
  }
3897
3909
  update(update) {
3910
+ this.view.observer.update(update);
3898
3911
  if (this.mouseSelection)
3899
3912
  this.mouseSelection.update(update);
3900
3913
  if (this.draggedContent && update.docChanged)
@@ -4492,6 +4505,8 @@ observers.blur = view => {
4492
4505
  updateForFocusChange(view);
4493
4506
  };
4494
4507
  observers.compositionstart = observers.compositionupdate = view => {
4508
+ if (view.observer.editContext)
4509
+ return; // Composition handled by edit context
4495
4510
  if (view.inputState.compositionFirstChange == null)
4496
4511
  view.inputState.compositionFirstChange = true;
4497
4512
  if (view.inputState.composing < 0) {
@@ -4500,6 +4515,8 @@ observers.compositionstart = observers.compositionupdate = view => {
4500
4515
  }
4501
4516
  };
4502
4517
  observers.compositionend = view => {
4518
+ if (view.observer.editContext)
4519
+ return; // Composition handled by edit context
4503
4520
  view.inputState.composing = -1;
4504
4521
  view.inputState.compositionEndedAt = Date.now();
4505
4522
  view.inputState.compositionPendingKey = true;
@@ -5693,12 +5710,12 @@ class ViewState {
5693
5710
  }
5694
5711
  gaps.push(gap);
5695
5712
  };
5696
- for (let line of this.viewportLines) {
5697
- if (line.length < doubleMargin)
5698
- continue;
5713
+ let checkLine = (line) => {
5714
+ if (line.length < doubleMargin || line.type != exports.BlockType.Text)
5715
+ return;
5699
5716
  let structure = lineStructure(line.from, line.to, this.stateDeco);
5700
5717
  if (structure.total < doubleMargin)
5701
- continue;
5718
+ return;
5702
5719
  let target = this.scrollTarget ? this.scrollTarget.range.head : null;
5703
5720
  let viewFrom, viewTo;
5704
5721
  if (wrapping) {
@@ -5738,6 +5755,12 @@ class ViewState {
5738
5755
  addGap(line.from, viewFrom, line, structure);
5739
5756
  if (viewTo < line.to)
5740
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);
5741
5764
  }
5742
5765
  return gaps;
5743
5766
  }
@@ -6397,35 +6420,7 @@ function applyDOMChange(view, domChange) {
6397
6420
  change = { from: sel.from, to: sel.to, insert: state.Text.of([" "]) };
6398
6421
  }
6399
6422
  if (change) {
6400
- if (browser.ios && view.inputState.flushIOSKey(change))
6401
- return true;
6402
- // Android browsers don't fire reasonable key events for enter,
6403
- // backspace, or delete. So this detects changes that look like
6404
- // they're caused by those keys, and reinterprets them as key
6405
- // events. (Some of these keys are also handled by beforeinput
6406
- // events and the pendingAndroidKey mechanism, but that's not
6407
- // reliable in all situations.)
6408
- if (browser.android &&
6409
- ((change.to == sel.to &&
6410
- // GBoard will sometimes remove a space it just inserted
6411
- // after a completion when you press enter
6412
- (change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
6413
- change.insert.length == 1 && change.insert.lines == 2 &&
6414
- dispatchKey(view.contentDOM, "Enter", 13)) ||
6415
- ((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
6416
- lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
6417
- dispatchKey(view.contentDOM, "Backspace", 8)) ||
6418
- (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
6419
- dispatchKey(view.contentDOM, "Delete", 46))))
6420
- return true;
6421
- let text = change.insert.toString();
6422
- if (view.inputState.composing >= 0)
6423
- view.inputState.composing++;
6424
- let defaultTr;
6425
- let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
6426
- if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
6427
- view.dispatch(defaultInsert());
6428
- return true;
6423
+ return applyDOMChangeInner(view, change, newSel, lastKey);
6429
6424
  }
6430
6425
  else if (newSel && !newSel.main.eq(sel)) {
6431
6426
  let scrollIntoView = false, userEvent = "select";
@@ -6441,6 +6436,38 @@ function applyDOMChange(view, domChange) {
6441
6436
  return false;
6442
6437
  }
6443
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
+ }
6444
6471
  function applyDefaultInsert(view, change, newSel) {
6445
6472
  let tr, startState = view.state, sel = startState.selection.main;
6446
6473
  if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
@@ -6567,6 +6594,7 @@ class DOMObserver {
6567
6594
  constructor(view) {
6568
6595
  this.view = view;
6569
6596
  this.active = false;
6597
+ this.editContext = null;
6570
6598
  // The known selection. Kept in our own object, as opposed to just
6571
6599
  // directly accessing the selection because:
6572
6600
  // - Safari doesn't report the right selection in shadow DOM
@@ -6611,6 +6639,10 @@ class DOMObserver {
6611
6639
  else
6612
6640
  this.flush();
6613
6641
  });
6642
+ if (window.EditContext && view.constructor.EDIT_CONTEXT !== false) {
6643
+ this.editContext = new EditContextManager(view);
6644
+ view.contentDOM.editContext = this.editContext.editContext;
6645
+ }
6614
6646
  if (useCharData)
6615
6647
  this.onCharData = (event) => {
6616
6648
  this.queue.push({ target: event.target,
@@ -6661,6 +6693,8 @@ class DOMObserver {
6661
6693
  onScroll(e) {
6662
6694
  if (this.intersecting)
6663
6695
  this.flush(false);
6696
+ if (this.editContext)
6697
+ this.view.requestMeasure(this.editContext.measureReq);
6664
6698
  this.onScrollChanged(e);
6665
6699
  }
6666
6700
  onResize() {
@@ -6970,6 +7004,10 @@ class DOMObserver {
6970
7004
  win.removeEventListener("beforeprint", this.onPrint);
6971
7005
  win.document.removeEventListener("selectionchange", this.onSelectionChange);
6972
7006
  }
7007
+ update(update) {
7008
+ if (this.editContext)
7009
+ this.editContext.update(update);
7010
+ }
6973
7011
  destroy() {
6974
7012
  var _a, _b, _c;
6975
7013
  this.stop();
@@ -7029,6 +7067,161 @@ function safariSelectionRangeHack(view, selection) {
7029
7067
  view.contentDOM.removeEventListener("beforeinput", read, true);
7030
7068
  return found ? buildSelectionRangeFromRange(view, found) : null;
7031
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
+ }
7032
7225
 
7033
7226
  // The editor's update state machine looks something like this:
7034
7227
  //
@@ -7851,6 +8044,8 @@ class EditorView {
7851
8044
  calling this.
7852
8045
  */
7853
8046
  destroy() {
8047
+ if (this.root.activeElement == this.contentDOM)
8048
+ this.contentDOM.blur();
7854
8049
  for (let plugin of this.plugins)
7855
8050
  plugin.destroy(this);
7856
8051
  this.plugins = [];
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
@@ -3891,6 +3903,7 @@ class InputState {
3891
3903
  this.mouseSelection = mouseSelection;
3892
3904
  }
3893
3905
  update(update) {
3906
+ this.view.observer.update(update);
3894
3907
  if (this.mouseSelection)
3895
3908
  this.mouseSelection.update(update);
3896
3909
  if (this.draggedContent && update.docChanged)
@@ -4488,6 +4501,8 @@ observers.blur = view => {
4488
4501
  updateForFocusChange(view);
4489
4502
  };
4490
4503
  observers.compositionstart = observers.compositionupdate = view => {
4504
+ if (view.observer.editContext)
4505
+ return; // Composition handled by edit context
4491
4506
  if (view.inputState.compositionFirstChange == null)
4492
4507
  view.inputState.compositionFirstChange = true;
4493
4508
  if (view.inputState.composing < 0) {
@@ -4496,6 +4511,8 @@ observers.compositionstart = observers.compositionupdate = view => {
4496
4511
  }
4497
4512
  };
4498
4513
  observers.compositionend = view => {
4514
+ if (view.observer.editContext)
4515
+ return; // Composition handled by edit context
4499
4516
  view.inputState.composing = -1;
4500
4517
  view.inputState.compositionEndedAt = Date.now();
4501
4518
  view.inputState.compositionPendingKey = true;
@@ -5688,12 +5705,12 @@ class ViewState {
5688
5705
  }
5689
5706
  gaps.push(gap);
5690
5707
  };
5691
- for (let line of this.viewportLines) {
5692
- if (line.length < doubleMargin)
5693
- continue;
5708
+ let checkLine = (line) => {
5709
+ if (line.length < doubleMargin || line.type != BlockType.Text)
5710
+ return;
5694
5711
  let structure = lineStructure(line.from, line.to, this.stateDeco);
5695
5712
  if (structure.total < doubleMargin)
5696
- continue;
5713
+ return;
5697
5714
  let target = this.scrollTarget ? this.scrollTarget.range.head : null;
5698
5715
  let viewFrom, viewTo;
5699
5716
  if (wrapping) {
@@ -5733,6 +5750,12 @@ class ViewState {
5733
5750
  addGap(line.from, viewFrom, line, structure);
5734
5751
  if (viewTo < line.to)
5735
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);
5736
5759
  }
5737
5760
  return gaps;
5738
5761
  }
@@ -6392,35 +6415,7 @@ function applyDOMChange(view, domChange) {
6392
6415
  change = { from: sel.from, to: sel.to, insert: Text.of([" "]) };
6393
6416
  }
6394
6417
  if (change) {
6395
- if (browser.ios && view.inputState.flushIOSKey(change))
6396
- return true;
6397
- // Android browsers don't fire reasonable key events for enter,
6398
- // backspace, or delete. So this detects changes that look like
6399
- // they're caused by those keys, and reinterprets them as key
6400
- // events. (Some of these keys are also handled by beforeinput
6401
- // events and the pendingAndroidKey mechanism, but that's not
6402
- // reliable in all situations.)
6403
- if (browser.android &&
6404
- ((change.to == sel.to &&
6405
- // GBoard will sometimes remove a space it just inserted
6406
- // after a completion when you press enter
6407
- (change.from == sel.from || change.from == sel.from - 1 && view.state.sliceDoc(change.from, sel.from) == " ") &&
6408
- change.insert.length == 1 && change.insert.lines == 2 &&
6409
- dispatchKey(view.contentDOM, "Enter", 13)) ||
6410
- ((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
6411
- lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
6412
- dispatchKey(view.contentDOM, "Backspace", 8)) ||
6413
- (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
6414
- dispatchKey(view.contentDOM, "Delete", 46))))
6415
- return true;
6416
- let text = change.insert.toString();
6417
- if (view.inputState.composing >= 0)
6418
- view.inputState.composing++;
6419
- let defaultTr;
6420
- let defaultInsert = () => defaultTr || (defaultTr = applyDefaultInsert(view, change, newSel));
6421
- if (!view.state.facet(inputHandler).some(h => h(view, change.from, change.to, text, defaultInsert)))
6422
- view.dispatch(defaultInsert());
6423
- return true;
6418
+ return applyDOMChangeInner(view, change, newSel, lastKey);
6424
6419
  }
6425
6420
  else if (newSel && !newSel.main.eq(sel)) {
6426
6421
  let scrollIntoView = false, userEvent = "select";
@@ -6436,6 +6431,38 @@ function applyDOMChange(view, domChange) {
6436
6431
  return false;
6437
6432
  }
6438
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
+ }
6439
6466
  function applyDefaultInsert(view, change, newSel) {
6440
6467
  let tr, startState = view.state, sel = startState.selection.main;
6441
6468
  if (change.from >= sel.from && change.to <= sel.to && change.to - change.from >= (sel.to - sel.from) / 3 &&
@@ -6562,6 +6589,7 @@ class DOMObserver {
6562
6589
  constructor(view) {
6563
6590
  this.view = view;
6564
6591
  this.active = false;
6592
+ this.editContext = null;
6565
6593
  // The known selection. Kept in our own object, as opposed to just
6566
6594
  // directly accessing the selection because:
6567
6595
  // - Safari doesn't report the right selection in shadow DOM
@@ -6606,6 +6634,10 @@ class DOMObserver {
6606
6634
  else
6607
6635
  this.flush();
6608
6636
  });
6637
+ if (window.EditContext && view.constructor.EDIT_CONTEXT !== false) {
6638
+ this.editContext = new EditContextManager(view);
6639
+ view.contentDOM.editContext = this.editContext.editContext;
6640
+ }
6609
6641
  if (useCharData)
6610
6642
  this.onCharData = (event) => {
6611
6643
  this.queue.push({ target: event.target,
@@ -6656,6 +6688,8 @@ class DOMObserver {
6656
6688
  onScroll(e) {
6657
6689
  if (this.intersecting)
6658
6690
  this.flush(false);
6691
+ if (this.editContext)
6692
+ this.view.requestMeasure(this.editContext.measureReq);
6659
6693
  this.onScrollChanged(e);
6660
6694
  }
6661
6695
  onResize() {
@@ -6965,6 +6999,10 @@ class DOMObserver {
6965
6999
  win.removeEventListener("beforeprint", this.onPrint);
6966
7000
  win.document.removeEventListener("selectionchange", this.onSelectionChange);
6967
7001
  }
7002
+ update(update) {
7003
+ if (this.editContext)
7004
+ this.editContext.update(update);
7005
+ }
6968
7006
  destroy() {
6969
7007
  var _a, _b, _c;
6970
7008
  this.stop();
@@ -7024,6 +7062,161 @@ function safariSelectionRangeHack(view, selection) {
7024
7062
  view.contentDOM.removeEventListener("beforeinput", read, true);
7025
7063
  return found ? buildSelectionRangeFromRange(view, found) : null;
7026
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
+ }
7027
7220
 
7028
7221
  // The editor's update state machine looks something like this:
7029
7222
  //
@@ -7846,6 +8039,8 @@ class EditorView {
7846
8039
  calling this.
7847
8040
  */
7848
8041
  destroy() {
8042
+ if (this.root.activeElement == this.contentDOM)
8043
+ this.contentDOM.blur();
7849
8044
  for (let plugin of this.plugins)
7850
8045
  plugin.destroy(this);
7851
8046
  this.plugins = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.27.0",
3
+ "version": "6.28.0",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",