@codemirror/view 6.17.0 → 6.18.0

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,25 @@
1
+ ## 6.18.0 (2023-09-05)
2
+
3
+ ### New features
4
+
5
+ The new `EditorView.scaleX` and `scaleY` properties return the CSS-transformed scale of the editor (or 1 when not scaled).
6
+
7
+ The editor now supports being scaled with CSS.
8
+
9
+ ## 6.17.1 (2023-08-31)
10
+
11
+ ### Bug fixes
12
+
13
+ Don't close the hover tooltip when the pointer moves over empty space caused by line breaks within the hovered range.
14
+
15
+ Fix a bug where on Chrome Android, if a virtual keyboard was slow to apply a change, the editor could end up dropping it.
16
+
17
+ Work around an issue where line-wise copy/cut didn't work in Firefox because the browser wasn't firing those events when nothing was selected.
18
+
19
+ Fix a crash triggered by the way some Android IME systems update the DOM.
20
+
21
+ Fix a bug that caused replacing a word by an emoji on Chrome Android to be treated as a backspace press.
22
+
1
23
  ## 6.17.0 (2023-08-28)
2
24
 
3
25
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -101,6 +101,7 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
101
101
  for (let cur = dom, stop = false; cur && !stop;) {
102
102
  if (cur.nodeType == 1) { // Element
103
103
  let bounding, top = cur == doc.body;
104
+ let scaleX = 1, scaleY = 1;
104
105
  if (top) {
105
106
  bounding = windowRect(win);
106
107
  }
@@ -112,9 +113,11 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
112
113
  continue;
113
114
  }
114
115
  let rect = cur.getBoundingClientRect();
116
+ scaleX = rect.width / cur.offsetWidth;
117
+ scaleY = rect.height / cur.offsetHeight;
115
118
  // Make sure scrollbar width isn't included in the rectangle
116
- bounding = { left: rect.left, right: rect.left + cur.clientWidth,
117
- top: rect.top, bottom: rect.top + cur.clientHeight };
119
+ bounding = { left: rect.left, right: rect.left + cur.clientWidth * scaleX,
120
+ top: rect.top, bottom: rect.top + cur.clientHeight * scaleY };
118
121
  }
119
122
  let moveX = 0, moveY = 0;
120
123
  if (y == "nearest") {
@@ -162,13 +165,13 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
162
165
  let movedX = 0, movedY = 0;
163
166
  if (moveY) {
164
167
  let start = cur.scrollTop;
165
- cur.scrollTop += moveY;
166
- movedY = cur.scrollTop - start;
168
+ cur.scrollTop += moveY / scaleY;
169
+ movedY = (cur.scrollTop - start) * scaleY;
167
170
  }
168
171
  if (moveX) {
169
172
  let start = cur.scrollLeft;
170
- cur.scrollLeft += moveX;
171
- movedX = cur.scrollLeft - start;
173
+ cur.scrollLeft += moveX / scaleX;
174
+ movedX = (cur.scrollLeft - start) * scaleX;
172
175
  }
173
176
  rect = { left: rect.left - movedX, top: rect.top - movedY,
174
177
  right: rect.right - movedX, bottom: rect.bottom - movedY };
@@ -2781,7 +2784,7 @@ class DocView extends ContentView {
2781
2784
  // messes with the scroll position during DOM mutation (though
2782
2785
  // no relayout is triggered and I cannot imagine how it can
2783
2786
  // recompute the scroll position without a layout)
2784
- this.dom.style.height = this.view.viewState.contentHeight + "px";
2787
+ this.dom.style.height = this.view.viewState.contentHeight / this.view.scaleY + "px";
2785
2788
  this.dom.style.flexBasis = this.minWidth ? this.minWidth + "px" : "";
2786
2789
  // Chrome will sometimes, when DOM mutations occur directly
2787
2790
  // around the selection, get confused and report a different
@@ -2853,7 +2856,7 @@ class DocView extends ContentView {
2853
2856
  }
2854
2857
  fixCompositionDOM(composition) {
2855
2858
  let fix = (dom, cView) => {
2856
- cView.flags |= 8 /* ViewFlag.Composition */;
2859
+ cView.flags |= 8 /* ViewFlag.Composition */ | (cView.children.some(c => c.flags & 7 /* ViewFlag.Dirty */) ? 1 /* ViewFlag.ChildDirty */ : 0);
2857
2860
  this.markedForComposition.add(cView);
2858
2861
  let prev = ContentView.get(dom);
2859
2862
  if (prev != cView) {
@@ -3119,7 +3122,7 @@ class DocView extends ContentView {
3119
3122
  let next = i == vs.viewports.length ? null : vs.viewports[i];
3120
3123
  let end = next ? next.from - 1 : this.length;
3121
3124
  if (end > pos) {
3122
- let height = vs.lineBlockAt(end).bottom - vs.lineBlockAt(pos).top;
3125
+ let height = (vs.lineBlockAt(end).bottom - vs.lineBlockAt(pos).top) / this.view.scaleY;
3123
3126
  deco.push(Decoration.replace({
3124
3127
  widget: new BlockGapWidget(height),
3125
3128
  block: true,
@@ -3184,7 +3187,7 @@ class BlockGapWidget extends WidgetType {
3184
3187
  }
3185
3188
  get estimatedHeight() { return this.height; }
3186
3189
  }
3187
- function findCompositionNode(view) {
3190
+ function findCompositionNode(view, dLen) {
3188
3191
  let sel = view.observer.selectionRange;
3189
3192
  let textNode = sel.focusNode && nearbyTextNode(sel.focusNode, sel.focusOffset, 0);
3190
3193
  if (!textNode)
@@ -3196,10 +3199,12 @@ function findCompositionNode(view) {
3196
3199
  to = from + cView.length;
3197
3200
  }
3198
3201
  else {
3202
+ let oldLen = Math.max(0, textNode.nodeValue.length - dLen);
3199
3203
  up: for (let offset = 0, node = textNode;;) {
3200
3204
  for (let sibling = node.previousSibling, cView; sibling; sibling = sibling.previousSibling) {
3201
3205
  if (cView = ContentView.get(sibling)) {
3202
- from = to = cView.posAtEnd + offset;
3206
+ to = cView.posAtEnd + offset;
3207
+ from = Math.max(0, to - oldLen);
3203
3208
  break up;
3204
3209
  }
3205
3210
  let reader = new DOMReader([], view.state);
@@ -3213,15 +3218,16 @@ function findCompositionNode(view) {
3213
3218
  return null;
3214
3219
  let parentView = ContentView.get(node);
3215
3220
  if (parentView) {
3216
- from = to = parentView.posAtStart + offset;
3221
+ from = parentView.posAtStart + offset;
3222
+ to = from + oldLen;
3217
3223
  break;
3218
3224
  }
3219
3225
  }
3220
3226
  }
3221
- return { from, to, node: textNode };
3227
+ return { from, to: to, node: textNode };
3222
3228
  }
3223
3229
  function findCompositionRange(view, changes) {
3224
- let found = findCompositionNode(view);
3230
+ let found = findCompositionNode(view, changes.newLength - changes.length);
3225
3231
  if (!found)
3226
3232
  return null;
3227
3233
  let { from: fromA, to: toA, node: textNode } = found;
@@ -3785,6 +3791,8 @@ class InputState {
3785
3791
  // issue where the composition vanishes when you press enter.
3786
3792
  if (browser.safari)
3787
3793
  view.contentDOM.addEventListener("input", () => null);
3794
+ if (browser.gecko)
3795
+ firefoxCopyCutHack(view.contentDOM.ownerDocument);
3788
3796
  }
3789
3797
  ensureHandlers(view, plugins) {
3790
3798
  var _a;
@@ -4490,6 +4498,18 @@ handlers.beforeinput = (view, event) => {
4490
4498
  }
4491
4499
  }
4492
4500
  };
4501
+ const appliedFirefoxHack = new Set;
4502
+ // In Firefox, when cut/copy handlers are added to the document, that
4503
+ // somehow avoids a bug where those events aren't fired when the
4504
+ // selection is empty. See https://github.com/codemirror/dev/issues/1082
4505
+ // and https://bugzilla.mozilla.org/show_bug.cgi?id=995961
4506
+ function firefoxCopyCutHack(doc) {
4507
+ if (!appliedFirefoxHack.has(doc)) {
4508
+ appliedFirefoxHack.add(doc);
4509
+ doc.addEventListener("copy", () => { });
4510
+ doc.addEventListener("cut", () => { });
4511
+ }
4512
+ }
4493
4513
 
4494
4514
  const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line", "break-spaces"];
4495
4515
  class HeightOracle {
@@ -5259,8 +5279,10 @@ class LineGap {
5259
5279
  }
5260
5280
  return true;
5261
5281
  }
5262
- draw(wrapping) {
5263
- return Decoration.replace({ widget: new LineGapWidget(this.size, wrapping) }).range(this.from, this.to);
5282
+ draw(viewState, wrapping) {
5283
+ return Decoration.replace({
5284
+ widget: new LineGapWidget(this.size * (wrapping ? viewState.scaleY : viewState.scaleX), wrapping)
5285
+ }).range(this.from, this.to);
5264
5286
  }
5265
5287
  }
5266
5288
  class LineGapWidget extends WidgetType {
@@ -5290,14 +5312,18 @@ class ViewState {
5290
5312
  // These are contentDOM-local coordinates
5291
5313
  this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
5292
5314
  this.inView = true;
5293
- this.paddingTop = 0;
5294
- this.paddingBottom = 0;
5295
- this.contentDOMWidth = 0;
5296
- this.contentDOMHeight = 0;
5297
- this.editorHeight = 0;
5298
- this.editorWidth = 0;
5299
- this.scrollTop = 0;
5315
+ this.paddingTop = 0; // Padding above the document, scaled
5316
+ this.paddingBottom = 0; // Padding below the document, scaled
5317
+ this.contentDOMWidth = 0; // contentDOM.getBoundingClientRect().width
5318
+ this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
5319
+ this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
5320
+ this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
5321
+ this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
5300
5322
  this.scrolledToBottom = true;
5323
+ // The CSS-transformation scale of the editor (transformed size /
5324
+ // concrete size)
5325
+ this.scaleX = 1;
5326
+ this.scaleY = 1;
5301
5327
  // The vertical position (document-relative) to which to anchor the
5302
5328
  // scroll position. -1 means anchor to the end of the document.
5303
5329
  this.scrollAnchorPos = 0;
@@ -5331,7 +5357,7 @@ class ViewState {
5331
5357
  this.updateViewportLines();
5332
5358
  this.updateForViewport();
5333
5359
  this.lineGaps = this.ensureLineGaps([]);
5334
- this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(false)));
5360
+ this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
5335
5361
  this.computeVisibleRanges();
5336
5362
  }
5337
5363
  updateForViewport() {
@@ -5403,8 +5429,23 @@ class ViewState {
5403
5429
  this.contentDOMHeight = domRect.height;
5404
5430
  this.mustMeasureContent = false;
5405
5431
  let result = 0, bias = 0;
5432
+ if (domRect.width && domRect.height) {
5433
+ let scaleX = domRect.width / dom.offsetWidth;
5434
+ let scaleY = domRect.height / dom.offsetHeight;
5435
+ if (scaleX > 0.995 && scaleX < 1.005)
5436
+ scaleX = 1;
5437
+ if (scaleY > 0.995 && scaleY < 1.005)
5438
+ scaleY = 1;
5439
+ if (this.scaleX != scaleX || this.scaleY != scaleY) {
5440
+ this.scaleX = scaleX;
5441
+ this.scaleY = scaleY;
5442
+ result |= 8 /* UpdateFlag.Geometry */;
5443
+ refresh = measureContent = true;
5444
+ }
5445
+ }
5406
5446
  // Vertical padding
5407
- let paddingTop = parseInt(style.paddingTop) || 0, paddingBottom = parseInt(style.paddingBottom) || 0;
5447
+ let paddingTop = (parseInt(style.paddingTop) || 0) * this.scaleY;
5448
+ let paddingBottom = (parseInt(style.paddingBottom) || 0) * this.scaleY;
5408
5449
  if (this.paddingTop != paddingTop || this.paddingBottom != paddingBottom) {
5409
5450
  this.paddingTop = paddingTop;
5410
5451
  this.paddingBottom = paddingBottom;
@@ -5416,9 +5457,10 @@ class ViewState {
5416
5457
  this.editorWidth = view.scrollDOM.clientWidth;
5417
5458
  result |= 8 /* UpdateFlag.Geometry */;
5418
5459
  }
5419
- if (this.scrollTop != view.scrollDOM.scrollTop) {
5460
+ let scrollTop = view.scrollDOM.scrollTop * this.scaleY;
5461
+ if (this.scrollTop != scrollTop) {
5420
5462
  this.scrollAnchorHeight = -1;
5421
- this.scrollTop = view.scrollDOM.scrollTop;
5463
+ this.scrollTop = scrollTop;
5422
5464
  }
5423
5465
  this.scrolledToBottom = isScrolledToBottom(view.scrollDOM);
5424
5466
  // Pixel viewport
@@ -5639,7 +5681,7 @@ class ViewState {
5639
5681
  updateLineGaps(gaps) {
5640
5682
  if (!LineGap.same(gaps, this.lineGaps)) {
5641
5683
  this.lineGaps = gaps;
5642
- this.lineGapDeco = Decoration.set(gaps.map(gap => gap.draw(this.heightOracle.lineWrapping)));
5684
+ this.lineGapDeco = Decoration.set(gaps.map(gap => gap.draw(this, this.heightOracle.lineWrapping)));
5643
5685
  }
5644
5686
  }
5645
5687
  computeVisibleRanges() {
@@ -6146,7 +6188,7 @@ function applyDOMChange(view, domChange) {
6146
6188
  change.insert.length == 1 && change.insert.lines == 2 &&
6147
6189
  dispatchKey(view.contentDOM, "Enter", 13)) ||
6148
6190
  ((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
6149
- lastKey == 8 && change.insert.length < change.to - change.from) &&
6191
+ lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
6150
6192
  dispatchKey(view.contentDOM, "Backspace", 8)) ||
6151
6193
  (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
6152
6194
  dispatchKey(view.contentDOM, "Delete", 46))))
@@ -6190,7 +6232,8 @@ function applyDefaultInsert(view, change, newSel) {
6190
6232
  if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
6191
6233
  change.to <= sel.to && change.to >= sel.to - 10) {
6192
6234
  let replaced = view.state.sliceDoc(change.from, change.to);
6193
- let composition = findCompositionNode(view) || view.state.doc.lineAt(sel.head);
6235
+ let composition = findCompositionNode(view, change.insert.length - (change.to - change.from)) ||
6236
+ view.state.doc.lineAt(sel.head);
6194
6237
  let offset = sel.to - change.to, size = sel.to - sel.from;
6195
6238
  tr = startState.changeByRange(range => {
6196
6239
  if (range.from == sel.from && range.to == sel.to)
@@ -7040,13 +7083,18 @@ class EditorView {
7040
7083
  return;
7041
7084
  if (this.measureScheduled > -1)
7042
7085
  this.win.cancelAnimationFrame(this.measureScheduled);
7086
+ if (this.observer.delayedAndroidKey) {
7087
+ this.measureScheduled = -1;
7088
+ this.requestMeasure();
7089
+ return;
7090
+ }
7043
7091
  this.measureScheduled = 0; // Prevent requestMeasure calls from scheduling another animation frame
7044
7092
  if (flush)
7045
7093
  this.observer.forceFlush();
7046
7094
  let updated = null;
7047
- let sDOM = this.scrollDOM, { scrollTop } = sDOM;
7095
+ let sDOM = this.scrollDOM, scrollTop = sDOM.scrollTop * this.scaleY;
7048
7096
  let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
7049
- if (scrollTop != this.viewState.scrollTop)
7097
+ if (Math.abs(scrollTop - this.viewState.scrollTop) > 1)
7050
7098
  scrollAnchorHeight = -1;
7051
7099
  this.viewState.scrollAnchorHeight = -1;
7052
7100
  try {
@@ -7123,7 +7171,8 @@ class EditorView {
7123
7171
  this.viewState.lineBlockAt(scrollAnchorPos).top;
7124
7172
  let diff = newAnchorHeight - scrollAnchorHeight;
7125
7173
  if (diff > 1 || diff < -1) {
7126
- scrollTop = sDOM.scrollTop = scrollTop + diff;
7174
+ scrollTop = scrollTop + diff;
7175
+ sDOM.scrollTop = scrollTop / this.scaleY;
7127
7176
  scrollAnchorHeight = -1;
7128
7177
  continue;
7129
7178
  }
@@ -7250,6 +7299,16 @@ class EditorView {
7250
7299
  return { top: this.viewState.paddingTop, bottom: this.viewState.paddingBottom };
7251
7300
  }
7252
7301
  /**
7302
+ If the editor is transformed with CSS, this provides the scale
7303
+ along the X axis. Otherwise, it will just be 1. Note that
7304
+ transforms other than translation and scaling are not supported.
7305
+ */
7306
+ get scaleX() { return this.viewState.scaleX; }
7307
+ /**
7308
+ Provide the CSS transformed scale along the Y axis.
7309
+ */
7310
+ get scaleY() { return this.viewState.scaleY; }
7311
+ /**
7253
7312
  Find the text line or block widget at the given vertical
7254
7313
  position (which is interpreted as relative to the [top of the
7255
7314
  document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop)).
@@ -8040,8 +8099,8 @@ class RectangleMarker {
8040
8099
  }
8041
8100
  function getBase(view) {
8042
8101
  let rect = view.scrollDOM.getBoundingClientRect();
8043
- let left = view.textDirection == exports.Direction.LTR ? rect.left : rect.right - view.scrollDOM.clientWidth;
8044
- return { left: left - view.scrollDOM.scrollLeft, top: rect.top - view.scrollDOM.scrollTop };
8102
+ let left = view.textDirection == exports.Direction.LTR ? rect.left : rect.right - view.scrollDOM.clientWidth * view.scaleX;
8103
+ return { left: left - view.scrollDOM.scrollLeft * view.scaleX, top: rect.top - view.scrollDOM.scrollTop * view.scaleY };
8045
8104
  }
8046
8105
  function wrappedLine(view, pos, inside) {
8047
8106
  let range = state.EditorSelection.cursor(pos);
@@ -8143,6 +8202,8 @@ class LayerView {
8143
8202
  this.view = view;
8144
8203
  this.layer = layer;
8145
8204
  this.drawn = [];
8205
+ this.scaleX = 1;
8206
+ this.scaleY = 1;
8146
8207
  this.measureReq = { read: this.measure.bind(this), write: this.draw.bind(this) };
8147
8208
  this.dom = view.scrollDOM.appendChild(document.createElement("div"));
8148
8209
  this.dom.classList.add("cm-layer");
@@ -8150,6 +8211,7 @@ class LayerView {
8150
8211
  this.dom.classList.add("cm-layer-above");
8151
8212
  if (layer.class)
8152
8213
  this.dom.classList.add(layer.class);
8214
+ this.scale();
8153
8215
  this.dom.setAttribute("aria-hidden", "true");
8154
8216
  this.setOrder(view.state);
8155
8217
  view.requestMeasure(this.measureReq);
@@ -8159,8 +8221,10 @@ class LayerView {
8159
8221
  update(update) {
8160
8222
  if (update.startState.facet(layerOrder) != update.state.facet(layerOrder))
8161
8223
  this.setOrder(update.state);
8162
- if (this.layer.update(update, this.dom) || update.geometryChanged)
8224
+ if (this.layer.update(update, this.dom) || update.geometryChanged) {
8225
+ this.scale();
8163
8226
  update.view.requestMeasure(this.measureReq);
8227
+ }
8164
8228
  }
8165
8229
  setOrder(state) {
8166
8230
  let pos = 0, order = state.facet(layerOrder);
@@ -8171,6 +8235,14 @@ class LayerView {
8171
8235
  measure() {
8172
8236
  return this.layer.markers(this.view);
8173
8237
  }
8238
+ scale() {
8239
+ let { scaleX, scaleY } = this.view;
8240
+ if (scaleX != this.scaleX || scaleY != this.scaleY) {
8241
+ this.scaleX = scaleX;
8242
+ this.scaleY = scaleY;
8243
+ this.dom.style.transform = `scale(${1 / scaleX}, ${1 / scaleY})`;
8244
+ }
8245
+ }
8174
8246
  draw(markers) {
8175
8247
  if (markers.length != this.drawn.length || markers.some((p, i) => !sameMarker(p, this.drawn[i]))) {
8176
8248
  let old = this.dom.firstChild, oldI = 0;
@@ -8340,23 +8412,25 @@ const drawDropCursor = ViewPlugin.fromClass(class {
8340
8412
  }
8341
8413
  }
8342
8414
  readPos() {
8343
- let pos = this.view.state.field(dropCursorPos);
8344
- let rect = pos != null && this.view.coordsAtPos(pos);
8415
+ let { view } = this;
8416
+ let pos = view.state.field(dropCursorPos);
8417
+ let rect = pos != null && view.coordsAtPos(pos);
8345
8418
  if (!rect)
8346
8419
  return null;
8347
- let outer = this.view.scrollDOM.getBoundingClientRect();
8420
+ let outer = view.scrollDOM.getBoundingClientRect();
8348
8421
  return {
8349
- left: rect.left - outer.left + this.view.scrollDOM.scrollLeft,
8350
- top: rect.top - outer.top + this.view.scrollDOM.scrollTop,
8422
+ left: rect.left - outer.left + view.scrollDOM.scrollLeft * view.scaleX,
8423
+ top: rect.top - outer.top + view.scrollDOM.scrollTop * view.scaleY,
8351
8424
  height: rect.bottom - rect.top
8352
8425
  };
8353
8426
  }
8354
8427
  drawCursor(pos) {
8355
8428
  if (this.cursor) {
8429
+ let { scaleX, scaleY } = this.view;
8356
8430
  if (pos) {
8357
- this.cursor.style.left = pos.left + "px";
8358
- this.cursor.style.top = pos.top + "px";
8359
- this.cursor.style.height = pos.height + "px";
8431
+ this.cursor.style.left = pos.left / scaleX + "px";
8432
+ this.cursor.style.top = pos.top / scaleY + "px";
8433
+ this.cursor.style.height = pos.height / scaleY + "px";
8360
8434
  }
8361
8435
  else {
8362
8436
  this.cursor.style.left = "-100000px";
@@ -8598,7 +8672,9 @@ function specialCharPlugin() {
8598
8672
  if (code == 9) {
8599
8673
  let line = doc.lineAt(pos);
8600
8674
  let size = view.state.tabSize, col = state.countColumn(line.text, size, pos - line.from);
8601
- return Decoration.replace({ widget: new TabWidget((size - (col % size)) * this.view.defaultCharacterWidth) });
8675
+ return Decoration.replace({
8676
+ widget: new TabWidget((size - (col % size)) * this.view.defaultCharacterWidth / this.view.scaleX)
8677
+ });
8602
8678
  }
8603
8679
  return this.decorationCache[code] ||
8604
8680
  (this.decorationCache[code] = Decoration.replace({ widget: new SpecialCharWidget(conf, code) }));
@@ -8675,7 +8751,8 @@ const plugin = ViewPlugin.fromClass(class {
8675
8751
  }
8676
8752
  update(update) {
8677
8753
  let { view } = update;
8678
- let height = view.viewState.editorHeight - view.defaultLineHeight - view.documentPadding.top - 0.5;
8754
+ let height = view.viewState.editorHeight * view.scaleY -
8755
+ view.defaultLineHeight - view.documentPadding.top - 0.5;
8679
8756
  if (height >= 0 && height != this.height) {
8680
8757
  this.height = height;
8681
8758
  this.attrs = { style: `padding-bottom: ${height}px` };
@@ -8972,6 +9049,7 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
8972
9049
  constructor(view) {
8973
9050
  this.view = view;
8974
9051
  this.inView = true;
9052
+ this.madeAbsolute = false;
8975
9053
  this.lastTransaction = 0;
8976
9054
  this.measureTimeout = -1;
8977
9055
  let config = view.state.facet(tooltipConfig);
@@ -9023,7 +9101,7 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
9023
9101
  this.observeIntersection();
9024
9102
  let shouldMeasure = updated || update.geometryChanged;
9025
9103
  let newConfig = update.state.facet(tooltipConfig);
9026
- if (newConfig.position != this.position) {
9104
+ if (newConfig.position != this.position && !this.madeAbsolute) {
9027
9105
  this.position = newConfig.position;
9028
9106
  for (let t of this.manager.tooltipViews)
9029
9107
  t.dom.style.position = this.position;
@@ -9071,6 +9149,27 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
9071
9149
  }
9072
9150
  readMeasure() {
9073
9151
  let editor = this.view.dom.getBoundingClientRect();
9152
+ let scaleX = 1, scaleY = 1, makeAbsolute = false;
9153
+ if (this.position == "fixed") {
9154
+ let views = this.manager.tooltipViews;
9155
+ // When the dialog's offset parent isn't the body, we are
9156
+ // probably in a transformed container, and should use absolute
9157
+ // positioning instead, since fixed positioning inside a
9158
+ // transform works in a very broken way.
9159
+ makeAbsolute = views.length > 0 && views[0].dom.offsetParent != this.container.ownerDocument.body;
9160
+ }
9161
+ if (makeAbsolute || this.position == "absolute") {
9162
+ if (this.parent) {
9163
+ let rect = this.parent.getBoundingClientRect();
9164
+ if (rect.width && rect.height) {
9165
+ scaleX = rect.width / this.parent.offsetWidth;
9166
+ scaleY = rect.height / this.parent.offsetHeight;
9167
+ }
9168
+ }
9169
+ else {
9170
+ ({ scaleX, scaleY } = this.view.viewState);
9171
+ }
9172
+ }
9074
9173
  return {
9075
9174
  editor,
9076
9175
  parent: this.parent ? this.container.getBoundingClientRect() : editor,
@@ -9080,11 +9179,18 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
9080
9179
  }),
9081
9180
  size: this.manager.tooltipViews.map(({ dom }) => dom.getBoundingClientRect()),
9082
9181
  space: this.view.state.facet(tooltipConfig).tooltipSpace(this.view),
9182
+ scaleX, scaleY, makeAbsolute
9083
9183
  };
9084
9184
  }
9085
9185
  writeMeasure(measured) {
9086
9186
  var _a;
9087
- let { editor, space } = measured;
9187
+ if (measured.makeAbsolute) {
9188
+ this.madeAbsolute = true;
9189
+ this.position = "absolute";
9190
+ for (let t of this.manager.tooltipViews)
9191
+ t.dom.style.position = "absolute";
9192
+ }
9193
+ let { editor, space, scaleX, scaleY } = measured;
9088
9194
  let others = [];
9089
9195
  for (let i = 0; i < this.manager.tooltips.length; i++) {
9090
9196
  let tooltip = this.manager.tooltips[i], tView = this.manager.tooltipViews[i], { dom } = tView;
@@ -9117,7 +9223,7 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
9117
9223
  continue;
9118
9224
  }
9119
9225
  knownHeight.set(tView, height);
9120
- dom.style.height = (height = spaceVert) + "px";
9226
+ dom.style.height = (height = spaceVert) / scaleY + "px";
9121
9227
  }
9122
9228
  else if (dom.style.height) {
9123
9229
  dom.style.height = "";
@@ -9129,15 +9235,17 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
9129
9235
  if (r.left < right && r.right > left && r.top < top + height && r.bottom > top)
9130
9236
  top = above ? r.top - height - 2 - arrowHeight : r.bottom + arrowHeight + 2;
9131
9237
  if (this.position == "absolute") {
9132
- dom.style.top = (top - measured.parent.top) + "px";
9133
- dom.style.left = (left - measured.parent.left) + "px";
9238
+ dom.style.top = (top - measured.parent.top) / scaleY + "px";
9239
+ dom.style.left = (left - measured.parent.left) / scaleX + "px";
9134
9240
  }
9135
9241
  else {
9136
- dom.style.top = top + "px";
9137
- dom.style.left = left + "px";
9242
+ dom.style.top = top / scaleY + "px";
9243
+ dom.style.left = left / scaleX + "px";
9244
+ }
9245
+ if (arrow) {
9246
+ let arrowLeft = pos.left + (ltr ? offset.x : -offset.x) - (left + 14 /* Arrow.Offset */ - 7 /* Arrow.Size */);
9247
+ arrow.style.left = arrowLeft / scaleX + "px";
9138
9248
  }
9139
- if (arrow)
9140
- arrow.style.left = `${pos.left + (ltr ? offset.x : -offset.x) - (left + 14 /* Arrow.Offset */ - 7 /* Arrow.Size */)}px`;
9141
9249
  if (tView.overlap !== true)
9142
9250
  others.push({ left, top, right, bottom: top + height });
9143
9251
  dom.classList.toggle("cm-tooltip-above", above);
@@ -9370,7 +9478,7 @@ class HoverPlugin {
9370
9478
  if (tooltip && !isInTooltip(this.lastMove.target) || this.pending) {
9371
9479
  let { pos } = tooltip || this.pending, end = (_a = tooltip === null || tooltip === void 0 ? void 0 : tooltip.end) !== null && _a !== void 0 ? _a : pos;
9372
9480
  if ((pos == end ? this.view.posAtCoords(this.lastMove) != pos
9373
- : !isOverRange(this.view, pos, end, event.clientX, event.clientY, 6 /* Hover.MaxDist */))) {
9481
+ : !isOverRange(this.view, pos, end, event.clientX, event.clientY))) {
9374
9482
  this.view.dispatch({ effects: this.setHover.of(null) });
9375
9483
  this.pending = null;
9376
9484
  }
@@ -9395,19 +9503,12 @@ function isInTooltip(elt) {
9395
9503
  return false;
9396
9504
  }
9397
9505
  function isOverRange(view, from, to, x, y, margin) {
9398
- let range = document.createRange();
9399
- let fromDOM = view.domAtPos(from), toDOM = view.domAtPos(to);
9400
- range.setEnd(toDOM.node, toDOM.offset);
9401
- range.setStart(fromDOM.node, fromDOM.offset);
9402
- let rects = range.getClientRects();
9403
- range.detach();
9404
- for (let i = 0; i < rects.length; i++) {
9405
- let rect = rects[i];
9406
- let dist = Math.max(rect.top - y, y - rect.bottom, rect.left - x, x - rect.right);
9407
- if (dist <= margin)
9408
- return true;
9409
- }
9410
- return false;
9506
+ let rect = view.scrollDOM.getBoundingClientRect();
9507
+ let docBottom = view.documentTop + view.documentPadding.top + view.contentHeight;
9508
+ if (rect.left > x || rect.right < x || rect.top > y || Math.min(rect.bottom, docBottom) < y)
9509
+ return false;
9510
+ let pos = view.posAtCoords({ x, y }, false);
9511
+ return pos >= from && pos <= to;
9411
9512
  }
9412
9513
  /**
9413
9514
  Set up a hover tooltip, which shows up when the pointer hovers
@@ -9746,7 +9847,7 @@ const gutterView = ViewPlugin.fromClass(class {
9746
9847
  this.dom = document.createElement("div");
9747
9848
  this.dom.className = "cm-gutters";
9748
9849
  this.dom.setAttribute("aria-hidden", "true");
9749
- this.dom.style.minHeight = this.view.contentHeight + "px";
9850
+ this.dom.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px";
9750
9851
  this.gutters = view.state.facet(activeGutters).map(conf => new SingleGutterView(view, conf));
9751
9852
  for (let gutter of this.gutters)
9752
9853
  this.dom.appendChild(gutter.dom);
@@ -9856,7 +9957,9 @@ const gutterView = ViewPlugin.fromClass(class {
9856
9957
  let value = view.plugin(plugin);
9857
9958
  if (!value || value.gutters.length == 0 || !value.fixed)
9858
9959
  return null;
9859
- return view.textDirection == exports.Direction.LTR ? { left: value.dom.offsetWidth } : { right: value.dom.offsetWidth };
9960
+ return view.textDirection == exports.Direction.LTR
9961
+ ? { left: value.dom.offsetWidth * view.scaleX }
9962
+ : { right: value.dom.offsetWidth * view.scaleX };
9860
9963
  })
9861
9964
  });
9862
9965
  function asArray(val) { return (Array.isArray(val) ? val : [val]); }
@@ -9973,10 +10076,12 @@ class GutterElement {
9973
10076
  this.update(view, height, above, markers);
9974
10077
  }
9975
10078
  update(view, height, above, markers) {
9976
- if (this.height != height)
9977
- this.dom.style.height = (this.height = height) + "px";
10079
+ if (this.height != height) {
10080
+ this.height = height;
10081
+ this.dom.style.height = height / view.scaleY + "px";
10082
+ }
9978
10083
  if (this.above != above)
9979
- this.dom.style.marginTop = (this.above = above) ? above + "px" : "";
10084
+ this.dom.style.marginTop = (this.above = above) ? above / view.scaleY + "px" : "";
9980
10085
  if (!sameMarkers(this.markers, markers))
9981
10086
  this.setMarkers(view, markers);
9982
10087
  }
package/dist/index.d.cts CHANGED
@@ -802,6 +802,16 @@ declare class EditorView {
802
802
  bottom: number;
803
803
  };
804
804
  /**
805
+ If the editor is transformed with CSS, this provides the scale
806
+ along the X axis. Otherwise, it will just be 1. Note that
807
+ transforms other than translation and scaling are not supported.
808
+ */
809
+ get scaleX(): number;
810
+ /**
811
+ Provide the CSS transformed scale along the Y axis.
812
+ */
813
+ get scaleY(): number;
814
+ /**
805
815
  Find the text line or block widget at the given vertical
806
816
  position (which is interpreted as relative to the [top of the
807
817
  document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop)).
@@ -1653,6 +1663,9 @@ declare function tooltips(config?: {
1653
1663
  On iOS, which at the time of writing still doesn't properly
1654
1664
  support fixed positioning, the library always uses absolute
1655
1665
  positioning.
1666
+
1667
+ If the tooltip parent element sits in a transformed element, the
1668
+ library also falls back to absolute positioning.
1656
1669
  */
1657
1670
  position?: "fixed" | "absolute";
1658
1671
  /**
package/dist/index.d.ts CHANGED
@@ -802,6 +802,16 @@ declare class EditorView {
802
802
  bottom: number;
803
803
  };
804
804
  /**
805
+ If the editor is transformed with CSS, this provides the scale
806
+ along the X axis. Otherwise, it will just be 1. Note that
807
+ transforms other than translation and scaling are not supported.
808
+ */
809
+ get scaleX(): number;
810
+ /**
811
+ Provide the CSS transformed scale along the Y axis.
812
+ */
813
+ get scaleY(): number;
814
+ /**
805
815
  Find the text line or block widget at the given vertical
806
816
  position (which is interpreted as relative to the [top of the
807
817
  document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop)).
@@ -1653,6 +1663,9 @@ declare function tooltips(config?: {
1653
1663
  On iOS, which at the time of writing still doesn't properly
1654
1664
  support fixed positioning, the library always uses absolute
1655
1665
  positioning.
1666
+
1667
+ If the tooltip parent element sits in a transformed element, the
1668
+ library also falls back to absolute positioning.
1656
1669
  */
1657
1670
  position?: "fixed" | "absolute";
1658
1671
  /**
package/dist/index.js CHANGED
@@ -99,6 +99,7 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
99
99
  for (let cur = dom, stop = false; cur && !stop;) {
100
100
  if (cur.nodeType == 1) { // Element
101
101
  let bounding, top = cur == doc.body;
102
+ let scaleX = 1, scaleY = 1;
102
103
  if (top) {
103
104
  bounding = windowRect(win);
104
105
  }
@@ -110,9 +111,11 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
110
111
  continue;
111
112
  }
112
113
  let rect = cur.getBoundingClientRect();
114
+ scaleX = rect.width / cur.offsetWidth;
115
+ scaleY = rect.height / cur.offsetHeight;
113
116
  // Make sure scrollbar width isn't included in the rectangle
114
- bounding = { left: rect.left, right: rect.left + cur.clientWidth,
115
- top: rect.top, bottom: rect.top + cur.clientHeight };
117
+ bounding = { left: rect.left, right: rect.left + cur.clientWidth * scaleX,
118
+ top: rect.top, bottom: rect.top + cur.clientHeight * scaleY };
116
119
  }
117
120
  let moveX = 0, moveY = 0;
118
121
  if (y == "nearest") {
@@ -160,13 +163,13 @@ function scrollRectIntoView(dom, rect, side, x, y, xMargin, yMargin, ltr) {
160
163
  let movedX = 0, movedY = 0;
161
164
  if (moveY) {
162
165
  let start = cur.scrollTop;
163
- cur.scrollTop += moveY;
164
- movedY = cur.scrollTop - start;
166
+ cur.scrollTop += moveY / scaleY;
167
+ movedY = (cur.scrollTop - start) * scaleY;
165
168
  }
166
169
  if (moveX) {
167
170
  let start = cur.scrollLeft;
168
- cur.scrollLeft += moveX;
169
- movedX = cur.scrollLeft - start;
171
+ cur.scrollLeft += moveX / scaleX;
172
+ movedX = (cur.scrollLeft - start) * scaleX;
170
173
  }
171
174
  rect = { left: rect.left - movedX, top: rect.top - movedY,
172
175
  right: rect.right - movedX, bottom: rect.bottom - movedY };
@@ -2777,7 +2780,7 @@ class DocView extends ContentView {
2777
2780
  // messes with the scroll position during DOM mutation (though
2778
2781
  // no relayout is triggered and I cannot imagine how it can
2779
2782
  // recompute the scroll position without a layout)
2780
- this.dom.style.height = this.view.viewState.contentHeight + "px";
2783
+ this.dom.style.height = this.view.viewState.contentHeight / this.view.scaleY + "px";
2781
2784
  this.dom.style.flexBasis = this.minWidth ? this.minWidth + "px" : "";
2782
2785
  // Chrome will sometimes, when DOM mutations occur directly
2783
2786
  // around the selection, get confused and report a different
@@ -2849,7 +2852,7 @@ class DocView extends ContentView {
2849
2852
  }
2850
2853
  fixCompositionDOM(composition) {
2851
2854
  let fix = (dom, cView) => {
2852
- cView.flags |= 8 /* ViewFlag.Composition */;
2855
+ cView.flags |= 8 /* ViewFlag.Composition */ | (cView.children.some(c => c.flags & 7 /* ViewFlag.Dirty */) ? 1 /* ViewFlag.ChildDirty */ : 0);
2853
2856
  this.markedForComposition.add(cView);
2854
2857
  let prev = ContentView.get(dom);
2855
2858
  if (prev != cView) {
@@ -3115,7 +3118,7 @@ class DocView extends ContentView {
3115
3118
  let next = i == vs.viewports.length ? null : vs.viewports[i];
3116
3119
  let end = next ? next.from - 1 : this.length;
3117
3120
  if (end > pos) {
3118
- let height = vs.lineBlockAt(end).bottom - vs.lineBlockAt(pos).top;
3121
+ let height = (vs.lineBlockAt(end).bottom - vs.lineBlockAt(pos).top) / this.view.scaleY;
3119
3122
  deco.push(Decoration.replace({
3120
3123
  widget: new BlockGapWidget(height),
3121
3124
  block: true,
@@ -3180,7 +3183,7 @@ class BlockGapWidget extends WidgetType {
3180
3183
  }
3181
3184
  get estimatedHeight() { return this.height; }
3182
3185
  }
3183
- function findCompositionNode(view) {
3186
+ function findCompositionNode(view, dLen) {
3184
3187
  let sel = view.observer.selectionRange;
3185
3188
  let textNode = sel.focusNode && nearbyTextNode(sel.focusNode, sel.focusOffset, 0);
3186
3189
  if (!textNode)
@@ -3192,10 +3195,12 @@ function findCompositionNode(view) {
3192
3195
  to = from + cView.length;
3193
3196
  }
3194
3197
  else {
3198
+ let oldLen = Math.max(0, textNode.nodeValue.length - dLen);
3195
3199
  up: for (let offset = 0, node = textNode;;) {
3196
3200
  for (let sibling = node.previousSibling, cView; sibling; sibling = sibling.previousSibling) {
3197
3201
  if (cView = ContentView.get(sibling)) {
3198
- from = to = cView.posAtEnd + offset;
3202
+ to = cView.posAtEnd + offset;
3203
+ from = Math.max(0, to - oldLen);
3199
3204
  break up;
3200
3205
  }
3201
3206
  let reader = new DOMReader([], view.state);
@@ -3209,15 +3214,16 @@ function findCompositionNode(view) {
3209
3214
  return null;
3210
3215
  let parentView = ContentView.get(node);
3211
3216
  if (parentView) {
3212
- from = to = parentView.posAtStart + offset;
3217
+ from = parentView.posAtStart + offset;
3218
+ to = from + oldLen;
3213
3219
  break;
3214
3220
  }
3215
3221
  }
3216
3222
  }
3217
- return { from, to, node: textNode };
3223
+ return { from, to: to, node: textNode };
3218
3224
  }
3219
3225
  function findCompositionRange(view, changes) {
3220
- let found = findCompositionNode(view);
3226
+ let found = findCompositionNode(view, changes.newLength - changes.length);
3221
3227
  if (!found)
3222
3228
  return null;
3223
3229
  let { from: fromA, to: toA, node: textNode } = found;
@@ -3781,6 +3787,8 @@ class InputState {
3781
3787
  // issue where the composition vanishes when you press enter.
3782
3788
  if (browser.safari)
3783
3789
  view.contentDOM.addEventListener("input", () => null);
3790
+ if (browser.gecko)
3791
+ firefoxCopyCutHack(view.contentDOM.ownerDocument);
3784
3792
  }
3785
3793
  ensureHandlers(view, plugins) {
3786
3794
  var _a;
@@ -4486,6 +4494,18 @@ handlers.beforeinput = (view, event) => {
4486
4494
  }
4487
4495
  }
4488
4496
  };
4497
+ const appliedFirefoxHack = /*@__PURE__*/new Set;
4498
+ // In Firefox, when cut/copy handlers are added to the document, that
4499
+ // somehow avoids a bug where those events aren't fired when the
4500
+ // selection is empty. See https://github.com/codemirror/dev/issues/1082
4501
+ // and https://bugzilla.mozilla.org/show_bug.cgi?id=995961
4502
+ function firefoxCopyCutHack(doc) {
4503
+ if (!appliedFirefoxHack.has(doc)) {
4504
+ appliedFirefoxHack.add(doc);
4505
+ doc.addEventListener("copy", () => { });
4506
+ doc.addEventListener("cut", () => { });
4507
+ }
4508
+ }
4489
4509
 
4490
4510
  const wrappingWhiteSpace = ["pre-wrap", "normal", "pre-line", "break-spaces"];
4491
4511
  class HeightOracle {
@@ -5254,8 +5274,10 @@ class LineGap {
5254
5274
  }
5255
5275
  return true;
5256
5276
  }
5257
- draw(wrapping) {
5258
- return Decoration.replace({ widget: new LineGapWidget(this.size, wrapping) }).range(this.from, this.to);
5277
+ draw(viewState, wrapping) {
5278
+ return Decoration.replace({
5279
+ widget: new LineGapWidget(this.size * (wrapping ? viewState.scaleY : viewState.scaleX), wrapping)
5280
+ }).range(this.from, this.to);
5259
5281
  }
5260
5282
  }
5261
5283
  class LineGapWidget extends WidgetType {
@@ -5285,14 +5307,18 @@ class ViewState {
5285
5307
  // These are contentDOM-local coordinates
5286
5308
  this.pixelViewport = { left: 0, right: window.innerWidth, top: 0, bottom: 0 };
5287
5309
  this.inView = true;
5288
- this.paddingTop = 0;
5289
- this.paddingBottom = 0;
5290
- this.contentDOMWidth = 0;
5291
- this.contentDOMHeight = 0;
5292
- this.editorHeight = 0;
5293
- this.editorWidth = 0;
5294
- this.scrollTop = 0;
5310
+ this.paddingTop = 0; // Padding above the document, scaled
5311
+ this.paddingBottom = 0; // Padding below the document, scaled
5312
+ this.contentDOMWidth = 0; // contentDOM.getBoundingClientRect().width
5313
+ this.contentDOMHeight = 0; // contentDOM.getBoundingClientRect().height
5314
+ this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
5315
+ this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
5316
+ this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
5295
5317
  this.scrolledToBottom = true;
5318
+ // The CSS-transformation scale of the editor (transformed size /
5319
+ // concrete size)
5320
+ this.scaleX = 1;
5321
+ this.scaleY = 1;
5296
5322
  // The vertical position (document-relative) to which to anchor the
5297
5323
  // scroll position. -1 means anchor to the end of the document.
5298
5324
  this.scrollAnchorPos = 0;
@@ -5326,7 +5352,7 @@ class ViewState {
5326
5352
  this.updateViewportLines();
5327
5353
  this.updateForViewport();
5328
5354
  this.lineGaps = this.ensureLineGaps([]);
5329
- this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(false)));
5355
+ this.lineGapDeco = Decoration.set(this.lineGaps.map(gap => gap.draw(this, false)));
5330
5356
  this.computeVisibleRanges();
5331
5357
  }
5332
5358
  updateForViewport() {
@@ -5398,8 +5424,23 @@ class ViewState {
5398
5424
  this.contentDOMHeight = domRect.height;
5399
5425
  this.mustMeasureContent = false;
5400
5426
  let result = 0, bias = 0;
5427
+ if (domRect.width && domRect.height) {
5428
+ let scaleX = domRect.width / dom.offsetWidth;
5429
+ let scaleY = domRect.height / dom.offsetHeight;
5430
+ if (scaleX > 0.995 && scaleX < 1.005)
5431
+ scaleX = 1;
5432
+ if (scaleY > 0.995 && scaleY < 1.005)
5433
+ scaleY = 1;
5434
+ if (this.scaleX != scaleX || this.scaleY != scaleY) {
5435
+ this.scaleX = scaleX;
5436
+ this.scaleY = scaleY;
5437
+ result |= 8 /* UpdateFlag.Geometry */;
5438
+ refresh = measureContent = true;
5439
+ }
5440
+ }
5401
5441
  // Vertical padding
5402
- let paddingTop = parseInt(style.paddingTop) || 0, paddingBottom = parseInt(style.paddingBottom) || 0;
5442
+ let paddingTop = (parseInt(style.paddingTop) || 0) * this.scaleY;
5443
+ let paddingBottom = (parseInt(style.paddingBottom) || 0) * this.scaleY;
5403
5444
  if (this.paddingTop != paddingTop || this.paddingBottom != paddingBottom) {
5404
5445
  this.paddingTop = paddingTop;
5405
5446
  this.paddingBottom = paddingBottom;
@@ -5411,9 +5452,10 @@ class ViewState {
5411
5452
  this.editorWidth = view.scrollDOM.clientWidth;
5412
5453
  result |= 8 /* UpdateFlag.Geometry */;
5413
5454
  }
5414
- if (this.scrollTop != view.scrollDOM.scrollTop) {
5455
+ let scrollTop = view.scrollDOM.scrollTop * this.scaleY;
5456
+ if (this.scrollTop != scrollTop) {
5415
5457
  this.scrollAnchorHeight = -1;
5416
- this.scrollTop = view.scrollDOM.scrollTop;
5458
+ this.scrollTop = scrollTop;
5417
5459
  }
5418
5460
  this.scrolledToBottom = isScrolledToBottom(view.scrollDOM);
5419
5461
  // Pixel viewport
@@ -5634,7 +5676,7 @@ class ViewState {
5634
5676
  updateLineGaps(gaps) {
5635
5677
  if (!LineGap.same(gaps, this.lineGaps)) {
5636
5678
  this.lineGaps = gaps;
5637
- this.lineGapDeco = Decoration.set(gaps.map(gap => gap.draw(this.heightOracle.lineWrapping)));
5679
+ this.lineGapDeco = Decoration.set(gaps.map(gap => gap.draw(this, this.heightOracle.lineWrapping)));
5638
5680
  }
5639
5681
  }
5640
5682
  computeVisibleRanges() {
@@ -6141,7 +6183,7 @@ function applyDOMChange(view, domChange) {
6141
6183
  change.insert.length == 1 && change.insert.lines == 2 &&
6142
6184
  dispatchKey(view.contentDOM, "Enter", 13)) ||
6143
6185
  ((change.from == sel.from - 1 && change.to == sel.to && change.insert.length == 0 ||
6144
- lastKey == 8 && change.insert.length < change.to - change.from) &&
6186
+ lastKey == 8 && change.insert.length < change.to - change.from && change.to > sel.head) &&
6145
6187
  dispatchKey(view.contentDOM, "Backspace", 8)) ||
6146
6188
  (change.from == sel.from && change.to == sel.to + 1 && change.insert.length == 0 &&
6147
6189
  dispatchKey(view.contentDOM, "Delete", 46))))
@@ -6185,7 +6227,8 @@ function applyDefaultInsert(view, change, newSel) {
6185
6227
  if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
6186
6228
  change.to <= sel.to && change.to >= sel.to - 10) {
6187
6229
  let replaced = view.state.sliceDoc(change.from, change.to);
6188
- let composition = findCompositionNode(view) || view.state.doc.lineAt(sel.head);
6230
+ let composition = findCompositionNode(view, change.insert.length - (change.to - change.from)) ||
6231
+ view.state.doc.lineAt(sel.head);
6189
6232
  let offset = sel.to - change.to, size = sel.to - sel.from;
6190
6233
  tr = startState.changeByRange(range => {
6191
6234
  if (range.from == sel.from && range.to == sel.to)
@@ -7035,13 +7078,18 @@ class EditorView {
7035
7078
  return;
7036
7079
  if (this.measureScheduled > -1)
7037
7080
  this.win.cancelAnimationFrame(this.measureScheduled);
7081
+ if (this.observer.delayedAndroidKey) {
7082
+ this.measureScheduled = -1;
7083
+ this.requestMeasure();
7084
+ return;
7085
+ }
7038
7086
  this.measureScheduled = 0; // Prevent requestMeasure calls from scheduling another animation frame
7039
7087
  if (flush)
7040
7088
  this.observer.forceFlush();
7041
7089
  let updated = null;
7042
- let sDOM = this.scrollDOM, { scrollTop } = sDOM;
7090
+ let sDOM = this.scrollDOM, scrollTop = sDOM.scrollTop * this.scaleY;
7043
7091
  let { scrollAnchorPos, scrollAnchorHeight } = this.viewState;
7044
- if (scrollTop != this.viewState.scrollTop)
7092
+ if (Math.abs(scrollTop - this.viewState.scrollTop) > 1)
7045
7093
  scrollAnchorHeight = -1;
7046
7094
  this.viewState.scrollAnchorHeight = -1;
7047
7095
  try {
@@ -7118,7 +7166,8 @@ class EditorView {
7118
7166
  this.viewState.lineBlockAt(scrollAnchorPos).top;
7119
7167
  let diff = newAnchorHeight - scrollAnchorHeight;
7120
7168
  if (diff > 1 || diff < -1) {
7121
- scrollTop = sDOM.scrollTop = scrollTop + diff;
7169
+ scrollTop = scrollTop + diff;
7170
+ sDOM.scrollTop = scrollTop / this.scaleY;
7122
7171
  scrollAnchorHeight = -1;
7123
7172
  continue;
7124
7173
  }
@@ -7245,6 +7294,16 @@ class EditorView {
7245
7294
  return { top: this.viewState.paddingTop, bottom: this.viewState.paddingBottom };
7246
7295
  }
7247
7296
  /**
7297
+ If the editor is transformed with CSS, this provides the scale
7298
+ along the X axis. Otherwise, it will just be 1. Note that
7299
+ transforms other than translation and scaling are not supported.
7300
+ */
7301
+ get scaleX() { return this.viewState.scaleX; }
7302
+ /**
7303
+ Provide the CSS transformed scale along the Y axis.
7304
+ */
7305
+ get scaleY() { return this.viewState.scaleY; }
7306
+ /**
7248
7307
  Find the text line or block widget at the given vertical
7249
7308
  position (which is interpreted as relative to the [top of the
7250
7309
  document](https://codemirror.net/6/docs/ref/#view.EditorView.documentTop)).
@@ -8035,8 +8094,8 @@ class RectangleMarker {
8035
8094
  }
8036
8095
  function getBase(view) {
8037
8096
  let rect = view.scrollDOM.getBoundingClientRect();
8038
- let left = view.textDirection == Direction.LTR ? rect.left : rect.right - view.scrollDOM.clientWidth;
8039
- return { left: left - view.scrollDOM.scrollLeft, top: rect.top - view.scrollDOM.scrollTop };
8097
+ let left = view.textDirection == Direction.LTR ? rect.left : rect.right - view.scrollDOM.clientWidth * view.scaleX;
8098
+ return { left: left - view.scrollDOM.scrollLeft * view.scaleX, top: rect.top - view.scrollDOM.scrollTop * view.scaleY };
8040
8099
  }
8041
8100
  function wrappedLine(view, pos, inside) {
8042
8101
  let range = EditorSelection.cursor(pos);
@@ -8138,6 +8197,8 @@ class LayerView {
8138
8197
  this.view = view;
8139
8198
  this.layer = layer;
8140
8199
  this.drawn = [];
8200
+ this.scaleX = 1;
8201
+ this.scaleY = 1;
8141
8202
  this.measureReq = { read: this.measure.bind(this), write: this.draw.bind(this) };
8142
8203
  this.dom = view.scrollDOM.appendChild(document.createElement("div"));
8143
8204
  this.dom.classList.add("cm-layer");
@@ -8145,6 +8206,7 @@ class LayerView {
8145
8206
  this.dom.classList.add("cm-layer-above");
8146
8207
  if (layer.class)
8147
8208
  this.dom.classList.add(layer.class);
8209
+ this.scale();
8148
8210
  this.dom.setAttribute("aria-hidden", "true");
8149
8211
  this.setOrder(view.state);
8150
8212
  view.requestMeasure(this.measureReq);
@@ -8154,8 +8216,10 @@ class LayerView {
8154
8216
  update(update) {
8155
8217
  if (update.startState.facet(layerOrder) != update.state.facet(layerOrder))
8156
8218
  this.setOrder(update.state);
8157
- if (this.layer.update(update, this.dom) || update.geometryChanged)
8219
+ if (this.layer.update(update, this.dom) || update.geometryChanged) {
8220
+ this.scale();
8158
8221
  update.view.requestMeasure(this.measureReq);
8222
+ }
8159
8223
  }
8160
8224
  setOrder(state) {
8161
8225
  let pos = 0, order = state.facet(layerOrder);
@@ -8166,6 +8230,14 @@ class LayerView {
8166
8230
  measure() {
8167
8231
  return this.layer.markers(this.view);
8168
8232
  }
8233
+ scale() {
8234
+ let { scaleX, scaleY } = this.view;
8235
+ if (scaleX != this.scaleX || scaleY != this.scaleY) {
8236
+ this.scaleX = scaleX;
8237
+ this.scaleY = scaleY;
8238
+ this.dom.style.transform = `scale(${1 / scaleX}, ${1 / scaleY})`;
8239
+ }
8240
+ }
8169
8241
  draw(markers) {
8170
8242
  if (markers.length != this.drawn.length || markers.some((p, i) => !sameMarker(p, this.drawn[i]))) {
8171
8243
  let old = this.dom.firstChild, oldI = 0;
@@ -8335,23 +8407,25 @@ const drawDropCursor = /*@__PURE__*/ViewPlugin.fromClass(class {
8335
8407
  }
8336
8408
  }
8337
8409
  readPos() {
8338
- let pos = this.view.state.field(dropCursorPos);
8339
- let rect = pos != null && this.view.coordsAtPos(pos);
8410
+ let { view } = this;
8411
+ let pos = view.state.field(dropCursorPos);
8412
+ let rect = pos != null && view.coordsAtPos(pos);
8340
8413
  if (!rect)
8341
8414
  return null;
8342
- let outer = this.view.scrollDOM.getBoundingClientRect();
8415
+ let outer = view.scrollDOM.getBoundingClientRect();
8343
8416
  return {
8344
- left: rect.left - outer.left + this.view.scrollDOM.scrollLeft,
8345
- top: rect.top - outer.top + this.view.scrollDOM.scrollTop,
8417
+ left: rect.left - outer.left + view.scrollDOM.scrollLeft * view.scaleX,
8418
+ top: rect.top - outer.top + view.scrollDOM.scrollTop * view.scaleY,
8346
8419
  height: rect.bottom - rect.top
8347
8420
  };
8348
8421
  }
8349
8422
  drawCursor(pos) {
8350
8423
  if (this.cursor) {
8424
+ let { scaleX, scaleY } = this.view;
8351
8425
  if (pos) {
8352
- this.cursor.style.left = pos.left + "px";
8353
- this.cursor.style.top = pos.top + "px";
8354
- this.cursor.style.height = pos.height + "px";
8426
+ this.cursor.style.left = pos.left / scaleX + "px";
8427
+ this.cursor.style.top = pos.top / scaleY + "px";
8428
+ this.cursor.style.height = pos.height / scaleY + "px";
8355
8429
  }
8356
8430
  else {
8357
8431
  this.cursor.style.left = "-100000px";
@@ -8593,7 +8667,9 @@ function specialCharPlugin() {
8593
8667
  if (code == 9) {
8594
8668
  let line = doc.lineAt(pos);
8595
8669
  let size = view.state.tabSize, col = countColumn(line.text, size, pos - line.from);
8596
- return Decoration.replace({ widget: new TabWidget((size - (col % size)) * this.view.defaultCharacterWidth) });
8670
+ return Decoration.replace({
8671
+ widget: new TabWidget((size - (col % size)) * this.view.defaultCharacterWidth / this.view.scaleX)
8672
+ });
8597
8673
  }
8598
8674
  return this.decorationCache[code] ||
8599
8675
  (this.decorationCache[code] = Decoration.replace({ widget: new SpecialCharWidget(conf, code) }));
@@ -8670,7 +8746,8 @@ const plugin = /*@__PURE__*/ViewPlugin.fromClass(class {
8670
8746
  }
8671
8747
  update(update) {
8672
8748
  let { view } = update;
8673
- let height = view.viewState.editorHeight - view.defaultLineHeight - view.documentPadding.top - 0.5;
8749
+ let height = view.viewState.editorHeight * view.scaleY -
8750
+ view.defaultLineHeight - view.documentPadding.top - 0.5;
8674
8751
  if (height >= 0 && height != this.height) {
8675
8752
  this.height = height;
8676
8753
  this.attrs = { style: `padding-bottom: ${height}px` };
@@ -8967,6 +9044,7 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
8967
9044
  constructor(view) {
8968
9045
  this.view = view;
8969
9046
  this.inView = true;
9047
+ this.madeAbsolute = false;
8970
9048
  this.lastTransaction = 0;
8971
9049
  this.measureTimeout = -1;
8972
9050
  let config = view.state.facet(tooltipConfig);
@@ -9018,7 +9096,7 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
9018
9096
  this.observeIntersection();
9019
9097
  let shouldMeasure = updated || update.geometryChanged;
9020
9098
  let newConfig = update.state.facet(tooltipConfig);
9021
- if (newConfig.position != this.position) {
9099
+ if (newConfig.position != this.position && !this.madeAbsolute) {
9022
9100
  this.position = newConfig.position;
9023
9101
  for (let t of this.manager.tooltipViews)
9024
9102
  t.dom.style.position = this.position;
@@ -9066,6 +9144,27 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
9066
9144
  }
9067
9145
  readMeasure() {
9068
9146
  let editor = this.view.dom.getBoundingClientRect();
9147
+ let scaleX = 1, scaleY = 1, makeAbsolute = false;
9148
+ if (this.position == "fixed") {
9149
+ let views = this.manager.tooltipViews;
9150
+ // When the dialog's offset parent isn't the body, we are
9151
+ // probably in a transformed container, and should use absolute
9152
+ // positioning instead, since fixed positioning inside a
9153
+ // transform works in a very broken way.
9154
+ makeAbsolute = views.length > 0 && views[0].dom.offsetParent != this.container.ownerDocument.body;
9155
+ }
9156
+ if (makeAbsolute || this.position == "absolute") {
9157
+ if (this.parent) {
9158
+ let rect = this.parent.getBoundingClientRect();
9159
+ if (rect.width && rect.height) {
9160
+ scaleX = rect.width / this.parent.offsetWidth;
9161
+ scaleY = rect.height / this.parent.offsetHeight;
9162
+ }
9163
+ }
9164
+ else {
9165
+ ({ scaleX, scaleY } = this.view.viewState);
9166
+ }
9167
+ }
9069
9168
  return {
9070
9169
  editor,
9071
9170
  parent: this.parent ? this.container.getBoundingClientRect() : editor,
@@ -9075,11 +9174,18 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
9075
9174
  }),
9076
9175
  size: this.manager.tooltipViews.map(({ dom }) => dom.getBoundingClientRect()),
9077
9176
  space: this.view.state.facet(tooltipConfig).tooltipSpace(this.view),
9177
+ scaleX, scaleY, makeAbsolute
9078
9178
  };
9079
9179
  }
9080
9180
  writeMeasure(measured) {
9081
9181
  var _a;
9082
- let { editor, space } = measured;
9182
+ if (measured.makeAbsolute) {
9183
+ this.madeAbsolute = true;
9184
+ this.position = "absolute";
9185
+ for (let t of this.manager.tooltipViews)
9186
+ t.dom.style.position = "absolute";
9187
+ }
9188
+ let { editor, space, scaleX, scaleY } = measured;
9083
9189
  let others = [];
9084
9190
  for (let i = 0; i < this.manager.tooltips.length; i++) {
9085
9191
  let tooltip = this.manager.tooltips[i], tView = this.manager.tooltipViews[i], { dom } = tView;
@@ -9112,7 +9218,7 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
9112
9218
  continue;
9113
9219
  }
9114
9220
  knownHeight.set(tView, height);
9115
- dom.style.height = (height = spaceVert) + "px";
9221
+ dom.style.height = (height = spaceVert) / scaleY + "px";
9116
9222
  }
9117
9223
  else if (dom.style.height) {
9118
9224
  dom.style.height = "";
@@ -9124,15 +9230,17 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
9124
9230
  if (r.left < right && r.right > left && r.top < top + height && r.bottom > top)
9125
9231
  top = above ? r.top - height - 2 - arrowHeight : r.bottom + arrowHeight + 2;
9126
9232
  if (this.position == "absolute") {
9127
- dom.style.top = (top - measured.parent.top) + "px";
9128
- dom.style.left = (left - measured.parent.left) + "px";
9233
+ dom.style.top = (top - measured.parent.top) / scaleY + "px";
9234
+ dom.style.left = (left - measured.parent.left) / scaleX + "px";
9129
9235
  }
9130
9236
  else {
9131
- dom.style.top = top + "px";
9132
- dom.style.left = left + "px";
9237
+ dom.style.top = top / scaleY + "px";
9238
+ dom.style.left = left / scaleX + "px";
9239
+ }
9240
+ if (arrow) {
9241
+ let arrowLeft = pos.left + (ltr ? offset.x : -offset.x) - (left + 14 /* Arrow.Offset */ - 7 /* Arrow.Size */);
9242
+ arrow.style.left = arrowLeft / scaleX + "px";
9133
9243
  }
9134
- if (arrow)
9135
- arrow.style.left = `${pos.left + (ltr ? offset.x : -offset.x) - (left + 14 /* Arrow.Offset */ - 7 /* Arrow.Size */)}px`;
9136
9244
  if (tView.overlap !== true)
9137
9245
  others.push({ left, top, right, bottom: top + height });
9138
9246
  dom.classList.toggle("cm-tooltip-above", above);
@@ -9365,7 +9473,7 @@ class HoverPlugin {
9365
9473
  if (tooltip && !isInTooltip(this.lastMove.target) || this.pending) {
9366
9474
  let { pos } = tooltip || this.pending, end = (_a = tooltip === null || tooltip === void 0 ? void 0 : tooltip.end) !== null && _a !== void 0 ? _a : pos;
9367
9475
  if ((pos == end ? this.view.posAtCoords(this.lastMove) != pos
9368
- : !isOverRange(this.view, pos, end, event.clientX, event.clientY, 6 /* Hover.MaxDist */))) {
9476
+ : !isOverRange(this.view, pos, end, event.clientX, event.clientY))) {
9369
9477
  this.view.dispatch({ effects: this.setHover.of(null) });
9370
9478
  this.pending = null;
9371
9479
  }
@@ -9390,19 +9498,12 @@ function isInTooltip(elt) {
9390
9498
  return false;
9391
9499
  }
9392
9500
  function isOverRange(view, from, to, x, y, margin) {
9393
- let range = document.createRange();
9394
- let fromDOM = view.domAtPos(from), toDOM = view.domAtPos(to);
9395
- range.setEnd(toDOM.node, toDOM.offset);
9396
- range.setStart(fromDOM.node, fromDOM.offset);
9397
- let rects = range.getClientRects();
9398
- range.detach();
9399
- for (let i = 0; i < rects.length; i++) {
9400
- let rect = rects[i];
9401
- let dist = Math.max(rect.top - y, y - rect.bottom, rect.left - x, x - rect.right);
9402
- if (dist <= margin)
9403
- return true;
9404
- }
9405
- return false;
9501
+ let rect = view.scrollDOM.getBoundingClientRect();
9502
+ let docBottom = view.documentTop + view.documentPadding.top + view.contentHeight;
9503
+ if (rect.left > x || rect.right < x || rect.top > y || Math.min(rect.bottom, docBottom) < y)
9504
+ return false;
9505
+ let pos = view.posAtCoords({ x, y }, false);
9506
+ return pos >= from && pos <= to;
9406
9507
  }
9407
9508
  /**
9408
9509
  Set up a hover tooltip, which shows up when the pointer hovers
@@ -9741,7 +9842,7 @@ const gutterView = /*@__PURE__*/ViewPlugin.fromClass(class {
9741
9842
  this.dom = document.createElement("div");
9742
9843
  this.dom.className = "cm-gutters";
9743
9844
  this.dom.setAttribute("aria-hidden", "true");
9744
- this.dom.style.minHeight = this.view.contentHeight + "px";
9845
+ this.dom.style.minHeight = (this.view.contentHeight / this.view.scaleY) + "px";
9745
9846
  this.gutters = view.state.facet(activeGutters).map(conf => new SingleGutterView(view, conf));
9746
9847
  for (let gutter of this.gutters)
9747
9848
  this.dom.appendChild(gutter.dom);
@@ -9851,7 +9952,9 @@ const gutterView = /*@__PURE__*/ViewPlugin.fromClass(class {
9851
9952
  let value = view.plugin(plugin);
9852
9953
  if (!value || value.gutters.length == 0 || !value.fixed)
9853
9954
  return null;
9854
- return view.textDirection == Direction.LTR ? { left: value.dom.offsetWidth } : { right: value.dom.offsetWidth };
9955
+ return view.textDirection == Direction.LTR
9956
+ ? { left: value.dom.offsetWidth * view.scaleX }
9957
+ : { right: value.dom.offsetWidth * view.scaleX };
9855
9958
  })
9856
9959
  });
9857
9960
  function asArray(val) { return (Array.isArray(val) ? val : [val]); }
@@ -9968,10 +10071,12 @@ class GutterElement {
9968
10071
  this.update(view, height, above, markers);
9969
10072
  }
9970
10073
  update(view, height, above, markers) {
9971
- if (this.height != height)
9972
- this.dom.style.height = (this.height = height) + "px";
10074
+ if (this.height != height) {
10075
+ this.height = height;
10076
+ this.dom.style.height = height / view.scaleY + "px";
10077
+ }
9973
10078
  if (this.above != above)
9974
- this.dom.style.marginTop = (this.above = above) ? above + "px" : "";
10079
+ this.dom.style.marginTop = (this.above = above) ? above / view.scaleY + "px" : "";
9975
10080
  if (!sameMarkers(this.markers, markers))
9976
10081
  this.setMarkers(view, markers);
9977
10082
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.17.0",
3
+ "version": "6.18.0",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",