@codemirror/view 6.39.15 → 6.39.16

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.16 (2026-03-02)
2
+
3
+ ### Bug fixes
4
+
5
+ Perform scroll stabilization on the document or wrapping scrollable elements, when the user scrolls the editor.
6
+
7
+ Fix an issue where changing decorations right before a composition could end up corrupting the visible DOM.
8
+
9
+ Fix an issue where some types of text input over a selection would be read as happening in wrong position.
10
+
1
11
  ## 6.39.15 (2026-02-20)
2
12
 
3
13
  ### 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
  }
@@ -4157,7 +4161,7 @@ function domBoundsAround(tile, from, to, offset) {
4157
4161
  }
4158
4162
  function applyDOMChange(view, domChange) {
4159
4163
  let change;
4160
- let { newSel } = domChange, sel = view.state.selection.main;
4164
+ let { newSel } = domChange, { state: state$1 } = view, sel = state$1.selection.main;
4161
4165
  let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
4162
4166
  if (domChange.bounds) {
4163
4167
  let { from, to } = domChange.bounds;
@@ -4168,8 +4172,15 @@ function applyDOMChange(view, domChange) {
4168
4172
  preferredPos = sel.to;
4169
4173
  preferredSide = "end";
4170
4174
  }
4171
- let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text, preferredPos - from, preferredSide);
4172
- if (diff) {
4175
+ let cmp = state$1.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff;
4176
+ if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
4177
+ cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
4178
+ cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
4179
+ // This looks like a selection replacement
4180
+ change = { from: sel.from, to: sel.to,
4181
+ insert: state.Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder)) };
4182
+ }
4183
+ else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
4173
4184
  // Chrome inserts two newlines when pressing shift-enter at the
4174
4185
  // end of a line. DomChange drops one of those.
4175
4186
  if (browser.chrome && lastKey == 13 &&
@@ -4179,16 +4190,12 @@ function applyDOMChange(view, domChange) {
4179
4190
  insert: state.Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
4180
4191
  }
4181
4192
  }
4182
- else if (newSel && (!view.hasFocus && view.state.facet(editable) || sameSelPos(newSel, sel))) {
4193
+ else if (newSel && (!view.hasFocus && state$1.facet(editable) || sameSelPos(newSel, sel))) {
4183
4194
  newSel = null;
4184
4195
  }
4185
4196
  if (!change && !newSel)
4186
4197
  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 &&
4198
+ if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
4192
4199
  /^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
4193
4200
  // Detect insert-period-on-double-space Mac and Android behavior,
4194
4201
  // and transform it into a regular space insert.
@@ -4196,18 +4203,7 @@ function applyDOMChange(view, domChange) {
4196
4203
  newSel = state.EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
4197
4204
  change = { from: change.from, to: change.to, insert: state.Text.of([change.insert.toString().replace(".", " ")]) };
4198
4205
  }
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) &&
4206
+ else if (state$1.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4211
4207
  view.inputState.insertingTextAt > Date.now() - 50) {
4212
4208
  // For a cross-line insertion, Chrome and Safari will crudely take
4213
4209
  // the text of the line after the selection, flattening any
@@ -4216,7 +4212,7 @@ function applyDOMChange(view, domChange) {
4216
4212
  // replace of the text provided by the beforeinput event.
4217
4213
  change = {
4218
4214
  from: sel.from, to: sel.to,
4219
- insert: view.state.toText(view.inputState.insertingText)
4215
+ insert: state$1.toText(view.inputState.insertingText)
4220
4216
  };
4221
4217
  }
4222
4218
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
@@ -4238,7 +4234,7 @@ function applyDOMChange(view, domChange) {
4238
4234
  scrollIntoView = true;
4239
4235
  userEvent = view.inputState.lastSelectionOrigin;
4240
4236
  if (userEvent == "select.pointer")
4241
- newSel = skipAtomsForSelection(view.state.facet(atomicRanges).map(f => f(view)), newSel);
4237
+ newSel = skipAtomsForSelection(state$1.facet(atomicRanges).map(f => f(view)), newSel);
4242
4238
  }
4243
4239
  view.dispatch({ selection: newSel, scrollIntoView, userEvent });
4244
4240
  return true;
@@ -4419,6 +4415,7 @@ class InputState {
4419
4415
  this.lastFocusTime = 0;
4420
4416
  this.lastScrollTop = 0;
4421
4417
  this.lastScrollLeft = 0;
4418
+ this.lastWheelEvent = 0;
4422
4419
  // On iOS, some keys need to have their default behavior happen
4423
4420
  // (after which we retroactively handle them and reset the DOM) to
4424
4421
  // avoid messing up the virtual keyboard state.
@@ -4848,6 +4845,9 @@ observers.scroll = view => {
4848
4845
  view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
4849
4846
  view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
4850
4847
  };
4848
+ observers.wheel = observers.mousewheel = view => {
4849
+ view.inputState.lastWheelEvent = Date.now();
4850
+ };
4851
4851
  handlers.keydown = (view, event) => {
4852
4852
  view.inputState.setSelectionOrigin("select");
4853
4853
  if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
@@ -6092,7 +6092,8 @@ class LineGapWidget extends WidgetType {
6092
6092
  get estimatedHeight() { return this.vertical ? this.size : -1; }
6093
6093
  }
6094
6094
  class ViewState {
6095
- constructor(state$1) {
6095
+ constructor(view, state$1) {
6096
+ this.view = view;
6096
6097
  this.state = state$1;
6097
6098
  // These are contentDOM-local coordinates
6098
6099
  this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
@@ -6103,12 +6104,14 @@ class ViewState {
6103
6104
  this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
6104
6105
  this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
6105
6106
  this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
6106
- this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
6107
- this.scrolledToBottom = false;
6108
6107
  // The CSS-transformation scale of the editor (transformed size /
6109
6108
  // concrete size)
6110
6109
  this.scaleX = 1;
6111
6110
  this.scaleY = 1;
6111
+ // Last seen vertical offset of the element at the top of the scroll
6112
+ // container, or top of the window if there's no wrapping scroller
6113
+ this.scrollOffset = 0;
6114
+ this.scrolledToBottom = false;
6112
6115
  // The vertical position (document-relative) to which to anchor the
6113
6116
  // scroll position. -1 means anchor to the end of the document.
6114
6117
  this.scrollAnchorPos = 0;
@@ -6146,6 +6149,7 @@ class ViewState {
6146
6149
  this.updateViewportLines();
6147
6150
  this.lineGaps = this.ensureLineGaps([]);
6148
6151
  this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
6152
+ this.scrollParent = view.scrollDOM;
6149
6153
  this.computeVisibleRanges();
6150
6154
  }
6151
6155
  updateForViewport() {
@@ -6179,7 +6183,7 @@ class ViewState {
6179
6183
  let contentChanges = update.changedRanges;
6180
6184
  let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : state.ChangeSet.empty(this.state.doc.length)));
6181
6185
  let prevHeight = this.heightMap.height;
6182
- let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollTop);
6186
+ let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
6183
6187
  clearHeightChangeFlag();
6184
6188
  this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
6185
6189
  if (this.heightMap.height != prevHeight || heightChangeFlag)
@@ -6211,8 +6215,8 @@ class ViewState {
6211
6215
  !update.state.facet(nativeSelectionHidden))
6212
6216
  this.mustEnforceCursorAssoc = true;
6213
6217
  }
6214
- measure(view) {
6215
- let dom = view.contentDOM, style = window.getComputedStyle(dom);
6218
+ measure() {
6219
+ let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
6216
6220
  let oracle = this.heightOracle;
6217
6221
  let whiteSpace = style.whiteSpace;
6218
6222
  this.defaultTextDirection = style.direction == "rtl" ? exports.Direction.RTL : exports.Direction.LTR;
@@ -6246,12 +6250,18 @@ class ViewState {
6246
6250
  this.editorWidth = view.scrollDOM.clientWidth;
6247
6251
  result |= 16 /* UpdateFlag.Geometry */;
6248
6252
  }
6249
- let scrollTop = view.scrollDOM.scrollTop * this.scaleY;
6250
- if (this.scrollTop != scrollTop) {
6253
+ let scrollParent = scrollableParents(this.view.contentDOM, false).y;
6254
+ if (scrollParent != this.scrollParent) {
6255
+ this.scrollParent = scrollParent;
6251
6256
  this.scrollAnchorHeight = -1;
6252
- this.scrollTop = scrollTop;
6257
+ this.scrollOffset = 0;
6253
6258
  }
6254
- this.scrolledToBottom = isScrolledToBottom(view.scrollDOM);
6259
+ let scrollOffset = this.getScrollOffset();
6260
+ if (this.scrollOffset != scrollOffset) {
6261
+ this.scrollAnchorHeight = -1;
6262
+ this.scrollOffset = scrollOffset;
6263
+ }
6264
+ this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
6255
6265
  // Pixel viewport
6256
6266
  let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
6257
6267
  let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
@@ -6528,9 +6538,14 @@ class ViewState {
6528
6538
  this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
6529
6539
  scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
6530
6540
  }
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];
6541
+ getScrollOffset() {
6542
+ let base = this.scrollParent == this.view.scrollDOM ? this.scrollParent.scrollTop
6543
+ : (this.scrollParent ? this.scrollParent.getBoundingClientRect().top : 0) - this.view.contentDOM.getBoundingClientRect().top;
6544
+ return base * this.scaleY;
6545
+ }
6546
+ scrollAnchorAt(scrollOffset) {
6547
+ let block = this.lineBlockAtHeight(scrollOffset + 8);
6548
+ return block.from >= this.viewport.from || this.viewportLines[0].top - scrollOffset > 200 ? block : this.viewportLines[0];
6534
6549
  }
6535
6550
  elementAtHeight(height) {
6536
6551
  return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
@@ -7777,7 +7792,7 @@ class EditorView {
7777
7792
  ((trs) => this.update(trs));
7778
7793
  this.dispatch = this.dispatch.bind(this);
7779
7794
  this._root = (config.root || getRoot(config.parent) || document);
7780
- this.viewState = new ViewState(config.state || state.EditorState.create(config));
7795
+ this.viewState = new ViewState(this, config.state || state.EditorState.create(config));
7781
7796
  if (config.scrollTo && config.scrollTo.is(scrollIntoView))
7782
7797
  this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
7783
7798
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
@@ -7932,7 +7947,7 @@ class EditorView {
7932
7947
  try {
7933
7948
  for (let plugin of this.plugins)
7934
7949
  plugin.destroy(this);
7935
- this.viewState = new ViewState(newState);
7950
+ this.viewState = new ViewState(this, newState);
7936
7951
  this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
7937
7952
  this.pluginMap.clear();
7938
7953
  for (let plugin of this.plugins)
@@ -8011,26 +8026,26 @@ class EditorView {
8011
8026
  if (flush)
8012
8027
  this.observer.forceFlush();
8013
8028
  let updated = null;
8014
- let sDOM = this.scrollDOM, scrollTop = sDOM.scrollTop * this.scaleY;
8029
+ let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
8015
8030
  let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
8016
- if (Math.abs(scrollTop - this.viewState.scrollTop) > 1)
8031
+ if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
8017
8032
  scrollAnchorHeight = -1;
8018
8033
  this.viewState.scrollAnchorHeight = -1;
8019
8034
  try {
8020
8035
  for (let i = 0;; i++) {
8021
8036
  if (scrollAnchorHeight < 0) {
8022
- if (isScrolledToBottom(sDOM)) {
8037
+ if (isScrolledToBottom(scroll || this.win)) {
8023
8038
  scrollAnchorPos = -1;
8024
8039
  scrollAnchorHeight = this.viewState.heightMap.height;
8025
8040
  }
8026
8041
  else {
8027
- let block = this.viewState.scrollAnchorAt(scrollTop);
8042
+ let block = this.viewState.scrollAnchorAt(scrollOffset);
8028
8043
  scrollAnchorPos = block.from;
8029
8044
  scrollAnchorHeight = block.top;
8030
8045
  }
8031
8046
  }
8032
8047
  this.updateState = 1 /* UpdateState.Measuring */;
8033
- let changed = this.viewState.measure(this);
8048
+ let changed = this.viewState.measure();
8034
8049
  if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
8035
8050
  break;
8036
8051
  if (i > 5) {
@@ -8091,10 +8106,15 @@ class EditorView {
8091
8106
  else {
8092
8107
  let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
8093
8108
  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;
8109
+ let diff = (newAnchorHeight - scrollAnchorHeight) / this.scaleY;
8110
+ if ((diff > 1 || diff < -1) &&
8111
+ (scroll == this.scrollDOM || this.hasFocus ||
8112
+ Math.max(this.inputState.lastWheelEvent, this.inputState.lastTouchTime) > Date.now() - 100)) {
8113
+ scrollOffset = scrollOffset + diff;
8114
+ if (scroll)
8115
+ scroll.scrollTop += diff;
8116
+ else
8117
+ this.win.scrollBy(0, diff);
8098
8118
  scrollAnchorHeight = -1;
8099
8119
  continue;
8100
8120
  }
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
  }
@@ -4153,7 +4157,7 @@ function domBoundsAround(tile, from, to, offset) {
4153
4157
  }
4154
4158
  function applyDOMChange(view, domChange) {
4155
4159
  let change;
4156
- let { newSel } = domChange, sel = view.state.selection.main;
4160
+ let { newSel } = domChange, { state } = view, sel = state.selection.main;
4157
4161
  let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
4158
4162
  if (domChange.bounds) {
4159
4163
  let { from, to } = domChange.bounds;
@@ -4164,8 +4168,15 @@ function applyDOMChange(view, domChange) {
4164
4168
  preferredPos = sel.to;
4165
4169
  preferredSide = "end";
4166
4170
  }
4167
- let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text, preferredPos - from, preferredSide);
4168
- if (diff) {
4171
+ let cmp = state.doc.sliceString(from, to, LineBreakPlaceholder), selEnd, diff;
4172
+ if (!sel.empty && sel.from >= from && sel.to <= to && (domChange.typeOver || cmp != domChange.text) &&
4173
+ cmp.slice(0, sel.from - from) == domChange.text.slice(0, sel.from - from) &&
4174
+ cmp.slice(sel.to - from) == domChange.text.slice(selEnd = domChange.text.length - (cmp.length - (sel.to - from)))) {
4175
+ // This looks like a selection replacement
4176
+ change = { from: sel.from, to: sel.to,
4177
+ insert: Text.of(domChange.text.slice(sel.from - from, selEnd).split(LineBreakPlaceholder)) };
4178
+ }
4179
+ else if (diff = findDiff(cmp, domChange.text, preferredPos - from, preferredSide)) {
4169
4180
  // Chrome inserts two newlines when pressing shift-enter at the
4170
4181
  // end of a line. DomChange drops one of those.
4171
4182
  if (browser.chrome && lastKey == 13 &&
@@ -4175,16 +4186,12 @@ function applyDOMChange(view, domChange) {
4175
4186
  insert: Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
4176
4187
  }
4177
4188
  }
4178
- else if (newSel && (!view.hasFocus && view.state.facet(editable) || sameSelPos(newSel, sel))) {
4189
+ else if (newSel && (!view.hasFocus && state.facet(editable) || sameSelPos(newSel, sel))) {
4179
4190
  newSel = null;
4180
4191
  }
4181
4192
  if (!change && !newSel)
4182
4193
  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 &&
4194
+ if ((browser.mac || browser.android) && change && change.from == change.to && change.from == sel.head - 1 &&
4188
4195
  /^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
4189
4196
  // Detect insert-period-on-double-space Mac and Android behavior,
4190
4197
  // and transform it into a regular space insert.
@@ -4192,18 +4199,7 @@ function applyDOMChange(view, domChange) {
4192
4199
  newSel = EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
4193
4200
  change = { from: change.from, to: change.to, insert: Text.of([change.insert.toString().replace(".", " ")]) };
4194
4201
  }
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) &&
4202
+ else if (state.doc.lineAt(sel.from).to < sel.to && view.docView.lineHasWidget(sel.to) &&
4207
4203
  view.inputState.insertingTextAt > Date.now() - 50) {
4208
4204
  // For a cross-line insertion, Chrome and Safari will crudely take
4209
4205
  // the text of the line after the selection, flattening any
@@ -4212,7 +4208,7 @@ function applyDOMChange(view, domChange) {
4212
4208
  // replace of the text provided by the beforeinput event.
4213
4209
  change = {
4214
4210
  from: sel.from, to: sel.to,
4215
- insert: view.state.toText(view.inputState.insertingText)
4211
+ insert: state.toText(view.inputState.insertingText)
4216
4212
  };
4217
4213
  }
4218
4214
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
@@ -4234,7 +4230,7 @@ function applyDOMChange(view, domChange) {
4234
4230
  scrollIntoView = true;
4235
4231
  userEvent = view.inputState.lastSelectionOrigin;
4236
4232
  if (userEvent == "select.pointer")
4237
- newSel = skipAtomsForSelection(view.state.facet(atomicRanges).map(f => f(view)), newSel);
4233
+ newSel = skipAtomsForSelection(state.facet(atomicRanges).map(f => f(view)), newSel);
4238
4234
  }
4239
4235
  view.dispatch({ selection: newSel, scrollIntoView, userEvent });
4240
4236
  return true;
@@ -4415,6 +4411,7 @@ class InputState {
4415
4411
  this.lastFocusTime = 0;
4416
4412
  this.lastScrollTop = 0;
4417
4413
  this.lastScrollLeft = 0;
4414
+ this.lastWheelEvent = 0;
4418
4415
  // On iOS, some keys need to have their default behavior happen
4419
4416
  // (after which we retroactively handle them and reset the DOM) to
4420
4417
  // avoid messing up the virtual keyboard state.
@@ -4844,6 +4841,9 @@ observers.scroll = view => {
4844
4841
  view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
4845
4842
  view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
4846
4843
  };
4844
+ observers.wheel = observers.mousewheel = view => {
4845
+ view.inputState.lastWheelEvent = Date.now();
4846
+ };
4847
4847
  handlers.keydown = (view, event) => {
4848
4848
  view.inputState.setSelectionOrigin("select");
4849
4849
  if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
@@ -6087,7 +6087,8 @@ class LineGapWidget extends WidgetType {
6087
6087
  get estimatedHeight() { return this.vertical ? this.size : -1; }
6088
6088
  }
6089
6089
  class ViewState {
6090
- constructor(state) {
6090
+ constructor(view, state) {
6091
+ this.view = view;
6091
6092
  this.state = state;
6092
6093
  // These are contentDOM-local coordinates
6093
6094
  this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
@@ -6098,12 +6099,14 @@ class ViewState {
6098
6099
  this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
6099
6100
  this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
6100
6101
  this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
6101
- this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
6102
- this.scrolledToBottom = false;
6103
6102
  // The CSS-transformation scale of the editor (transformed size /
6104
6103
  // concrete size)
6105
6104
  this.scaleX = 1;
6106
6105
  this.scaleY = 1;
6106
+ // Last seen vertical offset of the element at the top of the scroll
6107
+ // container, or top of the window if there's no wrapping scroller
6108
+ this.scrollOffset = 0;
6109
+ this.scrolledToBottom = false;
6107
6110
  // The vertical position (document-relative) to which to anchor the
6108
6111
  // scroll position. -1 means anchor to the end of the document.
6109
6112
  this.scrollAnchorPos = 0;
@@ -6141,6 +6144,7 @@ class ViewState {
6141
6144
  this.updateViewportLines();
6142
6145
  this.lineGaps = this.ensureLineGaps([]);
6143
6146
  this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
6147
+ this.scrollParent = view.scrollDOM;
6144
6148
  this.computeVisibleRanges();
6145
6149
  }
6146
6150
  updateForViewport() {
@@ -6174,7 +6178,7 @@ class ViewState {
6174
6178
  let contentChanges = update.changedRanges;
6175
6179
  let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : ChangeSet.empty(this.state.doc.length)));
6176
6180
  let prevHeight = this.heightMap.height;
6177
- let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollTop);
6181
+ let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
6178
6182
  clearHeightChangeFlag();
6179
6183
  this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
6180
6184
  if (this.heightMap.height != prevHeight || heightChangeFlag)
@@ -6206,8 +6210,8 @@ class ViewState {
6206
6210
  !update.state.facet(nativeSelectionHidden))
6207
6211
  this.mustEnforceCursorAssoc = true;
6208
6212
  }
6209
- measure(view) {
6210
- let dom = view.contentDOM, style = window.getComputedStyle(dom);
6213
+ measure() {
6214
+ let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
6211
6215
  let oracle = this.heightOracle;
6212
6216
  let whiteSpace = style.whiteSpace;
6213
6217
  this.defaultTextDirection = style.direction == "rtl" ? Direction.RTL : Direction.LTR;
@@ -6241,12 +6245,18 @@ class ViewState {
6241
6245
  this.editorWidth = view.scrollDOM.clientWidth;
6242
6246
  result |= 16 /* UpdateFlag.Geometry */;
6243
6247
  }
6244
- let scrollTop = view.scrollDOM.scrollTop * this.scaleY;
6245
- if (this.scrollTop != scrollTop) {
6248
+ let scrollParent = scrollableParents(this.view.contentDOM, false).y;
6249
+ if (scrollParent != this.scrollParent) {
6250
+ this.scrollParent = scrollParent;
6246
6251
  this.scrollAnchorHeight = -1;
6247
- this.scrollTop = scrollTop;
6252
+ this.scrollOffset = 0;
6248
6253
  }
6249
- this.scrolledToBottom = isScrolledToBottom(view.scrollDOM);
6254
+ let scrollOffset = this.getScrollOffset();
6255
+ if (this.scrollOffset != scrollOffset) {
6256
+ this.scrollAnchorHeight = -1;
6257
+ this.scrollOffset = scrollOffset;
6258
+ }
6259
+ this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
6250
6260
  // Pixel viewport
6251
6261
  let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
6252
6262
  let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
@@ -6523,9 +6533,14 @@ class ViewState {
6523
6533
  this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
6524
6534
  scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
6525
6535
  }
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];
6536
+ getScrollOffset() {
6537
+ let base = this.scrollParent == this.view.scrollDOM ? this.scrollParent.scrollTop
6538
+ : (this.scrollParent ? this.scrollParent.getBoundingClientRect().top : 0) - this.view.contentDOM.getBoundingClientRect().top;
6539
+ return base * this.scaleY;
6540
+ }
6541
+ scrollAnchorAt(scrollOffset) {
6542
+ let block = this.lineBlockAtHeight(scrollOffset + 8);
6543
+ return block.from >= this.viewport.from || this.viewportLines[0].top - scrollOffset > 200 ? block : this.viewportLines[0];
6529
6544
  }
6530
6545
  elementAtHeight(height) {
6531
6546
  return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
@@ -7772,7 +7787,7 @@ class EditorView {
7772
7787
  ((trs) => this.update(trs));
7773
7788
  this.dispatch = this.dispatch.bind(this);
7774
7789
  this._root = (config.root || getRoot(config.parent) || document);
7775
- this.viewState = new ViewState(config.state || EditorState.create(config));
7790
+ this.viewState = new ViewState(this, config.state || EditorState.create(config));
7776
7791
  if (config.scrollTo && config.scrollTo.is(scrollIntoView))
7777
7792
  this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
7778
7793
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
@@ -7927,7 +7942,7 @@ class EditorView {
7927
7942
  try {
7928
7943
  for (let plugin of this.plugins)
7929
7944
  plugin.destroy(this);
7930
- this.viewState = new ViewState(newState);
7945
+ this.viewState = new ViewState(this, newState);
7931
7946
  this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
7932
7947
  this.pluginMap.clear();
7933
7948
  for (let plugin of this.plugins)
@@ -8006,26 +8021,26 @@ class EditorView {
8006
8021
  if (flush)
8007
8022
  this.observer.forceFlush();
8008
8023
  let updated = null;
8009
- let sDOM = this.scrollDOM, scrollTop = sDOM.scrollTop * this.scaleY;
8024
+ let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
8010
8025
  let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
8011
- if (Math.abs(scrollTop - this.viewState.scrollTop) > 1)
8026
+ if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
8012
8027
  scrollAnchorHeight = -1;
8013
8028
  this.viewState.scrollAnchorHeight = -1;
8014
8029
  try {
8015
8030
  for (let i = 0;; i++) {
8016
8031
  if (scrollAnchorHeight < 0) {
8017
- if (isScrolledToBottom(sDOM)) {
8032
+ if (isScrolledToBottom(scroll || this.win)) {
8018
8033
  scrollAnchorPos = -1;
8019
8034
  scrollAnchorHeight = this.viewState.heightMap.height;
8020
8035
  }
8021
8036
  else {
8022
- let block = this.viewState.scrollAnchorAt(scrollTop);
8037
+ let block = this.viewState.scrollAnchorAt(scrollOffset);
8023
8038
  scrollAnchorPos = block.from;
8024
8039
  scrollAnchorHeight = block.top;
8025
8040
  }
8026
8041
  }
8027
8042
  this.updateState = 1 /* UpdateState.Measuring */;
8028
- let changed = this.viewState.measure(this);
8043
+ let changed = this.viewState.measure();
8029
8044
  if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
8030
8045
  break;
8031
8046
  if (i > 5) {
@@ -8086,10 +8101,15 @@ class EditorView {
8086
8101
  else {
8087
8102
  let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
8088
8103
  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;
8104
+ let diff = (newAnchorHeight - scrollAnchorHeight) / this.scaleY;
8105
+ if ((diff > 1 || diff < -1) &&
8106
+ (scroll == this.scrollDOM || this.hasFocus ||
8107
+ Math.max(this.inputState.lastWheelEvent, this.inputState.lastTouchTime) > Date.now() - 100)) {
8108
+ scrollOffset = scrollOffset + diff;
8109
+ if (scroll)
8110
+ scroll.scrollTop += diff;
8111
+ else
8112
+ this.win.scrollBy(0, diff);
8093
8113
  scrollAnchorHeight = -1;
8094
8114
  continue;
8095
8115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.39.15",
3
+ "version": "6.39.16",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",