@codemirror/view 6.39.15 → 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,23 @@
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
+
11
+ ## 6.39.16 (2026-03-02)
12
+
13
+ ### Bug fixes
14
+
15
+ Perform scroll stabilization on the document or wrapping scrollable elements, when the user scrolls the editor.
16
+
17
+ Fix an issue where changing decorations right before a composition could end up corrupting the visible DOM.
18
+
19
+ Fix an issue where some types of text input over a selection would be read as happening in wrong position.
20
+
1
21
  ## 6.39.15 (2026-02-20)
2
22
 
3
23
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -624,16 +624,16 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
624
624
  }
625
625
  }
626
626
  }
627
- function scrollableParents(dom) {
628
- let doc = dom.ownerDocument, x, y;
627
+ function scrollableParents(dom, getX = true) {
628
+ let doc = dom.ownerDocument, x = null, y = null;
629
629
  for (let cur = dom.parentNode; cur;) {
630
- if (cur == doc.body || (x && y)) {
630
+ if (cur == doc.body || ((!getX || x) && y)) {
631
631
  break;
632
632
  }
633
633
  else if (cur.nodeType == 1) {
634
634
  if (!y && cur.scrollHeight > cur.clientHeight)
635
635
  y = cur;
636
- if (!x && cur.scrollWidth > cur.clientWidth)
636
+ if (getX && !x && cur.scrollWidth > cur.clientWidth)
637
637
  x = cur;
638
638
  cur = cur.assignedSlot || cur.parentNode;
639
639
  }
@@ -758,6 +758,8 @@ function atElementStart(doc, selection) {
758
758
  }
759
759
  }
760
760
  function isScrolledToBottom(elt) {
761
+ if (elt instanceof Window)
762
+ return elt.pageYOffset > Math.max(0, elt.document.documentElement.scrollHeight - elt.innerHeight - 4);
761
763
  return elt.scrollTop > Math.max(1, elt.scrollHeight - elt.clientHeight - 4);
762
764
  }
763
765
  function textNodeBefore(startNode, startOffset) {
@@ -2656,7 +2658,7 @@ class TileUpdate {
2656
2658
  }
2657
2659
  else if (tile.isText()) {
2658
2660
  this.builder.ensureLine(null);
2659
- if (!from && to == tile.length) {
2661
+ if (!from && to == tile.length && !this.cache.reused.has(tile)) {
2660
2662
  this.builder.addText(tile.text, activeMarks, openMarks, this.cache.reuse(tile));
2661
2663
  }
2662
2664
  else {
@@ -2960,6 +2962,8 @@ class DocView {
2960
2962
  if (composition || changes.length) {
2961
2963
  let oldTile = this.tile;
2962
2964
  let builder = new TileUpdate(this.view, oldTile, this.blockWrappers, this.decorations, this.dynamicDecorationMap);
2965
+ if (composition && Tile.get(composition.text))
2966
+ builder.cache.reused.set(Tile.get(composition.text), 2 /* Reused.DOM */);
2963
2967
  this.tile = builder.run(changes, composition);
2964
2968
  destroyDropped(oldTile, builder.cache.reused);
2965
2969
  }
@@ -3859,6 +3863,9 @@ class InlineCoordsScan {
3859
3863
  if (rects)
3860
3864
  for (let i = 0; i < rects.length; i++) {
3861
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;
3862
3869
  if (rect.bottom < this.y) {
3863
3870
  if (!above || above.bottom < rect.bottom)
3864
3871
  above = rect;
@@ -4082,7 +4089,7 @@ class DOMChange {
4082
4089
  this.bounds = null;
4083
4090
  this.text = "";
4084
4091
  this.domChanged = start > -1;
4085
- let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
4092
+ let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView, curSel = view.state.selection;
4086
4093
  if (view.state.readOnly && start > -1) {
4087
4094
  // Ignore changes when the editor is read-only
4088
4095
  this.newSel = null;
@@ -4098,18 +4105,18 @@ class DOMChange {
4098
4105
  let domSel = view.observer.selectionRange;
4099
4106
  let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
4100
4107
  !contains(view.contentDOM, domSel.focusNode)
4101
- ? view.state.selection.main.head
4108
+ ? curSel.main.head
4102
4109
  : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
4103
4110
  let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
4104
4111
  !contains(view.contentDOM, domSel.anchorNode)
4105
- ? view.state.selection.main.anchor
4112
+ ? curSel.main.anchor
4106
4113
  : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
4107
4114
  // iOS will refuse to select the block gaps when doing
4108
4115
  // select-all.
4109
4116
  // Chrome will put the selection *inside* them, confusing
4110
4117
  // posFromDOM
4111
4118
  let vp = view.viewport;
4112
- if ((browser.ios || browser.chrome) && view.state.selection.main.empty && head != anchor &&
4119
+ if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
4113
4120
  (vp.from > 0 || vp.to < view.state.doc.length)) {
4114
4121
  let from = Math.min(head, anchor), to = Math.max(head, anchor);
4115
4122
  let offFrom = vp.from - from, offTo = vp.to - to;
@@ -4118,10 +4125,22 @@ class DOMChange {
4118
4125
  anchor = view.state.doc.length;
4119
4126
  }
4120
4127
  }
4121
- if (view.inputState.composing > -1 && view.state.selection.ranges.length > 1)
4122
- this.newSel = view.state.selection.replaceRange(state.EditorSelection.range(anchor, head));
4123
- 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 {
4124
4142
  this.newSel = state.EditorSelection.single(anchor, head);
4143
+ }
4125
4144
  }
4126
4145
  }
4127
4146
  }
@@ -4157,7 +4176,7 @@ function domBoundsAround(tile, from, to, offset) {
4157
4176
  }
4158
4177
  function applyDOMChange(view, domChange) {
4159
4178
  let change;
4160
- let { newSel } = domChange, sel = view.state.selection.main;
4179
+ let { newSel } = domChange, { state: state$1 } = view, sel = state$1.selection.main;
4161
4180
  let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
4162
4181
  if (domChange.bounds) {
4163
4182
  let { from, to } = domChange.bounds;
@@ -4168,8 +4187,15 @@ function applyDOMChange(view, domChange) {
4168
4187
  preferredPos = sel.to;
4169
4188
  preferredSide = "end";
4170
4189
  }
4171
- let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text, preferredPos - from, preferredSide);
4172
- if (diff) {
4190
+ let cmp = state$1.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff;
4191
+ if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
4192
+ cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
4193
+ cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
4194
+ // This looks like a selection replacement
4195
+ change = { from: sel.from, to: sel.to,
4196
+ insert: state.Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder)) };
4197
+ }
4198
+ else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
4173
4199
  // Chrome inserts two newlines when pressing shift-enter at the
4174
4200
  // end of a line. DomChange drops one of those.
4175
4201
  if (browser.chrome && lastKey == 13 &&
@@ -4179,16 +4205,12 @@ function applyDOMChange(view, domChange) {
4179
4205
  insert: state.Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
4180
4206
  }
4181
4207
  }
4182
- else if (newSel && (!view.hasFocus && view.state.facet(editable) || sameSelPos(newSel, sel))) {
4208
+ else if (newSel && (!view.hasFocus && state$1.facet(editable) || sameSelPos(newSel, sel))) {
4183
4209
  newSel = null;
4184
4210
  }
4185
4211
  if (!change && !newSel)
4186
4212
  return false;
4187
- if (!change && domChange.typeOver && !sel.empty && newSel && newSel.main.empty) {
4188
- // Heuristic to notice typing over a selected character
4189
- change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
4190
- }
4191
- else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
4213
+ if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
4192
4214
  /^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
4193
4215
  // Detect insert-period-on-double-space Mac and Android behavior,
4194
4216
  // and transform it into a regular space insert.
@@ -4196,18 +4218,7 @@ function applyDOMChange(view, domChange) {
4196
4218
  newSel = state.EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
4197
4219
  change = { from: change.from, to: change.to, insert: state.Text.of([change.insert.toString().replace(".", " ")]) };
4198
4220
  }
4199
- else if (change && change.from >= sel.from && change.to <= sel.to &&
4200
- (change.from != sel.from || change.to != sel.to) &&
4201
- (sel.to - sel.from) - (change.to - change.from) <= 4) {
4202
- // If the change is inside the selection and covers most of it,
4203
- // assume it is a selection replace (with identical characters at
4204
- // the start/end not included in the diff)
4205
- change = {
4206
- from: sel.from, to: sel.to,
4207
- insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
4208
- };
4209
- }
4210
- else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4221
+ else if (state$1.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4211
4222
  view.inputState.insertingTextAt > Date.now() - 50) {
4212
4223
  // For a cross-line insertion, Chrome and Safari will crudely take
4213
4224
  // the text of the line after the selection, flattening any
@@ -4216,7 +4227,7 @@ function applyDOMChange(view, domChange) {
4216
4227
  // replace of the text provided by the beforeinput event.
4217
4228
  change = {
4218
4229
  from: sel.from, to: sel.to,
4219
- insert: view.state.toText(view.inputState.insertingText)
4230
+ insert: state$1.toText(view.inputState.insertingText)
4220
4231
  };
4221
4232
  }
4222
4233
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
@@ -4238,7 +4249,7 @@ function applyDOMChange(view, domChange) {
4238
4249
  scrollIntoView = true;
4239
4250
  userEvent = view.inputState.lastSelectionOrigin;
4240
4251
  if (userEvent == "select.pointer")
4241
- newSel = skipAtomsForSelection(view.state.facet(atomicRanges).map(f => f(view)), newSel);
4252
+ newSel = skipAtomsForSelection(state$1.facet(atomicRanges).map(f => f(view)), newSel);
4242
4253
  }
4243
4254
  view.dispatch({ selection: newSel, scrollIntoView, userEvent });
4244
4255
  return true;
@@ -4416,9 +4427,12 @@ class InputState {
4416
4427
  this.lastKeyCode = 0;
4417
4428
  this.lastKeyTime = 0;
4418
4429
  this.lastTouchTime = 0;
4430
+ this.lastTouchX = 0;
4431
+ this.lastTouchY = 0;
4419
4432
  this.lastFocusTime = 0;
4420
4433
  this.lastScrollTop = 0;
4421
4434
  this.lastScrollLeft = 0;
4435
+ this.lastWheelEvent = 0;
4422
4436
  // On iOS, some keys need to have their default behavior happen
4423
4437
  // (after which we retroactively handle them and reset the DOM) to
4424
4438
  // avoid messing up the virtual keyboard state.
@@ -4848,6 +4862,9 @@ observers.scroll = view => {
4848
4862
  view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
4849
4863
  view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
4850
4864
  };
4865
+ observers.wheel = observers.mousewheel = view => {
4866
+ view.inputState.lastWheelEvent = Date.now();
4867
+ };
4851
4868
  handlers.keydown = (view, event) => {
4852
4869
  view.inputState.setSelectionOrigin("select");
4853
4870
  if (event.keyCode == 27 && view.inputState.tabFocusMode != 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");
@@ -6092,7 +6114,8 @@ class LineGapWidget extends WidgetType {
6092
6114
  get estimatedHeight() { return this.vertical ? this.size : -1; }
6093
6115
  }
6094
6116
  class ViewState {
6095
- constructor(state$1) {
6117
+ constructor(view, state$1) {
6118
+ this.view = view;
6096
6119
  this.state = state$1;
6097
6120
  // These are contentDOM-local coordinates
6098
6121
  this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
@@ -6103,12 +6126,14 @@ class ViewState {
6103
6126
  this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
6104
6127
  this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
6105
6128
  this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
6106
- this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
6107
- this.scrolledToBottom = false;
6108
6129
  // The CSS-transformation scale of the editor (transformed size /
6109
6130
  // concrete size)
6110
6131
  this.scaleX = 1;
6111
6132
  this.scaleY = 1;
6133
+ // Last seen vertical offset of the element at the top of the scroll
6134
+ // container, or top of the window if there's no wrapping scroller
6135
+ this.scrollOffset = 0;
6136
+ this.scrolledToBottom = false;
6112
6137
  // The vertical position (document-relative) to which to anchor the
6113
6138
  // scroll position. -1 means anchor to the end of the document.
6114
6139
  this.scrollAnchorPos = 0;
@@ -6146,6 +6171,7 @@ class ViewState {
6146
6171
  this.updateViewportLines();
6147
6172
  this.lineGaps = this.ensureLineGaps([]);
6148
6173
  this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
6174
+ this.scrollParent = view.scrollDOM;
6149
6175
  this.computeVisibleRanges();
6150
6176
  }
6151
6177
  updateForViewport() {
@@ -6179,7 +6205,7 @@ class ViewState {
6179
6205
  let contentChanges = update.changedRanges;
6180
6206
  let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : state.ChangeSet.empty(this.state.doc.length)));
6181
6207
  let prevHeight = this.heightMap.height;
6182
- let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollTop);
6208
+ let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
6183
6209
  clearHeightChangeFlag();
6184
6210
  this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
6185
6211
  if (this.heightMap.height != prevHeight || heightChangeFlag)
@@ -6211,8 +6237,8 @@ class ViewState {
6211
6237
  !update.state.facet(nativeSelectionHidden))
6212
6238
  this.mustEnforceCursorAssoc = true;
6213
6239
  }
6214
- measure(view) {
6215
- let dom = view.contentDOM, style = window.getComputedStyle(dom);
6240
+ measure() {
6241
+ let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
6216
6242
  let oracle = this.heightOracle;
6217
6243
  let whiteSpace = style.whiteSpace;
6218
6244
  this.defaultTextDirection = style.direction == "rtl" ? exports.Direction.RTL : exports.Direction.LTR;
@@ -6246,12 +6272,18 @@ class ViewState {
6246
6272
  this.editorWidth = view.scrollDOM.clientWidth;
6247
6273
  result |= 16 /* UpdateFlag.Geometry */;
6248
6274
  }
6249
- let scrollTop = view.scrollDOM.scrollTop * this.scaleY;
6250
- if (this.scrollTop != scrollTop) {
6275
+ let scrollParent = scrollableParents(this.view.contentDOM, false).y;
6276
+ if (scrollParent != this.scrollParent) {
6277
+ this.scrollParent = scrollParent;
6278
+ this.scrollAnchorHeight = -1;
6279
+ this.scrollOffset = 0;
6280
+ }
6281
+ let scrollOffset = this.getScrollOffset();
6282
+ if (this.scrollOffset != scrollOffset) {
6251
6283
  this.scrollAnchorHeight = -1;
6252
- this.scrollTop = scrollTop;
6284
+ this.scrollOffset = scrollOffset;
6253
6285
  }
6254
- this.scrolledToBottom = isScrolledToBottom(view.scrollDOM);
6286
+ this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
6255
6287
  // Pixel viewport
6256
6288
  let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
6257
6289
  let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
@@ -6528,9 +6560,14 @@ class ViewState {
6528
6560
  this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
6529
6561
  scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
6530
6562
  }
6531
- scrollAnchorAt(scrollTop) {
6532
- let block = this.lineBlockAtHeight(scrollTop + 8);
6533
- return block.from >= this.viewport.from || this.viewportLines[0].top - scrollTop > 200 ? block : this.viewportLines[0];
6563
+ getScrollOffset() {
6564
+ let base = this.scrollParent == this.view.scrollDOM ? this.scrollParent.scrollTop
6565
+ : (this.scrollParent ? this.scrollParent.getBoundingClientRect().top : 0) - this.view.contentDOM.getBoundingClientRect().top;
6566
+ return base * this.scaleY;
6567
+ }
6568
+ scrollAnchorAt(scrollOffset) {
6569
+ let block = this.lineBlockAtHeight(scrollOffset + 8);
6570
+ return block.from >= this.viewport.from || this.viewportLines[0].top - scrollOffset > 200 ? block : this.viewportLines[0];
6534
6571
  }
6535
6572
  elementAtHeight(height) {
6536
6573
  return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
@@ -6783,6 +6820,21 @@ const baseTheme$1 = buildTheme("." + baseThemeID, {
6783
6820
  "&dark .cm-cursor": {
6784
6821
  borderLeftColor: "#ddd"
6785
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" },
6786
6838
  ".cm-dropCursor": {
6787
6839
  position: "absolute"
6788
6840
  },
@@ -7777,7 +7829,7 @@ class EditorView {
7777
7829
  ((trs) => this.update(trs));
7778
7830
  this.dispatch = this.dispatch.bind(this);
7779
7831
  this._root = (config.root || getRoot(config.parent) || document);
7780
- this.viewState = new ViewState(config.state || state.EditorState.create(config));
7832
+ this.viewState = new ViewState(this, config.state || state.EditorState.create(config));
7781
7833
  if (config.scrollTo && config.scrollTo.is(scrollIntoView))
7782
7834
  this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
7783
7835
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
@@ -7932,7 +7984,7 @@ class EditorView {
7932
7984
  try {
7933
7985
  for (let plugin of this.plugins)
7934
7986
  plugin.destroy(this);
7935
- this.viewState = new ViewState(newState);
7987
+ this.viewState = new ViewState(this, newState);
7936
7988
  this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
7937
7989
  this.pluginMap.clear();
7938
7990
  for (let plugin of this.plugins)
@@ -8011,26 +8063,26 @@ class EditorView {
8011
8063
  if (flush)
8012
8064
  this.observer.forceFlush();
8013
8065
  let updated = null;
8014
- let sDOM = this.scrollDOM, scrollTop = sDOM.scrollTop * this.scaleY;
8066
+ let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
8015
8067
  let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
8016
- if (Math.abs(scrollTop - this.viewState.scrollTop) > 1)
8068
+ if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
8017
8069
  scrollAnchorHeight = -1;
8018
8070
  this.viewState.scrollAnchorHeight = -1;
8019
8071
  try {
8020
8072
  for (let i = 0;; i++) {
8021
8073
  if (scrollAnchorHeight < 0) {
8022
- if (isScrolledToBottom(sDOM)) {
8074
+ if (isScrolledToBottom(scroll || this.win)) {
8023
8075
  scrollAnchorPos = -1;
8024
8076
  scrollAnchorHeight = this.viewState.heightMap.height;
8025
8077
  }
8026
8078
  else {
8027
- let block = this.viewState.scrollAnchorAt(scrollTop);
8079
+ let block = this.viewState.scrollAnchorAt(scrollOffset);
8028
8080
  scrollAnchorPos = block.from;
8029
8081
  scrollAnchorHeight = block.top;
8030
8082
  }
8031
8083
  }
8032
8084
  this.updateState = 1 /* UpdateState.Measuring */;
8033
- let changed = this.viewState.measure(this);
8085
+ let changed = this.viewState.measure();
8034
8086
  if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
8035
8087
  break;
8036
8088
  if (i > 5) {
@@ -8091,10 +8143,15 @@ class EditorView {
8091
8143
  else {
8092
8144
  let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
8093
8145
  this.viewState.lineBlockAt(scrollAnchorPos).top;
8094
- let diff = newAnchorHeight - scrollAnchorHeight;
8095
- if (diff > 1 || diff < -1) {
8096
- scrollTop = scrollTop + diff;
8097
- sDOM.scrollTop = scrollTop / this.scaleY;
8146
+ let diff = (newAnchorHeight - scrollAnchorHeight) / this.scaleY;
8147
+ if ((diff > 1 || diff < -1) &&
8148
+ (scroll == this.scrollDOM || this.hasFocus ||
8149
+ Math.max(this.inputState.lastWheelEvent, this.inputState.lastTouchTime) > Date.now() - 100)) {
8150
+ scrollOffset = scrollOffset + diff;
8151
+ if (scroll)
8152
+ scroll.scrollTop += diff;
8153
+ else
8154
+ this.win.scrollBy(0, diff);
8098
8155
  scrollAnchorHeight = -1;
8099
8156
  continue;
8100
8157
  }
@@ -9342,7 +9399,8 @@ const selectionConfig = state.Facet.define({
9342
9399
  combine(configs) {
9343
9400
  return state.combineConfig(configs, {
9344
9401
  cursorBlinkRate: 1200,
9345
- drawRangeCursor: true
9402
+ drawRangeCursor: true,
9403
+ iosSelectionHandles: true
9346
9404
  }, {
9347
9405
  cursorBlinkRate: (a, b) => Math.min(a, b),
9348
9406
  drawRangeCursor: (a, b) => a || b
@@ -9394,7 +9452,7 @@ const cursorLayer = layer({
9394
9452
  let cursors = [];
9395
9453
  for (let r of state$1.selection.ranges) {
9396
9454
  let prim = r == state$1.selection.main;
9397
- if (r.empty || conf.drawRangeCursor) {
9455
+ if (r.empty || conf.drawRangeCursor && !(prim && browser.ios && conf.iosSelectionHandles)) {
9398
9456
  let className = prim ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
9399
9457
  let cursor = r.empty ? r : state.EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
9400
9458
  for (let piece of RectangleMarker.forRange(view, className, cursor))
@@ -9422,8 +9480,19 @@ function setBlinkRate(state, dom) {
9422
9480
  const selectionLayer = layer({
9423
9481
  above: false,
9424
9482
  markers(view) {
9425
- return view.state.selection.ranges.map(r => r.empty ? [] : RectangleMarker.forRange(view, "cm-selectionBackground", r))
9426
- .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;
9427
9496
  },
9428
9497
  update(update, dom) {
9429
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
@@ -621,16 +621,16 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
621
621
  }
622
622
  }
623
623
  }
624
- function scrollableParents(dom) {
625
- let doc = dom.ownerDocument, x, y;
624
+ function scrollableParents(dom, getX = true) {
625
+ let doc = dom.ownerDocument, x = null, y = null;
626
626
  for (let cur = dom.parentNode; cur;) {
627
- if (cur == doc.body || (x && y)) {
627
+ if (cur == doc.body || ((!getX || x) && y)) {
628
628
  break;
629
629
  }
630
630
  else if (cur.nodeType == 1) {
631
631
  if (!y && cur.scrollHeight > cur.clientHeight)
632
632
  y = cur;
633
- if (!x && cur.scrollWidth > cur.clientWidth)
633
+ if (getX && !x && cur.scrollWidth > cur.clientWidth)
634
634
  x = cur;
635
635
  cur = cur.assignedSlot || cur.parentNode;
636
636
  }
@@ -755,6 +755,8 @@ function atElementStart(doc, selection) {
755
755
  }
756
756
  }
757
757
  function isScrolledToBottom(elt) {
758
+ if (elt instanceof Window)
759
+ return elt.pageYOffset > Math.max(0, elt.document.documentElement.scrollHeight - elt.innerHeight - 4);
758
760
  return elt.scrollTop > Math.max(1, elt.scrollHeight - elt.clientHeight - 4);
759
761
  }
760
762
  function textNodeBefore(startNode, startOffset) {
@@ -2652,7 +2654,7 @@ class TileUpdate {
2652
2654
  }
2653
2655
  else if (tile.isText()) {
2654
2656
  this.builder.ensureLine(null);
2655
- if (!from && to == tile.length) {
2657
+ if (!from && to == tile.length && !this.cache.reused.has(tile)) {
2656
2658
  this.builder.addText(tile.text, activeMarks, openMarks, this.cache.reuse(tile));
2657
2659
  }
2658
2660
  else {
@@ -2956,6 +2958,8 @@ class DocView {
2956
2958
  if (composition || changes.length) {
2957
2959
  let oldTile = this.tile;
2958
2960
  let builder = new TileUpdate(this.view, oldTile, this.blockWrappers, this.decorations, this.dynamicDecorationMap);
2961
+ if (composition && Tile.get(composition.text))
2962
+ builder.cache.reused.set(Tile.get(composition.text), 2 /* Reused.DOM */);
2959
2963
  this.tile = builder.run(changes, composition);
2960
2964
  destroyDropped(oldTile, builder.cache.reused);
2961
2965
  }
@@ -3855,6 +3859,9 @@ class InlineCoordsScan {
3855
3859
  if (rects)
3856
3860
  for (let i = 0; i < rects.length; i++) {
3857
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;
3858
3865
  if (rect.bottom < this.y) {
3859
3866
  if (!above || above.bottom < rect.bottom)
3860
3867
  above = rect;
@@ -4078,7 +4085,7 @@ class DOMChange {
4078
4085
  this.bounds = null;
4079
4086
  this.text = "";
4080
4087
  this.domChanged = start > -1;
4081
- let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView;
4088
+ let { impreciseHead: iHead, impreciseAnchor: iAnchor } = view.docView, curSel = view.state.selection;
4082
4089
  if (view.state.readOnly && start > -1) {
4083
4090
  // Ignore changes when the editor is read-only
4084
4091
  this.newSel = null;
@@ -4094,18 +4101,18 @@ class DOMChange {
4094
4101
  let domSel = view.observer.selectionRange;
4095
4102
  let head = iHead && iHead.node == domSel.focusNode && iHead.offset == domSel.focusOffset ||
4096
4103
  !contains(view.contentDOM, domSel.focusNode)
4097
- ? view.state.selection.main.head
4104
+ ? curSel.main.head
4098
4105
  : view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset);
4099
4106
  let anchor = iAnchor && iAnchor.node == domSel.anchorNode && iAnchor.offset == domSel.anchorOffset ||
4100
4107
  !contains(view.contentDOM, domSel.anchorNode)
4101
- ? view.state.selection.main.anchor
4108
+ ? curSel.main.anchor
4102
4109
  : view.docView.posFromDOM(domSel.anchorNode, domSel.anchorOffset);
4103
4110
  // iOS will refuse to select the block gaps when doing
4104
4111
  // select-all.
4105
4112
  // Chrome will put the selection *inside* them, confusing
4106
4113
  // posFromDOM
4107
4114
  let vp = view.viewport;
4108
- if ((browser.ios || browser.chrome) && view.state.selection.main.empty && head != anchor &&
4115
+ if ((browser.ios || browser.chrome) && curSel.main.empty && head != anchor &&
4109
4116
  (vp.from > 0 || vp.to < view.state.doc.length)) {
4110
4117
  let from = Math.min(head, anchor), to = Math.max(head, anchor);
4111
4118
  let offFrom = vp.from - from, offTo = vp.to - to;
@@ -4114,10 +4121,22 @@ class DOMChange {
4114
4121
  anchor = view.state.doc.length;
4115
4122
  }
4116
4123
  }
4117
- if (view.inputState.composing > -1 && view.state.selection.ranges.length > 1)
4118
- this.newSel = view.state.selection.replaceRange(EditorSelection.range(anchor, head));
4119
- 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 {
4120
4138
  this.newSel = EditorSelection.single(anchor, head);
4139
+ }
4121
4140
  }
4122
4141
  }
4123
4142
  }
@@ -4153,7 +4172,7 @@ function domBoundsAround(tile, from, to, offset) {
4153
4172
  }
4154
4173
  function applyDOMChange(view, domChange) {
4155
4174
  let change;
4156
- let { newSel } = domChange, sel = view.state.selection.main;
4175
+ let { newSel } = domChange, { state } = view, sel = state.selection.main;
4157
4176
  let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
4158
4177
  if (domChange.bounds) {
4159
4178
  let { from, to } = domChange.bounds;
@@ -4164,8 +4183,15 @@ function applyDOMChange(view, domChange) {
4164
4183
  preferredPos = sel.to;
4165
4184
  preferredSide = "end";
4166
4185
  }
4167
- let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text, preferredPos - from, preferredSide);
4168
- if (diff) {
4186
+ let cmp = state.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff;
4187
+ if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
4188
+ cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
4189
+ cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
4190
+ // This looks like a selection replacement
4191
+ change = { from: sel.from, to: sel.to,
4192
+ insert: Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder)) };
4193
+ }
4194
+ else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
4169
4195
  // Chrome inserts two newlines when pressing shift-enter at the
4170
4196
  // end of a line. DomChange drops one of those.
4171
4197
  if (browser.chrome && lastKey == 13 &&
@@ -4175,16 +4201,12 @@ function applyDOMChange(view, domChange) {
4175
4201
  insert: Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
4176
4202
  }
4177
4203
  }
4178
- else if (newSel && (!view.hasFocus && view.state.facet(editable) || sameSelPos(newSel, sel))) {
4204
+ else if (newSel && (!view.hasFocus && state.facet(editable) || sameSelPos(newSel, sel))) {
4179
4205
  newSel = null;
4180
4206
  }
4181
4207
  if (!change && !newSel)
4182
4208
  return false;
4183
- if (!change && domChange.typeOver && !sel.empty && newSel && newSel.main.empty) {
4184
- // Heuristic to notice typing over a selected character
4185
- change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
4186
- }
4187
- else if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
4209
+ if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
4188
4210
  /^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
4189
4211
  // Detect insert-period-on-double-space Mac and Android behavior,
4190
4212
  // and transform it into a regular space insert.
@@ -4192,18 +4214,7 @@ function applyDOMChange(view, domChange) {
4192
4214
  newSel = EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
4193
4215
  change = { from: change.from, to: change.to, insert: Text.of([change.insert.toString().replace(".", " ")]) };
4194
4216
  }
4195
- else if (change && change.from >= sel.from && change.to <= sel.to &&
4196
- (change.from != sel.from || change.to != sel.to) &&
4197
- (sel.to - sel.from) - (change.to - change.from) <= 4) {
4198
- // If the change is inside the selection and covers most of it,
4199
- // assume it is a selection replace (with identical characters at
4200
- // the start/end not included in the diff)
4201
- change = {
4202
- from: sel.from, to: sel.to,
4203
- insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
4204
- };
4205
- }
4206
- else if (view.state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4217
+ else if (state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4207
4218
  view.inputState.insertingTextAt > Date.now() - 50) {
4208
4219
  // For a cross-line insertion, Chrome and Safari will crudely take
4209
4220
  // the text of the line after the selection, flattening any
@@ -4212,7 +4223,7 @@ function applyDOMChange(view, domChange) {
4212
4223
  // replace of the text provided by the beforeinput event.
4213
4224
  change = {
4214
4225
  from: sel.from, to: sel.to,
4215
- insert: view.state.toText(view.inputState.insertingText)
4226
+ insert: state.toText(view.inputState.insertingText)
4216
4227
  };
4217
4228
  }
4218
4229
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
@@ -4234,7 +4245,7 @@ function applyDOMChange(view, domChange) {
4234
4245
  scrollIntoView = true;
4235
4246
  userEvent = view.inputState.lastSelectionOrigin;
4236
4247
  if (userEvent == "select.pointer")
4237
- newSel = skipAtomsForSelection(view.state.facet(atomicRanges).map(f => f(view)), newSel);
4248
+ newSel = skipAtomsForSelection(state.facet(atomicRanges).map(f => f(view)), newSel);
4238
4249
  }
4239
4250
  view.dispatch({ selection: newSel, scrollIntoView, userEvent });
4240
4251
  return true;
@@ -4412,9 +4423,12 @@ class InputState {
4412
4423
  this.lastKeyCode = 0;
4413
4424
  this.lastKeyTime = 0;
4414
4425
  this.lastTouchTime = 0;
4426
+ this.lastTouchX = 0;
4427
+ this.lastTouchY = 0;
4415
4428
  this.lastFocusTime = 0;
4416
4429
  this.lastScrollTop = 0;
4417
4430
  this.lastScrollLeft = 0;
4431
+ this.lastWheelEvent = 0;
4418
4432
  // On iOS, some keys need to have their default behavior happen
4419
4433
  // (after which we retroactively handle them and reset the DOM) to
4420
4434
  // avoid messing up the virtual keyboard state.
@@ -4844,6 +4858,9 @@ observers.scroll = view => {
4844
4858
  view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
4845
4859
  view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
4846
4860
  };
4861
+ observers.wheel = observers.mousewheel = view => {
4862
+ view.inputState.lastWheelEvent = Date.now();
4863
+ };
4847
4864
  handlers.keydown = (view, event) => {
4848
4865
  view.inputState.setSelectionOrigin("select");
4849
4866
  if (event.keyCode == 27 && view.inputState.tabFocusMode != 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");
@@ -6087,7 +6109,8 @@ class LineGapWidget extends WidgetType {
6087
6109
  get estimatedHeight() { return this.vertical ? this.size : -1; }
6088
6110
  }
6089
6111
  class ViewState {
6090
- constructor(state) {
6112
+ constructor(view, state) {
6113
+ this.view = view;
6091
6114
  this.state = state;
6092
6115
  // These are contentDOM-local coordinates
6093
6116
  this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
@@ -6098,12 +6121,14 @@ class ViewState {
6098
6121
  this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
6099
6122
  this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
6100
6123
  this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
6101
- this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
6102
- this.scrolledToBottom = false;
6103
6124
  // The CSS-transformation scale of the editor (transformed size /
6104
6125
  // concrete size)
6105
6126
  this.scaleX = 1;
6106
6127
  this.scaleY = 1;
6128
+ // Last seen vertical offset of the element at the top of the scroll
6129
+ // container, or top of the window if there's no wrapping scroller
6130
+ this.scrollOffset = 0;
6131
+ this.scrolledToBottom = false;
6107
6132
  // The vertical position (document-relative) to which to anchor the
6108
6133
  // scroll position. -1 means anchor to the end of the document.
6109
6134
  this.scrollAnchorPos = 0;
@@ -6141,6 +6166,7 @@ class ViewState {
6141
6166
  this.updateViewportLines();
6142
6167
  this.lineGaps = this.ensureLineGaps([]);
6143
6168
  this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
6169
+ this.scrollParent = view.scrollDOM;
6144
6170
  this.computeVisibleRanges();
6145
6171
  }
6146
6172
  updateForViewport() {
@@ -6174,7 +6200,7 @@ class ViewState {
6174
6200
  let contentChanges = update.changedRanges;
6175
6201
  let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : ChangeSet.empty(this.state.doc.length)));
6176
6202
  let prevHeight = this.heightMap.height;
6177
- let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollTop);
6203
+ let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
6178
6204
  clearHeightChangeFlag();
6179
6205
  this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
6180
6206
  if (this.heightMap.height != prevHeight || heightChangeFlag)
@@ -6206,8 +6232,8 @@ class ViewState {
6206
6232
  !update.state.facet(nativeSelectionHidden))
6207
6233
  this.mustEnforceCursorAssoc = true;
6208
6234
  }
6209
- measure(view) {
6210
- let dom = view.contentDOM, style = window.getComputedStyle(dom);
6235
+ measure() {
6236
+ let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
6211
6237
  let oracle = this.heightOracle;
6212
6238
  let whiteSpace = style.whiteSpace;
6213
6239
  this.defaultTextDirection = style.direction == "rtl" ? Direction.RTL : Direction.LTR;
@@ -6241,12 +6267,18 @@ class ViewState {
6241
6267
  this.editorWidth = view.scrollDOM.clientWidth;
6242
6268
  result |= 16 /* UpdateFlag.Geometry */;
6243
6269
  }
6244
- let scrollTop = view.scrollDOM.scrollTop * this.scaleY;
6245
- if (this.scrollTop != scrollTop) {
6270
+ let scrollParent = scrollableParents(this.view.contentDOM, false).y;
6271
+ if (scrollParent != this.scrollParent) {
6272
+ this.scrollParent = scrollParent;
6273
+ this.scrollAnchorHeight = -1;
6274
+ this.scrollOffset = 0;
6275
+ }
6276
+ let scrollOffset = this.getScrollOffset();
6277
+ if (this.scrollOffset != scrollOffset) {
6246
6278
  this.scrollAnchorHeight = -1;
6247
- this.scrollTop = scrollTop;
6279
+ this.scrollOffset = scrollOffset;
6248
6280
  }
6249
- this.scrolledToBottom = isScrolledToBottom(view.scrollDOM);
6281
+ this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
6250
6282
  // Pixel viewport
6251
6283
  let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
6252
6284
  let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
@@ -6523,9 +6555,14 @@ class ViewState {
6523
6555
  this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
6524
6556
  scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
6525
6557
  }
6526
- scrollAnchorAt(scrollTop) {
6527
- let block = this.lineBlockAtHeight(scrollTop + 8);
6528
- return block.from >= this.viewport.from || this.viewportLines[0].top - scrollTop > 200 ? block : this.viewportLines[0];
6558
+ getScrollOffset() {
6559
+ let base = this.scrollParent == this.view.scrollDOM ? this.scrollParent.scrollTop
6560
+ : (this.scrollParent ? this.scrollParent.getBoundingClientRect().top : 0) - this.view.contentDOM.getBoundingClientRect().top;
6561
+ return base * this.scaleY;
6562
+ }
6563
+ scrollAnchorAt(scrollOffset) {
6564
+ let block = this.lineBlockAtHeight(scrollOffset + 8);
6565
+ return block.from >= this.viewport.from || this.viewportLines[0].top - scrollOffset > 200 ? block : this.viewportLines[0];
6529
6566
  }
6530
6567
  elementAtHeight(height) {
6531
6568
  return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
@@ -6778,6 +6815,21 @@ const baseTheme$1 = /*@__PURE__*/buildTheme("." + baseThemeID, {
6778
6815
  "&dark .cm-cursor": {
6779
6816
  borderLeftColor: "#ddd"
6780
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" },
6781
6833
  ".cm-dropCursor": {
6782
6834
  position: "absolute"
6783
6835
  },
@@ -7772,7 +7824,7 @@ class EditorView {
7772
7824
  ((trs) => this.update(trs));
7773
7825
  this.dispatch = this.dispatch.bind(this);
7774
7826
  this._root = (config.root || getRoot(config.parent) || document);
7775
- this.viewState = new ViewState(config.state || EditorState.create(config));
7827
+ this.viewState = new ViewState(this, config.state || EditorState.create(config));
7776
7828
  if (config.scrollTo && config.scrollTo.is(scrollIntoView))
7777
7829
  this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
7778
7830
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
@@ -7927,7 +7979,7 @@ class EditorView {
7927
7979
  try {
7928
7980
  for (let plugin of this.plugins)
7929
7981
  plugin.destroy(this);
7930
- this.viewState = new ViewState(newState);
7982
+ this.viewState = new ViewState(this, newState);
7931
7983
  this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
7932
7984
  this.pluginMap.clear();
7933
7985
  for (let plugin of this.plugins)
@@ -8006,26 +8058,26 @@ class EditorView {
8006
8058
  if (flush)
8007
8059
  this.observer.forceFlush();
8008
8060
  let updated = null;
8009
- let sDOM = this.scrollDOM, scrollTop = sDOM.scrollTop * this.scaleY;
8061
+ let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
8010
8062
  let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
8011
- if (Math.abs(scrollTop - this.viewState.scrollTop) > 1)
8063
+ if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
8012
8064
  scrollAnchorHeight = -1;
8013
8065
  this.viewState.scrollAnchorHeight = -1;
8014
8066
  try {
8015
8067
  for (let i = 0;; i++) {
8016
8068
  if (scrollAnchorHeight < 0) {
8017
- if (isScrolledToBottom(sDOM)) {
8069
+ if (isScrolledToBottom(scroll || this.win)) {
8018
8070
  scrollAnchorPos = -1;
8019
8071
  scrollAnchorHeight = this.viewState.heightMap.height;
8020
8072
  }
8021
8073
  else {
8022
- let block = this.viewState.scrollAnchorAt(scrollTop);
8074
+ let block = this.viewState.scrollAnchorAt(scrollOffset);
8023
8075
  scrollAnchorPos = block.from;
8024
8076
  scrollAnchorHeight = block.top;
8025
8077
  }
8026
8078
  }
8027
8079
  this.updateState = 1 /* UpdateState.Measuring */;
8028
- let changed = this.viewState.measure(this);
8080
+ let changed = this.viewState.measure();
8029
8081
  if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
8030
8082
  break;
8031
8083
  if (i > 5) {
@@ -8086,10 +8138,15 @@ class EditorView {
8086
8138
  else {
8087
8139
  let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
8088
8140
  this.viewState.lineBlockAt(scrollAnchorPos).top;
8089
- let diff = newAnchorHeight - scrollAnchorHeight;
8090
- if (diff > 1 || diff < -1) {
8091
- scrollTop = scrollTop + diff;
8092
- sDOM.scrollTop = scrollTop / this.scaleY;
8141
+ let diff = (newAnchorHeight - scrollAnchorHeight) / this.scaleY;
8142
+ if ((diff > 1 || diff < -1) &&
8143
+ (scroll == this.scrollDOM || this.hasFocus ||
8144
+ Math.max(this.inputState.lastWheelEvent, this.inputState.lastTouchTime) > Date.now() - 100)) {
8145
+ scrollOffset = scrollOffset + diff;
8146
+ if (scroll)
8147
+ scroll.scrollTop += diff;
8148
+ else
8149
+ this.win.scrollBy(0, diff);
8093
8150
  scrollAnchorHeight = -1;
8094
8151
  continue;
8095
8152
  }
@@ -9337,7 +9394,8 @@ const selectionConfig = /*@__PURE__*/Facet.define({
9337
9394
  combine(configs) {
9338
9395
  return combineConfig(configs, {
9339
9396
  cursorBlinkRate: 1200,
9340
- drawRangeCursor: true
9397
+ drawRangeCursor: true,
9398
+ iosSelectionHandles: true
9341
9399
  }, {
9342
9400
  cursorBlinkRate: (a, b) => Math.min(a, b),
9343
9401
  drawRangeCursor: (a, b) => a || b
@@ -9389,7 +9447,7 @@ const cursorLayer = /*@__PURE__*/layer({
9389
9447
  let cursors = [];
9390
9448
  for (let r of state.selection.ranges) {
9391
9449
  let prim = r == state.selection.main;
9392
- if (r.empty || conf.drawRangeCursor) {
9450
+ if (r.empty || conf.drawRangeCursor && !(prim && browser.ios && conf.iosSelectionHandles)) {
9393
9451
  let className = prim ? "cm-cursor cm-cursor-primary" : "cm-cursor cm-cursor-secondary";
9394
9452
  let cursor = r.empty ? r : EditorSelection.cursor(r.head, r.head > r.anchor ? -1 : 1);
9395
9453
  for (let piece of RectangleMarker.forRange(view, className, cursor))
@@ -9417,8 +9475,19 @@ function setBlinkRate(state, dom) {
9417
9475
  const selectionLayer = /*@__PURE__*/layer({
9418
9476
  above: false,
9419
9477
  markers(view) {
9420
- return view.state.selection.ranges.map(r => r.empty ? [] : RectangleMarker.forRange(view, "cm-selectionBackground", r))
9421
- .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;
9422
9491
  },
9423
9492
  update(update, dom) {
9424
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.15",
3
+ "version": "6.39.17",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",