@codemirror/view 6.39.14 → 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,23 @@
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
+
11
+ ## 6.39.15 (2026-02-20)
12
+
13
+ ### Bug fixes
14
+
15
+ Fix a regression where the editor would forget previously measured line heights without good reason.
16
+
17
+ Fix an issue where scrolling the cursor into view sometimes wouldn't work on Chrome Android.
18
+
19
+ Fix a bug that broke composition inside of block wrappers.
20
+
1
21
  ## 6.39.14 (2026-02-12)
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 {
@@ -2791,9 +2793,10 @@ class TileUpdate {
2791
2793
  marks.push(tile);
2792
2794
  else if (tile === null || tile === void 0 ? void 0 : tile.isLine())
2793
2795
  line = tile;
2796
+ else if (tile instanceof BlockWrapperTile) ; // Ignore
2794
2797
  else if (parent.nodeName == "DIV" && !line && parent != this.view.contentDOM)
2795
2798
  line = new LineTile(parent, lineBaseAttrs);
2796
- else
2799
+ else if (!line)
2797
2800
  marks.push(MarkTile.of(new MarkDecoration({ tagName: parent.nodeName.toLowerCase(), attributes: getAttrs(parent) }), parent));
2798
2801
  }
2799
2802
  return { line: line, marks };
@@ -2959,6 +2962,8 @@ class DocView {
2959
2962
  if (composition || changes.length) {
2960
2963
  let oldTile = this.tile;
2961
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 */);
2962
2967
  this.tile = builder.run(changes, composition);
2963
2968
  destroyDropped(oldTile, builder.cache.reused);
2964
2969
  }
@@ -3419,6 +3424,19 @@ class DocView {
3419
3424
  };
3420
3425
  let { offsetWidth, offsetHeight } = this.view.scrollDOM;
3421
3426
  scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == exports.Direction.LTR);
3427
+ // On mobile browsers, the visual viewport may be smaller than the
3428
+ // actual reported viewport, causing scrollRectIntoView to fail to
3429
+ // scroll properly. Unfortunately, this visual viewport cannot be
3430
+ // updated directly, and scrollIntoView is the only way a script
3431
+ // can affect it. So this tries to kludge around the problem by
3432
+ // calling scrollIntoView on the scroll target's line.
3433
+ if (window.visualViewport && window.innerHeight - window.visualViewport.height > 1 &&
3434
+ (rect.top > window.pageYOffset + window.visualViewport.offsetTop + window.visualViewport.height ||
3435
+ rect.bottom < window.pageYOffset + window.visualViewport.offsetTop)) {
3436
+ let line = this.view.docView.lineAt(range.head, 1);
3437
+ if (line)
3438
+ line.dom.scrollIntoView({ block: "nearest" });
3439
+ }
3422
3440
  }
3423
3441
  lineHasWidget(pos) {
3424
3442
  let scan = (child) => child.isWidget() || child.children.some(scan);
@@ -4143,7 +4161,7 @@ function domBoundsAround(tile, from, to, offset) {
4143
4161
  }
4144
4162
  function applyDOMChange(view, domChange) {
4145
4163
  let change;
4146
- let { newSel } = domChange, sel = view.state.selection.main;
4164
+ let { newSel } = domChange, { state: state$1 } = view, sel = state$1.selection.main;
4147
4165
  let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
4148
4166
  if (domChange.bounds) {
4149
4167
  let { from, to } = domChange.bounds;
@@ -4154,8 +4172,15 @@ function applyDOMChange(view, domChange) {
4154
4172
  preferredPos = sel.to;
4155
4173
  preferredSide = "end";
4156
4174
  }
4157
- let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text, preferredPos - from, preferredSide);
4158
- 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)) {
4159
4184
  // Chrome inserts two newlines when pressing shift-enter at the
4160
4185
  // end of a line. DomChange drops one of those.
4161
4186
  if (browser.chrome && lastKey == 13 &&
@@ -4165,16 +4190,12 @@ function applyDOMChange(view, domChange) {
4165
4190
  insert: state.Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
4166
4191
  }
4167
4192
  }
4168
- 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))) {
4169
4194
  newSel = null;
4170
4195
  }
4171
4196
  if (!change && !newSel)
4172
4197
  return false;
4173
- if (!change && domChange.typeOver && !sel.empty && newSel && newSel.main.empty) {
4174
- // Heuristic to notice typing over a selected character
4175
- change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
4176
- }
4177
- 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 &&
4178
4199
  /^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
4179
4200
  // Detect insert-period-on-double-space Mac and Android behavior,
4180
4201
  // and transform it into a regular space insert.
@@ -4182,18 +4203,7 @@ function applyDOMChange(view, domChange) {
4182
4203
  newSel = state.EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
4183
4204
  change = { from: change.from, to: change.to, insert: state.Text.of([change.insert.toString().replace(".", " ")]) };
4184
4205
  }
4185
- else if (change && change.from >= sel.from && change.to <= sel.to &&
4186
- (change.from != sel.from || change.to != sel.to) &&
4187
- (sel.to - sel.from) - (change.to - change.from) <= 4) {
4188
- // If the change is inside the selection and covers most of it,
4189
- // assume it is a selection replace (with identical characters at
4190
- // the start/end not included in the diff)
4191
- change = {
4192
- from: sel.from, to: sel.to,
4193
- insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
4194
- };
4195
- }
4196
- 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) &&
4197
4207
  view.inputState.insertingTextAt > Date.now() - 50) {
4198
4208
  // For a cross-line insertion, Chrome and Safari will crudely take
4199
4209
  // the text of the line after the selection, flattening any
@@ -4202,7 +4212,7 @@ function applyDOMChange(view, domChange) {
4202
4212
  // replace of the text provided by the beforeinput event.
4203
4213
  change = {
4204
4214
  from: sel.from, to: sel.to,
4205
- insert: view.state.toText(view.inputState.insertingText)
4215
+ insert: state$1.toText(view.inputState.insertingText)
4206
4216
  };
4207
4217
  }
4208
4218
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
@@ -4224,7 +4234,7 @@ function applyDOMChange(view, domChange) {
4224
4234
  scrollIntoView = true;
4225
4235
  userEvent = view.inputState.lastSelectionOrigin;
4226
4236
  if (userEvent == "select.pointer")
4227
- newSel = skipAtomsForSelection(view.state.facet(atomicRanges).map(f => f(view)), newSel);
4237
+ newSel = skipAtomsForSelection(state$1.facet(atomicRanges).map(f => f(view)), newSel);
4228
4238
  }
4229
4239
  view.dispatch({ selection: newSel, scrollIntoView, userEvent });
4230
4240
  return true;
@@ -4405,6 +4415,7 @@ class InputState {
4405
4415
  this.lastFocusTime = 0;
4406
4416
  this.lastScrollTop = 0;
4407
4417
  this.lastScrollLeft = 0;
4418
+ this.lastWheelEvent = 0;
4408
4419
  // On iOS, some keys need to have their default behavior happen
4409
4420
  // (after which we retroactively handle them and reset the DOM) to
4410
4421
  // avoid messing up the virtual keyboard state.
@@ -4834,6 +4845,9 @@ observers.scroll = view => {
4834
4845
  view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
4835
4846
  view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
4836
4847
  };
4848
+ observers.wheel = observers.mousewheel = view => {
4849
+ view.inputState.lastWheelEvent = Date.now();
4850
+ };
4837
4851
  handlers.keydown = (view, event) => {
4838
4852
  view.inputState.setSelectionOrigin("select");
4839
4853
  if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
@@ -6078,7 +6092,8 @@ class LineGapWidget extends WidgetType {
6078
6092
  get estimatedHeight() { return this.vertical ? this.size : -1; }
6079
6093
  }
6080
6094
  class ViewState {
6081
- constructor(state$1) {
6095
+ constructor(view, state$1) {
6096
+ this.view = view;
6082
6097
  this.state = state$1;
6083
6098
  // These are contentDOM-local coordinates
6084
6099
  this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
@@ -6089,12 +6104,14 @@ class ViewState {
6089
6104
  this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
6090
6105
  this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
6091
6106
  this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
6092
- this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
6093
- this.scrolledToBottom = false;
6094
6107
  // The CSS-transformation scale of the editor (transformed size /
6095
6108
  // concrete size)
6096
6109
  this.scaleX = 1;
6097
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;
6098
6115
  // The vertical position (document-relative) to which to anchor the
6099
6116
  // scroll position. -1 means anchor to the end of the document.
6100
6117
  this.scrollAnchorPos = 0;
@@ -6132,6 +6149,7 @@ class ViewState {
6132
6149
  this.updateViewportLines();
6133
6150
  this.lineGaps = this.ensureLineGaps([]);
6134
6151
  this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
6152
+ this.scrollParent = view.scrollDOM;
6135
6153
  this.computeVisibleRanges();
6136
6154
  }
6137
6155
  updateForViewport() {
@@ -6165,7 +6183,7 @@ class ViewState {
6165
6183
  let contentChanges = update.changedRanges;
6166
6184
  let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : state.ChangeSet.empty(this.state.doc.length)));
6167
6185
  let prevHeight = this.heightMap.height;
6168
- let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollTop);
6186
+ let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
6169
6187
  clearHeightChangeFlag();
6170
6188
  this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
6171
6189
  if (this.heightMap.height != prevHeight || heightChangeFlag)
@@ -6197,12 +6215,12 @@ class ViewState {
6197
6215
  !update.state.facet(nativeSelectionHidden))
6198
6216
  this.mustEnforceCursorAssoc = true;
6199
6217
  }
6200
- measure(view) {
6201
- let dom = view.contentDOM, style = window.getComputedStyle(dom);
6218
+ measure() {
6219
+ let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
6202
6220
  let oracle = this.heightOracle;
6203
6221
  let whiteSpace = style.whiteSpace;
6204
6222
  this.defaultTextDirection = style.direction == "rtl" ? exports.Direction.RTL : exports.Direction.LTR;
6205
- let refresh = this.heightOracle.mustRefreshForWrapping(whiteSpace) || this.mustMeasureContent;
6223
+ let refresh = this.heightOracle.mustRefreshForWrapping(whiteSpace) || this.mustMeasureContent === "refresh";
6206
6224
  let domRect = dom.getBoundingClientRect();
6207
6225
  let measureContent = refresh || this.mustMeasureContent || this.contentDOMHeight != domRect.height;
6208
6226
  this.contentDOMHeight = domRect.height;
@@ -6232,12 +6250,18 @@ class ViewState {
6232
6250
  this.editorWidth = view.scrollDOM.clientWidth;
6233
6251
  result |= 16 /* UpdateFlag.Geometry */;
6234
6252
  }
6235
- let scrollTop = view.scrollDOM.scrollTop * this.scaleY;
6236
- if (this.scrollTop != scrollTop) {
6253
+ let scrollParent = scrollableParents(this.view.contentDOM, false).y;
6254
+ if (scrollParent != this.scrollParent) {
6255
+ this.scrollParent = scrollParent;
6256
+ this.scrollAnchorHeight = -1;
6257
+ this.scrollOffset = 0;
6258
+ }
6259
+ let scrollOffset = this.getScrollOffset();
6260
+ if (this.scrollOffset != scrollOffset) {
6237
6261
  this.scrollAnchorHeight = -1;
6238
- this.scrollTop = scrollTop;
6262
+ this.scrollOffset = scrollOffset;
6239
6263
  }
6240
- this.scrolledToBottom = isScrolledToBottom(view.scrollDOM);
6264
+ this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
6241
6265
  // Pixel viewport
6242
6266
  let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
6243
6267
  let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
@@ -6514,9 +6538,14 @@ class ViewState {
6514
6538
  this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
6515
6539
  scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
6516
6540
  }
6517
- scrollAnchorAt(scrollTop) {
6518
- let block = this.lineBlockAtHeight(scrollTop + 8);
6519
- 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];
6520
6549
  }
6521
6550
  elementAtHeight(height) {
6522
6551
  return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
@@ -7763,7 +7792,7 @@ class EditorView {
7763
7792
  ((trs) => this.update(trs));
7764
7793
  this.dispatch = this.dispatch.bind(this);
7765
7794
  this._root = (config.root || getRoot(config.parent) || document);
7766
- this.viewState = new ViewState(config.state || state.EditorState.create(config));
7795
+ this.viewState = new ViewState(this, config.state || state.EditorState.create(config));
7767
7796
  if (config.scrollTo && config.scrollTo.is(scrollIntoView))
7768
7797
  this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
7769
7798
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
@@ -7779,7 +7808,7 @@ class EditorView {
7779
7808
  this.requestMeasure();
7780
7809
  if ((_a = document.fonts) === null || _a === void 0 ? void 0 : _a.ready)
7781
7810
  document.fonts.ready.then(() => {
7782
- this.viewState.mustMeasureContent = true;
7811
+ this.viewState.mustMeasureContent = "refresh";
7783
7812
  this.requestMeasure();
7784
7813
  });
7785
7814
  }
@@ -7918,7 +7947,7 @@ class EditorView {
7918
7947
  try {
7919
7948
  for (let plugin of this.plugins)
7920
7949
  plugin.destroy(this);
7921
- this.viewState = new ViewState(newState);
7950
+ this.viewState = new ViewState(this, newState);
7922
7951
  this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
7923
7952
  this.pluginMap.clear();
7924
7953
  for (let plugin of this.plugins)
@@ -7997,26 +8026,26 @@ class EditorView {
7997
8026
  if (flush)
7998
8027
  this.observer.forceFlush();
7999
8028
  let updated = null;
8000
- let sDOM = this.scrollDOM, scrollTop = sDOM.scrollTop * this.scaleY;
8029
+ let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
8001
8030
  let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
8002
- if (Math.abs(scrollTop - this.viewState.scrollTop) > 1)
8031
+ if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
8003
8032
  scrollAnchorHeight = -1;
8004
8033
  this.viewState.scrollAnchorHeight = -1;
8005
8034
  try {
8006
8035
  for (let i = 0;; i++) {
8007
8036
  if (scrollAnchorHeight < 0) {
8008
- if (isScrolledToBottom(sDOM)) {
8037
+ if (isScrolledToBottom(scroll || this.win)) {
8009
8038
  scrollAnchorPos = -1;
8010
8039
  scrollAnchorHeight = this.viewState.heightMap.height;
8011
8040
  }
8012
8041
  else {
8013
- let block = this.viewState.scrollAnchorAt(scrollTop);
8042
+ let block = this.viewState.scrollAnchorAt(scrollOffset);
8014
8043
  scrollAnchorPos = block.from;
8015
8044
  scrollAnchorHeight = block.top;
8016
8045
  }
8017
8046
  }
8018
8047
  this.updateState = 1 /* UpdateState.Measuring */;
8019
- let changed = this.viewState.measure(this);
8048
+ let changed = this.viewState.measure();
8020
8049
  if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
8021
8050
  break;
8022
8051
  if (i > 5) {
@@ -8077,10 +8106,15 @@ class EditorView {
8077
8106
  else {
8078
8107
  let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
8079
8108
  this.viewState.lineBlockAt(scrollAnchorPos).top;
8080
- let diff = newAnchorHeight - scrollAnchorHeight;
8081
- if (diff > 1 || diff < -1) {
8082
- scrollTop = scrollTop + diff;
8083
- 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);
8084
8118
  scrollAnchorHeight = -1;
8085
8119
  continue;
8086
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 {
@@ -2787,9 +2789,10 @@ class TileUpdate {
2787
2789
  marks.push(tile);
2788
2790
  else if (tile === null || tile === void 0 ? void 0 : tile.isLine())
2789
2791
  line = tile;
2792
+ else if (tile instanceof BlockWrapperTile) ; // Ignore
2790
2793
  else if (parent.nodeName == "DIV" && !line && parent != this.view.contentDOM)
2791
2794
  line = new LineTile(parent, lineBaseAttrs);
2792
- else
2795
+ else if (!line)
2793
2796
  marks.push(MarkTile.of(new MarkDecoration({ tagName: parent.nodeName.toLowerCase(), attributes: getAttrs(parent) }), parent));
2794
2797
  }
2795
2798
  return { line: line, marks };
@@ -2955,6 +2958,8 @@ class DocView {
2955
2958
  if (composition || changes.length) {
2956
2959
  let oldTile = this.tile;
2957
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 */);
2958
2963
  this.tile = builder.run(changes, composition);
2959
2964
  destroyDropped(oldTile, builder.cache.reused);
2960
2965
  }
@@ -3415,6 +3420,19 @@ class DocView {
3415
3420
  };
3416
3421
  let { offsetWidth, offsetHeight } = this.view.scrollDOM;
3417
3422
  scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == Direction.LTR);
3423
+ // On mobile browsers, the visual viewport may be smaller than the
3424
+ // actual reported viewport, causing scrollRectIntoView to fail to
3425
+ // scroll properly. Unfortunately, this visual viewport cannot be
3426
+ // updated directly, and scrollIntoView is the only way a script
3427
+ // can affect it. So this tries to kludge around the problem by
3428
+ // calling scrollIntoView on the scroll target's line.
3429
+ if (window.visualViewport && window.innerHeight - window.visualViewport.height > 1 &&
3430
+ (rect.top > window.pageYOffset + window.visualViewport.offsetTop + window.visualViewport.height ||
3431
+ rect.bottom < window.pageYOffset + window.visualViewport.offsetTop)) {
3432
+ let line = this.view.docView.lineAt(range.head, 1);
3433
+ if (line)
3434
+ line.dom.scrollIntoView({ block: "nearest" });
3435
+ }
3418
3436
  }
3419
3437
  lineHasWidget(pos) {
3420
3438
  let scan = (child) => child.isWidget() || child.children.some(scan);
@@ -4139,7 +4157,7 @@ function domBoundsAround(tile, from, to, offset) {
4139
4157
  }
4140
4158
  function applyDOMChange(view, domChange) {
4141
4159
  let change;
4142
- let { newSel } = domChange, sel = view.state.selection.main;
4160
+ let { newSel } = domChange, { state } = view, sel = state.selection.main;
4143
4161
  let lastKey = view.inputState.lastKeyTime > Date.now() - 100 ? view.inputState.lastKeyCode : -1;
4144
4162
  if (domChange.bounds) {
4145
4163
  let { from, to } = domChange.bounds;
@@ -4150,8 +4168,15 @@ function applyDOMChange(view, domChange) {
4150
4168
  preferredPos = sel.to;
4151
4169
  preferredSide = "end";
4152
4170
  }
4153
- let diff = findDiff(view.state.doc.sliceString(from, to, LineBreakPlaceholder), domChange.text, preferredPos - from, preferredSide);
4154
- 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)) {
4155
4180
  // Chrome inserts two newlines when pressing shift-enter at the
4156
4181
  // end of a line. DomChange drops one of those.
4157
4182
  if (browser.chrome && lastKey == 13 &&
@@ -4161,16 +4186,12 @@ function applyDOMChange(view, domChange) {
4161
4186
  insert: Text.of(domChange.text.slice(diff.from, diff.toB).split(LineBreakPlaceholder)) };
4162
4187
  }
4163
4188
  }
4164
- else if (newSel && (!view.hasFocus && view.state.facet(editable) || sameSelPos(newSel, sel))) {
4189
+ else if (newSel && (!view.hasFocus && state.facet(editable) || sameSelPos(newSel, sel))) {
4165
4190
  newSel = null;
4166
4191
  }
4167
4192
  if (!change && !newSel)
4168
4193
  return false;
4169
- if (!change && domChange.typeOver && !sel.empty && newSel && newSel.main.empty) {
4170
- // Heuristic to notice typing over a selected character
4171
- change = { from: sel.from, to: sel.to, insert: view.state.doc.slice(sel.from, sel.to) };
4172
- }
4173
- 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 &&
4174
4195
  /^\. ?$/.test(change.insert.toString()) && view.contentDOM.getAttribute("autocorrect") == "off") {
4175
4196
  // Detect insert-period-on-double-space Mac and Android behavior,
4176
4197
  // and transform it into a regular space insert.
@@ -4178,18 +4199,7 @@ function applyDOMChange(view, domChange) {
4178
4199
  newSel = EditorSelection.single(newSel.main.anchor - 1, newSel.main.head - 1);
4179
4200
  change = { from: change.from, to: change.to, insert: Text.of([change.insert.toString().replace(".", " ")]) };
4180
4201
  }
4181
- else if (change && change.from >= sel.from && change.to <= sel.to &&
4182
- (change.from != sel.from || change.to != sel.to) &&
4183
- (sel.to - sel.from) - (change.to - change.from) <= 4) {
4184
- // If the change is inside the selection and covers most of it,
4185
- // assume it is a selection replace (with identical characters at
4186
- // the start/end not included in the diff)
4187
- change = {
4188
- from: sel.from, to: sel.to,
4189
- insert: view.state.doc.slice(sel.from, change.from).append(change.insert).append(view.state.doc.slice(change.to, sel.to))
4190
- };
4191
- }
4192
- 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) &&
4193
4203
  view.inputState.insertingTextAt > Date.now() - 50) {
4194
4204
  // For a cross-line insertion, Chrome and Safari will crudely take
4195
4205
  // the text of the line after the selection, flattening any
@@ -4198,7 +4208,7 @@ function applyDOMChange(view, domChange) {
4198
4208
  // replace of the text provided by the beforeinput event.
4199
4209
  change = {
4200
4210
  from: sel.from, to: sel.to,
4201
- insert: view.state.toText(view.inputState.insertingText)
4211
+ insert: state.toText(view.inputState.insertingText)
4202
4212
  };
4203
4213
  }
4204
4214
  else if (browser.chrome && change && change.from == change.to && change.from == sel.head &&
@@ -4220,7 +4230,7 @@ function applyDOMChange(view, domChange) {
4220
4230
  scrollIntoView = true;
4221
4231
  userEvent = view.inputState.lastSelectionOrigin;
4222
4232
  if (userEvent == "select.pointer")
4223
- newSel = skipAtomsForSelection(view.state.facet(atomicRanges).map(f => f(view)), newSel);
4233
+ newSel = skipAtomsForSelection(state.facet(atomicRanges).map(f => f(view)), newSel);
4224
4234
  }
4225
4235
  view.dispatch({ selection: newSel, scrollIntoView, userEvent });
4226
4236
  return true;
@@ -4401,6 +4411,7 @@ class InputState {
4401
4411
  this.lastFocusTime = 0;
4402
4412
  this.lastScrollTop = 0;
4403
4413
  this.lastScrollLeft = 0;
4414
+ this.lastWheelEvent = 0;
4404
4415
  // On iOS, some keys need to have their default behavior happen
4405
4416
  // (after which we retroactively handle them and reset the DOM) to
4406
4417
  // avoid messing up the virtual keyboard state.
@@ -4830,6 +4841,9 @@ observers.scroll = view => {
4830
4841
  view.inputState.lastScrollTop = view.scrollDOM.scrollTop;
4831
4842
  view.inputState.lastScrollLeft = view.scrollDOM.scrollLeft;
4832
4843
  };
4844
+ observers.wheel = observers.mousewheel = view => {
4845
+ view.inputState.lastWheelEvent = Date.now();
4846
+ };
4833
4847
  handlers.keydown = (view, event) => {
4834
4848
  view.inputState.setSelectionOrigin("select");
4835
4849
  if (event.keyCode == 27 && view.inputState.tabFocusMode != 0)
@@ -6073,7 +6087,8 @@ class LineGapWidget extends WidgetType {
6073
6087
  get estimatedHeight() { return this.vertical ? this.size : -1; }
6074
6088
  }
6075
6089
  class ViewState {
6076
- constructor(state) {
6090
+ constructor(view, state) {
6091
+ this.view = view;
6077
6092
  this.state = state;
6078
6093
  // These are contentDOM-local coordinates
6079
6094
  this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
@@ -6084,12 +6099,14 @@ class ViewState {
6084
6099
  this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
6085
6100
  this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
6086
6101
  this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
6087
- this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
6088
- this.scrolledToBottom = false;
6089
6102
  // The CSS-transformation scale of the editor (transformed size /
6090
6103
  // concrete size)
6091
6104
  this.scaleX = 1;
6092
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;
6093
6110
  // The vertical position (document-relative) to which to anchor the
6094
6111
  // scroll position. -1 means anchor to the end of the document.
6095
6112
  this.scrollAnchorPos = 0;
@@ -6127,6 +6144,7 @@ class ViewState {
6127
6144
  this.updateViewportLines();
6128
6145
  this.lineGaps = this.ensureLineGaps([]);
6129
6146
  this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
6147
+ this.scrollParent = view.scrollDOM;
6130
6148
  this.computeVisibleRanges();
6131
6149
  }
6132
6150
  updateForViewport() {
@@ -6160,7 +6178,7 @@ class ViewState {
6160
6178
  let contentChanges = update.changedRanges;
6161
6179
  let heightChanges = ChangedRange.extendWithRanges(contentChanges, heightRelevantDecoChanges(prevDeco, this.stateDeco, update ? update.changes : ChangeSet.empty(this.state.doc.length)));
6162
6180
  let prevHeight = this.heightMap.height;
6163
- let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollTop);
6181
+ let scrollAnchor = this.scrolledToBottom ? null : this.scrollAnchorAt(this.scrollOffset);
6164
6182
  clearHeightChangeFlag();
6165
6183
  this.heightMap = this.heightMap.applyChanges(this.stateDeco, update.startState.doc, this.heightOracle.setDoc(this.state.doc), heightChanges);
6166
6184
  if (this.heightMap.height != prevHeight || heightChangeFlag)
@@ -6192,12 +6210,12 @@ class ViewState {
6192
6210
  !update.state.facet(nativeSelectionHidden))
6193
6211
  this.mustEnforceCursorAssoc = true;
6194
6212
  }
6195
- measure(view) {
6196
- let dom = view.contentDOM, style = window.getComputedStyle(dom);
6213
+ measure() {
6214
+ let { view } = this, dom = view.contentDOM, style = window.getComputedStyle(dom);
6197
6215
  let oracle = this.heightOracle;
6198
6216
  let whiteSpace = style.whiteSpace;
6199
6217
  this.defaultTextDirection = style.direction == "rtl" ? Direction.RTL : Direction.LTR;
6200
- let refresh = this.heightOracle.mustRefreshForWrapping(whiteSpace) || this.mustMeasureContent;
6218
+ let refresh = this.heightOracle.mustRefreshForWrapping(whiteSpace) || this.mustMeasureContent === "refresh";
6201
6219
  let domRect = dom.getBoundingClientRect();
6202
6220
  let measureContent = refresh || this.mustMeasureContent || this.contentDOMHeight != domRect.height;
6203
6221
  this.contentDOMHeight = domRect.height;
@@ -6227,12 +6245,18 @@ class ViewState {
6227
6245
  this.editorWidth = view.scrollDOM.clientWidth;
6228
6246
  result |= 16 /* UpdateFlag.Geometry */;
6229
6247
  }
6230
- let scrollTop = view.scrollDOM.scrollTop * this.scaleY;
6231
- if (this.scrollTop != scrollTop) {
6248
+ let scrollParent = scrollableParents(this.view.contentDOM, false).y;
6249
+ if (scrollParent != this.scrollParent) {
6250
+ this.scrollParent = scrollParent;
6251
+ this.scrollAnchorHeight = -1;
6252
+ this.scrollOffset = 0;
6253
+ }
6254
+ let scrollOffset = this.getScrollOffset();
6255
+ if (this.scrollOffset != scrollOffset) {
6232
6256
  this.scrollAnchorHeight = -1;
6233
- this.scrollTop = scrollTop;
6257
+ this.scrollOffset = scrollOffset;
6234
6258
  }
6235
- this.scrolledToBottom = isScrolledToBottom(view.scrollDOM);
6259
+ this.scrolledToBottom = isScrolledToBottom(this.scrollParent || view.win);
6236
6260
  // Pixel viewport
6237
6261
  let pixelViewport = (this.printing ? fullPixelRange : visiblePixelRange)(dom, this.paddingTop);
6238
6262
  let dTop = pixelViewport.top - this.pixelViewport.top, dBottom = pixelViewport.bottom - this.pixelViewport.bottom;
@@ -6509,9 +6533,14 @@ class ViewState {
6509
6533
  this.viewportLines.find(l => l.top <= height && l.bottom >= height)) ||
6510
6534
  scaleBlock(this.heightMap.lineAt(this.scaler.fromDOM(height), QueryType.ByHeight, this.heightOracle, 0, 0), this.scaler);
6511
6535
  }
6512
- scrollAnchorAt(scrollTop) {
6513
- let block = this.lineBlockAtHeight(scrollTop + 8);
6514
- 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];
6515
6544
  }
6516
6545
  elementAtHeight(height) {
6517
6546
  return scaleBlock(this.heightMap.blockAt(this.scaler.fromDOM(height), this.heightOracle, 0, 0), this.scaler);
@@ -7758,7 +7787,7 @@ class EditorView {
7758
7787
  ((trs) => this.update(trs));
7759
7788
  this.dispatch = this.dispatch.bind(this);
7760
7789
  this._root = (config.root || getRoot(config.parent) || document);
7761
- this.viewState = new ViewState(config.state || EditorState.create(config));
7790
+ this.viewState = new ViewState(this, config.state || EditorState.create(config));
7762
7791
  if (config.scrollTo && config.scrollTo.is(scrollIntoView))
7763
7792
  this.viewState.scrollTarget = config.scrollTo.value.clip(this.viewState.state);
7764
7793
  this.plugins = this.state.facet(viewPlugin).map(spec => new PluginInstance(spec));
@@ -7774,7 +7803,7 @@ class EditorView {
7774
7803
  this.requestMeasure();
7775
7804
  if ((_a = document.fonts) === null || _a === void 0 ? void 0 : _a.ready)
7776
7805
  document.fonts.ready.then(() => {
7777
- this.viewState.mustMeasureContent = true;
7806
+ this.viewState.mustMeasureContent = "refresh";
7778
7807
  this.requestMeasure();
7779
7808
  });
7780
7809
  }
@@ -7913,7 +7942,7 @@ class EditorView {
7913
7942
  try {
7914
7943
  for (let plugin of this.plugins)
7915
7944
  plugin.destroy(this);
7916
- this.viewState = new ViewState(newState);
7945
+ this.viewState = new ViewState(this, newState);
7917
7946
  this.plugins = newState.facet(viewPlugin).map(spec => new PluginInstance(spec));
7918
7947
  this.pluginMap.clear();
7919
7948
  for (let plugin of this.plugins)
@@ -7992,26 +8021,26 @@ class EditorView {
7992
8021
  if (flush)
7993
8022
  this.observer.forceFlush();
7994
8023
  let updated = null;
7995
- let sDOM = this.scrollDOM, scrollTop = sDOM.scrollTop * this.scaleY;
8024
+ let scroll = this.viewState.scrollParent, scrollOffset = this.viewState.getScrollOffset();
7996
8025
  let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
7997
- if (Math.abs(scrollTop - this.viewState.scrollTop) > 1)
8026
+ if (Math.abs(scrollOffset - this.viewState.scrollOffset) > 1)
7998
8027
  scrollAnchorHeight = -1;
7999
8028
  this.viewState.scrollAnchorHeight = -1;
8000
8029
  try {
8001
8030
  for (let i = 0;; i++) {
8002
8031
  if (scrollAnchorHeight < 0) {
8003
- if (isScrolledToBottom(sDOM)) {
8032
+ if (isScrolledToBottom(scroll || this.win)) {
8004
8033
  scrollAnchorPos = -1;
8005
8034
  scrollAnchorHeight = this.viewState.heightMap.height;
8006
8035
  }
8007
8036
  else {
8008
- let block = this.viewState.scrollAnchorAt(scrollTop);
8037
+ let block = this.viewState.scrollAnchorAt(scrollOffset);
8009
8038
  scrollAnchorPos = block.from;
8010
8039
  scrollAnchorHeight = block.top;
8011
8040
  }
8012
8041
  }
8013
8042
  this.updateState = 1 /* UpdateState.Measuring */;
8014
- let changed = this.viewState.measure(this);
8043
+ let changed = this.viewState.measure();
8015
8044
  if (!changed && !this.measureRequests.length && this.viewState.scrollTarget == null)
8016
8045
  break;
8017
8046
  if (i > 5) {
@@ -8072,10 +8101,15 @@ class EditorView {
8072
8101
  else {
8073
8102
  let newAnchorHeight = scrollAnchorPos < 0 ? this.viewState.heightMap.height :
8074
8103
  this.viewState.lineBlockAt(scrollAnchorPos).top;
8075
- let diff = newAnchorHeight - scrollAnchorHeight;
8076
- if (diff > 1 || diff < -1) {
8077
- scrollTop = scrollTop + diff;
8078
- 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);
8079
8113
  scrollAnchorHeight = -1;
8080
8114
  continue;
8081
8115
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.39.14",
3
+ "version": "6.39.16",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",