@codemirror/view 0.19.17 → 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,27 @@
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
+
7
+ ## 0.19.20 (2021-11-19)
8
+
9
+ ### Bug fixes
10
+
11
+ Run a measure cycle when the editor's size spontaneously changes.
12
+
13
+ ## 0.19.19 (2021-11-17)
14
+
15
+ ### Bug fixes
16
+
17
+ Fix a bug that caused the precedence of `editorAttributes` and `contentAttributes` to be inverted, making lower-precedence extensions override higher-precedence ones.
18
+
19
+ ## 0.19.18 (2021-11-16)
20
+
21
+ ### Bug fixes
22
+
23
+ Fix an issue where the editor wasn't aware it was line-wrapping with its own `lineWrapping` extension enabled.
24
+
1
25
  ## 0.19.17 (2021-11-16)
2
26
 
3
27
  ### 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;
@@ -1771,11 +1774,17 @@ class PluginInstance {
1771
1774
  }
1772
1775
  }
1773
1776
  PluginInstance.dummy = new PluginInstance(ViewPlugin.define(() => ({})));
1777
+ function combineFacetAttrs(values) {
1778
+ let result = {};
1779
+ for (let i = values.length - 1; i >= 0; i--)
1780
+ combineAttrs(values[i], result);
1781
+ return result;
1782
+ }
1774
1783
  const editorAttributes = state.Facet.define({
1775
- combine: values => values.reduce((a, b) => combineAttrs(b, a), {})
1784
+ combine: combineFacetAttrs
1776
1785
  });
1777
1786
  const contentAttributes = state.Facet.define({
1778
- combine: values => values.reduce((a, b) => combineAttrs(b, a), {})
1787
+ combine: combineFacetAttrs
1779
1788
  });
1780
1789
  // Provide decorations
1781
1790
  const decorations = state.Facet.define();
@@ -1937,6 +1946,10 @@ class DocView extends ContentView {
1937
1946
  // we don't mess it up when reading it back it
1938
1947
  this.impreciseAnchor = null;
1939
1948
  this.impreciseHead = null;
1949
+ this.forceSelection = false;
1950
+ // Used by the resize observer to ignore resizes that we caused
1951
+ // ourselves
1952
+ this.lastUpdate = Date.now();
1940
1953
  this.setDOM(view.contentDOM);
1941
1954
  this.children = [new LineView];
1942
1955
  this.children[0].setParent(this);
@@ -1969,21 +1982,22 @@ class DocView extends ContentView {
1969
1982
  // getSelection than the one that it actually shows to the user.
1970
1983
  // This forces a selection update when lines are joined to work
1971
1984
  // around that. Issue #54
1972
- let forceSelection = (browser.ie || browser.chrome) && !this.compositionDeco.size && update &&
1973
- 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;
1974
1988
  let prevDeco = this.decorations, deco = this.updateDeco();
1975
1989
  let decoDiff = findChangedDeco(prevDeco, deco, update.changes);
1976
1990
  changedRanges = ChangedRange.extendWithRanges(changedRanges, decoDiff);
1977
- let pointerSel = update.transactions.some(tr => tr.isUserEvent("select.pointer"));
1978
1991
  if (this.dirty == 0 /* Not */ && changedRanges.length == 0 &&
1979
1992
  !(update.flags & 4 /* Viewport */) &&
1980
1993
  update.state.selection.main.from >= this.view.viewport.from &&
1981
1994
  update.state.selection.main.to <= this.view.viewport.to) {
1982
- this.updateSelection(forceSelection, pointerSel);
1983
1995
  return false;
1984
1996
  }
1985
1997
  else {
1986
- 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();
1987
2001
  return true;
1988
2002
  }
1989
2003
  }
@@ -1991,13 +2005,15 @@ class DocView extends ContentView {
1991
2005
  if (this.dirty) {
1992
2006
  this.view.observer.ignore(() => this.view.docView.sync());
1993
2007
  this.dirty = 0 /* Not */;
2008
+ this.updateSelection(true);
1994
2009
  }
1995
- if (sel)
2010
+ else {
1996
2011
  this.updateSelection();
2012
+ }
1997
2013
  }
1998
2014
  // Used both by update and checkLayout do perform the actual DOM
1999
2015
  // update
2000
- updateInner(changes, deco, oldLength, forceSelection = false, pointerSel = false) {
2016
+ updateInner(changes, deco, oldLength) {
2001
2017
  this.updateChildren(changes, deco, oldLength);
2002
2018
  let { observer } = this.view;
2003
2019
  observer.ignore(() => {
@@ -2015,8 +2031,7 @@ class DocView extends ContentView {
2015
2031
  this.sync(track);
2016
2032
  this.dirty = 0 /* Not */;
2017
2033
  if (track && (track.written || observer.selectionRange.focusNode != track.node))
2018
- forceSelection = true;
2019
- this.updateSelection(forceSelection, pointerSel);
2034
+ this.forceSelection = true;
2020
2035
  this.dom.style.height = "";
2021
2036
  });
2022
2037
  let gaps = [];
@@ -2102,10 +2117,14 @@ class DocView extends ContentView {
2102
2117
  this.replaceChildren(fromI, toI, content);
2103
2118
  }
2104
2119
  // Sync the DOM selection to this.state.selection
2105
- updateSelection(force = false, fromPointer = false) {
2120
+ updateSelection(mustRead = false, fromPointer = false) {
2121
+ if (mustRead)
2122
+ this.view.observer.readSelectionRange();
2106
2123
  if (!(fromPointer || this.mayControlSelection()) ||
2107
2124
  browser.ios && this.view.inputState.rapidCompositionStart)
2108
2125
  return;
2126
+ let force = this.forceSelection;
2127
+ this.forceSelection = false;
2109
2128
  let main = this.view.state.selection.main;
2110
2129
  // FIXME need to handle the case where the selection falls inside a block range
2111
2130
  let anchor = this.domAtPos(main.anchor);
@@ -3742,7 +3761,7 @@ handlers.beforeinput = (view, event) => {
3742
3761
  }
3743
3762
  };
3744
3763
 
3745
- const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line"];
3764
+ const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line", "break-spaces"];
3746
3765
  class HeightOracle {
3747
3766
  constructor() {
3748
3767
  this.doc = text.Text.empty;
@@ -4476,6 +4495,7 @@ class ViewState {
4476
4495
  this.paddingTop = 0;
4477
4496
  this.paddingBottom = 0;
4478
4497
  this.contentWidth = 0;
4498
+ this.editorHeight = 0;
4479
4499
  this.heightOracle = new HeightOracle;
4480
4500
  // See VP.MaxDOMHeight
4481
4501
  this.scaler = IdScaler;
@@ -4575,6 +4595,10 @@ class ViewState {
4575
4595
  this.contentWidth = contentWidth;
4576
4596
  result |= 8 /* Geometry */;
4577
4597
  }
4598
+ if (this.editorHeight != docView.view.scrollDOM.clientHeight) {
4599
+ this.editorHeight = docView.view.scrollDOM.clientHeight;
4600
+ result |= 8 /* Geometry */;
4601
+ }
4578
4602
  if (dTop > 0 && dBottom > 0)
4579
4603
  bias = Math.max(dTop, dBottom);
4580
4604
  else if (dTop < 0 && dBottom < 0)
@@ -5060,24 +5084,30 @@ class DOMObserver {
5060
5084
  this.onChange = onChange;
5061
5085
  this.onScrollChanged = onScrollChanged;
5062
5086
  this.active = false;
5063
- 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;
5064
5096
  this.delayedFlush = -1;
5097
+ this.resizeTimeout = -1;
5065
5098
  this.queue = [];
5066
- this.lastFlush = 0;
5067
5099
  this.scrollTargets = [];
5068
5100
  this.intersection = null;
5101
+ this.resize = null;
5069
5102
  this.intersecting = false;
5070
5103
  this.gapIntersection = null;
5071
5104
  this.gaps = [];
5072
- // Used to work around a Safari Selection/shadow DOM bug (#414)
5073
- this._selectionRange = null;
5074
5105
  // Timeout for scheduling check of the parents that need scroll handlers
5075
5106
  this.parentCheck = -1;
5076
5107
  this.dom = view.contentDOM;
5077
5108
  this.observer = new MutationObserver(mutations => {
5078
5109
  for (let mut of mutations)
5079
5110
  this.queue.push(mut);
5080
- this._selectionRange = null;
5081
5111
  // IE11 will sometimes (on typing over a selection or
5082
5112
  // backspacing out a single character text node) call the
5083
5113
  // observer callback before actually updating the DOM.
@@ -5102,6 +5132,16 @@ class DOMObserver {
5102
5132
  this.flushSoon();
5103
5133
  };
5104
5134
  this.onSelectionChange = this.onSelectionChange.bind(this);
5135
+ if (typeof ResizeObserver == "function") {
5136
+ this.resize = new ResizeObserver(() => {
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);
5142
+ });
5143
+ this.resize.observe(view.scrollDOM);
5144
+ }
5105
5145
  this.start();
5106
5146
  this.onScroll = this.onScroll.bind(this);
5107
5147
  window.addEventListener("scroll", this.onScroll);
@@ -5122,10 +5162,12 @@ class DOMObserver {
5122
5162
  }, {});
5123
5163
  }
5124
5164
  this.listenForScroll();
5165
+ this.readSelectionRange();
5166
+ this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
5125
5167
  }
5126
5168
  onScroll(e) {
5127
5169
  if (this.intersecting)
5128
- this.flush();
5170
+ this.flush(false);
5129
5171
  this.onScrollChanged(e);
5130
5172
  }
5131
5173
  updateGaps(gaps) {
@@ -5137,8 +5179,8 @@ class DOMObserver {
5137
5179
  }
5138
5180
  }
5139
5181
  onSelectionChange(event) {
5140
- if (this.lastFlush < Date.now() - 50)
5141
- this._selectionRange = null;
5182
+ if (!this.readSelectionRange())
5183
+ return;
5142
5184
  let { view } = this, sel = this.selectionRange;
5143
5185
  if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(view.dom, sel))
5144
5186
  return;
@@ -5153,24 +5195,22 @@ class DOMObserver {
5153
5195
  sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset))
5154
5196
  this.flushSoon();
5155
5197
  else
5156
- this.flush();
5157
- }
5158
- get selectionRange() {
5159
- if (!this._selectionRange) {
5160
- let { root } = this.view, sel = getSelection(root);
5161
- // The Selection object is broken in shadow roots in Safari. See
5162
- // https://github.com/codemirror/codemirror.next/issues/414
5163
- if (browser.safari && root.nodeType == 11 && deepActiveElement() == this.view.contentDOM)
5164
- sel = safariSelectionRangeHack(this.view) || sel;
5165
- this._selectionRange = sel;
5166
- }
5167
- 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;
5168
5210
  }
5169
5211
  setSelectionRange(anchor, head) {
5170
- var _a;
5171
- if (!((_a = this._selectionRange) === null || _a === void 0 ? void 0 : _a.type))
5172
- this._selectionRange = { anchorNode: anchor.node, anchorOffset: anchor.offset,
5173
- focusNode: head.node, focusOffset: head.offset };
5212
+ this.selectionRange.set(anchor.node, anchor.offset, head.node, head.offset);
5213
+ this.selectionChanged = false;
5174
5214
  }
5175
5215
  listenForScroll() {
5176
5216
  this.parentCheck = -1;
@@ -5217,7 +5257,6 @@ class DOMObserver {
5217
5257
  if (this.active)
5218
5258
  return;
5219
5259
  this.observer.observe(this.dom, observeOptions);
5220
- this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
5221
5260
  if (useCharData)
5222
5261
  this.dom.addEventListener("DOMCharacterDataModified", this.onCharData);
5223
5262
  this.active = true;
@@ -5227,18 +5266,14 @@ class DOMObserver {
5227
5266
  return;
5228
5267
  this.active = false;
5229
5268
  this.observer.disconnect();
5230
- this.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange);
5231
5269
  if (useCharData)
5232
5270
  this.dom.removeEventListener("DOMCharacterDataModified", this.onCharData);
5233
5271
  }
5234
- clearSelection() {
5235
- this.ignoreSelection.set(this.selectionRange);
5236
- }
5237
5272
  // Throw away any pending changes
5238
5273
  clear() {
5239
5274
  this.observer.takeRecords();
5240
5275
  this.queue.length = 0;
5241
- this.clearSelection();
5276
+ this.selectionChanged = false;
5242
5277
  }
5243
5278
  flushSoon() {
5244
5279
  if (this.delayedFlush < 0)
@@ -5275,24 +5310,24 @@ class DOMObserver {
5275
5310
  return { from, to, typeOver };
5276
5311
  }
5277
5312
  // Apply pending changes, if any
5278
- flush() {
5313
+ flush(readSelection = true) {
5314
+ if (readSelection)
5315
+ this.readSelectionRange();
5279
5316
  // Completely hold off flushing when pending keys are set—the code
5280
5317
  // managing those will make sure processRecords is called and the
5281
5318
  // view is resynchronized after
5282
5319
  if (this.delayedFlush >= 0 || this.view.inputState.pendingAndroidKey)
5283
5320
  return;
5284
- this.lastFlush = Date.now();
5285
5321
  let { from, to, typeOver } = this.processRecords();
5286
- let selection = this.selectionRange;
5287
- let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
5322
+ let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
5288
5323
  if (from < 0 && !newSel)
5289
5324
  return;
5325
+ this.selectionChanged = false;
5290
5326
  let startState = this.view.state;
5291
5327
  this.onChange(from, to, typeOver);
5292
5328
  // The view wasn't updated
5293
5329
  if (this.view.state == startState)
5294
5330
  this.view.docView.reset(newSel);
5295
- this.clearSelection();
5296
5331
  }
5297
5332
  readMutation(rec) {
5298
5333
  let cView = this.view.docView.nearest(rec.target);
@@ -5315,15 +5350,16 @@ class DOMObserver {
5315
5350
  }
5316
5351
  }
5317
5352
  destroy() {
5353
+ var _a, _b, _c;
5318
5354
  this.stop();
5319
- if (this.intersection)
5320
- this.intersection.disconnect();
5321
- if (this.gapIntersection)
5322
- this.gapIntersection.disconnect();
5355
+ (_a = this.intersection) === null || _a === void 0 ? void 0 : _a.disconnect();
5356
+ (_b = this.gapIntersection) === null || _b === void 0 ? void 0 : _b.disconnect();
5357
+ (_c = this.resize) === null || _c === void 0 ? void 0 : _c.disconnect();
5323
5358
  for (let dom of this.scrollTargets)
5324
5359
  dom.removeEventListener("scroll", this.onScroll);
5325
5360
  window.removeEventListener("scroll", this.onScroll);
5326
5361
  clearTimeout(this.parentCheck);
5362
+ clearTimeout(this.resizeTimeout);
5327
5363
  }
5328
5364
  }
5329
5365
  function findChild(cView, dom, dir) {
@@ -5336,6 +5372,7 @@ function findChild(cView, dom, dir) {
5336
5372
  }
5337
5373
  return null;
5338
5374
  }
5375
+ // Used to work around a Safari Selection/shadow DOM bug (#414)
5339
5376
  function safariSelectionRangeHack(view) {
5340
5377
  let found = null;
5341
5378
  // Because Safari (at least in 2018-2021) doesn't provide regular
@@ -5755,6 +5792,7 @@ class EditorView {
5755
5792
  this.mountStyles();
5756
5793
  this.updateAttrs();
5757
5794
  this.showAnnouncements(transactions);
5795
+ this.docView.updateSelection(redrawn, transactions.some(tr => tr.isUserEvent("select.pointer")));
5758
5796
  }
5759
5797
  finally {
5760
5798
  this.updateState = 0 /* Idle */;
@@ -5832,7 +5870,7 @@ class EditorView {
5832
5870
  return;
5833
5871
  if (this.measureScheduled > -1)
5834
5872
  cancelAnimationFrame(this.measureScheduled);
5835
- this.measureScheduled = -1; // Prevent requestMeasure calls from scheduling another animation frame
5873
+ this.measureScheduled = 0; // Prevent requestMeasure calls from scheduling another animation frame
5836
5874
  if (flush)
5837
5875
  this.observer.flush();
5838
5876
  let updated = null;
@@ -5872,8 +5910,7 @@ class EditorView {
5872
5910
  this.inputState.update(update);
5873
5911
  }
5874
5912
  this.updateAttrs();
5875
- if (changed)
5876
- this.docView.update(update);
5913
+ let redrawn = changed > 0 && this.docView.update(update);
5877
5914
  for (let i = 0; i < measuring.length; i++)
5878
5915
  if (measured[i] != BadMeasure) {
5879
5916
  try {
@@ -5887,14 +5924,16 @@ class EditorView {
5887
5924
  this.docView.scrollIntoView(this.viewState.scrollTarget);
5888
5925
  this.viewState.scrollTarget = null;
5889
5926
  }
5927
+ if (changed)
5928
+ this.docView.updateSelection(redrawn);
5890
5929
  if (this.viewport.from == oldViewport.from && this.viewport.to == oldViewport.to && this.measureRequests.length == 0)
5891
5930
  break;
5892
5931
  }
5893
5932
  }
5894
5933
  finally {
5895
5934
  this.updateState = 0 /* Idle */;
5935
+ this.measureScheduled = -1;
5896
5936
  }
5897
- this.measureScheduled = -1;
5898
5937
  if (updated && !updated.empty)
5899
5938
  for (let listener of this.state.facet(updateListener))
5900
5939
  listener(updated);
@@ -5911,8 +5950,6 @@ class EditorView {
5911
5950
  let editorAttrs = combineAttrs(this.state.facet(editorAttributes), {
5912
5951
  class: "cm-editor" + (this.hasFocus ? " cm-focused " : " ") + this.themeClasses
5913
5952
  });
5914
- updateAttrs(this.dom, this.editorAttrs, editorAttrs);
5915
- this.editorAttrs = editorAttrs;
5916
5953
  let contentAttrs = {
5917
5954
  spellcheck: "false",
5918
5955
  autocorrect: "off",
@@ -5927,7 +5964,11 @@ class EditorView {
5927
5964
  if (this.state.readOnly)
5928
5965
  contentAttrs["aria-readonly"] = "true";
5929
5966
  combineAttrs(this.state.facet(contentAttributes), contentAttrs);
5930
- 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;
5931
5972
  this.contentAttrs = contentAttrs;
5932
5973
  }
5933
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;
@@ -1767,11 +1770,17 @@ class PluginInstance {
1767
1770
  }
1768
1771
  }
1769
1772
  PluginInstance.dummy = /*@__PURE__*/new PluginInstance(/*@__PURE__*/ViewPlugin.define(() => ({})));
1773
+ function combineFacetAttrs(values) {
1774
+ let result = {};
1775
+ for (let i = values.length - 1; i >= 0; i--)
1776
+ combineAttrs(values[i], result);
1777
+ return result;
1778
+ }
1770
1779
  const editorAttributes = /*@__PURE__*/Facet.define({
1771
- combine: values => values.reduce((a, b) => combineAttrs(b, a), {})
1780
+ combine: combineFacetAttrs
1772
1781
  });
1773
1782
  const contentAttributes = /*@__PURE__*/Facet.define({
1774
- combine: values => values.reduce((a, b) => combineAttrs(b, a), {})
1783
+ combine: combineFacetAttrs
1775
1784
  });
1776
1785
  // Provide decorations
1777
1786
  const decorations = /*@__PURE__*/Facet.define();
@@ -1933,6 +1942,10 @@ class DocView extends ContentView {
1933
1942
  // we don't mess it up when reading it back it
1934
1943
  this.impreciseAnchor = null;
1935
1944
  this.impreciseHead = null;
1945
+ this.forceSelection = false;
1946
+ // Used by the resize observer to ignore resizes that we caused
1947
+ // ourselves
1948
+ this.lastUpdate = Date.now();
1936
1949
  this.setDOM(view.contentDOM);
1937
1950
  this.children = [new LineView];
1938
1951
  this.children[0].setParent(this);
@@ -1965,21 +1978,22 @@ class DocView extends ContentView {
1965
1978
  // getSelection than the one that it actually shows to the user.
1966
1979
  // This forces a selection update when lines are joined to work
1967
1980
  // around that. Issue #54
1968
- let forceSelection = (browser.ie || browser.chrome) && !this.compositionDeco.size && update &&
1969
- 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;
1970
1984
  let prevDeco = this.decorations, deco = this.updateDeco();
1971
1985
  let decoDiff = findChangedDeco(prevDeco, deco, update.changes);
1972
1986
  changedRanges = ChangedRange.extendWithRanges(changedRanges, decoDiff);
1973
- let pointerSel = update.transactions.some(tr => tr.isUserEvent("select.pointer"));
1974
1987
  if (this.dirty == 0 /* Not */ && changedRanges.length == 0 &&
1975
1988
  !(update.flags & 4 /* Viewport */) &&
1976
1989
  update.state.selection.main.from >= this.view.viewport.from &&
1977
1990
  update.state.selection.main.to <= this.view.viewport.to) {
1978
- this.updateSelection(forceSelection, pointerSel);
1979
1991
  return false;
1980
1992
  }
1981
1993
  else {
1982
- 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();
1983
1997
  return true;
1984
1998
  }
1985
1999
  }
@@ -1987,13 +2001,15 @@ class DocView extends ContentView {
1987
2001
  if (this.dirty) {
1988
2002
  this.view.observer.ignore(() => this.view.docView.sync());
1989
2003
  this.dirty = 0 /* Not */;
2004
+ this.updateSelection(true);
1990
2005
  }
1991
- if (sel)
2006
+ else {
1992
2007
  this.updateSelection();
2008
+ }
1993
2009
  }
1994
2010
  // Used both by update and checkLayout do perform the actual DOM
1995
2011
  // update
1996
- updateInner(changes, deco, oldLength, forceSelection = false, pointerSel = false) {
2012
+ updateInner(changes, deco, oldLength) {
1997
2013
  this.updateChildren(changes, deco, oldLength);
1998
2014
  let { observer } = this.view;
1999
2015
  observer.ignore(() => {
@@ -2011,8 +2027,7 @@ class DocView extends ContentView {
2011
2027
  this.sync(track);
2012
2028
  this.dirty = 0 /* Not */;
2013
2029
  if (track && (track.written || observer.selectionRange.focusNode != track.node))
2014
- forceSelection = true;
2015
- this.updateSelection(forceSelection, pointerSel);
2030
+ this.forceSelection = true;
2016
2031
  this.dom.style.height = "";
2017
2032
  });
2018
2033
  let gaps = [];
@@ -2098,10 +2113,14 @@ class DocView extends ContentView {
2098
2113
  this.replaceChildren(fromI, toI, content);
2099
2114
  }
2100
2115
  // Sync the DOM selection to this.state.selection
2101
- updateSelection(force = false, fromPointer = false) {
2116
+ updateSelection(mustRead = false, fromPointer = false) {
2117
+ if (mustRead)
2118
+ this.view.observer.readSelectionRange();
2102
2119
  if (!(fromPointer || this.mayControlSelection()) ||
2103
2120
  browser.ios && this.view.inputState.rapidCompositionStart)
2104
2121
  return;
2122
+ let force = this.forceSelection;
2123
+ this.forceSelection = false;
2105
2124
  let main = this.view.state.selection.main;
2106
2125
  // FIXME need to handle the case where the selection falls inside a block range
2107
2126
  let anchor = this.domAtPos(main.anchor);
@@ -3737,7 +3756,7 @@ handlers.beforeinput = (view, event) => {
3737
3756
  }
3738
3757
  };
3739
3758
 
3740
- const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line"];
3759
+ const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line", "break-spaces"];
3741
3760
  class HeightOracle {
3742
3761
  constructor() {
3743
3762
  this.doc = Text.empty;
@@ -4470,6 +4489,7 @@ class ViewState {
4470
4489
  this.paddingTop = 0;
4471
4490
  this.paddingBottom = 0;
4472
4491
  this.contentWidth = 0;
4492
+ this.editorHeight = 0;
4473
4493
  this.heightOracle = new HeightOracle;
4474
4494
  // See VP.MaxDOMHeight
4475
4495
  this.scaler = IdScaler;
@@ -4569,6 +4589,10 @@ class ViewState {
4569
4589
  this.contentWidth = contentWidth;
4570
4590
  result |= 8 /* Geometry */;
4571
4591
  }
4592
+ if (this.editorHeight != docView.view.scrollDOM.clientHeight) {
4593
+ this.editorHeight = docView.view.scrollDOM.clientHeight;
4594
+ result |= 8 /* Geometry */;
4595
+ }
4572
4596
  if (dTop > 0 && dBottom > 0)
4573
4597
  bias = Math.max(dTop, dBottom);
4574
4598
  else if (dTop < 0 && dBottom < 0)
@@ -5054,24 +5078,30 @@ class DOMObserver {
5054
5078
  this.onChange = onChange;
5055
5079
  this.onScrollChanged = onScrollChanged;
5056
5080
  this.active = false;
5057
- 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;
5058
5090
  this.delayedFlush = -1;
5091
+ this.resizeTimeout = -1;
5059
5092
  this.queue = [];
5060
- this.lastFlush = 0;
5061
5093
  this.scrollTargets = [];
5062
5094
  this.intersection = null;
5095
+ this.resize = null;
5063
5096
  this.intersecting = false;
5064
5097
  this.gapIntersection = null;
5065
5098
  this.gaps = [];
5066
- // Used to work around a Safari Selection/shadow DOM bug (#414)
5067
- this._selectionRange = null;
5068
5099
  // Timeout for scheduling check of the parents that need scroll handlers
5069
5100
  this.parentCheck = -1;
5070
5101
  this.dom = view.contentDOM;
5071
5102
  this.observer = new MutationObserver(mutations => {
5072
5103
  for (let mut of mutations)
5073
5104
  this.queue.push(mut);
5074
- this._selectionRange = null;
5075
5105
  // IE11 will sometimes (on typing over a selection or
5076
5106
  // backspacing out a single character text node) call the
5077
5107
  // observer callback before actually updating the DOM.
@@ -5096,6 +5126,16 @@ class DOMObserver {
5096
5126
  this.flushSoon();
5097
5127
  };
5098
5128
  this.onSelectionChange = this.onSelectionChange.bind(this);
5129
+ if (typeof ResizeObserver == "function") {
5130
+ this.resize = new ResizeObserver(() => {
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);
5136
+ });
5137
+ this.resize.observe(view.scrollDOM);
5138
+ }
5099
5139
  this.start();
5100
5140
  this.onScroll = this.onScroll.bind(this);
5101
5141
  window.addEventListener("scroll", this.onScroll);
@@ -5116,10 +5156,12 @@ class DOMObserver {
5116
5156
  }, {});
5117
5157
  }
5118
5158
  this.listenForScroll();
5159
+ this.readSelectionRange();
5160
+ this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
5119
5161
  }
5120
5162
  onScroll(e) {
5121
5163
  if (this.intersecting)
5122
- this.flush();
5164
+ this.flush(false);
5123
5165
  this.onScrollChanged(e);
5124
5166
  }
5125
5167
  updateGaps(gaps) {
@@ -5131,8 +5173,8 @@ class DOMObserver {
5131
5173
  }
5132
5174
  }
5133
5175
  onSelectionChange(event) {
5134
- if (this.lastFlush < Date.now() - 50)
5135
- this._selectionRange = null;
5176
+ if (!this.readSelectionRange())
5177
+ return;
5136
5178
  let { view } = this, sel = this.selectionRange;
5137
5179
  if (view.state.facet(editable) ? view.root.activeElement != this.dom : !hasSelection(view.dom, sel))
5138
5180
  return;
@@ -5147,24 +5189,22 @@ class DOMObserver {
5147
5189
  sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset))
5148
5190
  this.flushSoon();
5149
5191
  else
5150
- this.flush();
5151
- }
5152
- get selectionRange() {
5153
- if (!this._selectionRange) {
5154
- let { root } = this.view, sel = getSelection(root);
5155
- // The Selection object is broken in shadow roots in Safari. See
5156
- // https://github.com/codemirror/codemirror.next/issues/414
5157
- if (browser.safari && root.nodeType == 11 && deepActiveElement() == this.view.contentDOM)
5158
- sel = safariSelectionRangeHack(this.view) || sel;
5159
- this._selectionRange = sel;
5160
- }
5161
- 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;
5162
5204
  }
5163
5205
  setSelectionRange(anchor, head) {
5164
- var _a;
5165
- if (!((_a = this._selectionRange) === null || _a === void 0 ? void 0 : _a.type))
5166
- this._selectionRange = { anchorNode: anchor.node, anchorOffset: anchor.offset,
5167
- focusNode: head.node, focusOffset: head.offset };
5206
+ this.selectionRange.set(anchor.node, anchor.offset, head.node, head.offset);
5207
+ this.selectionChanged = false;
5168
5208
  }
5169
5209
  listenForScroll() {
5170
5210
  this.parentCheck = -1;
@@ -5211,7 +5251,6 @@ class DOMObserver {
5211
5251
  if (this.active)
5212
5252
  return;
5213
5253
  this.observer.observe(this.dom, observeOptions);
5214
- this.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange);
5215
5254
  if (useCharData)
5216
5255
  this.dom.addEventListener("DOMCharacterDataModified", this.onCharData);
5217
5256
  this.active = true;
@@ -5221,18 +5260,14 @@ class DOMObserver {
5221
5260
  return;
5222
5261
  this.active = false;
5223
5262
  this.observer.disconnect();
5224
- this.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange);
5225
5263
  if (useCharData)
5226
5264
  this.dom.removeEventListener("DOMCharacterDataModified", this.onCharData);
5227
5265
  }
5228
- clearSelection() {
5229
- this.ignoreSelection.set(this.selectionRange);
5230
- }
5231
5266
  // Throw away any pending changes
5232
5267
  clear() {
5233
5268
  this.observer.takeRecords();
5234
5269
  this.queue.length = 0;
5235
- this.clearSelection();
5270
+ this.selectionChanged = false;
5236
5271
  }
5237
5272
  flushSoon() {
5238
5273
  if (this.delayedFlush < 0)
@@ -5269,24 +5304,24 @@ class DOMObserver {
5269
5304
  return { from, to, typeOver };
5270
5305
  }
5271
5306
  // Apply pending changes, if any
5272
- flush() {
5307
+ flush(readSelection = true) {
5308
+ if (readSelection)
5309
+ this.readSelectionRange();
5273
5310
  // Completely hold off flushing when pending keys are set—the code
5274
5311
  // managing those will make sure processRecords is called and the
5275
5312
  // view is resynchronized after
5276
5313
  if (this.delayedFlush >= 0 || this.view.inputState.pendingAndroidKey)
5277
5314
  return;
5278
- this.lastFlush = Date.now();
5279
5315
  let { from, to, typeOver } = this.processRecords();
5280
- let selection = this.selectionRange;
5281
- let newSel = !this.ignoreSelection.eq(selection) && hasSelection(this.dom, selection);
5316
+ let newSel = this.selectionChanged && hasSelection(this.dom, this.selectionRange);
5282
5317
  if (from < 0 && !newSel)
5283
5318
  return;
5319
+ this.selectionChanged = false;
5284
5320
  let startState = this.view.state;
5285
5321
  this.onChange(from, to, typeOver);
5286
5322
  // The view wasn't updated
5287
5323
  if (this.view.state == startState)
5288
5324
  this.view.docView.reset(newSel);
5289
- this.clearSelection();
5290
5325
  }
5291
5326
  readMutation(rec) {
5292
5327
  let cView = this.view.docView.nearest(rec.target);
@@ -5309,15 +5344,16 @@ class DOMObserver {
5309
5344
  }
5310
5345
  }
5311
5346
  destroy() {
5347
+ var _a, _b, _c;
5312
5348
  this.stop();
5313
- if (this.intersection)
5314
- this.intersection.disconnect();
5315
- if (this.gapIntersection)
5316
- this.gapIntersection.disconnect();
5349
+ (_a = this.intersection) === null || _a === void 0 ? void 0 : _a.disconnect();
5350
+ (_b = this.gapIntersection) === null || _b === void 0 ? void 0 : _b.disconnect();
5351
+ (_c = this.resize) === null || _c === void 0 ? void 0 : _c.disconnect();
5317
5352
  for (let dom of this.scrollTargets)
5318
5353
  dom.removeEventListener("scroll", this.onScroll);
5319
5354
  window.removeEventListener("scroll", this.onScroll);
5320
5355
  clearTimeout(this.parentCheck);
5356
+ clearTimeout(this.resizeTimeout);
5321
5357
  }
5322
5358
  }
5323
5359
  function findChild(cView, dom, dir) {
@@ -5330,6 +5366,7 @@ function findChild(cView, dom, dir) {
5330
5366
  }
5331
5367
  return null;
5332
5368
  }
5369
+ // Used to work around a Safari Selection/shadow DOM bug (#414)
5333
5370
  function safariSelectionRangeHack(view) {
5334
5371
  let found = null;
5335
5372
  // Because Safari (at least in 2018-2021) doesn't provide regular
@@ -5749,6 +5786,7 @@ class EditorView {
5749
5786
  this.mountStyles();
5750
5787
  this.updateAttrs();
5751
5788
  this.showAnnouncements(transactions);
5789
+ this.docView.updateSelection(redrawn, transactions.some(tr => tr.isUserEvent("select.pointer")));
5752
5790
  }
5753
5791
  finally {
5754
5792
  this.updateState = 0 /* Idle */;
@@ -5826,7 +5864,7 @@ class EditorView {
5826
5864
  return;
5827
5865
  if (this.measureScheduled > -1)
5828
5866
  cancelAnimationFrame(this.measureScheduled);
5829
- this.measureScheduled = -1; // Prevent requestMeasure calls from scheduling another animation frame
5867
+ this.measureScheduled = 0; // Prevent requestMeasure calls from scheduling another animation frame
5830
5868
  if (flush)
5831
5869
  this.observer.flush();
5832
5870
  let updated = null;
@@ -5866,8 +5904,7 @@ class EditorView {
5866
5904
  this.inputState.update(update);
5867
5905
  }
5868
5906
  this.updateAttrs();
5869
- if (changed)
5870
- this.docView.update(update);
5907
+ let redrawn = changed > 0 && this.docView.update(update);
5871
5908
  for (let i = 0; i < measuring.length; i++)
5872
5909
  if (measured[i] != BadMeasure) {
5873
5910
  try {
@@ -5881,14 +5918,16 @@ class EditorView {
5881
5918
  this.docView.scrollIntoView(this.viewState.scrollTarget);
5882
5919
  this.viewState.scrollTarget = null;
5883
5920
  }
5921
+ if (changed)
5922
+ this.docView.updateSelection(redrawn);
5884
5923
  if (this.viewport.from == oldViewport.from && this.viewport.to == oldViewport.to && this.measureRequests.length == 0)
5885
5924
  break;
5886
5925
  }
5887
5926
  }
5888
5927
  finally {
5889
5928
  this.updateState = 0 /* Idle */;
5929
+ this.measureScheduled = -1;
5890
5930
  }
5891
- this.measureScheduled = -1;
5892
5931
  if (updated && !updated.empty)
5893
5932
  for (let listener of this.state.facet(updateListener))
5894
5933
  listener(updated);
@@ -5905,8 +5944,6 @@ class EditorView {
5905
5944
  let editorAttrs = combineAttrs(this.state.facet(editorAttributes), {
5906
5945
  class: "cm-editor" + (this.hasFocus ? " cm-focused " : " ") + this.themeClasses
5907
5946
  });
5908
- updateAttrs(this.dom, this.editorAttrs, editorAttrs);
5909
- this.editorAttrs = editorAttrs;
5910
5947
  let contentAttrs = {
5911
5948
  spellcheck: "false",
5912
5949
  autocorrect: "off",
@@ -5921,7 +5958,11 @@ class EditorView {
5921
5958
  if (this.state.readOnly)
5922
5959
  contentAttrs["aria-readonly"] = "true";
5923
5960
  combineAttrs(this.state.facet(contentAttributes), contentAttrs);
5924
- 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;
5925
5966
  this.contentAttrs = contentAttrs;
5926
5967
  }
5927
5968
  showAnnouncements(trs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "0.19.17",
3
+ "version": "0.19.21",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",