@codemirror/view 6.39.16 → 6.39.17

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.39.17 (2026-03-10)
2
+
3
+ ### Bug fixes
4
+
5
+ Improve touch tap-selection on line wrapping boundaries.
6
+
7
+ Make `drawSelection` draw our own selection handles on iOS.
8
+
9
+ Fix an issue where `posAtCoords`, when querying line wrapping points, got confused by extra empty client rectangles produced by Safari.
10
+
1
11
  ## 6.39.16 (2026-03-02)
2
12
 
3
13
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -3863,6 +3863,9 @@ class InlineCoordsScan {
3863
3863
  if (rects)
3864
3864
  for (let i = 0; i < rects.length; i++) {
3865
3865
  let rect = rects[i], side = 0;
3866
+ // Ignore empty rectangles when there are other rectangles
3867
+ if (rect.width == 0 && rects.length > 1)
3868
+ continue;
3866
3869
  if (rect.bottom < this.y) {
3867
3870
  if (!above || above.bottom < rect.bottom)
3868
3871
  above = rect;
@@ -4086,7 +4089,7 @@ class DOMChange {
4086
4089
  this.bounds = null;
4087
4090
  this.text = "";
4088
4091
  this.domChanged = start > -1;
4089
- let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
4092
+ let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView, curSel = view.state.selection;
4090
4093
  if (view.state.readOnly && start > -1) {
4091
4094
  // Ignore changes when the editor is read-only
4092
4095
  this.newSel = null;
@@ -4102,18 +4105,18 @@ class DOMChange {
4102
4105
  let domSel = view.observer.selectionRange;
4103
4106
  let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
4104
4107
  !contains(view.contentDOM, domSel.focusNode)
4105
- ? view.state.selection.main.head
4108
+ ? curSel.main.head
4106
4109
  : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
4107
4110
  let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
4108
4111
  !contains(view.contentDOM, domSel.anchorNode)
4109
- ? view.state.selection.main.anchor
4112
+ ? curSel.main.anchor
4110
4113
  : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
4111
4114
  // iOS will refuse to select the block gaps when doing
4112
4115
  // select-all.
4113
4116
  // Chrome will put the selection *inside* them, confusing
4114
4117
  // posFromDOM
4115
4118
  let vp = view.viewport;
4116
- if ((browser.ios || browser.chrome) && view.state.selection.main.empty && head != anchor &&
4119
+ if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
4117
4120
  (vp.from > 0 || vp.to < view.state.doc.length)) {
4118
4121
  let from = Math.min(head, anchor), to = Math.max(head, anchor);
4119
4122
  let offFrom = vp.from - from, offTo = vp.to - to;
@@ -4122,10 +4125,22 @@ class DOMChange {
4122
4125
  anchor = view.state.doc.length;
4123
4126
  }
4124
4127
  }
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
4128
+ if (view.inputState.composing > -1 && curSel.ranges.length > 1) {
4129
+ this.newSel = curSel.replaceRange(state.EditorSelection.range(anchor, head));
4130
+ }
4131
+ else if (view.lineWrapping && anchor == head && !(curSel.main.empty && curSel.main.head == head) &&
4132
+ view.inputState.lastTouchTime > Date.now() - 100) {
4133
+ // If this is a cursor selection change in a line-wrapping
4134
+ // editor that may have been a touch, use the last touch
4135
+ // position to assign a side to the cursor.
4136
+ let before = view.coordsAtPos(head, -1), assoc = 0;
4137
+ if (before)
4138
+ assoc = view.inputState.lastTouchY <= before.bottom ? -1 : 1;
4139
+ this.newSel = state.EditorSelection.create([state.EditorSelection.cursor(head, assoc)]);
4140
+ }
4141
+ else {
4128
4142
  this.newSel = state.EditorSelection.single(anchor, head);
4143
+ }
4129
4144
  }
4130
4145
  }
4131
4146
  }
@@ -4412,6 +4427,8 @@ class InputState {
4412
4427
  this.lastKeyCode = 0;
4413
4428
  this.lastKeyTime = 0;
4414
4429
  this.lastTouchTime = 0;
4430
+ this.lastTouchX = 0;
4431
+ this.lastTouchY = 0;
4415
4432
  this.lastFocusTime = 0;
4416
4433
  this.lastScrollTop = 0;
4417
4434
  this.lastScrollLeft = 0;
@@ -4855,8 +4872,13 @@ handlers.keydown = (view, event) => {
4855
4872
  return false;
4856
4873
  };
4857
4874
  observers.touchstart = (view, e) => {
4858
- view.inputState.lastTouchTime = Date.now();
4859
- view.inputState.setSelectionOrigin("select.pointer");
4875
+ let iState = view.inputState, touch = e.targetTouches[0];
4876
+ iState.lastTouchTime = Date.now();
4877
+ if (touch) {
4878
+ iState.lastTouchX = touch.clientX;
4879
+ iState.lastTouchY = touch.clientY;
4880
+ }
4881
+ iState.setSelectionOrigin("select.pointer");
4860
4882
  };
4861
4883
  observers.touchmove = view => {
4862
4884
  view.inputState.setSelectionOrigin("select.pointer");
@@ -6798,6 +6820,21 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
6798
6820
  "&dark .cm-cursor": {
6799
6821
  borderLeftColor: "#ddd"
6800
6822
  },
6823
+ ".cm-selectionHandle": {
6824
+ backgroundColor: "currentColor",
6825
+ width: "1.5px"
6826
+ },
6827
+ ".cm-selectionHandle-start::before, .cm-selectionHandle-end::before": {
6828
+ content: '""',
6829
+ backgroundColor: "inherit",
6830
+ borderRadius: "50%",
6831
+ width: "8px",
6832
+ height: "8px",
6833
+ position: "absolute",
6834
+ left: "-3.25px"
6835
+ },
6836
+ ".cm-selectionHandle-start::before": { top: "-8px" },
6837
+ ".cm-selectionHandle-end::before": { bottom: "-8px" },
6801
6838
  ".cm-dropCursor": {
6802
6839
  position: "absolute"
6803
6840
  },
@@ -9362,7 +9399,8 @@ const selectionConfig = state.Facet.define({
9362
9399
  combine(configs) {
9363
9400
  return state.combineConfig(configs, {
9364
9401
  cursorBlinkRate: 1200,
9365
- drawRangeCursor: true
9402
+ drawRangeCursor: true,
9403
+ iosSelectionHandles: true
9366
9404
  }, {
9367
9405
  cursorBlinkRate: (a, b) => Math.min(a, b),
9368
9406
  drawRangeCursor: (a, b) => a || b
@@ -9414,7 +9452,7 @@ const cursorLayer = layer({
9414
9452
  let cursors = [];
9415
9453
  for (let r of state$1.selection.ranges) {
9416
9454
  let prim = r == state$1.selection.main;
9417
- if (r.empty || conf.drawRangeCursor) {
9455
+ if (r.empty || conf.drawRangeCursor && !(prim && browser.ios && conf.iosSelectionHandles)) {
9418
9456
  let className = prim ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
9419
9457
  let cursor = r.empty ? r : state.EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
9420
9458
  for (let piece of RectangleMarker.forRange(view, className, cursor))
@@ -9442,8 +9480,19 @@ function setBlinkRate(state, dom) {
9442
9480
  const selectionLayer = layer({
9443
9481
  above: false,
9444
9482
  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));
9483
+ let markers = [], { main, ranges } = view.state.selection;
9484
+ for (let r of ranges)
9485
+ if (!r.empty) {
9486
+ for (let marker of RectangleMarker.forRange(view, "cm-selectionBackground", r))
9487
+ markers.push(marker);
9488
+ }
9489
+ if (browser.ios && !main.empty && view.state.facet(selectionConfig).iosSelectionHandles) {
9490
+ for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-start", state.EditorSelection.cursor(main.from, 1)))
9491
+ markers.push(piece);
9492
+ for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-end", state.EditorSelection.cursor(main.to, 1)))
9493
+ markers.push(piece);
9494
+ }
9495
+ return markers;
9447
9496
  },
9448
9497
  update(update, dom) {
9449
9498
  return update.docChanged || update.selectionSet || update.viewportChanged || configChanged(update);
package/dist/index.d.cts CHANGED
@@ -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
@@ -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
@@ -3859,6 +3859,9 @@ class InlineCoordsScan {
3859
3859
  if (rects)
3860
3860
  for (let i = 0; i < rects.length; i++) {
3861
3861
  let rect = rects[i], side = 0;
3862
+ // Ignore empty rectangles when there are other rectangles
3863
+ if (rect.width == 0 && rects.length > 1)
3864
+ continue;
3862
3865
  if (rect.bottom < this.y) {
3863
3866
  if (!above || above.bottom < rect.bottom)
3864
3867
  above = rect;
@@ -4082,7 +4085,7 @@ class DOMChange {
4082
4085
  this.bounds = null;
4083
4086
  this.text = "";
4084
4087
  this.domChanged = start > -1;
4085
- let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
4088
+ let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView, curSel = view.state.selection;
4086
4089
  if (view.state.readOnly && start > -1) {
4087
4090
  // Ignore changes when the editor is read-only
4088
4091
  this.newSel = null;
@@ -4098,18 +4101,18 @@ class DOMChange {
4098
4101
  let domSel = view.observer.selectionRange;
4099
4102
  let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
4100
4103
  !contains(view.contentDOM, domSel.focusNode)
4101
- ? view.state.selection.main.head
4104
+ ? curSel.main.head
4102
4105
  : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
4103
4106
  let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
4104
4107
  !contains(view.contentDOM, domSel.anchorNode)
4105
- ? view.state.selection.main.anchor
4108
+ ? curSel.main.anchor
4106
4109
  : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
4107
4110
  // iOS will refuse to select the block gaps when doing
4108
4111
  // select-all.
4109
4112
  // Chrome will put the selection *inside* them, confusing
4110
4113
  // posFromDOM
4111
4114
  let vp = view.viewport;
4112
- if ((browser.ios || browser.chrome) && view.state.selection.main.empty && head != anchor &&
4115
+ if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
4113
4116
  (vp.from > 0 || vp.to < view.state.doc.length)) {
4114
4117
  let from = Math.min(head, anchor), to = Math.max(head, anchor);
4115
4118
  let offFrom = vp.from - from, offTo = vp.to - to;
@@ -4118,10 +4121,22 @@ class DOMChange {
4118
4121
  anchor = view.state.doc.length;
4119
4122
  }
4120
4123
  }
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
4124
+ if (view.inputState.composing > -1 && curSel.ranges.length > 1) {
4125
+ this.newSel = curSel.replaceRange(EditorSelection.range(anchor, head));
4126
+ }
4127
+ else if (view.lineWrapping && anchor == head && !(curSel.main.empty && curSel.main.head == head) &&
4128
+ view.inputState.lastTouchTime > Date.now() - 100) {
4129
+ // If this is a cursor selection change in a line-wrapping
4130
+ // editor that may have been a touch, use the last touch
4131
+ // position to assign a side to the cursor.
4132
+ let before = view.coordsAtPos(head, -1), assoc = 0;
4133
+ if (before)
4134
+ assoc = view.inputState.lastTouchY <= before.bottom ? -1 : 1;
4135
+ this.newSel = EditorSelection.create([EditorSelection.cursor(head, assoc)]);
4136
+ }
4137
+ else {
4124
4138
  this.newSel = EditorSelection.single(anchor, head);
4139
+ }
4125
4140
  }
4126
4141
  }
4127
4142
  }
@@ -4408,6 +4423,8 @@ class InputState {
4408
4423
  this.lastKeyCode = 0;
4409
4424
  this.lastKeyTime = 0;
4410
4425
  this.lastTouchTime = 0;
4426
+ this.lastTouchX = 0;
4427
+ this.lastTouchY = 0;
4411
4428
  this.lastFocusTime = 0;
4412
4429
  this.lastScrollTop = 0;
4413
4430
  this.lastScrollLeft = 0;
@@ -4851,8 +4868,13 @@ handlers.keydown = (view, event) => {
4851
4868
  return false;
4852
4869
  };
4853
4870
  observers.touchstart = (view, e) => {
4854
- view.inputState.lastTouchTime = Date.now();
4855
- view.inputState.setSelectionOrigin("select.pointer");
4871
+ let iState = view.inputState, touch = e.targetTouches[0];
4872
+ iState.lastTouchTime = Date.now();
4873
+ if (touch) {
4874
+ iState.lastTouchX = touch.clientX;
4875
+ iState.lastTouchY = touch.clientY;
4876
+ }
4877
+ iState.setSelectionOrigin("select.pointer");
4856
4878
  };
4857
4879
  observers.touchmove = view => {
4858
4880
  view.inputState.setSelectionOrigin("select.pointer");
@@ -6793,6 +6815,21 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
6793
6815
  "&dark .cm-cursor": {
6794
6816
  borderLeftColor: "#ddd"
6795
6817
  },
6818
+ ".cm-selectionHandle": {
6819
+ backgroundColor: "currentColor",
6820
+ width: "1.5px"
6821
+ },
6822
+ ".cm-selectionHandle-start::before, .cm-selectionHandle-end::before": {
6823
+ content: '""',
6824
+ backgroundColor: "inherit",
6825
+ borderRadius: "50%",
6826
+ width: "8px",
6827
+ height: "8px",
6828
+ position: "absolute",
6829
+ left: "-3.25px"
6830
+ },
6831
+ ".cm-selectionHandle-start::before": { top: "-8px" },
6832
+ ".cm-selectionHandle-end::before": { bottom: "-8px" },
6796
6833
  ".cm-dropCursor": {
6797
6834
  position: "absolute"
6798
6835
  },
@@ -9357,7 +9394,8 @@ const selectionConfig = /*@__PURE__*/Facet.define({
9357
9394
  combine(configs) {
9358
9395
  return combineConfig(configs, {
9359
9396
  cursorBlinkRate: 1200,
9360
- drawRangeCursor: true
9397
+ drawRangeCursor: true,
9398
+ iosSelectionHandles: true
9361
9399
  }, {
9362
9400
  cursorBlinkRate: (a, b) => Math.min(a, b),
9363
9401
  drawRangeCursor: (a, b) => a || b
@@ -9409,7 +9447,7 @@ const cursorLayer = /*@__PURE__*/layer({
9409
9447
  let cursors = [];
9410
9448
  for (let r of state.selection.ranges) {
9411
9449
  let prim = r == state.selection.main;
9412
- if (r.empty || conf.drawRangeCursor) {
9450
+ if (r.empty || conf.drawRangeCursor && !(prim && browser.ios && conf.iosSelectionHandles)) {
9413
9451
  let className = prim ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
9414
9452
  let cursor = r.empty ? r : EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
9415
9453
  for (let piece of RectangleMarker.forRange(view, className, cursor))
@@ -9437,8 +9475,19 @@ function setBlinkRate(state, dom) {
9437
9475
  const selectionLayer = /*@__PURE__*/layer({
9438
9476
  above: false,
9439
9477
  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));
9478
+ let markers = [], { main, ranges } = view.state.selection;
9479
+ for (let r of ranges)
9480
+ if (!r.empty) {
9481
+ for (let marker of RectangleMarker.forRange(view, "cm-selectionBackground", r))
9482
+ markers.push(marker);
9483
+ }
9484
+ if (browser.ios && !main.empty && view.state.facet(selectionConfig).iosSelectionHandles) {
9485
+ for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-start", EditorSelection.cursor(main.from, 1)))
9486
+ markers.push(piece);
9487
+ for (let piece of RectangleMarker.forRange(view, "cm-selectionHandle cm-selectionHandle-end", EditorSelection.cursor(main.to, 1)))
9488
+ markers.push(piece);
9489
+ }
9490
+ return markers;
9442
9491
  },
9443
9492
  update(update, dom) {
9444
9493
  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.39.17",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",