@codemirror/view 6.9.2 → 6.9.4

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,33 @@
1
+ ## 6.9.4 (2023-04-11)
2
+
3
+ ### Bug fixes
4
+
5
+ Make the editor scroll while dragging a selection near its sides, even if the cursor isn't outside the scrollable element.
6
+
7
+ Fix a bug that interrupted composition after widgets in some circumstances on Firefox.
8
+
9
+ Make sure the last change in a composition has its user event set to `input.type.compose`, even if the `compositionend` event fires before the changes are applied.
10
+
11
+ Make it possible to remove additional selection ranges by clicking on them with ctrl/cmd held, even if they aren't cursors.
12
+
13
+ Keep widget buffers between widgets and compositions, since removing them confuses IME on macOS Firefox.
14
+
15
+ Fix a bug where, for DOM changes that put the selection in the middle of the changed range, the editor incorrectly set its selection state.
16
+
17
+ Fix a bug where `coordsAtPos` could return a coordinates before the line break when querying a line-wrapped position with a positive `side`.
18
+
19
+ ## 6.9.3 (2023-03-21)
20
+
21
+ ### Bug fixes
22
+
23
+ Work around a Firefox issue that caused `coordsAtPos` to return rectangles with the full line height on empty lines.
24
+
25
+ Opening a context menu by clicking below the content element but inside the editor now properly shows the browser's menu for editable elements.
26
+
27
+ Fix an issue that broke composition (especially of Chinese IME) after widget decorations.
28
+
29
+ Fix an issue that would cause the cursor to jump around during compositions inside nested mark decorations.
30
+
1
31
  ## 6.9.2 (2023-03-08)
2
32
 
3
33
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -520,6 +520,7 @@ class ContentView {
520
520
  static get(node) { return node.cmView; }
521
521
  get isEditable() { return true; }
522
522
  get isWidget() { return false; }
523
+ get isHidden() { return false; }
523
524
  merge(from, to, source, hasStart, openStart, openEnd) {
524
525
  return false;
525
526
  }
@@ -857,7 +858,7 @@ class WidgetView extends ContentView {
857
858
  become(other) {
858
859
  if (other.length == this.length && other instanceof WidgetView && other.side == this.side) {
859
860
  if (this.widget.constructor == other.widget.constructor) {
860
- if (!this.widget.eq(other.widget))
861
+ if (!this.widget.compare(other.widget))
861
862
  this.markDirty(true);
862
863
  if (this.dom && !this.prevWidget)
863
864
  this.prevWidget = this.widget;
@@ -879,7 +880,9 @@ class WidgetView extends ContentView {
879
880
  return text ? text.slice(start, start + this.length) : state.Text.empty;
880
881
  }
881
882
  domAtPos(pos) {
882
- return pos == 0 ? DOMPos.before(this.dom) : DOMPos.after(this.dom, pos == this.length);
883
+ return (this.length ? pos == 0 : this.side > 0)
884
+ ? DOMPos.before(this.dom)
885
+ : DOMPos.after(this.dom, pos == this.length);
883
886
  }
884
887
  domBoundsAround() { return null; }
885
888
  coordsAt(pos, side) {
@@ -895,6 +898,7 @@ class WidgetView extends ContentView {
895
898
  }
896
899
  get isEditable() { return false; }
897
900
  get isWidget() { return true; }
901
+ get isHidden() { return this.widget.isHidden; }
898
902
  destroy() {
899
903
  super.destroy();
900
904
  if (this.dom)
@@ -957,8 +961,9 @@ function scanCompositionTree(pos, side, view, text, enterView, fromText) {
957
961
  }
958
962
  function posFromDOMInCompositionTree(node, offset, view, text) {
959
963
  if (view instanceof MarkView) {
964
+ let pos = 0;
960
965
  for (let child of view.children) {
961
- let pos = 0, hasComp = contains(child.dom, text);
966
+ let hasComp = contains(child.dom, text);
962
967
  if (contains(child.dom, node))
963
968
  return pos + (hasComp ? posFromDOMInCompositionTree(node, offset, child, text) : child.localPosFromDOM(node, offset));
964
969
  pos += hasComp ? text.nodeValue.length : child.length;
@@ -992,7 +997,7 @@ class WidgetBufferView extends ContentView {
992
997
  }
993
998
  }
994
999
  getSide() { return this.side; }
995
- domAtPos(pos) { return DOMPos.before(this.dom); }
1000
+ domAtPos(pos) { return this.side > 0 ? DOMPos.before(this.dom) : DOMPos.after(this.dom); }
996
1001
  localPosFromDOM() { return 0; }
997
1002
  domBoundsAround() { return null; }
998
1003
  coordsAt(pos) {
@@ -1006,6 +1011,7 @@ class WidgetBufferView extends ContentView {
1006
1011
  get overrideDOMText() {
1007
1012
  return state.Text.empty;
1008
1013
  }
1014
+ get isHidden() { return true; }
1009
1015
  }
1010
1016
  TextView.prototype.children = WidgetView.prototype.children = WidgetBufferView.prototype.children = noChildren;
1011
1017
  function inlineSiblingRect(view, side) {
@@ -1079,7 +1085,8 @@ function coordsInChildren(view, pos, side) {
1079
1085
  if (child.children.length) {
1080
1086
  scan(child, pos - off);
1081
1087
  }
1082
- else if (!after && (end > pos || off == end && child.getSide() > 0)) {
1088
+ else if ((!after || after instanceof WidgetBufferView && side > 0) &&
1089
+ (end > pos || off == end && child.getSide() > 0)) {
1083
1090
  after = child;
1084
1091
  afterPos = pos - off;
1085
1092
  }
@@ -1193,6 +1200,10 @@ class WidgetType {
1193
1200
  */
1194
1201
  get customView() { return null; }
1195
1202
  /**
1203
+ @internal
1204
+ */
1205
+ get isHidden() { return false; }
1206
+ /**
1196
1207
  This is called when the an instance of the widget is removed
1197
1208
  from the editor view.
1198
1209
  */
@@ -1516,7 +1527,7 @@ class LineView extends ContentView {
1516
1527
  measureTextSize() {
1517
1528
  if (this.children.length == 0 || this.length > 20)
1518
1529
  return null;
1519
- let totalWidth = 0;
1530
+ let totalWidth = 0, textHeight;
1520
1531
  for (let child of this.children) {
1521
1532
  if (!(child instanceof TextView) || /[^ -~]/.test(child.text))
1522
1533
  return null;
@@ -1524,14 +1535,26 @@ class LineView extends ContentView {
1524
1535
  if (rects.length != 1)
1525
1536
  return null;
1526
1537
  totalWidth += rects[0].width;
1538
+ textHeight = rects[0].height;
1527
1539
  }
1528
1540
  return !totalWidth ? null : {
1529
1541
  lineHeight: this.dom.getBoundingClientRect().height,
1530
- charWidth: totalWidth / this.length
1542
+ charWidth: totalWidth / this.length,
1543
+ textHeight
1531
1544
  };
1532
1545
  }
1533
1546
  coordsAt(pos, side) {
1534
- return coordsInChildren(this, pos, side);
1547
+ let rect = coordsInChildren(this, pos, side);
1548
+ // Correct rectangle height for empty lines when the returned
1549
+ // height is larger than the text height.
1550
+ if (!this.children.length && rect && this.parent) {
1551
+ let { heightOracle } = this.parent.view.viewState, height = rect.bottom - rect.top;
1552
+ if (Math.abs(height - heightOracle.lineHeight) < 2 && heightOracle.textHeight < height) {
1553
+ let dist = (height - heightOracle.textHeight) / 2;
1554
+ return { top: rect.top + dist, bottom: rect.bottom - dist, left: rect.left, right: rect.left };
1555
+ }
1556
+ }
1557
+ return rect;
1535
1558
  }
1536
1559
  become(_other) { return false; }
1537
1560
  get type() { return exports.BlockType.Text; }
@@ -1592,7 +1615,7 @@ class BlockWidgetView extends ContentView {
1592
1615
  become(other) {
1593
1616
  if (other instanceof BlockWidgetView && other.type == this.type &&
1594
1617
  other.widget.constructor == this.widget.constructor) {
1595
- if (!other.widget.eq(this.widget))
1618
+ if (!other.widget.compare(this.widget))
1596
1619
  this.markDirty(true);
1597
1620
  if (this.dom && !this.prevWidget)
1598
1621
  this.prevWidget = this.widget;
@@ -1723,10 +1746,11 @@ class ContentBuilder {
1723
1746
  }
1724
1747
  else {
1725
1748
  let view = WidgetView.create(deco.widget || new NullWidget("span"), len, len ? 0 : deco.startSide);
1726
- let cursorBefore = this.atCursorPos && !view.isEditable && openStart <= active.length && (from < to || deco.startSide > 0);
1749
+ let cursorBefore = this.atCursorPos && !view.isEditable && openStart <= active.length &&
1750
+ (from < to || deco.startSide > 0);
1727
1751
  let cursorAfter = !view.isEditable && (from < to || openStart > active.length || deco.startSide <= 0);
1728
1752
  let line = this.getLine();
1729
- if (this.pendingBuffer == 2 /* Buf.IfCursor */ && !cursorBefore)
1753
+ if (this.pendingBuffer == 2 /* Buf.IfCursor */ && !cursorBefore && !view.isEditable)
1730
1754
  this.pendingBuffer = 0 /* Buf.No */;
1731
1755
  this.flushBuffer(active);
1732
1756
  if (cursorBefore) {
@@ -1780,6 +1804,7 @@ class NullWidget extends WidgetType {
1780
1804
  eq(other) { return other.tag == this.tag; }
1781
1805
  toDOM() { return document.createElement(this.tag); }
1782
1806
  updateDOM(elt) { return elt.nodeName.toLowerCase() == this.tag; }
1807
+ get isHidden() { return true; }
1783
1808
  }
1784
1809
 
1785
1810
  const clickAddsSelectionRange = state.Facet.define();
@@ -2633,7 +2658,7 @@ class DocView extends ContentView {
2633
2658
  let head = main.empty ? anchor : this.domAtPos(main.head);
2634
2659
  // Always reset on Firefox when next to an uneditable node to
2635
2660
  // avoid invisible cursor bugs (#111)
2636
- if (browser.gecko && main.empty && betweenUneditable(anchor)) {
2661
+ if (browser.gecko && main.empty && !this.compositionDeco.size && betweenUneditable(anchor)) {
2637
2662
  let dummy = document.createTextNode("");
2638
2663
  this.view.observer.ignore(() => anchor.node.insertBefore(dummy, anchor.node.childNodes[anchor.offset] || null));
2639
2664
  anchor = head = new DOMPos(dummy, 0);
@@ -2812,7 +2837,7 @@ class DocView extends ContentView {
2812
2837
  }
2813
2838
  }
2814
2839
  // If no workable line exists, force a layout of a measurable element
2815
- let dummy = document.createElement("div"), lineHeight, charWidth;
2840
+ let dummy = document.createElement("div"), lineHeight, charWidth, textHeight;
2816
2841
  dummy.className = "cm-line";
2817
2842
  dummy.style.width = "99999px";
2818
2843
  dummy.textContent = "abc def ghi jkl mno pqr stu";
@@ -2821,9 +2846,10 @@ class DocView extends ContentView {
2821
2846
  let rect = clientRectsFor(dummy.firstChild)[0];
2822
2847
  lineHeight = dummy.getBoundingClientRect().height;
2823
2848
  charWidth = rect ? rect.width / 27 : 7;
2849
+ textHeight = rect ? rect.height : lineHeight;
2824
2850
  dummy.remove();
2825
2851
  });
2826
- return { lineHeight, charWidth };
2852
+ return { lineHeight, charWidth, textHeight };
2827
2853
  }
2828
2854
  childCursor(pos = this.length) {
2829
2855
  // Move back to start of last element when possible, so that
@@ -2988,22 +3014,32 @@ class CompositionWidget extends WidgetType {
2988
3014
  ignoreEvent() { return false; }
2989
3015
  get customView() { return CompositionView; }
2990
3016
  }
2991
- function nearbyTextNode(node, offset, side) {
2992
- for (;;) {
2993
- if (node.nodeType == 3)
2994
- return node;
2995
- if (node.nodeType == 1 && offset > 0 && side <= 0) {
2996
- node = node.childNodes[offset - 1];
2997
- offset = maxOffset(node);
2998
- }
2999
- else if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
3000
- node = node.childNodes[offset];
3001
- offset = 0;
3017
+ function nearbyTextNode(startNode, startOffset, side) {
3018
+ if (side <= 0)
3019
+ for (let node = startNode, offset = startOffset;;) {
3020
+ if (node.nodeType == 3)
3021
+ return node;
3022
+ if (node.nodeType == 1 && offset > 0) {
3023
+ node = node.childNodes[offset - 1];
3024
+ offset = maxOffset(node);
3025
+ }
3026
+ else {
3027
+ break;
3028
+ }
3002
3029
  }
3003
- else {
3004
- return null;
3030
+ if (side >= 0)
3031
+ for (let node = startNode, offset = startOffset;;) {
3032
+ if (node.nodeType == 3)
3033
+ return node;
3034
+ if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
3035
+ node = node.childNodes[offset];
3036
+ offset = 0;
3037
+ }
3038
+ else {
3039
+ break;
3040
+ }
3005
3041
  }
3006
- }
3042
+ return null;
3007
3043
  }
3008
3044
  function nextToUneditable(node, offset) {
3009
3045
  if (node.nodeType != 1)
@@ -3416,7 +3452,15 @@ class InputState {
3416
3452
  // first, false means first has already been marked for this
3417
3453
  // composition)
3418
3454
  this.compositionFirstChange = null;
3455
+ // End time of the previous composition
3419
3456
  this.compositionEndedAt = 0;
3457
+ // Used in a kludge to detect when an Enter keypress should be
3458
+ // considered part of the composition on Safari, which fires events
3459
+ // in the wrong order
3460
+ this.compositionPendingKey = false;
3461
+ // Used to categorize changes as part of a composition, even when
3462
+ // the mutation events fire shortly after the compositionend event
3463
+ this.compositionPendingChange = false;
3420
3464
  this.mouseSelection = null;
3421
3465
  let handleEvent = (handler, event) => {
3422
3466
  if (this.ignoreDuringComposition(event))
@@ -3439,8 +3483,16 @@ class InputState {
3439
3483
  this.registeredEvents.push(type);
3440
3484
  }
3441
3485
  view.scrollDOM.addEventListener("mousedown", (event) => {
3442
- if (event.target == view.scrollDOM && event.clientY > view.contentDOM.getBoundingClientRect().bottom)
3486
+ if (event.target == view.scrollDOM && event.clientY > view.contentDOM.getBoundingClientRect().bottom) {
3443
3487
  handleEvent(handlers.mousedown, event);
3488
+ if (!event.defaultPrevented && event.button == 2) {
3489
+ // Make sure the content covers the entire scroller height, in order
3490
+ // to catch a native context menu click below it
3491
+ let start = view.contentDOM.style.minHeight;
3492
+ view.contentDOM.style.minHeight = "100%";
3493
+ setTimeout(() => view.contentDOM.style.minHeight = start, 200);
3494
+ }
3495
+ }
3444
3496
  });
3445
3497
  if (browser.chrome && browser.chrome_version == 102) { // FIXME remove at some point
3446
3498
  // On Chrome 102, viewport updates somehow stop wheel-based
@@ -3565,8 +3617,8 @@ class InputState {
3565
3617
  // compositionend and keydown events are sometimes emitted in the
3566
3618
  // wrong order. The key event should still be ignored, even when
3567
3619
  // it happens after the compositionend event.
3568
- if (browser.safari && !browser.ios && Date.now() - this.compositionEndedAt < 100) {
3569
- this.compositionEndedAt = 0;
3620
+ if (browser.safari && !browser.ios && this.compositionPendingKey && Date.now() - this.compositionEndedAt < 100) {
3621
+ this.compositionPendingKey = false;
3570
3622
  return true;
3571
3623
  }
3572
3624
  return false;
@@ -3598,8 +3650,9 @@ const PendingKeys = [
3598
3650
  const EmacsyPendingKeys = "dthko";
3599
3651
  // Key codes for modifier keys
3600
3652
  const modifierCodes = [16, 17, 18, 20, 91, 92, 224, 225];
3653
+ const dragScrollMargin = 6;
3601
3654
  function dragScrollSpeed(dist) {
3602
- return dist * 0.7 + 8;
3655
+ return Math.max(0, dist) * 0.7 + 8;
3603
3656
  }
3604
3657
  class MouseSelection {
3605
3658
  constructor(view, startEvent, style, mustSelect) {
@@ -3636,13 +3689,13 @@ class MouseSelection {
3636
3689
  let sx = 0, sy = 0;
3637
3690
  let rect = ((_a = this.scrollParent) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect())
3638
3691
  || { left: 0, top: 0, right: this.view.win.innerWidth, bottom: this.view.win.innerHeight };
3639
- if (event.clientX <= rect.left)
3692
+ if (event.clientX <= rect.left + dragScrollMargin)
3640
3693
  sx = -dragScrollSpeed(rect.left - event.clientX);
3641
- else if (event.clientX >= rect.right)
3694
+ else if (event.clientX >= rect.right - dragScrollMargin)
3642
3695
  sx = dragScrollSpeed(event.clientX - rect.right);
3643
- if (event.clientY <= rect.top)
3696
+ if (event.clientY <= rect.top + dragScrollMargin)
3644
3697
  sy = -dragScrollSpeed(rect.top - event.clientY);
3645
- else if (event.clientY >= rect.bottom)
3698
+ else if (event.clientY >= rect.bottom - dragScrollMargin)
3646
3699
  sy = dragScrollSpeed(event.clientY - rect.bottom);
3647
3700
  this.setScrollSpeed(sx, sy);
3648
3701
  }
@@ -3889,7 +3942,7 @@ function basicMouseSelection(view, event) {
3889
3942
  }
3890
3943
  },
3891
3944
  get(event, extend, multiple) {
3892
- let cur = queryPos(view, event);
3945
+ let cur = queryPos(view, event), removed;
3893
3946
  let range = rangeForClick(view, cur.pos, cur.bias, type);
3894
3947
  if (start.pos != cur.pos && !extend) {
3895
3948
  let startRange = rangeForClick(view, start.pos, start.bias, type);
@@ -3898,8 +3951,8 @@ function basicMouseSelection(view, event) {
3898
3951
  }
3899
3952
  if (extend)
3900
3953
  return startSel.replaceRange(startSel.main.extend(range.from, range.to));
3901
- else if (multiple && startSel.ranges.length > 1 && startSel.ranges.some(r => r.eq(range)))
3902
- return removeRange(startSel, range);
3954
+ else if (multiple && type == 1 && startSel.ranges.length > 1 && (removed = removeRangeAround(startSel, cur.pos)))
3955
+ return removed;
3903
3956
  else if (multiple)
3904
3957
  return startSel.addRange(range);
3905
3958
  else
@@ -3907,11 +3960,13 @@ function basicMouseSelection(view, event) {
3907
3960
  }
3908
3961
  };
3909
3962
  }
3910
- function removeRange(sel, range) {
3911
- for (let i = 0;; i++) {
3912
- if (sel.ranges[i].eq(range))
3963
+ function removeRangeAround(sel, pos) {
3964
+ for (let i = 0; i < sel.ranges.length; i++) {
3965
+ let { from, to } = sel.ranges[i];
3966
+ if (from <= pos && to >= pos)
3913
3967
  return state.EditorSelection.create(sel.ranges.slice(0, i).concat(sel.ranges.slice(i + 1)), sel.mainIndex == i ? 0 : sel.mainIndex - (sel.mainIndex > i ? 1 : 0));
3914
3968
  }
3969
+ return null;
3915
3970
  }
3916
3971
  handlers.dragstart = (view, event) => {
3917
3972
  let { selection: { main } } = view.state;
@@ -4088,6 +4143,8 @@ handlers.compositionstart = handlers.compositionupdate = view => {
4088
4143
  handlers.compositionend = view => {
4089
4144
  view.inputState.composing = -1;
4090
4145
  view.inputState.compositionEndedAt = Date.now();
4146
+ view.inputState.compositionPendingKey = true;
4147
+ view.inputState.compositionPendingChange = view.observer.pendingRecords().length > 0;
4091
4148
  view.inputState.compositionFirstChange = null;
4092
4149
  if (browser.chrome && browser.android)
4093
4150
  view.observer.flushSoon();
@@ -4134,8 +4191,9 @@ class HeightOracle {
4134
4191
  this.lineWrapping = lineWrapping;
4135
4192
  this.doc = state.Text.empty;
4136
4193
  this.heightSamples = {};
4137
- this.lineHeight = 14;
4194
+ this.lineHeight = 14; // The height of an entire line (line-height)
4138
4195
  this.charWidth = 7;
4196
+ this.textHeight = 14; // The height of the actual font (font-size)
4139
4197
  this.lineLength = 30;
4140
4198
  // Used to track, during updateHeight, if any actual heights changed
4141
4199
  this.heightChanged = false;
@@ -4170,12 +4228,13 @@ class HeightOracle {
4170
4228
  }
4171
4229
  return newHeight;
4172
4230
  }
4173
- refresh(whiteSpace, lineHeight, charWidth, lineLength, knownHeights) {
4231
+ refresh(whiteSpace, lineHeight, charWidth, textHeight, lineLength, knownHeights) {
4174
4232
  let lineWrapping = wrappingWhiteSpace.indexOf(whiteSpace) > -1;
4175
4233
  let changed = Math.round(lineHeight) != Math.round(this.lineHeight) || this.lineWrapping != lineWrapping;
4176
4234
  this.lineWrapping = lineWrapping;
4177
4235
  this.lineHeight = lineHeight;
4178
4236
  this.charWidth = charWidth;
4237
+ this.textHeight = textHeight;
4179
4238
  this.lineLength = lineLength;
4180
4239
  if (changed) {
4181
4240
  this.heightSamples = {};
@@ -5023,8 +5082,8 @@ class ViewState {
5023
5082
  if (oracle.mustRefreshForHeights(lineHeights))
5024
5083
  refresh = true;
5025
5084
  if (refresh || oracle.lineWrapping && Math.abs(contentWidth - this.contentDOMWidth) > oracle.charWidth) {
5026
- let { lineHeight, charWidth } = view.docView.measureTextSize();
5027
- refresh = lineHeight > 0 && oracle.refresh(whiteSpace, lineHeight, charWidth, contentWidth / charWidth, lineHeights);
5085
+ let { lineHeight, charWidth, textHeight } = view.docView.measureTextSize();
5086
+ refresh = lineHeight > 0 && oracle.refresh(whiteSpace, lineHeight, charWidth, textHeight, contentWidth / charWidth, lineHeights);
5028
5087
  if (refresh) {
5029
5088
  view.docView.minWidth = 0;
5030
5089
  result |= 8 /* UpdateFlag.Geometry */;
@@ -5741,8 +5800,7 @@ function applyDOMChange(view, domChange) {
5741
5800
  }
5742
5801
  else {
5743
5802
  let changes = startState.changes(change);
5744
- let mainSel = newSel && !startState.selection.main.eq(newSel.main) && newSel.main.to <= changes.newLength
5745
- ? newSel.main : undefined;
5803
+ let mainSel = newSel && newSel.main.to <= changes.newLength ? newSel.main : undefined;
5746
5804
  // Try to apply a composition change to all cursors
5747
5805
  if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
5748
5806
  change.to <= sel.to && change.to >= sel.to - 10) {
@@ -5776,7 +5834,9 @@ function applyDOMChange(view, domChange) {
5776
5834
  }
5777
5835
  }
5778
5836
  let userEvent = "input.type";
5779
- if (view.composing) {
5837
+ if (view.composing ||
5838
+ view.inputState.compositionPendingChange && view.inputState.compositionEndedAt > Date.now() - 50) {
5839
+ view.inputState.compositionPendingChange = false;
5780
5840
  userEvent += ".compose";
5781
5841
  if (view.inputState.compositionFirstChange) {
5782
5842
  userEvent += ".start";
@@ -6152,10 +6212,13 @@ class DOMObserver {
6152
6212
  }
6153
6213
  this.flush();
6154
6214
  }
6155
- processRecords() {
6156
- let records = this.queue;
6215
+ pendingRecords() {
6157
6216
  for (let mut of this.observer.takeRecords())
6158
- records.push(mut);
6217
+ this.queue.push(mut);
6218
+ return this.queue;
6219
+ }
6220
+ processRecords() {
6221
+ let records = this.pendingRecords();
6159
6222
  if (records.length)
6160
6223
  this.queue = [];
6161
6224
  let from = -1, to = -1, typeOver = false;
package/dist/index.js CHANGED
@@ -516,6 +516,7 @@ class ContentView {
516
516
  static get(node) { return node.cmView; }
517
517
  get isEditable() { return true; }
518
518
  get isWidget() { return false; }
519
+ get isHidden() { return false; }
519
520
  merge(from, to, source, hasStart, openStart, openEnd) {
520
521
  return false;
521
522
  }
@@ -853,7 +854,7 @@ class WidgetView extends ContentView {
853
854
  become(other) {
854
855
  if (other.length == this.length && other instanceof WidgetView && other.side == this.side) {
855
856
  if (this.widget.constructor == other.widget.constructor) {
856
- if (!this.widget.eq(other.widget))
857
+ if (!this.widget.compare(other.widget))
857
858
  this.markDirty(true);
858
859
  if (this.dom && !this.prevWidget)
859
860
  this.prevWidget = this.widget;
@@ -875,7 +876,9 @@ class WidgetView extends ContentView {
875
876
  return text ? text.slice(start, start + this.length) : Text.empty;
876
877
  }
877
878
  domAtPos(pos) {
878
- return pos == 0 ? DOMPos.before(this.dom) : DOMPos.after(this.dom, pos == this.length);
879
+ return (this.length ? pos == 0 : this.side > 0)
880
+ ? DOMPos.before(this.dom)
881
+ : DOMPos.after(this.dom, pos == this.length);
879
882
  }
880
883
  domBoundsAround() { return null; }
881
884
  coordsAt(pos, side) {
@@ -891,6 +894,7 @@ class WidgetView extends ContentView {
891
894
  }
892
895
  get isEditable() { return false; }
893
896
  get isWidget() { return true; }
897
+ get isHidden() { return this.widget.isHidden; }
894
898
  destroy() {
895
899
  super.destroy();
896
900
  if (this.dom)
@@ -953,8 +957,9 @@ function scanCompositionTree(pos, side, view, text, enterView, fromText) {
953
957
  }
954
958
  function posFromDOMInCompositionTree(node, offset, view, text) {
955
959
  if (view instanceof MarkView) {
960
+ let pos = 0;
956
961
  for (let child of view.children) {
957
- let pos = 0, hasComp = contains(child.dom, text);
962
+ let hasComp = contains(child.dom, text);
958
963
  if (contains(child.dom, node))
959
964
  return pos + (hasComp ? posFromDOMInCompositionTree(node, offset, child, text) : child.localPosFromDOM(node, offset));
960
965
  pos += hasComp ? text.nodeValue.length : child.length;
@@ -988,7 +993,7 @@ class WidgetBufferView extends ContentView {
988
993
  }
989
994
  }
990
995
  getSide() { return this.side; }
991
- domAtPos(pos) { return DOMPos.before(this.dom); }
996
+ domAtPos(pos) { return this.side > 0 ? DOMPos.before(this.dom) : DOMPos.after(this.dom); }
992
997
  localPosFromDOM() { return 0; }
993
998
  domBoundsAround() { return null; }
994
999
  coordsAt(pos) {
@@ -1002,6 +1007,7 @@ class WidgetBufferView extends ContentView {
1002
1007
  get overrideDOMText() {
1003
1008
  return Text.empty;
1004
1009
  }
1010
+ get isHidden() { return true; }
1005
1011
  }
1006
1012
  TextView.prototype.children = WidgetView.prototype.children = WidgetBufferView.prototype.children = noChildren;
1007
1013
  function inlineSiblingRect(view, side) {
@@ -1075,7 +1081,8 @@ function coordsInChildren(view, pos, side) {
1075
1081
  if (child.children.length) {
1076
1082
  scan(child, pos - off);
1077
1083
  }
1078
- else if (!after && (end > pos || off == end && child.getSide() > 0)) {
1084
+ else if ((!after || after instanceof WidgetBufferView && side > 0) &&
1085
+ (end > pos || off == end && child.getSide() > 0)) {
1079
1086
  after = child;
1080
1087
  afterPos = pos - off;
1081
1088
  }
@@ -1189,6 +1196,10 @@ class WidgetType {
1189
1196
  */
1190
1197
  get customView() { return null; }
1191
1198
  /**
1199
+ @internal
1200
+ */
1201
+ get isHidden() { return false; }
1202
+ /**
1192
1203
  This is called when the an instance of the widget is removed
1193
1204
  from the editor view.
1194
1205
  */
@@ -1511,7 +1522,7 @@ class LineView extends ContentView {
1511
1522
  measureTextSize() {
1512
1523
  if (this.children.length == 0 || this.length > 20)
1513
1524
  return null;
1514
- let totalWidth = 0;
1525
+ let totalWidth = 0, textHeight;
1515
1526
  for (let child of this.children) {
1516
1527
  if (!(child instanceof TextView) || /[^ -~]/.test(child.text))
1517
1528
  return null;
@@ -1519,14 +1530,26 @@ class LineView extends ContentView {
1519
1530
  if (rects.length != 1)
1520
1531
  return null;
1521
1532
  totalWidth += rects[0].width;
1533
+ textHeight = rects[0].height;
1522
1534
  }
1523
1535
  return !totalWidth ? null : {
1524
1536
  lineHeight: this.dom.getBoundingClientRect().height,
1525
- charWidth: totalWidth / this.length
1537
+ charWidth: totalWidth / this.length,
1538
+ textHeight
1526
1539
  };
1527
1540
  }
1528
1541
  coordsAt(pos, side) {
1529
- return coordsInChildren(this, pos, side);
1542
+ let rect = coordsInChildren(this, pos, side);
1543
+ // Correct rectangle height for empty lines when the returned
1544
+ // height is larger than the text height.
1545
+ if (!this.children.length && rect && this.parent) {
1546
+ let { heightOracle } = this.parent.view.viewState, height = rect.bottom - rect.top;
1547
+ if (Math.abs(height - heightOracle.lineHeight) < 2 && heightOracle.textHeight < height) {
1548
+ let dist = (height - heightOracle.textHeight) / 2;
1549
+ return { top: rect.top + dist, bottom: rect.bottom - dist, left: rect.left, right: rect.left };
1550
+ }
1551
+ }
1552
+ return rect;
1530
1553
  }
1531
1554
  become(_other) { return false; }
1532
1555
  get type() { return BlockType.Text; }
@@ -1587,7 +1610,7 @@ class BlockWidgetView extends ContentView {
1587
1610
  become(other) {
1588
1611
  if (other instanceof BlockWidgetView && other.type == this.type &&
1589
1612
  other.widget.constructor == this.widget.constructor) {
1590
- if (!other.widget.eq(this.widget))
1613
+ if (!other.widget.compare(this.widget))
1591
1614
  this.markDirty(true);
1592
1615
  if (this.dom && !this.prevWidget)
1593
1616
  this.prevWidget = this.widget;
@@ -1718,10 +1741,11 @@ class ContentBuilder {
1718
1741
  }
1719
1742
  else {
1720
1743
  let view = WidgetView.create(deco.widget || new NullWidget("span"), len, len ? 0 : deco.startSide);
1721
- let cursorBefore = this.atCursorPos && !view.isEditable && openStart <= active.length && (from < to || deco.startSide > 0);
1744
+ let cursorBefore = this.atCursorPos && !view.isEditable && openStart <= active.length &&
1745
+ (from < to || deco.startSide > 0);
1722
1746
  let cursorAfter = !view.isEditable && (from < to || openStart > active.length || deco.startSide <= 0);
1723
1747
  let line = this.getLine();
1724
- if (this.pendingBuffer == 2 /* Buf.IfCursor */ && !cursorBefore)
1748
+ if (this.pendingBuffer == 2 /* Buf.IfCursor */ && !cursorBefore && !view.isEditable)
1725
1749
  this.pendingBuffer = 0 /* Buf.No */;
1726
1750
  this.flushBuffer(active);
1727
1751
  if (cursorBefore) {
@@ -1775,6 +1799,7 @@ class NullWidget extends WidgetType {
1775
1799
  eq(other) { return other.tag == this.tag; }
1776
1800
  toDOM() { return document.createElement(this.tag); }
1777
1801
  updateDOM(elt) { return elt.nodeName.toLowerCase() == this.tag; }
1802
+ get isHidden() { return true; }
1778
1803
  }
1779
1804
 
1780
1805
  const clickAddsSelectionRange = /*@__PURE__*/Facet.define();
@@ -2627,7 +2652,7 @@ class DocView extends ContentView {
2627
2652
  let head = main.empty ? anchor : this.domAtPos(main.head);
2628
2653
  // Always reset on Firefox when next to an uneditable node to
2629
2654
  // avoid invisible cursor bugs (#111)
2630
- if (browser.gecko && main.empty && betweenUneditable(anchor)) {
2655
+ if (browser.gecko && main.empty && !this.compositionDeco.size && betweenUneditable(anchor)) {
2631
2656
  let dummy = document.createTextNode("");
2632
2657
  this.view.observer.ignore(() => anchor.node.insertBefore(dummy, anchor.node.childNodes[anchor.offset] || null));
2633
2658
  anchor = head = new DOMPos(dummy, 0);
@@ -2806,7 +2831,7 @@ class DocView extends ContentView {
2806
2831
  }
2807
2832
  }
2808
2833
  // If no workable line exists, force a layout of a measurable element
2809
- let dummy = document.createElement("div"), lineHeight, charWidth;
2834
+ let dummy = document.createElement("div"), lineHeight, charWidth, textHeight;
2810
2835
  dummy.className = "cm-line";
2811
2836
  dummy.style.width = "99999px";
2812
2837
  dummy.textContent = "abc def ghi jkl mno pqr stu";
@@ -2815,9 +2840,10 @@ class DocView extends ContentView {
2815
2840
  let rect = clientRectsFor(dummy.firstChild)[0];
2816
2841
  lineHeight = dummy.getBoundingClientRect().height;
2817
2842
  charWidth = rect ? rect.width / 27 : 7;
2843
+ textHeight = rect ? rect.height : lineHeight;
2818
2844
  dummy.remove();
2819
2845
  });
2820
- return { lineHeight, charWidth };
2846
+ return { lineHeight, charWidth, textHeight };
2821
2847
  }
2822
2848
  childCursor(pos = this.length) {
2823
2849
  // Move back to start of last element when possible, so that
@@ -2982,22 +3008,32 @@ class CompositionWidget extends WidgetType {
2982
3008
  ignoreEvent() { return false; }
2983
3009
  get customView() { return CompositionView; }
2984
3010
  }
2985
- function nearbyTextNode(node, offset, side) {
2986
- for (;;) {
2987
- if (node.nodeType == 3)
2988
- return node;
2989
- if (node.nodeType == 1 && offset > 0 && side <= 0) {
2990
- node = node.childNodes[offset - 1];
2991
- offset = maxOffset(node);
2992
- }
2993
- else if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
2994
- node = node.childNodes[offset];
2995
- offset = 0;
3011
+ function nearbyTextNode(startNode, startOffset, side) {
3012
+ if (side <= 0)
3013
+ for (let node = startNode, offset = startOffset;;) {
3014
+ if (node.nodeType == 3)
3015
+ return node;
3016
+ if (node.nodeType == 1 && offset > 0) {
3017
+ node = node.childNodes[offset - 1];
3018
+ offset = maxOffset(node);
3019
+ }
3020
+ else {
3021
+ break;
3022
+ }
2996
3023
  }
2997
- else {
2998
- return null;
3024
+ if (side >= 0)
3025
+ for (let node = startNode, offset = startOffset;;) {
3026
+ if (node.nodeType == 3)
3027
+ return node;
3028
+ if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
3029
+ node = node.childNodes[offset];
3030
+ offset = 0;
3031
+ }
3032
+ else {
3033
+ break;
3034
+ }
2999
3035
  }
3000
- }
3036
+ return null;
3001
3037
  }
3002
3038
  function nextToUneditable(node, offset) {
3003
3039
  if (node.nodeType != 1)
@@ -3410,7 +3446,15 @@ class InputState {
3410
3446
  // first, false means first has already been marked for this
3411
3447
  // composition)
3412
3448
  this.compositionFirstChange = null;
3449
+ // End time of the previous composition
3413
3450
  this.compositionEndedAt = 0;
3451
+ // Used in a kludge to detect when an Enter keypress should be
3452
+ // considered part of the composition on Safari, which fires events
3453
+ // in the wrong order
3454
+ this.compositionPendingKey = false;
3455
+ // Used to categorize changes as part of a composition, even when
3456
+ // the mutation events fire shortly after the compositionend event
3457
+ this.compositionPendingChange = false;
3414
3458
  this.mouseSelection = null;
3415
3459
  let handleEvent = (handler, event) => {
3416
3460
  if (this.ignoreDuringComposition(event))
@@ -3433,8 +3477,16 @@ class InputState {
3433
3477
  this.registeredEvents.push(type);
3434
3478
  }
3435
3479
  view.scrollDOM.addEventListener("mousedown", (event) => {
3436
- if (event.target == view.scrollDOM && event.clientY > view.contentDOM.getBoundingClientRect().bottom)
3480
+ if (event.target == view.scrollDOM && event.clientY > view.contentDOM.getBoundingClientRect().bottom) {
3437
3481
  handleEvent(handlers.mousedown, event);
3482
+ if (!event.defaultPrevented && event.button == 2) {
3483
+ // Make sure the content covers the entire scroller height, in order
3484
+ // to catch a native context menu click below it
3485
+ let start = view.contentDOM.style.minHeight;
3486
+ view.contentDOM.style.minHeight = "100%";
3487
+ setTimeout(() => view.contentDOM.style.minHeight = start, 200);
3488
+ }
3489
+ }
3438
3490
  });
3439
3491
  if (browser.chrome && browser.chrome_version == 102) { // FIXME remove at some point
3440
3492
  // On Chrome 102, viewport updates somehow stop wheel-based
@@ -3559,8 +3611,8 @@ class InputState {
3559
3611
  // compositionend and keydown events are sometimes emitted in the
3560
3612
  // wrong order. The key event should still be ignored, even when
3561
3613
  // it happens after the compositionend event.
3562
- if (browser.safari && !browser.ios && Date.now() - this.compositionEndedAt < 100) {
3563
- this.compositionEndedAt = 0;
3614
+ if (browser.safari && !browser.ios && this.compositionPendingKey && Date.now() - this.compositionEndedAt < 100) {
3615
+ this.compositionPendingKey = false;
3564
3616
  return true;
3565
3617
  }
3566
3618
  return false;
@@ -3592,8 +3644,9 @@ const PendingKeys = [
3592
3644
  const EmacsyPendingKeys = "dthko";
3593
3645
  // Key codes for modifier keys
3594
3646
  const modifierCodes = [16, 17, 18, 20, 91, 92, 224, 225];
3647
+ const dragScrollMargin = 6;
3595
3648
  function dragScrollSpeed(dist) {
3596
- return dist * 0.7 + 8;
3649
+ return Math.max(0, dist) * 0.7 + 8;
3597
3650
  }
3598
3651
  class MouseSelection {
3599
3652
  constructor(view, startEvent, style, mustSelect) {
@@ -3630,13 +3683,13 @@ class MouseSelection {
3630
3683
  let sx = 0, sy = 0;
3631
3684
  let rect = ((_a = this.scrollParent) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect())
3632
3685
  || { left: 0, top: 0, right: this.view.win.innerWidth, bottom: this.view.win.innerHeight };
3633
- if (event.clientX <= rect.left)
3686
+ if (event.clientX <= rect.left + dragScrollMargin)
3634
3687
  sx = -dragScrollSpeed(rect.left - event.clientX);
3635
- else if (event.clientX >= rect.right)
3688
+ else if (event.clientX >= rect.right - dragScrollMargin)
3636
3689
  sx = dragScrollSpeed(event.clientX - rect.right);
3637
- if (event.clientY <= rect.top)
3690
+ if (event.clientY <= rect.top + dragScrollMargin)
3638
3691
  sy = -dragScrollSpeed(rect.top - event.clientY);
3639
- else if (event.clientY >= rect.bottom)
3692
+ else if (event.clientY >= rect.bottom - dragScrollMargin)
3640
3693
  sy = dragScrollSpeed(event.clientY - rect.bottom);
3641
3694
  this.setScrollSpeed(sx, sy);
3642
3695
  }
@@ -3883,7 +3936,7 @@ function basicMouseSelection(view, event) {
3883
3936
  }
3884
3937
  },
3885
3938
  get(event, extend, multiple) {
3886
- let cur = queryPos(view, event);
3939
+ let cur = queryPos(view, event), removed;
3887
3940
  let range = rangeForClick(view, cur.pos, cur.bias, type);
3888
3941
  if (start.pos != cur.pos && !extend) {
3889
3942
  let startRange = rangeForClick(view, start.pos, start.bias, type);
@@ -3892,8 +3945,8 @@ function basicMouseSelection(view, event) {
3892
3945
  }
3893
3946
  if (extend)
3894
3947
  return startSel.replaceRange(startSel.main.extend(range.from, range.to));
3895
- else if (multiple && startSel.ranges.length > 1 && startSel.ranges.some(r => r.eq(range)))
3896
- return removeRange(startSel, range);
3948
+ else if (multiple && type == 1 && startSel.ranges.length > 1 && (removed = removeRangeAround(startSel, cur.pos)))
3949
+ return removed;
3897
3950
  else if (multiple)
3898
3951
  return startSel.addRange(range);
3899
3952
  else
@@ -3901,11 +3954,13 @@ function basicMouseSelection(view, event) {
3901
3954
  }
3902
3955
  };
3903
3956
  }
3904
- function removeRange(sel, range) {
3905
- for (let i = 0;; i++) {
3906
- if (sel.ranges[i].eq(range))
3957
+ function removeRangeAround(sel, pos) {
3958
+ for (let i = 0; i < sel.ranges.length; i++) {
3959
+ let { from, to } = sel.ranges[i];
3960
+ if (from <= pos && to >= pos)
3907
3961
  return EditorSelection.create(sel.ranges.slice(0, i).concat(sel.ranges.slice(i + 1)), sel.mainIndex == i ? 0 : sel.mainIndex - (sel.mainIndex > i ? 1 : 0));
3908
3962
  }
3963
+ return null;
3909
3964
  }
3910
3965
  handlers.dragstart = (view, event) => {
3911
3966
  let { selection: { main } } = view.state;
@@ -4082,6 +4137,8 @@ handlers.compositionstart = handlers.compositionupdate = view => {
4082
4137
  handlers.compositionend = view => {
4083
4138
  view.inputState.composing = -1;
4084
4139
  view.inputState.compositionEndedAt = Date.now();
4140
+ view.inputState.compositionPendingKey = true;
4141
+ view.inputState.compositionPendingChange = view.observer.pendingRecords().length > 0;
4085
4142
  view.inputState.compositionFirstChange = null;
4086
4143
  if (browser.chrome && browser.android)
4087
4144
  view.observer.flushSoon();
@@ -4128,8 +4185,9 @@ class HeightOracle {
4128
4185
  this.lineWrapping = lineWrapping;
4129
4186
  this.doc = Text.empty;
4130
4187
  this.heightSamples = {};
4131
- this.lineHeight = 14;
4188
+ this.lineHeight = 14; // The height of an entire line (line-height)
4132
4189
  this.charWidth = 7;
4190
+ this.textHeight = 14; // The height of the actual font (font-size)
4133
4191
  this.lineLength = 30;
4134
4192
  // Used to track, during updateHeight, if any actual heights changed
4135
4193
  this.heightChanged = false;
@@ -4164,12 +4222,13 @@ class HeightOracle {
4164
4222
  }
4165
4223
  return newHeight;
4166
4224
  }
4167
- refresh(whiteSpace, lineHeight, charWidth, lineLength, knownHeights) {
4225
+ refresh(whiteSpace, lineHeight, charWidth, textHeight, lineLength, knownHeights) {
4168
4226
  let lineWrapping = wrappingWhiteSpace.indexOf(whiteSpace) > -1;
4169
4227
  let changed = Math.round(lineHeight) != Math.round(this.lineHeight) || this.lineWrapping != lineWrapping;
4170
4228
  this.lineWrapping = lineWrapping;
4171
4229
  this.lineHeight = lineHeight;
4172
4230
  this.charWidth = charWidth;
4231
+ this.textHeight = textHeight;
4173
4232
  this.lineLength = lineLength;
4174
4233
  if (changed) {
4175
4234
  this.heightSamples = {};
@@ -5016,8 +5075,8 @@ class ViewState {
5016
5075
  if (oracle.mustRefreshForHeights(lineHeights))
5017
5076
  refresh = true;
5018
5077
  if (refresh || oracle.lineWrapping && Math.abs(contentWidth - this.contentDOMWidth) > oracle.charWidth) {
5019
- let { lineHeight, charWidth } = view.docView.measureTextSize();
5020
- refresh = lineHeight > 0 && oracle.refresh(whiteSpace, lineHeight, charWidth, contentWidth / charWidth, lineHeights);
5078
+ let { lineHeight, charWidth, textHeight } = view.docView.measureTextSize();
5079
+ refresh = lineHeight > 0 && oracle.refresh(whiteSpace, lineHeight, charWidth, textHeight, contentWidth / charWidth, lineHeights);
5021
5080
  if (refresh) {
5022
5081
  view.docView.minWidth = 0;
5023
5082
  result |= 8 /* UpdateFlag.Geometry */;
@@ -5734,8 +5793,7 @@ function applyDOMChange(view, domChange) {
5734
5793
  }
5735
5794
  else {
5736
5795
  let changes = startState.changes(change);
5737
- let mainSel = newSel && !startState.selection.main.eq(newSel.main) && newSel.main.to <= changes.newLength
5738
- ? newSel.main : undefined;
5796
+ let mainSel = newSel && newSel.main.to <= changes.newLength ? newSel.main : undefined;
5739
5797
  // Try to apply a composition change to all cursors
5740
5798
  if (startState.selection.ranges.length > 1 && view.inputState.composing >= 0 &&
5741
5799
  change.to <= sel.to && change.to >= sel.to - 10) {
@@ -5769,7 +5827,9 @@ function applyDOMChange(view, domChange) {
5769
5827
  }
5770
5828
  }
5771
5829
  let userEvent = "input.type";
5772
- if (view.composing) {
5830
+ if (view.composing ||
5831
+ view.inputState.compositionPendingChange && view.inputState.compositionEndedAt > Date.now() - 50) {
5832
+ view.inputState.compositionPendingChange = false;
5773
5833
  userEvent += ".compose";
5774
5834
  if (view.inputState.compositionFirstChange) {
5775
5835
  userEvent += ".start";
@@ -6145,10 +6205,13 @@ class DOMObserver {
6145
6205
  }
6146
6206
  this.flush();
6147
6207
  }
6148
- processRecords() {
6149
- let records = this.queue;
6208
+ pendingRecords() {
6150
6209
  for (let mut of this.observer.takeRecords())
6151
- records.push(mut);
6210
+ this.queue.push(mut);
6211
+ return this.queue;
6212
+ }
6213
+ processRecords() {
6214
+ let records = this.pendingRecords();
6152
6215
  if (records.length)
6153
6216
  this.queue = [];
6154
6217
  let from = -1, to = -1, typeOver = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.9.2",
3
+ "version": "6.9.4",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",