@codemirror/view 6.39.16 → 6.40.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,27 @@
1
+ ## 6.40.0 (2026-03-12)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix a bug that caused Shift-Enter/Backspace/Delete on iOS to lose the shift modifier when delivered to key event handlers.
6
+
7
+ Fix an issue where `EditorView.moveVertically` could move to the wrong place in wrapped lines with a large line height.
8
+
9
+ Make sure the selection head associativity is properly set for mouse selections made with shift held down.
10
+
11
+ ### New features
12
+
13
+ `WidgetType.updateDOM` is now called with the previous widget value as third argument.
14
+
15
+ ## 6.39.17 (2026-03-10)
16
+
17
+ ### Bug fixes
18
+
19
+ Improve touch tap-selection on line wrapping boundaries.
20
+
21
+ Make `drawSelection` draw our own selection handles on iOS.
22
+
23
+ Fix an issue where `posAtCoords`, when querying line wrapping points, got confused by extra empty client rectangles produced by Safari.
24
+
1
25
  ## 6.39.16 (2026-03-02)
2
26
 
3
27
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -134,7 +134,7 @@ class WidgetType {
134
134
  couldn't (in which case the widget will be redrawn). The default
135
135
  implementation just returns false.
136
136
  */
137
- updateDOM(dom, view) { return false; }
137
+ updateDOM(dom, view, from) { return false; }
138
138
  /**
139
139
  @internal
140
140
  */
@@ -2539,7 +2539,7 @@ class TileCache {
2539
2539
  let tile = widgets[i];
2540
2540
  if (!this.reused.has(tile) &&
2541
2541
  (pass == 0 ? tile.widget.compare(widget)
2542
- : tile.widget.constructor == widget.constructor && widget.updateDOM(tile.dom, this.view))) {
2542
+ : tile.widget.constructor == widget.constructor && widget.updateDOM(tile.dom, this.view, tile.widget))) {
2543
2543
  widgets.splice(i, 1);
2544
2544
  if (i < this.index[0])
2545
2545
  this.index[0]--;
@@ -3395,6 +3395,7 @@ class DocView {
3395
3395
  this.blockWrappers = this.view.state.facet(blockWrappers).map(v => typeof v == "function" ? v(this.view) : v);
3396
3396
  }
3397
3397
  scrollIntoView(target) {
3398
+ var _a;
3398
3399
  if (target.isSnapshot) {
3399
3400
  let ref = this.view.viewState.lineBlockAt(target.range.head);
3400
3401
  this.view.scrollDOM.scrollTop = ref.top - target.yMargin;
@@ -3411,7 +3412,7 @@ class DocView {
3411
3412
  }
3412
3413
  }
3413
3414
  let { range } = target;
3414
- let rect = this.coordsAt(range.head, range.empty ? range.assoc : range.head > range.anchor ? -1 : 1), other;
3415
+ let rect = this.coordsAt(range.head, (_a = range.assoc) !== null && _a !== void 0 ? _a : (range.empty ? 0 : range.head > range.anchor ? -1 : 1)), other;
3415
3416
  if (!rect)
3416
3417
  return;
3417
3418
  if (!range.empty && (other = this.coordsAt(range.anchor, range.anchor > range.head ? -1 : 1)))
@@ -3678,7 +3679,8 @@ function moveVertically(view, start, forward, distance) {
3678
3679
  return state.EditorSelection.cursor(startPos, start.assoc);
3679
3680
  let goal = start.goalColumn, startY;
3680
3681
  let rect = view.contentDOM.getBoundingClientRect();
3681
- let startCoords = view.coordsAtPos(startPos, (start.empty ? start.assoc : 0) || (forward ? 1 : -1)), docTop = view.documentTop;
3682
+ let startCoords = view.coordsAtPos(startPos, start.assoc || ((start.empty ? forward : start.head == start.from) ? 1 : -1));
3683
+ let docTop = view.documentTop;
3682
3684
  if (startCoords) {
3683
3685
  if (goal == null)
3684
3686
  goal = startCoords.left - rect.left;
@@ -3691,9 +3693,16 @@ function moveVertically(view, start, forward, distance) {
3691
3693
  startY = (dir < 0 ? line.top : line.bottom) + docTop;
3692
3694
  }
3693
3695
  let resolvedGoal = rect.left + goal;
3694
- let dist = distance !== null && distance !== void 0 ? distance : (view.viewState.heightOracle.textHeight >> 1);
3695
- let pos = posAtCoords(view, { x: resolvedGoal, y: startY + dist * dir }, false, dir);
3696
- return state.EditorSelection.cursor(pos.pos, pos.assoc, undefined, goal);
3696
+ let halfText = view.viewState.heightOracle.textHeight >> 1, dist = distance !== null && distance !== void 0 ? distance : halfText;
3697
+ for (let scan = 0;; scan += halfText) {
3698
+ let y = startY + (dist + scan) * dir;
3699
+ let pos = posAtCoords(view, { x: resolvedGoal, y }, false, dir);
3700
+ if (forward ? y > rect.bottom : y < rect.top)
3701
+ return state.EditorSelection.cursor(pos.pos, pos.assoc);
3702
+ let posCoords = view.coordsAtPos(pos.pos, pos.assoc), mid = posCoords ? (posCoords.top + posCoords.bottom) / 2 : 0;
3703
+ if (!posCoords || (forward ? mid > startY : mid < startY))
3704
+ return state.EditorSelection.cursor(pos.pos, pos.assoc, undefined, goal);
3705
+ }
3697
3706
  }
3698
3707
  function skipAtomicRanges(atoms, pos, bias) {
3699
3708
  for (;;) {
@@ -3863,6 +3872,9 @@ class InlineCoordsScan {
3863
3872
  if (rects)
3864
3873
  for (let i = 0; i < rects.length; i++) {
3865
3874
  let rect = rects[i], side = 0;
3875
+ // Ignore empty rectangles when there are other rectangles
3876
+ if (rect.width == 0 && rects.length > 1)
3877
+ continue;
3866
3878
  if (rect.bottom < this.y) {
3867
3879
  if (!above || above.bottom < rect.bottom)
3868
3880
  above = rect;
@@ -4086,7 +4098,7 @@ class DOMChange {
4086
4098
  this.bounds = null;
4087
4099
  this.text = "";
4088
4100
  this.domChanged = start > -1;
4089
- let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
4101
+ let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView, curSel = view.state.selection;
4090
4102
  if (view.state.readOnly && start > -1) {
4091
4103
  // Ignore changes when the editor is read-only
4092
4104
  this.newSel = null;
@@ -4102,18 +4114,18 @@ class DOMChange {
4102
4114
  let domSel = view.observer.selectionRange;
4103
4115
  let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
4104
4116
  !contains(view.contentDOM, domSel.focusNode)
4105
- ? view.state.selection.main.head
4117
+ ? curSel.main.head
4106
4118
  : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
4107
4119
  let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
4108
4120
  !contains(view.contentDOM, domSel.anchorNode)
4109
- ? view.state.selection.main.anchor
4121
+ ? curSel.main.anchor
4110
4122
  : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
4111
4123
  // iOS will refuse to select the block gaps when doing
4112
4124
  // select-all.
4113
4125
  // Chrome will put the selection *inside* them, confusing
4114
4126
  // posFromDOM
4115
4127
  let vp = view.viewport;
4116
- if ((browser.ios || browser.chrome) && view.state.selection.main.empty && head != anchor &&
4128
+ if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
4117
4129
  (vp.from > 0 || vp.to < view.state.doc.length)) {
4118
4130
  let from = Math.min(head, anchor), to = Math.max(head, anchor);
4119
4131
  let offFrom = vp.from - from, offTo = vp.to - to;
@@ -4122,10 +4134,22 @@ class DOMChange {
4122
4134
  anchor = view.state.doc.length;
4123
4135
  }
4124
4136
  }
4125
- if (view.inputState.composing > -1 && view.state.selection.ranges.length > 1)
4126
- this.newSel = view.state.selection.replaceRange(state.EditorSelection.range(anchor, head));
4127
- else
4137
+ if (view.inputState.composing > -1 && curSel.ranges.length > 1) {
4138
+ this.newSel = curSel.replaceRange(state.EditorSelection.range(anchor, head));
4139
+ }
4140
+ else if (view.lineWrapping && anchor == head && !(curSel.main.empty && curSel.main.head == head) &&
4141
+ view.inputState.lastTouchTime > Date.now() - 100) {
4142
+ // If this is a cursor selection change in a line-wrapping
4143
+ // editor that may have been a touch, use the last touch
4144
+ // position to assign a side to the cursor.
4145
+ let before = view.coordsAtPos(head, -1), assoc = 0;
4146
+ if (before)
4147
+ assoc = view.inputState.lastTouchY <= before.bottom ? -1 : 1;
4148
+ this.newSel = state.EditorSelection.create([state.EditorSelection.cursor(head, assoc)]);
4149
+ }
4150
+ else {
4128
4151
  this.newSel = state.EditorSelection.single(anchor, head);
4152
+ }
4129
4153
  }
4130
4154
  }
4131
4155
  }
@@ -4412,6 +4436,8 @@ class InputState {
4412
4436
  this.lastKeyCode = 0;
4413
4437
  this.lastKeyTime = 0;
4414
4438
  this.lastTouchTime = 0;
4439
+ this.lastTouchX = 0;
4440
+ this.lastTouchY = 0;
4415
4441
  this.lastFocusTime = 0;
4416
4442
  this.lastScrollTop = 0;
4417
4443
  this.lastScrollLeft = 0;
@@ -4535,9 +4561,9 @@ class InputState {
4535
4561
  // applyDOMChange, notify key handlers of it and reset to
4536
4562
  // the state they produce.
4537
4563
  let pending;
4538
- if (browser.ios && !event.synthetic && !event.altKey && !event.metaKey &&
4564
+ if (browser.ios && !event.synthetic && !event.altKey && !event.metaKey && !event.shiftKey &&
4539
4565
  ((pending = PendingKeys.find(key => key.keyCode == event.keyCode)) && !event.ctrlKey ||
4540
- EmacsyPendingKeys.indexOf(event.key) > -1 && event.ctrlKey && !event.shiftKey)) {
4566
+ EmacsyPendingKeys.indexOf(event.key) > -1 && event.ctrlKey)) {
4541
4567
  this.pendingIOSKey = pending || event;
4542
4568
  setTimeout(() => this.flushIOSKey(), 250);
4543
4569
  return true;
@@ -4855,8 +4881,13 @@ handlers.keydown = (view, event) => {
4855
4881
  return false;
4856
4882
  };
4857
4883
  observers.touchstart = (view, e) => {
4858
- view.inputState.lastTouchTime = Date.now();
4859
- view.inputState.setSelectionOrigin("select.pointer");
4884
+ let iState = view.inputState, touch = e.targetTouches[0];
4885
+ iState.lastTouchTime = Date.now();
4886
+ if (touch) {
4887
+ iState.lastTouchX = touch.clientX;
4888
+ iState.lastTouchY = touch.clientY;
4889
+ }
4890
+ iState.setSelectionOrigin("select.pointer");
4860
4891
  };
4861
4892
  observers.touchmove = view => {
4862
4893
  view.inputState.setSelectionOrigin("select.pointer");
@@ -4936,10 +4967,10 @@ function basicMouseSelection(view, event) {
4936
4967
  if (start.pos != cur.pos && !extend) {
4937
4968
  let startRange = rangeForClick(view, start.pos, start.assoc, type);
4938
4969
  let from = Math.min(startRange.from, range.from), to = Math.max(startRange.to, range.to);
4939
- range = from < range.from ? state.EditorSelection.range(from, to) : state.EditorSelection.range(to, from);
4970
+ range = from < range.from ? state.EditorSelection.range(from, to, range.assoc) : state.EditorSelection.range(to, from, range.assoc);
4940
4971
  }
4941
4972
  if (extend)
4942
- return startSel.replaceRange(startSel.main.extend(range.from, range.to));
4973
+ return startSel.replaceRange(startSel.main.extend(range.from, range.to, range.assoc));
4943
4974
  else if (multiple && type == 1 && startSel.ranges.length > 1 && (removed = removeRangeAround(startSel, cur.pos)))
4944
4975
  return removed;
4945
4976
  else if (multiple)
@@ -6798,6 +6829,21 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
6798
6829
  "&dark .cm-cursor": {
6799
6830
  borderLeftColor: "#ddd"
6800
6831
  },
6832
+ ".cm-selectionHandle": {
6833
+ backgroundColor: "currentColor",
6834
+ width: "1.5px"
6835
+ },
6836
+ ".cm-selectionHandle-start::before, .cm-selectionHandle-end::before": {
6837
+ content: '""',
6838
+ backgroundColor: "inherit",
6839
+ borderRadius: "50%",
6840
+ width: "8px",
6841
+ height: "8px",
6842
+ position: "absolute",
6843
+ left: "-3.25px"
6844
+ },
6845
+ ".cm-selectionHandle-start::before": { top: "-8px" },
6846
+ ".cm-selectionHandle-end::before": { bottom: "-8px" },
6801
6847
  ".cm-dropCursor": {
6802
6848
  position: "absolute"
6803
6849
  },
@@ -9362,7 +9408,8 @@ const selectionConfig = state.Facet.define({
9362
9408
  combine(configs) {
9363
9409
  return state.combineConfig(configs, {
9364
9410
  cursorBlinkRate: 1200,
9365
- drawRangeCursor: true
9411
+ drawRangeCursor: true,
9412
+ iosSelectionHandles: true
9366
9413
  }, {
9367
9414
  cursorBlinkRate: (a, b) => Math.min(a, b),
9368
9415
  drawRangeCursor: (a, b) => a || b
@@ -9414,9 +9461,9 @@ const cursorLayer = layer({
9414
9461
  let cursors = [];
9415
9462
  for (let r of state$1.selection.ranges) {
9416
9463
  let prim = r == state$1.selection.main;
9417
- if (r.empty || conf.drawRangeCursor) {
9464
+ if (r.empty || conf.drawRangeCursor && !(prim && browser.ios && conf.iosSelectionHandles)) {
9418
9465
  let className = prim ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
9419
- let cursor = r.empty ? r : state.EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
9466
+ let cursor = r.empty ? r : state.EditorSelection.cursor(r.head, r.assoc);
9420
9467
  for (let piece of RectangleMarker.forRange(view, className, cursor))
9421
9468
  cursors.push(piece);
9422
9469
  }
@@ -9442,8 +9489,19 @@ function setBlinkRate(state, dom) {
9442
9489
  const selectionLayer = layer({
9443
9490
  above: false,
9444
9491
  markers(view) {
9445
- return view.state.selection.ranges.map(r => r.empty ? [] : RectangleMarker.forRange(view, "cm-selectionBackground", r))
9446
- .reduce((a, b) => a.concat(b));
9492
+ let markers = [], { main, ranges } = view.state.selection;
9493
+ for (let r of ranges)
9494
+ if (!r.empty) {
9495
+ for (let marker of RectangleMarker.forRange(view, "cm-selectionBackground", r))
9496
+ markers.push(marker);
9497
+ }
9498
+ if (browser.ios && !main.empty && view.state.facet(selectionConfig).iosSelectionHandles) {
9499
+ for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-start", state.EditorSelection.cursor(main.from, 1)))
9500
+ markers.push(piece);
9501
+ for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-end", state.EditorSelection.cursor(main.to, 1)))
9502
+ markers.push(piece);
9503
+ }
9504
+ return markers;
9447
9505
  },
9448
9506
  update(update, dom) {
9449
9507
  return update.docChanged || update.selectionSet || update.viewportChanged || configChanged(update);
package/dist/index.d.cts CHANGED
@@ -224,7 +224,7 @@ declare abstract class WidgetType {
224
224
  couldn't (in which case the widget will be redrawn). The default
225
225
  implementation just returns false.
226
226
  */
227
- updateDOM(dom: HTMLElement, view: EditorView): boolean;
227
+ updateDOM(dom: HTMLElement, view: EditorView, from: this): boolean;
228
228
  /**
229
229
  The estimated height this widget will have, to be used when
230
230
  estimating the height of content that hasn't been drawn. May
@@ -1573,6 +1573,12 @@ type SelectionConfig = {
1573
1573
  true.
1574
1574
  */
1575
1575
  drawRangeCursor?: boolean;
1576
+ /**
1577
+ Because hiding the cursor also hides the selection handles in
1578
+ the iOS browser, when this is enabled (the default), the
1579
+ extension draws handles on the side of the selection in iOS.
1580
+ */
1581
+ iosSelectionHandles?: boolean;
1576
1582
  };
1577
1583
  /**
1578
1584
  Returns an extension that hides the browser's native selection and
package/dist/index.d.ts CHANGED
@@ -224,7 +224,7 @@ declare abstract class WidgetType {
224
224
  couldn't (in which case the widget will be redrawn). The default
225
225
  implementation just returns false.
226
226
  */
227
- updateDOM(dom: HTMLElement, view: EditorView): boolean;
227
+ updateDOM(dom: HTMLElement, view: EditorView, from: this): boolean;
228
228
  /**
229
229
  The estimated height this widget will have, to be used when
230
230
  estimating the height of content that hasn't been drawn. May
@@ -1573,6 +1573,12 @@ type SelectionConfig = {
1573
1573
  true.
1574
1574
  */
1575
1575
  drawRangeCursor?: boolean;
1576
+ /**
1577
+ Because hiding the cursor also hides the selection handles in
1578
+ the iOS browser, when this is enabled (the default), the
1579
+ extension draws handles on the side of the selection in iOS.
1580
+ */
1581
+ iosSelectionHandles?: boolean;
1576
1582
  };
1577
1583
  /**
1578
1584
  Returns an extension that hides the browser's native selection and
package/dist/index.js CHANGED
@@ -132,7 +132,7 @@ class WidgetType {
132
132
  couldn't (in which case the widget will be redrawn). The default
133
133
  implementation just returns false.
134
134
  */
135
- updateDOM(dom, view) { return false; }
135
+ updateDOM(dom, view, from) { return false; }
136
136
  /**
137
137
  @internal
138
138
  */
@@ -2535,7 +2535,7 @@ class TileCache {
2535
2535
  let tile = widgets[i];
2536
2536
  if (!this.reused.has(tile) &&
2537
2537
  (pass == 0 ? tile.widget.compare(widget)
2538
- : tile.widget.constructor == widget.constructor && widget.updateDOM(tile.dom, this.view))) {
2538
+ : tile.widget.constructor == widget.constructor && widget.updateDOM(tile.dom, this.view, tile.widget))) {
2539
2539
  widgets.splice(i, 1);
2540
2540
  if (i < this.index[0])
2541
2541
  this.index[0]--;
@@ -3391,6 +3391,7 @@ class DocView {
3391
3391
  this.blockWrappers = this.view.state.facet(blockWrappers).map(v => typeof v == "function" ? v(this.view) : v);
3392
3392
  }
3393
3393
  scrollIntoView(target) {
3394
+ var _a;
3394
3395
  if (target.isSnapshot) {
3395
3396
  let ref = this.view.viewState.lineBlockAt(target.range.head);
3396
3397
  this.view.scrollDOM.scrollTop = ref.top - target.yMargin;
@@ -3407,7 +3408,7 @@ class DocView {
3407
3408
  }
3408
3409
  }
3409
3410
  let { range } = target;
3410
- let rect = this.coordsAt(range.head, range.empty ? range.assoc : range.head > range.anchor ? -1 : 1), other;
3411
+ let rect = this.coordsAt(range.head, (_a = range.assoc) !== null && _a !== void 0 ? _a : (range.empty ? 0 : range.head > range.anchor ? -1 : 1)), other;
3411
3412
  if (!rect)
3412
3413
  return;
3413
3414
  if (!range.empty && (other = this.coordsAt(range.anchor, range.anchor > range.head ? -1 : 1)))
@@ -3674,7 +3675,8 @@ function moveVertically(view, start, forward, distance) {
3674
3675
  return EditorSelection.cursor(startPos, start.assoc);
3675
3676
  let goal = start.goalColumn, startY;
3676
3677
  let rect = view.contentDOM.getBoundingClientRect();
3677
- let startCoords = view.coordsAtPos(startPos, (start.empty ? start.assoc : 0) || (forward ? 1 : -1)), docTop = view.documentTop;
3678
+ let startCoords = view.coordsAtPos(startPos, start.assoc || ((start.empty ? forward : start.head == start.from) ? 1 : -1));
3679
+ let docTop = view.documentTop;
3678
3680
  if (startCoords) {
3679
3681
  if (goal == null)
3680
3682
  goal = startCoords.left - rect.left;
@@ -3687,9 +3689,16 @@ function moveVertically(view, start, forward, distance) {
3687
3689
  startY = (dir < 0 ? line.top : line.bottom) + docTop;
3688
3690
  }
3689
3691
  let resolvedGoal = rect.left + goal;
3690
- let dist = distance !== null && distance !== void 0 ? distance : (view.viewState.heightOracle.textHeight >> 1);
3691
- let pos = posAtCoords(view, { x: resolvedGoal, y: startY + dist * dir }, false, dir);
3692
- return EditorSelection.cursor(pos.pos, pos.assoc, undefined, goal);
3692
+ let halfText = view.viewState.heightOracle.textHeight >> 1, dist = distance !== null && distance !== void 0 ? distance : halfText;
3693
+ for (let scan = 0;; scan += halfText) {
3694
+ let y = startY + (dist + scan) * dir;
3695
+ let pos = posAtCoords(view, { x: resolvedGoal, y }, false, dir);
3696
+ if (forward ? y > rect.bottom : y < rect.top)
3697
+ return EditorSelection.cursor(pos.pos, pos.assoc);
3698
+ let posCoords = view.coordsAtPos(pos.pos, pos.assoc), mid = posCoords ? (posCoords.top + posCoords.bottom) / 2 : 0;
3699
+ if (!posCoords || (forward ? mid > startY : mid < startY))
3700
+ return EditorSelection.cursor(pos.pos, pos.assoc, undefined, goal);
3701
+ }
3693
3702
  }
3694
3703
  function skipAtomicRanges(atoms, pos, bias) {
3695
3704
  for (;;) {
@@ -3859,6 +3868,9 @@ class InlineCoordsScan {
3859
3868
  if (rects)
3860
3869
  for (let i = 0; i < rects.length; i++) {
3861
3870
  let rect = rects[i], side = 0;
3871
+ // Ignore empty rectangles when there are other rectangles
3872
+ if (rect.width == 0 && rects.length > 1)
3873
+ continue;
3862
3874
  if (rect.bottom < this.y) {
3863
3875
  if (!above || above.bottom < rect.bottom)
3864
3876
  above = rect;
@@ -4082,7 +4094,7 @@ class DOMChange {
4082
4094
  this.bounds = null;
4083
4095
  this.text = "";
4084
4096
  this.domChanged = start > -1;
4085
- let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
4097
+ let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView, curSel = view.state.selection;
4086
4098
  if (view.state.readOnly && start > -1) {
4087
4099
  // Ignore changes when the editor is read-only
4088
4100
  this.newSel = null;
@@ -4098,18 +4110,18 @@ class DOMChange {
4098
4110
  let domSel = view.observer.selectionRange;
4099
4111
  let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
4100
4112
  !contains(view.contentDOM, domSel.focusNode)
4101
- ? view.state.selection.main.head
4113
+ ? curSel.main.head
4102
4114
  : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
4103
4115
  let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
4104
4116
  !contains(view.contentDOM, domSel.anchorNode)
4105
- ? view.state.selection.main.anchor
4117
+ ? curSel.main.anchor
4106
4118
  : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
4107
4119
  // iOS will refuse to select the block gaps when doing
4108
4120
  // select-all.
4109
4121
  // Chrome will put the selection *inside* them, confusing
4110
4122
  // posFromDOM
4111
4123
  let vp = view.viewport;
4112
- if ((browser.ios || browser.chrome) && view.state.selection.main.empty && head != anchor &&
4124
+ if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
4113
4125
  (vp.from > 0 || vp.to < view.state.doc.length)) {
4114
4126
  let from = Math.min(head, anchor), to = Math.max(head, anchor);
4115
4127
  let offFrom = vp.from - from, offTo = vp.to - to;
@@ -4118,10 +4130,22 @@ class DOMChange {
4118
4130
  anchor = view.state.doc.length;
4119
4131
  }
4120
4132
  }
4121
- if (view.inputState.composing > -1 && view.state.selection.ranges.length > 1)
4122
- this.newSel = view.state.selection.replaceRange(EditorSelection.range(anchor, head));
4123
- else
4133
+ if (view.inputState.composing > -1 && curSel.ranges.length > 1) {
4134
+ this.newSel = curSel.replaceRange(EditorSelection.range(anchor, head));
4135
+ }
4136
+ else if (view.lineWrapping && anchor == head && !(curSel.main.empty && curSel.main.head == head) &&
4137
+ view.inputState.lastTouchTime > Date.now() - 100) {
4138
+ // If this is a cursor selection change in a line-wrapping
4139
+ // editor that may have been a touch, use the last touch
4140
+ // position to assign a side to the cursor.
4141
+ let before = view.coordsAtPos(head, -1), assoc = 0;
4142
+ if (before)
4143
+ assoc = view.inputState.lastTouchY <= before.bottom ? -1 : 1;
4144
+ this.newSel = EditorSelection.create([EditorSelection.cursor(head, assoc)]);
4145
+ }
4146
+ else {
4124
4147
  this.newSel = EditorSelection.single(anchor, head);
4148
+ }
4125
4149
  }
4126
4150
  }
4127
4151
  }
@@ -4408,6 +4432,8 @@ class InputState {
4408
4432
  this.lastKeyCode = 0;
4409
4433
  this.lastKeyTime = 0;
4410
4434
  this.lastTouchTime = 0;
4435
+ this.lastTouchX = 0;
4436
+ this.lastTouchY = 0;
4411
4437
  this.lastFocusTime = 0;
4412
4438
  this.lastScrollTop = 0;
4413
4439
  this.lastScrollLeft = 0;
@@ -4531,9 +4557,9 @@ class InputState {
4531
4557
  // applyDOMChange, notify key handlers of it and reset to
4532
4558
  // the state they produce.
4533
4559
  let pending;
4534
- if (browser.ios && !event.synthetic && !event.altKey && !event.metaKey &&
4560
+ if (browser.ios && !event.synthetic && !event.altKey && !event.metaKey && !event.shiftKey &&
4535
4561
  ((pending = PendingKeys.find(key => key.keyCode == event.keyCode)) && !event.ctrlKey ||
4536
- EmacsyPendingKeys.indexOf(event.key) > -1 && event.ctrlKey && !event.shiftKey)) {
4562
+ EmacsyPendingKeys.indexOf(event.key) > -1 && event.ctrlKey)) {
4537
4563
  this.pendingIOSKey = pending || event;
4538
4564
  setTimeout(() => this.flushIOSKey(), 250);
4539
4565
  return true;
@@ -4851,8 +4877,13 @@ handlers.keydown = (view, event) => {
4851
4877
  return false;
4852
4878
  };
4853
4879
  observers.touchstart = (view, e) => {
4854
- view.inputState.lastTouchTime = Date.now();
4855
- view.inputState.setSelectionOrigin("select.pointer");
4880
+ let iState = view.inputState, touch = e.targetTouches[0];
4881
+ iState.lastTouchTime = Date.now();
4882
+ if (touch) {
4883
+ iState.lastTouchX = touch.clientX;
4884
+ iState.lastTouchY = touch.clientY;
4885
+ }
4886
+ iState.setSelectionOrigin("select.pointer");
4856
4887
  };
4857
4888
  observers.touchmove = view => {
4858
4889
  view.inputState.setSelectionOrigin("select.pointer");
@@ -4932,10 +4963,10 @@ function basicMouseSelection(view, event) {
4932
4963
  if (start.pos != cur.pos && !extend) {
4933
4964
  let startRange = rangeForClick(view, start.pos, start.assoc, type);
4934
4965
  let from = Math.min(startRange.from, range.from), to = Math.max(startRange.to, range.to);
4935
- range = from < range.from ? EditorSelection.range(from, to) : EditorSelection.range(to, from);
4966
+ range = from < range.from ? EditorSelection.range(from, to, range.assoc) : EditorSelection.range(to, from, range.assoc);
4936
4967
  }
4937
4968
  if (extend)
4938
- return startSel.replaceRange(startSel.main.extend(range.from, range.to));
4969
+ return startSel.replaceRange(startSel.main.extend(range.from, range.to, range.assoc));
4939
4970
  else if (multiple && type == 1 && startSel.ranges.length > 1 && (removed = removeRangeAround(startSel, cur.pos)))
4940
4971
  return removed;
4941
4972
  else if (multiple)
@@ -6793,6 +6824,21 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
6793
6824
  "&dark .cm-cursor": {
6794
6825
  borderLeftColor: "#ddd"
6795
6826
  },
6827
+ ".cm-selectionHandle": {
6828
+ backgroundColor: "currentColor",
6829
+ width: "1.5px"
6830
+ },
6831
+ ".cm-selectionHandle-start::before, .cm-selectionHandle-end::before": {
6832
+ content: '""',
6833
+ backgroundColor: "inherit",
6834
+ borderRadius: "50%",
6835
+ width: "8px",
6836
+ height: "8px",
6837
+ position: "absolute",
6838
+ left: "-3.25px"
6839
+ },
6840
+ ".cm-selectionHandle-start::before": { top: "-8px" },
6841
+ ".cm-selectionHandle-end::before": { bottom: "-8px" },
6796
6842
  ".cm-dropCursor": {
6797
6843
  position: "absolute"
6798
6844
  },
@@ -9357,7 +9403,8 @@ const selectionConfig = /*@__PURE__*/Facet.define({
9357
9403
  combine(configs) {
9358
9404
  return combineConfig(configs, {
9359
9405
  cursorBlinkRate: 1200,
9360
- drawRangeCursor: true
9406
+ drawRangeCursor: true,
9407
+ iosSelectionHandles: true
9361
9408
  }, {
9362
9409
  cursorBlinkRate: (a, b) => Math.min(a, b),
9363
9410
  drawRangeCursor: (a, b) => a || b
@@ -9409,9 +9456,9 @@ const cursorLayer = /*@__PURE__*/layer({
9409
9456
  let cursors = [];
9410
9457
  for (let r of state.selection.ranges) {
9411
9458
  let prim = r == state.selection.main;
9412
- if (r.empty || conf.drawRangeCursor) {
9459
+ if (r.empty || conf.drawRangeCursor && !(prim && browser.ios && conf.iosSelectionHandles)) {
9413
9460
  let className = prim ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
9414
- let cursor = r.empty ? r : EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
9461
+ let cursor = r.empty ? r : EditorSelection.cursor(r.head, r.assoc);
9415
9462
  for (let piece of RectangleMarker.forRange(view, className, cursor))
9416
9463
  cursors.push(piece);
9417
9464
  }
@@ -9437,8 +9484,19 @@ function setBlinkRate(state, dom) {
9437
9484
  const selectionLayer = /*@__PURE__*/layer({
9438
9485
  above: false,
9439
9486
  markers(view) {
9440
- return view.state.selection.ranges.map(r => r.empty ? [] : RectangleMarker.forRange(view, "cm-selectionBackground", r))
9441
- .reduce((a, b) => a.concat(b));
9487
+ let markers = [], { main, ranges } = view.state.selection;
9488
+ for (let r of ranges)
9489
+ if (!r.empty) {
9490
+ for (let marker of RectangleMarker.forRange(view, "cm-selectionBackground", r))
9491
+ markers.push(marker);
9492
+ }
9493
+ if (browser.ios && !main.empty && view.state.facet(selectionConfig).iosSelectionHandles) {
9494
+ for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-start", EditorSelection.cursor(main.from, 1)))
9495
+ markers.push(piece);
9496
+ for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-end", EditorSelection.cursor(main.to, 1)))
9497
+ markers.push(piece);
9498
+ }
9499
+ return markers;
9442
9500
  },
9443
9501
  update(update, dom) {
9444
9502
  return update.docChanged || update.selectionSet || update.viewportChanged || configChanged(update);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.39.16",
3
+ "version": "6.40.0",
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.5.0",
29
+ "@codemirror/state": "^6.6.0",
30
30
  "crelt": "^1.0.6",
31
31
  "style-mod": "^4.1.0",
32
32
  "w3c-keyname": "^2.2.4"