@codemirror/view 0.19.20 → 0.19.21

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,9 @@
1
+ ## 0.19.21 (2021-11-26)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix a problem where the DOM update would unnecessarily trigger browser relayouts.
6
+
1
7
  ## 0.19.20 (2021-11-19)
2
8
 
3
9
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -186,7 +186,7 @@ function scrollRectIntoView(dom, rect, side, center) {
186
186
  }
187
187
  }
188
188
  }
189
- class DOMSelection {
189
+ class DOMSelectionState {
190
190
  constructor() {
191
191
  this.anchorNode = null;
192
192
  this.anchorOffset = 0;
@@ -197,11 +197,14 @@ class DOMSelection {
197
197
  return this.anchorNode == domSel.anchorNode && this.anchorOffset == domSel.anchorOffset &&
198
198
  this.focusNode == domSel.focusNode && this.focusOffset == domSel.focusOffset;
199
199
  }
200
- set(domSel) {
201
- this.anchorNode = domSel.anchorNode;
202
- this.anchorOffset = domSel.anchorOffset;
203
- this.focusNode = domSel.focusNode;
204
- this.focusOffset = domSel.focusOffset;
200
+ setRange(range) {
201
+ this.set(range.anchorNode, range.anchorOffset, range.focusNode, range.focusOffset);
202
+ }
203
+ set(anchorNode, anchorOffset, focusNode, focusOffset) {
204
+ this.anchorNode = anchorNode;
205
+ this.anchorOffset = anchorOffset;
206
+ this.focusNode = focusNode;
207
+ this.focusOffset = focusOffset;
205
208
  }
206
209
  }
207
210
  let preventScrollSupported = null;
@@ -1943,9 +1946,10 @@ class DocView extends ContentView {
1943
1946
  // we don't mess it up when reading it back it
1944
1947
  this.impreciseAnchor = null;
1945
1948
  this.impreciseHead = null;
1949
+ this.forceSelection = false;
1946
1950
  // Used by the resize observer to ignore resizes that we caused
1947
1951
  // ourselves
1948
- this.lastUpdate = 0;
1952
+ this.lastUpdate = Date.now();
1949
1953
  this.setDOM(view.contentDOM);
1950
1954
  this.children = [new LineView];
1951
1955
  this.children[0].setParent(this);
@@ -1959,7 +1963,6 @@ class DocView extends ContentView {
1959
1963
  // position, if we know the editor is going to scroll that position
1960
1964
  // into view.
1961
1965
  update(update) {
1962
- this.lastUpdate = Date.now();
1963
1966
  let changedRanges = update.changedRanges;
1964
1967
  if (this.minWidth > 0 && changedRanges.length) {
1965
1968
  if (!changedRanges.every(({ fromA, toA }) => toA < this.minWidthFrom || fromA > this.minWidthTo)) {
@@ -1979,21 +1982,22 @@ class DocView extends ContentView {
1979
1982
  // getSelection than the one that it actually shows to the user.
1980
1983
  // This forces a selection update when lines are joined to work
1981
1984
  // around that. Issue #54
1982
- let forceSelection = (browser.ie || browser.chrome) && !this.compositionDeco.size && update &&
1983
- update.state.doc.lines != update.startState.doc.lines;
1985
+ if ((browser.ie || browser.chrome) && !this.compositionDeco.size && update &&
1986
+ update.state.doc.lines != update.startState.doc.lines)
1987
+ this.forceSelection = true;
1984
1988
  let prevDeco = this.decorations, deco = this.updateDeco();
1985
1989
  let decoDiff = findChangedDeco(prevDeco, deco, update.changes);
1986
1990
  changedRanges = ChangedRange.extendWithRanges(changedRanges, decoDiff);
1987
- let pointerSel = update.transactions.some(tr => tr.isUserEvent("select.pointer"));
1988
1991
  if (this.dirty == 0 /* Not */ && changedRanges.length == 0 &&
1989
1992
  !(update.flags & 4 /* Viewport */) &&
1990
1993
  update.state.selection.main.from >= this.view.viewport.from &&
1991
1994
  update.state.selection.main.to <= this.view.viewport.to) {
1992
- this.updateSelection(forceSelection, pointerSel);
1993
1995
  return false;
1994
1996
  }
1995
1997
  else {
1996
- this.updateInner(changedRanges, deco, update.startState.doc.length, forceSelection, pointerSel);
1998
+ this.updateInner(changedRanges, deco, update.startState.doc.length);
1999
+ if (update.transactions.length)
2000
+ this.lastUpdate = Date.now();
1997
2001
  return true;
1998
2002
  }
1999
2003
  }
@@ -2001,13 +2005,15 @@ class DocView extends ContentView {
2001
2005
  if (this.dirty) {
2002
2006
  this.view.observer.ignore(() => this.view.docView.sync());
2003
2007
  this.dirty = 0 /* Not */;
2008
+ this.updateSelection(true);
2004
2009
  }
2005
- if (sel)
2010
+ else {
2006
2011
  this.updateSelection();
2012
+ }
2007
2013
  }
2008
2014
  // Used both by update and checkLayout do perform the actual DOM
2009
2015
  // update
2010
- updateInner(changes, deco, oldLength, forceSelection = false, pointerSel = false) {
2016
+ updateInner(changes, deco, oldLength) {
2011
2017
  this.updateChildren(changes, deco, oldLength);
2012
2018
  let { observer } = this.view;
2013
2019
  observer.ignore(() => {
@@ -2025,8 +2031,7 @@ class DocView extends ContentView {
2025
2031
  this.sync(track);
2026
2032
  this.dirty = 0 /* Not */;
2027
2033
  if (track && (track.written || observer.selectionRange.focusNode != track.node))
2028
- forceSelection = true;
2029
- this.updateSelection(forceSelection, pointerSel);
2034
+ this.forceSelection = true;
2030
2035
  this.dom.style.height = "";
2031
2036
  });
2032
2037
  let gaps = [];
@@ -2112,10 +2117,14 @@ class DocView extends ContentView {
2112
2117
  this.replaceChildren(fromI, toI, content);
2113
2118
  }
2114
2119
  // Sync the DOM selection to this.state.selection
2115
- updateSelection(force = false, fromPointer = false) {
2120
+ updateSelection(mustRead = false, fromPointer = false) {
2121
+ if (mustRead)
2122
+ this.view.observer.readSelectionRange();
2116
2123
  if (!(fromPointer || this.mayControlSelection()) ||
2117
2124
  browser.ios && this.view.inputState.rapidCompositionStart)
2118
2125
  return;
2126
+ let force = this.forceSelection;
2127
+ this.forceSelection = false;
2119
2128
  let main = this.view.state.selection.main;
2120
2129
  // FIXME need to handle the case where the selection falls inside a block range
2121
2130
  let anchor = this.domAtPos(main.anchor);
@@ -5075,25 +5084,30 @@ class DOMObserver {
5075
5084
  this.onChange = onChange;
5076
5085
  this.onScrollChanged = onScrollChanged;
5077
5086
  this.active = false;
5078
- this.ignoreSelection = new DOMSelection;
5087
+ // The known selection. Kept in our own object, as opposed to just
5088
+ // directly accessing the selection because:
5089
+ // - Safari doesn't report the right selection in shadow DOM
5090
+ // - Reading from the selection forces a DOM layout
5091
+ // - This way, we can ignore selectionchange events if we have
5092
+ // already seen the 'new' selection
5093
+ this.selectionRange = new DOMSelectionState;
5094
+ // Set when a selection change is detected, cleared on flush
5095
+ this.selectionChanged = false;
5079
5096
  this.delayedFlush = -1;
5097
+ this.resizeTimeout = -1;
5080
5098
  this.queue = [];
5081
- this.lastFlush = 0;
5082
5099
  this.scrollTargets = [];
5083
5100
  this.intersection = null;
5084
5101
  this.resize = null;
5085
5102
  this.intersecting = false;
5086
5103
  this.gapIntersection = null;
5087
5104
  this.gaps = [];
5088
- // Used to work around a Safari Selection/shadow DOM bug (#414)
5089
- this._selectionRange = null;
5090
5105
  // Timeout for scheduling check of the parents that need scroll handlers
5091
5106
  this.parentCheck = -1;
5092
5107
  this.dom = view.contentDOM;
5093
5108
  this.observer = new MutationObserver(mutations => {
5094
5109
  for (let mut of mutations)
5095
5110
  this.queue.push(mut);
5096
- this._selectionRange = null;
5097
5111
  // IE11 will sometimes (on typing over a selection or
5098
5112
  // backspacing out a single character text node) call the
5099
5113
  // observer callback before actually updating the DOM.
@@ -5120,8 +5134,11 @@ class DOMObserver {
5120
5134
  this.onSelectionChange = this.onSelectionChange.bind(this);
5121
5135
  if (typeof ResizeObserver == "function") {
5122
5136
  this.resize = new ResizeObserver(() => {
5123
- if (this.view.docView.lastUpdate < Date.now() - 100)
5124
- this.view.requestMeasure();
5137
+ if (this.view.docView.lastUpdate < Date.now() - 75 && this.resizeTimeout < 0)
5138
+ this.resizeTimeout = setTimeout(() => {
5139
+ this.resizeTimeout = -1;
5140
+ this.view.requestMeasure();
5141
+ }, 50);
5125
5142
  });
5126
5143
  this.resize.observe(view.scrollDOM);
5127
5144
  }
@@ -5145,10 +5162,12 @@ class DOMObserver {
5145
5162
  }, {});
5146
5163
  }
5147
5164
  this.listenForScroll();
5165
+ this.readSelectionRange();
5166
+ this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
5148
5167
  }
5149
5168
  onScroll(e) {
5150
5169
  if (this.intersecting)
5151
- this.flush();
5170
+ this.flush(false);
5152
5171
  this.onScrollChanged(e);
5153
5172
  }
5154
5173
  updateGaps(gaps) {
@@ -5160,8 +5179,8 @@ class DOMObserver {
5160
5179
  }
5161
5180
  }
5162
5181
  onSelectionChange(event) {
5163
- if (this.lastFlush < Date.now() - 50)
5164
- this._selectionRange = null;
5182
+ if (!this.readSelectionRange())
5183
+ return;
5165
5184
  let { view } = this, sel = this.selectionRange;
5166
5185
  if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(view.dom, sel))
5167
5186
  return;
@@ -5176,24 +5195,22 @@ class DOMObserver {
5176
5195
  sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset))
5177
5196
  this.flushSoon();
5178
5197
  else
5179
- this.flush();
5180
- }
5181
- get selectionRange() {
5182
- if (!this._selectionRange) {
5183
- let { root } = this.view, sel = getSelection(root);
5184
- // The Selection object is broken in shadow roots in Safari. See
5185
- // https://github.com/codemirror/codemirror.next/issues/414
5186
- if (browser.safari && root.nodeType == 11 && deepActiveElement() == this.view.contentDOM)
5187
- sel = safariSelectionRangeHack(this.view) || sel;
5188
- this._selectionRange = sel;
5189
- }
5190
- return this._selectionRange;
5198
+ this.flush(false);
5199
+ }
5200
+ readSelectionRange() {
5201
+ let { root } = this.view, domSel = getSelection(root);
5202
+ // The Selection object is broken in shadow roots in Safari. See
5203
+ // https://github.com/codemirror/codemirror.next/issues/414
5204
+ let range = browser.safari && root.nodeType == 11 && deepActiveElement() == this.view.contentDOM &&
5205
+ safariSelectionRangeHack(this.view) || domSel;
5206
+ if (this.selectionRange.eq(range))
5207
+ return false;
5208
+ this.selectionRange.setRange(range);
5209
+ return this.selectionChanged = true;
5191
5210
  }
5192
5211
  setSelectionRange(anchor, head) {
5193
- var _a;
5194
- if (!((_a = this._selectionRange) === null || _a === void 0 ? void 0 : _a.type))
5195
- this._selectionRange = { anchorNode: anchor.node, anchorOffset: anchor.offset,
5196
- focusNode: head.node, focusOffset: head.offset };
5212
+ this.selectionRange.set(anchor.node, anchor.offset, head.node, head.offset);
5213
+ this.selectionChanged = false;
5197
5214
  }
5198
5215
  listenForScroll() {
5199
5216
  this.parentCheck = -1;
@@ -5240,7 +5257,6 @@ class DOMObserver {
5240
5257
  if (this.active)
5241
5258
  return;
5242
5259
  this.observer.observe(this.dom, observeOptions);
5243
- this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
5244
5260
  if (useCharData)
5245
5261
  this.dom.addEventListener("DOMCharacterDataModified", this.onCharData);
5246
5262
  this.active = true;
@@ -5250,18 +5266,14 @@ class DOMObserver {
5250
5266
  return;
5251
5267
  this.active = false;
5252
5268
  this.observer.disconnect();
5253
- this.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange);
5254
5269
  if (useCharData)
5255
5270
  this.dom.removeEventListener("DOMCharacterDataModified", this.onCharData);
5256
5271
  }
5257
- clearSelection() {
5258
- this.ignoreSelection.set(this.selectionRange);
5259
- }
5260
5272
  // Throw away any pending changes
5261
5273
  clear() {
5262
5274
  this.observer.takeRecords();
5263
5275
  this.queue.length = 0;
5264
- this.clearSelection();
5276
+ this.selectionChanged = false;
5265
5277
  }
5266
5278
  flushSoon() {
5267
5279
  if (this.delayedFlush < 0)
@@ -5298,24 +5310,24 @@ class DOMObserver {
5298
5310
  return { from, to, typeOver };
5299
5311
  }
5300
5312
  // Apply pending changes, if any
5301
- flush() {
5313
+ flush(readSelection = true) {
5314
+ if (readSelection)
5315
+ this.readSelectionRange();
5302
5316
  // Completely hold off flushing when pending keys are set—the code
5303
5317
  // managing those will make sure processRecords is called and the
5304
5318
  // view is resynchronized after
5305
5319
  if (this.delayedFlush >= 0 || this.view.inputState.pendingAndroidKey)
5306
5320
  return;
5307
- this.lastFlush = Date.now();
5308
5321
  let { from, to, typeOver } = this.processRecords();
5309
- let selection = this.selectionRange;
5310
- let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
5322
+ let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
5311
5323
  if (from < 0 && !newSel)
5312
5324
  return;
5325
+ this.selectionChanged = false;
5313
5326
  let startState = this.view.state;
5314
5327
  this.onChange(from, to, typeOver);
5315
5328
  // The view wasn't updated
5316
5329
  if (this.view.state == startState)
5317
5330
  this.view.docView.reset(newSel);
5318
- this.clearSelection();
5319
5331
  }
5320
5332
  readMutation(rec) {
5321
5333
  let cView = this.view.docView.nearest(rec.target);
@@ -5347,6 +5359,7 @@ class DOMObserver {
5347
5359
  dom.removeEventListener("scroll", this.onScroll);
5348
5360
  window.removeEventListener("scroll", this.onScroll);
5349
5361
  clearTimeout(this.parentCheck);
5362
+ clearTimeout(this.resizeTimeout);
5350
5363
  }
5351
5364
  }
5352
5365
  function findChild(cView, dom, dir) {
@@ -5359,6 +5372,7 @@ function findChild(cView, dom, dir) {
5359
5372
  }
5360
5373
  return null;
5361
5374
  }
5375
+ // Used to work around a Safari Selection/shadow DOM bug (#414)
5362
5376
  function safariSelectionRangeHack(view) {
5363
5377
  let found = null;
5364
5378
  // Because Safari (at least in 2018-2021) doesn't provide regular
@@ -5778,6 +5792,7 @@ class EditorView {
5778
5792
  this.mountStyles();
5779
5793
  this.updateAttrs();
5780
5794
  this.showAnnouncements(transactions);
5795
+ this.docView.updateSelection(redrawn, transactions.some(tr => tr.isUserEvent("select.pointer")));
5781
5796
  }
5782
5797
  finally {
5783
5798
  this.updateState = 0 /* Idle */;
@@ -5895,8 +5910,7 @@ class EditorView {
5895
5910
  this.inputState.update(update);
5896
5911
  }
5897
5912
  this.updateAttrs();
5898
- if (changed)
5899
- this.docView.update(update);
5913
+ let redrawn = changed > 0 && this.docView.update(update);
5900
5914
  for (let i = 0; i < measuring.length; i++)
5901
5915
  if (measured[i] != BadMeasure) {
5902
5916
  try {
@@ -5910,6 +5924,8 @@ class EditorView {
5910
5924
  this.docView.scrollIntoView(this.viewState.scrollTarget);
5911
5925
  this.viewState.scrollTarget = null;
5912
5926
  }
5927
+ if (changed)
5928
+ this.docView.updateSelection(redrawn);
5913
5929
  if (this.viewport.from == oldViewport.from && this.viewport.to == oldViewport.to && this.measureRequests.length == 0)
5914
5930
  break;
5915
5931
  }
@@ -5934,8 +5950,6 @@ class EditorView {
5934
5950
  let editorAttrs = combineAttrs(this.state.facet(editorAttributes), {
5935
5951
  class: "cm-editor" + (this.hasFocus ? " cm-focused " : " ") + this.themeClasses
5936
5952
  });
5937
- updateAttrs(this.dom, this.editorAttrs, editorAttrs);
5938
- this.editorAttrs = editorAttrs;
5939
5953
  let contentAttrs = {
5940
5954
  spellcheck: "false",
5941
5955
  autocorrect: "off",
@@ -5950,7 +5964,11 @@ class EditorView {
5950
5964
  if (this.state.readOnly)
5951
5965
  contentAttrs["aria-readonly"] = "true";
5952
5966
  combineAttrs(this.state.facet(contentAttributes), contentAttrs);
5953
- updateAttrs(this.contentDOM, this.contentAttrs, contentAttrs);
5967
+ this.observer.ignore(() => {
5968
+ updateAttrs(this.contentDOM, this.contentAttrs, contentAttrs);
5969
+ updateAttrs(this.dom, this.editorAttrs, editorAttrs);
5970
+ });
5971
+ this.editorAttrs = editorAttrs;
5954
5972
  this.contentAttrs = contentAttrs;
5955
5973
  }
5956
5974
  showAnnouncements(trs) {
package/dist/index.js CHANGED
@@ -183,7 +183,7 @@ function scrollRectIntoView(dom, rect, side, center) {
183
183
  }
184
184
  }
185
185
  }
186
- class DOMSelection {
186
+ class DOMSelectionState {
187
187
  constructor() {
188
188
  this.anchorNode = null;
189
189
  this.anchorOffset = 0;
@@ -194,11 +194,14 @@ class DOMSelection {
194
194
  return this.anchorNode == domSel.anchorNode && this.anchorOffset == domSel.anchorOffset &&
195
195
  this.focusNode == domSel.focusNode && this.focusOffset == domSel.focusOffset;
196
196
  }
197
- set(domSel) {
198
- this.anchorNode = domSel.anchorNode;
199
- this.anchorOffset = domSel.anchorOffset;
200
- this.focusNode = domSel.focusNode;
201
- this.focusOffset = domSel.focusOffset;
197
+ setRange(range) {
198
+ this.set(range.anchorNode, range.anchorOffset, range.focusNode, range.focusOffset);
199
+ }
200
+ set(anchorNode, anchorOffset, focusNode, focusOffset) {
201
+ this.anchorNode = anchorNode;
202
+ this.anchorOffset = anchorOffset;
203
+ this.focusNode = focusNode;
204
+ this.focusOffset = focusOffset;
202
205
  }
203
206
  }
204
207
  let preventScrollSupported = null;
@@ -1939,9 +1942,10 @@ class DocView extends ContentView {
1939
1942
  // we don't mess it up when reading it back it
1940
1943
  this.impreciseAnchor = null;
1941
1944
  this.impreciseHead = null;
1945
+ this.forceSelection = false;
1942
1946
  // Used by the resize observer to ignore resizes that we caused
1943
1947
  // ourselves
1944
- this.lastUpdate = 0;
1948
+ this.lastUpdate = Date.now();
1945
1949
  this.setDOM(view.contentDOM);
1946
1950
  this.children = [new LineView];
1947
1951
  this.children[0].setParent(this);
@@ -1955,7 +1959,6 @@ class DocView extends ContentView {
1955
1959
  // position, if we know the editor is going to scroll that position
1956
1960
  // into view.
1957
1961
  update(update) {
1958
- this.lastUpdate = Date.now();
1959
1962
  let changedRanges = update.changedRanges;
1960
1963
  if (this.minWidth > 0 && changedRanges.length) {
1961
1964
  if (!changedRanges.every(({ fromA, toA }) => toA < this.minWidthFrom || fromA > this.minWidthTo)) {
@@ -1975,21 +1978,22 @@ class DocView extends ContentView {
1975
1978
  // getSelection than the one that it actually shows to the user.
1976
1979
  // This forces a selection update when lines are joined to work
1977
1980
  // around that. Issue #54
1978
- let forceSelection = (browser.ie || browser.chrome) && !this.compositionDeco.size && update &&
1979
- update.state.doc.lines != update.startState.doc.lines;
1981
+ if ((browser.ie || browser.chrome) && !this.compositionDeco.size && update &&
1982
+ update.state.doc.lines != update.startState.doc.lines)
1983
+ this.forceSelection = true;
1980
1984
  let prevDeco = this.decorations, deco = this.updateDeco();
1981
1985
  let decoDiff = findChangedDeco(prevDeco, deco, update.changes);
1982
1986
  changedRanges = ChangedRange.extendWithRanges(changedRanges, decoDiff);
1983
- let pointerSel = update.transactions.some(tr => tr.isUserEvent("select.pointer"));
1984
1987
  if (this.dirty == 0 /* Not */ && changedRanges.length == 0 &&
1985
1988
  !(update.flags & 4 /* Viewport */) &&
1986
1989
  update.state.selection.main.from >= this.view.viewport.from &&
1987
1990
  update.state.selection.main.to <= this.view.viewport.to) {
1988
- this.updateSelection(forceSelection, pointerSel);
1989
1991
  return false;
1990
1992
  }
1991
1993
  else {
1992
- this.updateInner(changedRanges, deco, update.startState.doc.length, forceSelection, pointerSel);
1994
+ this.updateInner(changedRanges, deco, update.startState.doc.length);
1995
+ if (update.transactions.length)
1996
+ this.lastUpdate = Date.now();
1993
1997
  return true;
1994
1998
  }
1995
1999
  }
@@ -1997,13 +2001,15 @@ class DocView extends ContentView {
1997
2001
  if (this.dirty) {
1998
2002
  this.view.observer.ignore(() => this.view.docView.sync());
1999
2003
  this.dirty = 0 /* Not */;
2004
+ this.updateSelection(true);
2000
2005
  }
2001
- if (sel)
2006
+ else {
2002
2007
  this.updateSelection();
2008
+ }
2003
2009
  }
2004
2010
  // Used both by update and checkLayout do perform the actual DOM
2005
2011
  // update
2006
- updateInner(changes, deco, oldLength, forceSelection = false, pointerSel = false) {
2012
+ updateInner(changes, deco, oldLength) {
2007
2013
  this.updateChildren(changes, deco, oldLength);
2008
2014
  let { observer } = this.view;
2009
2015
  observer.ignore(() => {
@@ -2021,8 +2027,7 @@ class DocView extends ContentView {
2021
2027
  this.sync(track);
2022
2028
  this.dirty = 0 /* Not */;
2023
2029
  if (track && (track.written || observer.selectionRange.focusNode != track.node))
2024
- forceSelection = true;
2025
- this.updateSelection(forceSelection, pointerSel);
2030
+ this.forceSelection = true;
2026
2031
  this.dom.style.height = "";
2027
2032
  });
2028
2033
  let gaps = [];
@@ -2108,10 +2113,14 @@ class DocView extends ContentView {
2108
2113
  this.replaceChildren(fromI, toI, content);
2109
2114
  }
2110
2115
  // Sync the DOM selection to this.state.selection
2111
- updateSelection(force = false, fromPointer = false) {
2116
+ updateSelection(mustRead = false, fromPointer = false) {
2117
+ if (mustRead)
2118
+ this.view.observer.readSelectionRange();
2112
2119
  if (!(fromPointer || this.mayControlSelection()) ||
2113
2120
  browser.ios && this.view.inputState.rapidCompositionStart)
2114
2121
  return;
2122
+ let force = this.forceSelection;
2123
+ this.forceSelection = false;
2115
2124
  let main = this.view.state.selection.main;
2116
2125
  // FIXME need to handle the case where the selection falls inside a block range
2117
2126
  let anchor = this.domAtPos(main.anchor);
@@ -5069,25 +5078,30 @@ class DOMObserver {
5069
5078
  this.onChange = onChange;
5070
5079
  this.onScrollChanged = onScrollChanged;
5071
5080
  this.active = false;
5072
- this.ignoreSelection = new DOMSelection;
5081
+ // The known selection. Kept in our own object, as opposed to just
5082
+ // directly accessing the selection because:
5083
+ // - Safari doesn't report the right selection in shadow DOM
5084
+ // - Reading from the selection forces a DOM layout
5085
+ // - This way, we can ignore selectionchange events if we have
5086
+ // already seen the 'new' selection
5087
+ this.selectionRange = new DOMSelectionState;
5088
+ // Set when a selection change is detected, cleared on flush
5089
+ this.selectionChanged = false;
5073
5090
  this.delayedFlush = -1;
5091
+ this.resizeTimeout = -1;
5074
5092
  this.queue = [];
5075
- this.lastFlush = 0;
5076
5093
  this.scrollTargets = [];
5077
5094
  this.intersection = null;
5078
5095
  this.resize = null;
5079
5096
  this.intersecting = false;
5080
5097
  this.gapIntersection = null;
5081
5098
  this.gaps = [];
5082
- // Used to work around a Safari Selection/shadow DOM bug (#414)
5083
- this._selectionRange = null;
5084
5099
  // Timeout for scheduling check of the parents that need scroll handlers
5085
5100
  this.parentCheck = -1;
5086
5101
  this.dom = view.contentDOM;
5087
5102
  this.observer = new MutationObserver(mutations => {
5088
5103
  for (let mut of mutations)
5089
5104
  this.queue.push(mut);
5090
- this._selectionRange = null;
5091
5105
  // IE11 will sometimes (on typing over a selection or
5092
5106
  // backspacing out a single character text node) call the
5093
5107
  // observer callback before actually updating the DOM.
@@ -5114,8 +5128,11 @@ class DOMObserver {
5114
5128
  this.onSelectionChange = this.onSelectionChange.bind(this);
5115
5129
  if (typeof ResizeObserver == "function") {
5116
5130
  this.resize = new ResizeObserver(() => {
5117
- if (this.view.docView.lastUpdate < Date.now() - 100)
5118
- this.view.requestMeasure();
5131
+ if (this.view.docView.lastUpdate < Date.now() - 75 && this.resizeTimeout < 0)
5132
+ this.resizeTimeout = setTimeout(() => {
5133
+ this.resizeTimeout = -1;
5134
+ this.view.requestMeasure();
5135
+ }, 50);
5119
5136
  });
5120
5137
  this.resize.observe(view.scrollDOM);
5121
5138
  }
@@ -5139,10 +5156,12 @@ class DOMObserver {
5139
5156
  }, {});
5140
5157
  }
5141
5158
  this.listenForScroll();
5159
+ this.readSelectionRange();
5160
+ this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
5142
5161
  }
5143
5162
  onScroll(e) {
5144
5163
  if (this.intersecting)
5145
- this.flush();
5164
+ this.flush(false);
5146
5165
  this.onScrollChanged(e);
5147
5166
  }
5148
5167
  updateGaps(gaps) {
@@ -5154,8 +5173,8 @@ class DOMObserver {
5154
5173
  }
5155
5174
  }
5156
5175
  onSelectionChange(event) {
5157
- if (this.lastFlush < Date.now() - 50)
5158
- this._selectionRange = null;
5176
+ if (!this.readSelectionRange())
5177
+ return;
5159
5178
  let { view } = this, sel = this.selectionRange;
5160
5179
  if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(view.dom, sel))
5161
5180
  return;
@@ -5170,24 +5189,22 @@ class DOMObserver {
5170
5189
  sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset))
5171
5190
  this.flushSoon();
5172
5191
  else
5173
- this.flush();
5174
- }
5175
- get selectionRange() {
5176
- if (!this._selectionRange) {
5177
- let { root } = this.view, sel = getSelection(root);
5178
- // The Selection object is broken in shadow roots in Safari. See
5179
- // https://github.com/codemirror/codemirror.next/issues/414
5180
- if (browser.safari && root.nodeType == 11 && deepActiveElement() == this.view.contentDOM)
5181
- sel = safariSelectionRangeHack(this.view) || sel;
5182
- this._selectionRange = sel;
5183
- }
5184
- return this._selectionRange;
5192
+ this.flush(false);
5193
+ }
5194
+ readSelectionRange() {
5195
+ let { root } = this.view, domSel = getSelection(root);
5196
+ // The Selection object is broken in shadow roots in Safari. See
5197
+ // https://github.com/codemirror/codemirror.next/issues/414
5198
+ let range = browser.safari && root.nodeType == 11 && deepActiveElement() == this.view.contentDOM &&
5199
+ safariSelectionRangeHack(this.view) || domSel;
5200
+ if (this.selectionRange.eq(range))
5201
+ return false;
5202
+ this.selectionRange.setRange(range);
5203
+ return this.selectionChanged = true;
5185
5204
  }
5186
5205
  setSelectionRange(anchor, head) {
5187
- var _a;
5188
- if (!((_a = this._selectionRange) === null || _a === void 0 ? void 0 : _a.type))
5189
- this._selectionRange = { anchorNode: anchor.node, anchorOffset: anchor.offset,
5190
- focusNode: head.node, focusOffset: head.offset };
5206
+ this.selectionRange.set(anchor.node, anchor.offset, head.node, head.offset);
5207
+ this.selectionChanged = false;
5191
5208
  }
5192
5209
  listenForScroll() {
5193
5210
  this.parentCheck = -1;
@@ -5234,7 +5251,6 @@ class DOMObserver {
5234
5251
  if (this.active)
5235
5252
  return;
5236
5253
  this.observer.observe(this.dom, observeOptions);
5237
- this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
5238
5254
  if (useCharData)
5239
5255
  this.dom.addEventListener("DOMCharacterDataModified", this.onCharData);
5240
5256
  this.active = true;
@@ -5244,18 +5260,14 @@ class DOMObserver {
5244
5260
  return;
5245
5261
  this.active = false;
5246
5262
  this.observer.disconnect();
5247
- this.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange);
5248
5263
  if (useCharData)
5249
5264
  this.dom.removeEventListener("DOMCharacterDataModified", this.onCharData);
5250
5265
  }
5251
- clearSelection() {
5252
- this.ignoreSelection.set(this.selectionRange);
5253
- }
5254
5266
  // Throw away any pending changes
5255
5267
  clear() {
5256
5268
  this.observer.takeRecords();
5257
5269
  this.queue.length = 0;
5258
- this.clearSelection();
5270
+ this.selectionChanged = false;
5259
5271
  }
5260
5272
  flushSoon() {
5261
5273
  if (this.delayedFlush < 0)
@@ -5292,24 +5304,24 @@ class DOMObserver {
5292
5304
  return { from, to, typeOver };
5293
5305
  }
5294
5306
  // Apply pending changes, if any
5295
- flush() {
5307
+ flush(readSelection = true) {
5308
+ if (readSelection)
5309
+ this.readSelectionRange();
5296
5310
  // Completely hold off flushing when pending keys are set—the code
5297
5311
  // managing those will make sure processRecords is called and the
5298
5312
  // view is resynchronized after
5299
5313
  if (this.delayedFlush >= 0 || this.view.inputState.pendingAndroidKey)
5300
5314
  return;
5301
- this.lastFlush = Date.now();
5302
5315
  let { from, to, typeOver } = this.processRecords();
5303
- let selection = this.selectionRange;
5304
- let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
5316
+ let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
5305
5317
  if (from < 0 && !newSel)
5306
5318
  return;
5319
+ this.selectionChanged = false;
5307
5320
  let startState = this.view.state;
5308
5321
  this.onChange(from, to, typeOver);
5309
5322
  // The view wasn't updated
5310
5323
  if (this.view.state == startState)
5311
5324
  this.view.docView.reset(newSel);
5312
- this.clearSelection();
5313
5325
  }
5314
5326
  readMutation(rec) {
5315
5327
  let cView = this.view.docView.nearest(rec.target);
@@ -5341,6 +5353,7 @@ class DOMObserver {
5341
5353
  dom.removeEventListener("scroll", this.onScroll);
5342
5354
  window.removeEventListener("scroll", this.onScroll);
5343
5355
  clearTimeout(this.parentCheck);
5356
+ clearTimeout(this.resizeTimeout);
5344
5357
  }
5345
5358
  }
5346
5359
  function findChild(cView, dom, dir) {
@@ -5353,6 +5366,7 @@ function findChild(cView, dom, dir) {
5353
5366
  }
5354
5367
  return null;
5355
5368
  }
5369
+ // Used to work around a Safari Selection/shadow DOM bug (#414)
5356
5370
  function safariSelectionRangeHack(view) {
5357
5371
  let found = null;
5358
5372
  // Because Safari (at least in 2018-2021) doesn't provide regular
@@ -5772,6 +5786,7 @@ class EditorView {
5772
5786
  this.mountStyles();
5773
5787
  this.updateAttrs();
5774
5788
  this.showAnnouncements(transactions);
5789
+ this.docView.updateSelection(redrawn, transactions.some(tr => tr.isUserEvent("select.pointer")));
5775
5790
  }
5776
5791
  finally {
5777
5792
  this.updateState = 0 /* Idle */;
@@ -5889,8 +5904,7 @@ class EditorView {
5889
5904
  this.inputState.update(update);
5890
5905
  }
5891
5906
  this.updateAttrs();
5892
- if (changed)
5893
- this.docView.update(update);
5907
+ let redrawn = changed > 0 && this.docView.update(update);
5894
5908
  for (let i = 0; i < measuring.length; i++)
5895
5909
  if (measured[i] != BadMeasure) {
5896
5910
  try {
@@ -5904,6 +5918,8 @@ class EditorView {
5904
5918
  this.docView.scrollIntoView(this.viewState.scrollTarget);
5905
5919
  this.viewState.scrollTarget = null;
5906
5920
  }
5921
+ if (changed)
5922
+ this.docView.updateSelection(redrawn);
5907
5923
  if (this.viewport.from == oldViewport.from && this.viewport.to == oldViewport.to && this.measureRequests.length == 0)
5908
5924
  break;
5909
5925
  }
@@ -5928,8 +5944,6 @@ class EditorView {
5928
5944
  let editorAttrs = combineAttrs(this.state.facet(editorAttributes), {
5929
5945
  class: "cm-editor" + (this.hasFocus ? " cm-focused " : " ") + this.themeClasses
5930
5946
  });
5931
- updateAttrs(this.dom, this.editorAttrs, editorAttrs);
5932
- this.editorAttrs = editorAttrs;
5933
5947
  let contentAttrs = {
5934
5948
  spellcheck: "false",
5935
5949
  autocorrect: "off",
@@ -5944,7 +5958,11 @@ class EditorView {
5944
5958
  if (this.state.readOnly)
5945
5959
  contentAttrs["aria-readonly"] = "true";
5946
5960
  combineAttrs(this.state.facet(contentAttributes), contentAttrs);
5947
- updateAttrs(this.contentDOM, this.contentAttrs, contentAttrs);
5961
+ this.observer.ignore(() => {
5962
+ updateAttrs(this.contentDOM, this.contentAttrs, contentAttrs);
5963
+ updateAttrs(this.dom, this.editorAttrs, editorAttrs);
5964
+ });
5965
+ this.editorAttrs = editorAttrs;
5948
5966
  this.contentAttrs = contentAttrs;
5949
5967
  }
5950
5968
  showAnnouncements(trs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "0.19.20",
3
+ "version": "0.19.21",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",