@codemirror/view 6.43.0 → 6.43.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## 6.43.2 (2026-06-23)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix using select-all from the native context menu on Chrome Android.
6
+
7
+ On iOS when autocapitalize is enabled, ignore the shift modifier on virtual keyboard Enter or Backspace presses.
8
+
9
+ Work around an issue where Chrome Android scrolled the editor up when tapping an empty line to focus it.
10
+
11
+ Create undirectional selection ranges for double and triple clicks.
12
+
13
+ ## 6.43.1 (2026-06-09)
14
+
15
+ ### Bug fixes
16
+
17
+ Work around an issue in with some IMEs (like Tibetan) in MacOS Chrome where composition could get interrupted needlessly.
18
+
19
+ Fix a crash when calling on a line with only `display: none` content.
20
+
21
+ Improve reuse of widgets near composition, which in some cases prevents Safari from doing odd thing with the selection.
22
+
1
23
  ## 6.43.0 (2026-05-14)
2
24
 
3
25
  ### New features
package/dist/index.cjs CHANGED
@@ -2631,6 +2631,7 @@ class TileUpdate {
2631
2631
  if (composition && next.fromA <= composition.range.fromA && next.toA >= composition.range.toA) {
2632
2632
  this.forward(next.fromA, composition.range.fromA, composition.range.fromA < composition.range.toA ? 1 : -1);
2633
2633
  this.emit(posB, composition.range.fromB);
2634
+ this.builder.flushBuffer();
2634
2635
  this.cache.clear(); // Must not reuse DOM across composition
2635
2636
  this.builder.addComposition(composition, compositionContext);
2636
2637
  this.text.skip(composition.range.toB - composition.range.fromB);
@@ -3610,7 +3611,7 @@ function groupAt(state$1, pos, bias = 1) {
3610
3611
  break;
3611
3612
  to = next;
3612
3613
  }
3613
- return state.EditorSelection.range(from + line.from, to + line.from);
3614
+ return state.EditorSelection.undirectionalRange(from + line.from, to + line.from);
3614
3615
  }
3615
3616
  function posAtCoordsImprecise(view, contentRect, block, x, y) {
3616
3617
  let into = Math.round((x - contentRect.left) * view.defaultCharacterWidth);
@@ -3747,8 +3748,12 @@ function skipAtomsForSelection(atoms, sel) {
3747
3748
  else {
3748
3749
  let from = skipAtomicRanges(atoms, range.from, -1);
3749
3750
  let to = skipAtomicRanges(atoms, range.to, 1);
3750
- if (from != range.from || to != range.to)
3751
- updated = state.EditorSelection.range(range.from == range.anchor ? from : to, range.from == range.head ? from : to);
3751
+ if (from != range.from || to != range.to) {
3752
+ if (range.undirectional)
3753
+ updated = state.EditorSelection.undirectionalRange(range.from, range.to);
3754
+ else
3755
+ updated = state.EditorSelection.range(range.from == range.anchor ? from : to, range.from == range.head ? from : to);
3756
+ }
3752
3757
  }
3753
3758
  if (updated) {
3754
3759
  if (!ranges)
@@ -3844,12 +3849,11 @@ class InlineCoordsScan {
3844
3849
  }
3845
3850
  // Scan through the rectangles for the content of a tile with inline
3846
3851
  // content, looking for one that overlaps the queried position
3847
- // vertically andis
3848
- // closest horizontally. The caller is responsible for dividing its
3849
- // content into N pieces, and pass an array with N+1 positions
3850
- // (including the position after the last piece). For a text tile,
3851
- // these will be character clusters, for a composite tile, these
3852
- // will be child tiles.
3852
+ // vertically and is closest horizontally. The caller is responsible
3853
+ // for dividing its content into N pieces, and pass an array with
3854
+ // N+1 positions (including the position after the last piece). For
3855
+ // a text tile, these will be character clusters, for a composite
3856
+ // tile, these will be child tiles.
3853
3857
  scan(positions, getRects, recursed = false) {
3854
3858
  let lo = 0, hi = positions.length - 1, seen = new Set();
3855
3859
  let bidi = this.bidiIn(positions[0], positions[hi]);
@@ -3921,6 +3925,8 @@ class InlineCoordsScan {
3921
3925
  // If no element with y overlap is found, find the nearest element
3922
3926
  // on the y axis, move this.y into it, and retry the scan.
3923
3927
  if (!closestRect) {
3928
+ if (!below && !above)
3929
+ return { i: positions[0], after: false };
3924
3930
  let side = above && (!below || (this.y - above.bottom < below.top - this.y)) ? above : below;
3925
3931
  this.y = (side.top + side.bottom) / 2;
3926
3932
  return this.scan(positions, getRects, true);
@@ -4153,7 +4159,8 @@ class DOMChange {
4153
4159
  // Chrome will put the selection *inside* them, confusing
4154
4160
  // posFromDOM
4155
4161
  let vp = view.viewport;
4156
- if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
4162
+ if ((browser.ios || browser.chrome) && head != anchor &&
4163
+ Math.min(head, anchor) <= curSel.main.from && Math.max(head, anchor) >= curSel.main.to &&
4157
4164
  (vp.from > 0 || vp.to < view.state.doc.length)) {
4158
4165
  let from = Math.min(head, anchor), to = Math.max(head, anchor);
4159
4166
  let offFrom = vp.from - from, offTo = vp.to - to;
@@ -4474,13 +4481,11 @@ class InputState {
4474
4481
  // (after which we retroactively handle them and reset the DOM) to
4475
4482
  // avoid messing up the virtual keyboard state.
4476
4483
  this.pendingIOSKey = undefined;
4477
- /**
4478
- When enabled (>-1), tab presses are not given to key handlers,
4479
- leaving the browser's default behavior. If >0, the mode expires
4480
- at that timestamp, and any other keypress clears it.
4481
- Esc enables temporary tab focus mode for two seconds when not
4482
- otherwise handled.
4483
- */
4484
+ // When enabled (>-1), tab presses are not given to key handlers,
4485
+ // leaving the browser's default behavior. If >0, the mode expires
4486
+ // at that timestamp, and any other keypress clears it.
4487
+ // Esc enables temporary tab focus mode for two seconds when not
4488
+ // otherwise handled.
4484
4489
  this.tabFocusMode = -1;
4485
4490
  this.lastSelectionOrigin = null;
4486
4491
  this.lastSelectionTime = 0;
@@ -4588,11 +4593,16 @@ class InputState {
4588
4593
  // state. So we let it go through, and then, in
4589
4594
  // applyDOMChange, notify key handlers of it and reset to
4590
4595
  // the state they produce.
4591
- let pending;
4592
- if (browser.ios && !event.synthetic && !event.altKey && !event.metaKey && !event.shiftKey &&
4593
- ((pending = PendingKeys.find(key => key.keyCode == event.keyCode)) && !event.ctrlKey ||
4596
+ if (browser.ios && !event.synthetic && !event.altKey && !event.metaKey &&
4597
+ (PendingKeys.some(key => key.keyCode == event.keyCode) && !event.ctrlKey ||
4594
4598
  EmacsyPendingKeys.indexOf(event.key) > -1 && event.ctrlKey)) {
4595
- this.pendingIOSKey = pending || event;
4599
+ let mods = { ctrlKey: event.ctrlKey, altKey: event.altKey, metaKey: event.metaKey, shiftKey: event.shiftKey };
4600
+ // On iOS with autocapitalize, drop the shift modifier for these
4601
+ // keys, since it will be set at the start of every sentence.
4602
+ if (mods.shiftKey && browser.ios && !/^(off|none)$/.test(this.view.contentDOM.autocapitalize) &&
4603
+ iosVirtualKeyboardOpen(this.view.win))
4604
+ mods.shiftKey = false;
4605
+ this.pendingIOSKey = { key: event.key, keyCode: event.keyCode, mods };
4596
4606
  setTimeout(() => this.flushIOSKey(), 250);
4597
4607
  return true;
4598
4608
  }
@@ -4608,7 +4618,7 @@ class InputState {
4608
4618
  if (key.key == "Enter" && change && change.from < change.to && /^\S+$/.test(change.insert.toString()))
4609
4619
  return false;
4610
4620
  this.pendingIOSKey = undefined;
4611
- return dispatchKey(this.view.contentDOM, key.key, key.keyCode, key instanceof KeyboardEvent ? key : undefined);
4621
+ return dispatchKey(this.view.contentDOM, key.key, key.keyCode, key.mods);
4612
4622
  }
4613
4623
  ignoreDuringComposition(event) {
4614
4624
  if (!/^key/.test(event.type) || event.synthetic)
@@ -4646,6 +4656,11 @@ class InputState {
4646
4656
  this.mouseSelection.destroy();
4647
4657
  }
4648
4658
  }
4659
+ function iosVirtualKeyboardOpen(win) {
4660
+ if (!win.visualViewport)
4661
+ return false;
4662
+ return win.visualViewport.height * win.visualViewport.scale / win.document.documentElement.clientHeight < 0.85;
4663
+ }
4649
4664
  function bindHandler(plugin, handler) {
4650
4665
  return (view, event) => {
4651
4666
  try {
@@ -4965,7 +4980,7 @@ function rangeForClick(view, pos, bias, type) {
4965
4980
  let from = visual ? visual.posAtStart : line.from, to = visual ? visual.posAtEnd : line.to;
4966
4981
  if (to < view.state.doc.length && to == line.to)
4967
4982
  to++;
4968
- return state.EditorSelection.range(from, to);
4983
+ return state.EditorSelection.undirectionalRange(from, to);
4969
4984
  }
4970
4985
  }
4971
4986
  const BadMouseDetail = browser.ie && browser.ie_version <= 11;
@@ -5023,7 +5038,7 @@ handlers.dragstart = (view, event) => {
5023
5038
  if (tile && tile.isWidget()) {
5024
5039
  let from = tile.posAtStart, to = from + tile.length;
5025
5040
  if (from >= range.to || to <= range.from)
5026
- range = state.EditorSelection.range(from, to);
5041
+ range = state.EditorSelection.undirectionalRange(from, to);
5027
5042
  }
5028
5043
  }
5029
5044
  let { inputState } = view;
@@ -6814,6 +6829,7 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
6814
6829
  padding: "0 2px 0 6px"
6815
6830
  },
6816
6831
  ".cm-layer": {
6832
+ userSelect: "none", // #1708
6817
6833
  position: "absolute",
6818
6834
  left: 0,
6819
6835
  top: 0,
@@ -7648,7 +7664,6 @@ class EditContextManager {
7648
7664
  for (let event in this.handlers)
7649
7665
  context.addEventListener(event, this.handlers[event]);
7650
7666
  this.measureReq = { read: view => {
7651
- this.editContext.updateControlBounds(view.contentDOM.getBoundingClientRect());
7652
7667
  let sel = getSelection(view.root);
7653
7668
  if (sel && sel.rangeCount)
7654
7669
  this.editContext.updateSelectionBounds(sel.getRangeAt(0).getBoundingClientRect());
package/dist/index.js CHANGED
@@ -2627,6 +2627,7 @@ class TileUpdate {
2627
2627
  if (composition && next.fromA <= composition.range.fromA && next.toA >= composition.range.toA) {
2628
2628
  this.forward(next.fromA, composition.range.fromA, composition.range.fromA < composition.range.toA ? 1 : -1);
2629
2629
  this.emit(posB, composition.range.fromB);
2630
+ this.builder.flushBuffer();
2630
2631
  this.cache.clear(); // Must not reuse DOM across composition
2631
2632
  this.builder.addComposition(composition, compositionContext);
2632
2633
  this.text.skip(composition.range.toB - composition.range.fromB);
@@ -3606,7 +3607,7 @@ function groupAt(state, pos, bias = 1) {
3606
3607
  break;
3607
3608
  to = next;
3608
3609
  }
3609
- return EditorSelection.range(from + line.from, to + line.from);
3610
+ return EditorSelection.undirectionalRange(from + line.from, to + line.from);
3610
3611
  }
3611
3612
  function posAtCoordsImprecise(view, contentRect, block, x, y) {
3612
3613
  let into = Math.round((x - contentRect.left) * view.defaultCharacterWidth);
@@ -3743,8 +3744,12 @@ function skipAtomsForSelection(atoms, sel) {
3743
3744
  else {
3744
3745
  let from = skipAtomicRanges(atoms, range.from, -1);
3745
3746
  let to = skipAtomicRanges(atoms, range.to, 1);
3746
- if (from != range.from || to != range.to)
3747
- updated = EditorSelection.range(range.from == range.anchor ? from : to, range.from == range.head ? from : to);
3747
+ if (from != range.from || to != range.to) {
3748
+ if (range.undirectional)
3749
+ updated = EditorSelection.undirectionalRange(range.from, range.to);
3750
+ else
3751
+ updated = EditorSelection.range(range.from == range.anchor ? from : to, range.from == range.head ? from : to);
3752
+ }
3748
3753
  }
3749
3754
  if (updated) {
3750
3755
  if (!ranges)
@@ -3840,12 +3845,11 @@ class InlineCoordsScan {
3840
3845
  }
3841
3846
  // Scan through the rectangles for the content of a tile with inline
3842
3847
  // content, looking for one that overlaps the queried position
3843
- // vertically andis
3844
- // closest horizontally. The caller is responsible for dividing its
3845
- // content into N pieces, and pass an array with N+1 positions
3846
- // (including the position after the last piece). For a text tile,
3847
- // these will be character clusters, for a composite tile, these
3848
- // will be child tiles.
3848
+ // vertically and is closest horizontally. The caller is responsible
3849
+ // for dividing its content into N pieces, and pass an array with
3850
+ // N+1 positions (including the position after the last piece). For
3851
+ // a text tile, these will be character clusters, for a composite
3852
+ // tile, these will be child tiles.
3849
3853
  scan(positions, getRects, recursed = false) {
3850
3854
  let lo = 0, hi = positions.length - 1, seen = new Set();
3851
3855
  let bidi = this.bidiIn(positions[0], positions[hi]);
@@ -3917,6 +3921,8 @@ class InlineCoordsScan {
3917
3921
  // If no element with y overlap is found, find the nearest element
3918
3922
  // on the y axis, move this.y into it, and retry the scan.
3919
3923
  if (!closestRect) {
3924
+ if (!below && !above)
3925
+ return { i: positions[0], after: false };
3920
3926
  let side = above && (!below || (this.y - above.bottom < below.top - this.y)) ? above : below;
3921
3927
  this.y = (side.top + side.bottom) / 2;
3922
3928
  return this.scan(positions, getRects, true);
@@ -4149,7 +4155,8 @@ class DOMChange {
4149
4155
  // Chrome will put the selection *inside* them, confusing
4150
4156
  // posFromDOM
4151
4157
  let vp = view.viewport;
4152
- if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
4158
+ if ((browser.ios || browser.chrome) && head != anchor &&
4159
+ Math.min(head, anchor) <= curSel.main.from && Math.max(head, anchor) >= curSel.main.to &&
4153
4160
  (vp.from > 0 || vp.to < view.state.doc.length)) {
4154
4161
  let from = Math.min(head, anchor), to = Math.max(head, anchor);
4155
4162
  let offFrom = vp.from - from, offTo = vp.to - to;
@@ -4470,13 +4477,11 @@ class InputState {
4470
4477
  // (after which we retroactively handle them and reset the DOM) to
4471
4478
  // avoid messing up the virtual keyboard state.
4472
4479
  this.pendingIOSKey = undefined;
4473
- /**
4474
- When enabled (>-1), tab presses are not given to key handlers,
4475
- leaving the browser's default behavior. If >0, the mode expires
4476
- at that timestamp, and any other keypress clears it.
4477
- Esc enables temporary tab focus mode for two seconds when not
4478
- otherwise handled.
4479
- */
4480
+ // When enabled (>-1), tab presses are not given to key handlers,
4481
+ // leaving the browser's default behavior. If >0, the mode expires
4482
+ // at that timestamp, and any other keypress clears it.
4483
+ // Esc enables temporary tab focus mode for two seconds when not
4484
+ // otherwise handled.
4480
4485
  this.tabFocusMode = -1;
4481
4486
  this.lastSelectionOrigin = null;
4482
4487
  this.lastSelectionTime = 0;
@@ -4584,11 +4589,16 @@ class InputState {
4584
4589
  // state. So we let it go through, and then, in
4585
4590
  // applyDOMChange, notify key handlers of it and reset to
4586
4591
  // the state they produce.
4587
- let pending;
4588
- if (browser.ios && !event.synthetic && !event.altKey && !event.metaKey && !event.shiftKey &&
4589
- ((pending = PendingKeys.find(key => key.keyCode == event.keyCode)) && !event.ctrlKey ||
4592
+ if (browser.ios && !event.synthetic && !event.altKey && !event.metaKey &&
4593
+ (PendingKeys.some(key => key.keyCode == event.keyCode) && !event.ctrlKey ||
4590
4594
  EmacsyPendingKeys.indexOf(event.key) > -1 && event.ctrlKey)) {
4591
- this.pendingIOSKey = pending || event;
4595
+ let mods = { ctrlKey: event.ctrlKey, altKey: event.altKey, metaKey: event.metaKey, shiftKey: event.shiftKey };
4596
+ // On iOS with autocapitalize, drop the shift modifier for these
4597
+ // keys, since it will be set at the start of every sentence.
4598
+ if (mods.shiftKey && browser.ios && !/^(off|none)$/.test(this.view.contentDOM.autocapitalize) &&
4599
+ iosVirtualKeyboardOpen(this.view.win))
4600
+ mods.shiftKey = false;
4601
+ this.pendingIOSKey = { key: event.key, keyCode: event.keyCode, mods };
4592
4602
  setTimeout(() => this.flushIOSKey(), 250);
4593
4603
  return true;
4594
4604
  }
@@ -4604,7 +4614,7 @@ class InputState {
4604
4614
  if (key.key == "Enter" && change && change.from < change.to && /^\S+$/.test(change.insert.toString()))
4605
4615
  return false;
4606
4616
  this.pendingIOSKey = undefined;
4607
- return dispatchKey(this.view.contentDOM, key.key, key.keyCode, key instanceof KeyboardEvent ? key : undefined);
4617
+ return dispatchKey(this.view.contentDOM, key.key, key.keyCode, key.mods);
4608
4618
  }
4609
4619
  ignoreDuringComposition(event) {
4610
4620
  if (!/^key/.test(event.type) || event.synthetic)
@@ -4642,6 +4652,11 @@ class InputState {
4642
4652
  this.mouseSelection.destroy();
4643
4653
  }
4644
4654
  }
4655
+ function iosVirtualKeyboardOpen(win) {
4656
+ if (!win.visualViewport)
4657
+ return false;
4658
+ return win.visualViewport.height * win.visualViewport.scale / win.document.documentElement.clientHeight < 0.85;
4659
+ }
4645
4660
  function bindHandler(plugin, handler) {
4646
4661
  return (view, event) => {
4647
4662
  try {
@@ -4961,7 +4976,7 @@ function rangeForClick(view, pos, bias, type) {
4961
4976
  let from = visual ? visual.posAtStart : line.from, to = visual ? visual.posAtEnd : line.to;
4962
4977
  if (to < view.state.doc.length && to == line.to)
4963
4978
  to++;
4964
- return EditorSelection.range(from, to);
4979
+ return EditorSelection.undirectionalRange(from, to);
4965
4980
  }
4966
4981
  }
4967
4982
  const BadMouseDetail = browser.ie && browser.ie_version <= 11;
@@ -5019,7 +5034,7 @@ handlers.dragstart = (view, event) => {
5019
5034
  if (tile && tile.isWidget()) {
5020
5035
  let from = tile.posAtStart, to = from + tile.length;
5021
5036
  if (from >= range.to || to <= range.from)
5022
- range = EditorSelection.range(from, to);
5037
+ range = EditorSelection.undirectionalRange(from, to);
5023
5038
  }
5024
5039
  }
5025
5040
  let { inputState } = view;
@@ -6809,6 +6824,7 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
6809
6824
  padding: "0 2px 0 6px"
6810
6825
  },
6811
6826
  ".cm-layer": {
6827
+ userSelect: "none", // #1708
6812
6828
  position: "absolute",
6813
6829
  left: 0,
6814
6830
  top: 0,
@@ -7643,7 +7659,6 @@ class EditContextManager {
7643
7659
  for (let event in this.handlers)
7644
7660
  context.addEventListener(event, this.handlers[event]);
7645
7661
  this.measureReq = { read: view => {
7646
- this.editContext.updateControlBounds(view.contentDOM.getBoundingClientRect());
7647
7662
  let sel = getSelection(view.root);
7648
7663
  if (sel && sel.rangeCount)
7649
7664
  this.editContext.updateSelectionBounds(sel.getRangeAt(0).getBoundingClientRect());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.43.0",
3
+ "version": "6.43.2",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",
@@ -26,7 +26,7 @@
26
26
  "sideEffects": false,
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@codemirror/state": "^6.6.0",
29
+ "@codemirror/state": "^6.7.0",
30
30
  "crelt": "^1.0.6",
31
31
  "style-mod": "^4.1.0",
32
32
  "w3c-keyname": "^2.2.4"