@codemirror/view 6.26.1 → 6.26.3

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,21 @@
1
+ ## 6.26.3 (2024-04-12)
2
+
3
+ ### Bug fixes
4
+
5
+ Fix an issue where dispatching an update to an editor before it measured itself for the first time could cause the scroll position to incorrectly move.
6
+
7
+ Fix a crash when multiple tooltips with arrows are shown.
8
+
9
+ ## 6.26.2 (2024-04-09)
10
+
11
+ ### Bug fixes
12
+
13
+ Improve behavior of `scrollPastEnd` in a scaled editor.
14
+
15
+ When available, use `Selection.getComposedRanges` on Safari to find the selection inside a shadow DOM.
16
+
17
+ Remove the workaround that avoided inappropriate styling on composed text after a decoration again, since it breaks the stock Android virtual keyboard.
18
+
1
19
  ## 6.26.1 (2024-03-28)
2
20
 
3
21
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -2704,11 +2704,10 @@ class DocView extends ContentView {
2704
2704
  super();
2705
2705
  this.view = view;
2706
2706
  this.decorations = [];
2707
- this.dynamicDecorationMap = [false];
2707
+ this.dynamicDecorationMap = [];
2708
2708
  this.domChanged = null;
2709
2709
  this.hasComposition = null;
2710
2710
  this.markedForComposition = new Set;
2711
- this.compositionBarrier = Decoration.none;
2712
2711
  this.lastCompositionAfterCursor = false;
2713
2712
  // Track a minimum width for the editor. When measuring sizes in
2714
2713
  // measureVisibleLineHeights, this is updated to point at the width
@@ -2973,7 +2972,7 @@ class DocView extends ContentView {
2973
2972
  // composition, avoid moving it across it and disrupting the
2974
2973
  // composition.
2975
2974
  suppressWidgetCursorChange(sel, cursor) {
2976
- return this.hasComposition && cursor.empty && !this.compositionBarrier.size &&
2975
+ return this.hasComposition && cursor.empty &&
2977
2976
  isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset) &&
2978
2977
  this.posFromDOM(sel.focusNode, sel.focusOffset) == cursor.head;
2979
2978
  }
@@ -3181,7 +3180,7 @@ class DocView extends ContentView {
3181
3180
  return Decoration.set(deco);
3182
3181
  }
3183
3182
  updateDeco() {
3184
- let i = 1;
3183
+ let i = 0;
3185
3184
  let allDeco = this.view.state.facet(decorations).map(d => {
3186
3185
  let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
3187
3186
  return dynamic ? d(this.view) : d;
@@ -3197,7 +3196,6 @@ class DocView extends ContentView {
3197
3196
  allDeco.push(state.RangeSet.join(outerDeco));
3198
3197
  }
3199
3198
  this.decorations = [
3200
- this.compositionBarrier,
3201
3199
  ...allDeco,
3202
3200
  this.computeBlockGapDeco(),
3203
3201
  this.view.viewState.lineGapDeco
@@ -3206,34 +3204,6 @@ class DocView extends ContentView {
3206
3204
  this.dynamicDecorationMap[i++] = false;
3207
3205
  return this.decorations;
3208
3206
  }
3209
- // Starting a composition will style the inserted text with the
3210
- // style of the text before it, and this is only cleared when the
3211
- // composition ends, because touching it before that will abort it.
3212
- // This (called from compositionstart handler) tries to notice when
3213
- // the cursor is after a non-inclusive mark, where the styling could
3214
- // be jarring, and insert an ad-hoc widget before the cursor to
3215
- // isolate it from the style before it.
3216
- maybeCreateCompositionBarrier() {
3217
- let { main: { head, empty } } = this.view.state.selection;
3218
- if (!empty)
3219
- return false;
3220
- let found = null;
3221
- for (let set of this.decorations) {
3222
- set.between(head, head, (from, to, value) => {
3223
- if (value.point)
3224
- found = false;
3225
- else if (value.endSide < 0 && from < head && to == head)
3226
- found = true;
3227
- });
3228
- if (found === false)
3229
- break;
3230
- }
3231
- this.compositionBarrier = found ? Decoration.set(compositionBarrierWidget.range(head)) : Decoration.none;
3232
- return !!found;
3233
- }
3234
- clearCompositionBarrier() {
3235
- this.compositionBarrier = Decoration.none;
3236
- }
3237
3207
  scrollIntoView(target) {
3238
3208
  if (target.isSnapshot) {
3239
3209
  let ref = this.view.viewState.lineBlockAt(target.range.head);
@@ -3266,7 +3236,6 @@ class DocView extends ContentView {
3266
3236
  scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == exports.Direction.LTR);
3267
3237
  }
3268
3238
  }
3269
- const compositionBarrierWidget = Decoration.widget({ side: -1, widget: NullWidget.inline });
3270
3239
  function betweenUneditable(pos) {
3271
3240
  return pos.node.nodeType == 1 && pos.node.firstChild &&
3272
3241
  (pos.offset == 0 || pos.node.childNodes[pos.offset - 1].contentEditable == "false") &&
@@ -4508,10 +4477,6 @@ observers.compositionstart = observers.compositionupdate = view => {
4508
4477
  if (view.inputState.composing < 0) {
4509
4478
  // FIXME possibly set a timeout to clear it again on Android
4510
4479
  view.inputState.composing = 0;
4511
- if (view.docView.maybeCreateCompositionBarrier()) {
4512
- view.update([]);
4513
- view.docView.clearCompositionBarrier();
4514
- }
4515
4480
  }
4516
4481
  };
4517
4482
  observers.compositionend = view => {
@@ -5400,7 +5365,7 @@ class ViewState {
5400
5365
  this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
5401
5366
  this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
5402
5367
  this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
5403
- this.scrolledToBottom = true;
5368
+ this.scrolledToBottom = false;
5404
5369
  // The CSS-transformation scale of the editor (transformed size /
5405
5370
  // concrete size)
5406
5371
  this.scaleX = 1;
@@ -6711,9 +6676,12 @@ class DOMObserver {
6711
6676
  let { view } = this;
6712
6677
  // The Selection object is broken in shadow roots in Safari. See
6713
6678
  // https://github.com/codemirror/dev/issues/414
6679
+ let selection = getSelection(view.root);
6680
+ if (!selection)
6681
+ return false;
6714
6682
  let range = browser.safari && view.root.nodeType == 11 &&
6715
6683
  deepActiveElement(this.dom.ownerDocument) == this.dom &&
6716
- safariSelectionRangeHack(this.view) || getSelection(view.root);
6684
+ safariSelectionRangeHack(this.view, selection) || selection;
6717
6685
  if (!range || this.selectionRange.eq(range))
6718
6686
  return false;
6719
6687
  let local = hasSelection(this.dom, range);
@@ -6985,8 +6953,24 @@ function findChild(cView, dom, dir) {
6985
6953
  }
6986
6954
  return null;
6987
6955
  }
6956
+ function buildSelectionRangeFromRange(view, range) {
6957
+ let anchorNode = range.startContainer, anchorOffset = range.startOffset;
6958
+ let focusNode = range.endContainer, focusOffset = range.endOffset;
6959
+ let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
6960
+ // Since such a range doesn't distinguish between anchor and head,
6961
+ // use a heuristic that flips it around if its end matches the
6962
+ // current anchor.
6963
+ if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
6964
+ [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
6965
+ return { anchorNode, anchorOffset, focusNode, focusOffset };
6966
+ }
6988
6967
  // Used to work around a Safari Selection/shadow DOM bug (#414)
6989
- function safariSelectionRangeHack(view) {
6968
+ function safariSelectionRangeHack(view, selection) {
6969
+ if (selection.getComposedRanges) {
6970
+ let range = selection.getComposedRanges(view.root)[0];
6971
+ if (range)
6972
+ return buildSelectionRangeFromRange(view, range);
6973
+ }
6990
6974
  let found = null;
6991
6975
  // Because Safari (at least in 2018-2021) doesn't provide regular
6992
6976
  // access to the selection inside a shadowroot, we have to perform a
@@ -7001,17 +6985,7 @@ function safariSelectionRangeHack(view) {
7001
6985
  view.contentDOM.addEventListener("beforeinput", read, true);
7002
6986
  view.dom.ownerDocument.execCommand("indent");
7003
6987
  view.contentDOM.removeEventListener("beforeinput", read, true);
7004
- if (!found)
7005
- return null;
7006
- let anchorNode = found.startContainer, anchorOffset = found.startOffset;
7007
- let focusNode = found.endContainer, focusOffset = found.endOffset;
7008
- let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
7009
- // Since such a range doesn't distinguish between anchor and head,
7010
- // use a heuristic that flips it around if its end matches the
7011
- // current anchor.
7012
- if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
7013
- [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
7014
- return { anchorNode, anchorOffset, focusNode, focusOffset };
6988
+ return found ? buildSelectionRangeFromRange(view, found) : null;
7015
6989
  }
7016
6990
 
7017
6991
  // The editor's update state machine looks something like this:
@@ -9102,7 +9076,7 @@ const plugin = ViewPlugin.fromClass(class {
9102
9076
  }
9103
9077
  update(update) {
9104
9078
  let { view } = update;
9105
- let height = view.viewState.editorHeight * view.scaleY -
9079
+ let height = view.viewState.editorHeight -
9106
9080
  view.defaultLineHeight - view.documentPadding.top - 0.5;
9107
9081
  if (height >= 0 && height != this.height) {
9108
9082
  this.height = height;
@@ -9497,7 +9471,7 @@ const tooltipPlugin = ViewPlugin.fromClass(class {
9497
9471
  if (tooltip.arrow && !tooltipView.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")) {
9498
9472
  let arrow = document.createElement("div");
9499
9473
  arrow.className = "cm-tooltip-arrow";
9500
- tooltipView.dom.insertBefore(arrow, before);
9474
+ tooltipView.dom.appendChild(arrow);
9501
9475
  }
9502
9476
  tooltipView.dom.style.position = this.position;
9503
9477
  tooltipView.dom.style.top = Outside;
package/dist/index.js CHANGED
@@ -2700,11 +2700,10 @@ class DocView extends ContentView {
2700
2700
  super();
2701
2701
  this.view = view;
2702
2702
  this.decorations = [];
2703
- this.dynamicDecorationMap = [false];
2703
+ this.dynamicDecorationMap = [];
2704
2704
  this.domChanged = null;
2705
2705
  this.hasComposition = null;
2706
2706
  this.markedForComposition = new Set;
2707
- this.compositionBarrier = Decoration.none;
2708
2707
  this.lastCompositionAfterCursor = false;
2709
2708
  // Track a minimum width for the editor. When measuring sizes in
2710
2709
  // measureVisibleLineHeights, this is updated to point at the width
@@ -2969,7 +2968,7 @@ class DocView extends ContentView {
2969
2968
  // composition, avoid moving it across it and disrupting the
2970
2969
  // composition.
2971
2970
  suppressWidgetCursorChange(sel, cursor) {
2972
- return this.hasComposition && cursor.empty && !this.compositionBarrier.size &&
2971
+ return this.hasComposition && cursor.empty &&
2973
2972
  isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset) &&
2974
2973
  this.posFromDOM(sel.focusNode, sel.focusOffset) == cursor.head;
2975
2974
  }
@@ -3177,7 +3176,7 @@ class DocView extends ContentView {
3177
3176
  return Decoration.set(deco);
3178
3177
  }
3179
3178
  updateDeco() {
3180
- let i = 1;
3179
+ let i = 0;
3181
3180
  let allDeco = this.view.state.facet(decorations).map(d => {
3182
3181
  let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
3183
3182
  return dynamic ? d(this.view) : d;
@@ -3193,7 +3192,6 @@ class DocView extends ContentView {
3193
3192
  allDeco.push(RangeSet.join(outerDeco));
3194
3193
  }
3195
3194
  this.decorations = [
3196
- this.compositionBarrier,
3197
3195
  ...allDeco,
3198
3196
  this.computeBlockGapDeco(),
3199
3197
  this.view.viewState.lineGapDeco
@@ -3202,34 +3200,6 @@ class DocView extends ContentView {
3202
3200
  this.dynamicDecorationMap[i++] = false;
3203
3201
  return this.decorations;
3204
3202
  }
3205
- // Starting a composition will style the inserted text with the
3206
- // style of the text before it, and this is only cleared when the
3207
- // composition ends, because touching it before that will abort it.
3208
- // This (called from compositionstart handler) tries to notice when
3209
- // the cursor is after a non-inclusive mark, where the styling could
3210
- // be jarring, and insert an ad-hoc widget before the cursor to
3211
- // isolate it from the style before it.
3212
- maybeCreateCompositionBarrier() {
3213
- let { main: { head, empty } } = this.view.state.selection;
3214
- if (!empty)
3215
- return false;
3216
- let found = null;
3217
- for (let set of this.decorations) {
3218
- set.between(head, head, (from, to, value) => {
3219
- if (value.point)
3220
- found = false;
3221
- else if (value.endSide < 0 && from < head && to == head)
3222
- found = true;
3223
- });
3224
- if (found === false)
3225
- break;
3226
- }
3227
- this.compositionBarrier = found ? Decoration.set(compositionBarrierWidget.range(head)) : Decoration.none;
3228
- return !!found;
3229
- }
3230
- clearCompositionBarrier() {
3231
- this.compositionBarrier = Decoration.none;
3232
- }
3233
3203
  scrollIntoView(target) {
3234
3204
  if (target.isSnapshot) {
3235
3205
  let ref = this.view.viewState.lineBlockAt(target.range.head);
@@ -3262,7 +3232,6 @@ class DocView extends ContentView {
3262
3232
  scrollRectIntoView(this.view.scrollDOM, targetRect, range.head < range.anchor ? -1 : 1, target.x, target.y, Math.max(Math.min(target.xMargin, offsetWidth), -offsetWidth), Math.max(Math.min(target.yMargin, offsetHeight), -offsetHeight), this.view.textDirection == Direction.LTR);
3263
3233
  }
3264
3234
  }
3265
- const compositionBarrierWidget = /*@__PURE__*/Decoration.widget({ side: -1, widget: NullWidget.inline });
3266
3235
  function betweenUneditable(pos) {
3267
3236
  return pos.node.nodeType == 1 && pos.node.firstChild &&
3268
3237
  (pos.offset == 0 || pos.node.childNodes[pos.offset - 1].contentEditable == "false") &&
@@ -4504,10 +4473,6 @@ observers.compositionstart = observers.compositionupdate = view => {
4504
4473
  if (view.inputState.composing < 0) {
4505
4474
  // FIXME possibly set a timeout to clear it again on Android
4506
4475
  view.inputState.composing = 0;
4507
- if (view.docView.maybeCreateCompositionBarrier()) {
4508
- view.update([]);
4509
- view.docView.clearCompositionBarrier();
4510
- }
4511
4476
  }
4512
4477
  };
4513
4478
  observers.compositionend = view => {
@@ -5395,7 +5360,7 @@ class ViewState {
5395
5360
  this.editorHeight = 0; // scrollDOM.clientHeight, unscaled
5396
5361
  this.editorWidth = 0; // scrollDOM.clientWidth, unscaled
5397
5362
  this.scrollTop = 0; // Last seen scrollDOM.scrollTop, scaled
5398
- this.scrolledToBottom = true;
5363
+ this.scrolledToBottom = false;
5399
5364
  // The CSS-transformation scale of the editor (transformed size /
5400
5365
  // concrete size)
5401
5366
  this.scaleX = 1;
@@ -6706,9 +6671,12 @@ class DOMObserver {
6706
6671
  let { view } = this;
6707
6672
  // The Selection object is broken in shadow roots in Safari. See
6708
6673
  // https://github.com/codemirror/dev/issues/414
6674
+ let selection = getSelection(view.root);
6675
+ if (!selection)
6676
+ return false;
6709
6677
  let range = browser.safari && view.root.nodeType == 11 &&
6710
6678
  deepActiveElement(this.dom.ownerDocument) == this.dom &&
6711
- safariSelectionRangeHack(this.view) || getSelection(view.root);
6679
+ safariSelectionRangeHack(this.view, selection) || selection;
6712
6680
  if (!range || this.selectionRange.eq(range))
6713
6681
  return false;
6714
6682
  let local = hasSelection(this.dom, range);
@@ -6980,8 +6948,24 @@ function findChild(cView, dom, dir) {
6980
6948
  }
6981
6949
  return null;
6982
6950
  }
6951
+ function buildSelectionRangeFromRange(view, range) {
6952
+ let anchorNode = range.startContainer, anchorOffset = range.startOffset;
6953
+ let focusNode = range.endContainer, focusOffset = range.endOffset;
6954
+ let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
6955
+ // Since such a range doesn't distinguish between anchor and head,
6956
+ // use a heuristic that flips it around if its end matches the
6957
+ // current anchor.
6958
+ if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
6959
+ [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
6960
+ return { anchorNode, anchorOffset, focusNode, focusOffset };
6961
+ }
6983
6962
  // Used to work around a Safari Selection/shadow DOM bug (#414)
6984
- function safariSelectionRangeHack(view) {
6963
+ function safariSelectionRangeHack(view, selection) {
6964
+ if (selection.getComposedRanges) {
6965
+ let range = selection.getComposedRanges(view.root)[0];
6966
+ if (range)
6967
+ return buildSelectionRangeFromRange(view, range);
6968
+ }
6985
6969
  let found = null;
6986
6970
  // Because Safari (at least in 2018-2021) doesn't provide regular
6987
6971
  // access to the selection inside a shadowroot, we have to perform a
@@ -6996,17 +6980,7 @@ function safariSelectionRangeHack(view) {
6996
6980
  view.contentDOM.addEventListener("beforeinput", read, true);
6997
6981
  view.dom.ownerDocument.execCommand("indent");
6998
6982
  view.contentDOM.removeEventListener("beforeinput", read, true);
6999
- if (!found)
7000
- return null;
7001
- let anchorNode = found.startContainer, anchorOffset = found.startOffset;
7002
- let focusNode = found.endContainer, focusOffset = found.endOffset;
7003
- let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
7004
- // Since such a range doesn't distinguish between anchor and head,
7005
- // use a heuristic that flips it around if its end matches the
7006
- // current anchor.
7007
- if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
7008
- [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
7009
- return { anchorNode, anchorOffset, focusNode, focusOffset };
6983
+ return found ? buildSelectionRangeFromRange(view, found) : null;
7010
6984
  }
7011
6985
 
7012
6986
  // The editor's update state machine looks something like this:
@@ -9097,7 +9071,7 @@ const plugin = /*@__PURE__*/ViewPlugin.fromClass(class {
9097
9071
  }
9098
9072
  update(update) {
9099
9073
  let { view } = update;
9100
- let height = view.viewState.editorHeight * view.scaleY -
9074
+ let height = view.viewState.editorHeight -
9101
9075
  view.defaultLineHeight - view.documentPadding.top - 0.5;
9102
9076
  if (height >= 0 && height != this.height) {
9103
9077
  this.height = height;
@@ -9492,7 +9466,7 @@ const tooltipPlugin = /*@__PURE__*/ViewPlugin.fromClass(class {
9492
9466
  if (tooltip.arrow && !tooltipView.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")) {
9493
9467
  let arrow = document.createElement("div");
9494
9468
  arrow.className = "cm-tooltip-arrow";
9495
- tooltipView.dom.insertBefore(arrow, before);
9469
+ tooltipView.dom.appendChild(arrow);
9496
9470
  }
9497
9471
  tooltipView.dom.style.position = this.position;
9498
9472
  tooltipView.dom.style.top = Outside;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.26.1",
3
+ "version": "6.26.3",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",