@codemirror/view 6.26.0 → 6.26.2

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,23 @@
1
+ ## 6.26.2 (2024-04-09)
2
+
3
+ ### Bug fixes
4
+
5
+ Improve behavior of `scrollPastEnd` in a scaled editor.
6
+
7
+ When available, use `Selection.getComposedRanges` on Safari to find the selection inside a shadow DOM.
8
+
9
+ Remove the workaround that avoided inappropriate styling on composed text after a decoration again, since it breaks the stock Android virtual keyboard.
10
+
11
+ ## 6.26.1 (2024-03-28)
12
+
13
+ ### Bug fixes
14
+
15
+ Fix the editor getting stuck in composition when Safari fails to fire a compositionend event for a dead key composition.
16
+
17
+ Fix an issue where, with IME systems that kept the cursor at the start of the composed text, the editor misidentified the target node and disrupted composition.
18
+
19
+ Fix a bug where in a line-wrapped editor, with some content, the initial scroll position would be off from the top of the document.
20
+
1
21
  ## 6.26.0 (2024-03-14)
2
22
 
3
23
  ### Bug fixes
package/dist/index.cjs CHANGED
@@ -61,6 +61,9 @@ function domIndex(node) {
61
61
  return index;
62
62
  }
63
63
  }
64
+ function isBlockElement(node) {
65
+ return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
66
+ }
64
67
  function scanFor(node, off, targetNode, targetOff, dir) {
65
68
  for (;;) {
66
69
  if (node == targetNode && off == targetOff)
@@ -342,6 +345,46 @@ function atElementStart(doc, selection) {
342
345
  function isScrolledToBottom(elt) {
343
346
  return elt.scrollTop > Math.max(1, elt.scrollHeight - elt.clientHeight - 4);
344
347
  }
348
+ function textNodeBefore(startNode, startOffset) {
349
+ for (let node = startNode, offset = startOffset;;) {
350
+ if (node.nodeType == 3 && offset > 0) {
351
+ return { node: node, offset: offset };
352
+ }
353
+ else if (node.nodeType == 1 && offset > 0) {
354
+ if (node.contentEditable == "false")
355
+ return null;
356
+ node = node.childNodes[offset - 1];
357
+ offset = maxOffset(node);
358
+ }
359
+ else if (node.parentNode && !isBlockElement(node)) {
360
+ offset = domIndex(node);
361
+ node = node.parentNode;
362
+ }
363
+ else {
364
+ return null;
365
+ }
366
+ }
367
+ }
368
+ function textNodeAfter(startNode, startOffset) {
369
+ for (let node = startNode, offset = startOffset;;) {
370
+ if (node.nodeType == 3 && offset < node.nodeValue.length) {
371
+ return { node: node, offset: offset };
372
+ }
373
+ else if (node.nodeType == 1 && offset < node.childNodes.length) {
374
+ if (node.contentEditable == "false")
375
+ return null;
376
+ node = node.childNodes[offset];
377
+ offset = 0;
378
+ }
379
+ else if (node.parentNode && !isBlockElement(node)) {
380
+ offset = domIndex(node) + 1;
381
+ node = node.parentNode;
382
+ }
383
+ else {
384
+ return null;
385
+ }
386
+ }
387
+ }
345
388
 
346
389
  class DOMPos {
347
390
  constructor(node, offset, precise = true) {
@@ -2661,11 +2704,11 @@ class DocView extends ContentView {
2661
2704
  super();
2662
2705
  this.view = view;
2663
2706
  this.decorations = [];
2664
- this.dynamicDecorationMap = [false];
2707
+ this.dynamicDecorationMap = [];
2665
2708
  this.domChanged = null;
2666
2709
  this.hasComposition = null;
2667
2710
  this.markedForComposition = new Set;
2668
- this.compositionBarrier = Decoration.none;
2711
+ this.lastCompositionAfterCursor = false;
2669
2712
  // Track a minimum width for the editor. When measuring sizes in
2670
2713
  // measureVisibleLineHeights, this is updated to point at the width
2671
2714
  // of a given element and its extent in the document. When a change
@@ -2882,7 +2925,7 @@ class DocView extends ContentView {
2882
2925
  if (browser.gecko) {
2883
2926
  let nextTo = nextToUneditable(anchor.node, anchor.offset);
2884
2927
  if (nextTo && nextTo != (1 /* NextTo.Before */ | 2 /* NextTo.After */)) {
2885
- let text = nearbyTextNode(anchor.node, anchor.offset, nextTo == 1 /* NextTo.Before */ ? 1 : -1);
2928
+ let text = (nextTo == 1 /* NextTo.Before */ ? textNodeBefore : textNodeAfter)(anchor.node, anchor.offset);
2886
2929
  if (text)
2887
2930
  anchor = new DOMPos(text.node, text.offset);
2888
2931
  }
@@ -2929,7 +2972,7 @@ class DocView extends ContentView {
2929
2972
  // composition, avoid moving it across it and disrupting the
2930
2973
  // composition.
2931
2974
  suppressWidgetCursorChange(sel, cursor) {
2932
- return this.hasComposition && cursor.empty && !this.compositionBarrier.size &&
2975
+ return this.hasComposition && cursor.empty &&
2933
2976
  isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset) &&
2934
2977
  this.posFromDOM(sel.focusNode, sel.focusOffset) == cursor.head;
2935
2978
  }
@@ -3137,7 +3180,7 @@ class DocView extends ContentView {
3137
3180
  return Decoration.set(deco);
3138
3181
  }
3139
3182
  updateDeco() {
3140
- let i = 1;
3183
+ let i = 0;
3141
3184
  let allDeco = this.view.state.facet(decorations).map(d => {
3142
3185
  let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
3143
3186
  return dynamic ? d(this.view) : d;
@@ -3153,7 +3196,6 @@ class DocView extends ContentView {
3153
3196
  allDeco.push(state.RangeSet.join(outerDeco));
3154
3197
  }
3155
3198
  this.decorations = [
3156
- this.compositionBarrier,
3157
3199
  ...allDeco,
3158
3200
  this.computeBlockGapDeco(),
3159
3201
  this.view.viewState.lineGapDeco
@@ -3162,34 +3204,6 @@ class DocView extends ContentView {
3162
3204
  this.dynamicDecorationMap[i++] = false;
3163
3205
  return this.decorations;
3164
3206
  }
3165
- // Starting a composition will style the inserted text with the
3166
- // style of the text before it, and this is only cleared when the
3167
- // composition ends, because touching it before that will abort it.
3168
- // This (called from compositionstart handler) tries to notice when
3169
- // the cursor is after a non-inclusive mark, where the styling could
3170
- // be jarring, and insert an ad-hoc widget before the cursor to
3171
- // isolate it from the style before it.
3172
- maybeCreateCompositionBarrier() {
3173
- let { main: { head, empty } } = this.view.state.selection;
3174
- if (!empty)
3175
- return false;
3176
- let found = null;
3177
- for (let set of this.decorations) {
3178
- set.between(head, head, (from, to, value) => {
3179
- if (value.point)
3180
- found = false;
3181
- else if (value.endSide < 0 && from < head && to == head)
3182
- found = true;
3183
- });
3184
- if (found === false)
3185
- break;
3186
- }
3187
- this.compositionBarrier = found ? Decoration.set(compositionBarrierWidget.range(head)) : Decoration.none;
3188
- return !!found;
3189
- }
3190
- clearCompositionBarrier() {
3191
- this.compositionBarrier = Decoration.none;
3192
- }
3193
3207
  scrollIntoView(target) {
3194
3208
  if (target.isSnapshot) {
3195
3209
  let ref = this.view.viewState.lineBlockAt(target.range.head);
@@ -3222,7 +3236,6 @@ class DocView extends ContentView {
3222
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);
3223
3237
  }
3224
3238
  }
3225
- const compositionBarrierWidget = Decoration.widget({ side: -1, widget: NullWidget.inline });
3226
3239
  function betweenUneditable(pos) {
3227
3240
  return pos.node.nodeType == 1 && pos.node.firstChild &&
3228
3241
  (pos.offset == 0 || pos.node.childNodes[pos.offset - 1].contentEditable == "false") &&
@@ -3250,7 +3263,23 @@ class BlockGapWidget extends WidgetType {
3250
3263
  }
3251
3264
  function findCompositionNode(view, headPos) {
3252
3265
  let sel = view.observer.selectionRange;
3253
- let textNode = sel.focusNode && nearbyTextNode(sel.focusNode, sel.focusOffset, 0);
3266
+ if (!sel.focusNode)
3267
+ return null;
3268
+ let textBefore = textNodeBefore(sel.focusNode, sel.focusOffset);
3269
+ let textAfter = textNodeAfter(sel.focusNode, sel.focusOffset);
3270
+ let textNode = textBefore || textAfter;
3271
+ if (textAfter && textBefore && textAfter.node != textBefore.node) {
3272
+ let descAfter = ContentView.get(textAfter.node);
3273
+ if (!descAfter || descAfter instanceof TextView && descAfter.text != textAfter.node.nodeValue) {
3274
+ textNode = textAfter;
3275
+ }
3276
+ else if (view.docView.lastCompositionAfterCursor) {
3277
+ let descBefore = ContentView.get(textBefore.node);
3278
+ if (!(!descBefore || descBefore instanceof TextView && descBefore.text != textBefore.node.nodeValue))
3279
+ textNode = textAfter;
3280
+ }
3281
+ }
3282
+ view.docView.lastCompositionAfterCursor = textNode != textBefore;
3254
3283
  if (!textNode)
3255
3284
  return null;
3256
3285
  let from = headPos - textNode.offset;
@@ -3285,33 +3314,6 @@ function findCompositionRange(view, changes, headPos) {
3285
3314
  return null;
3286
3315
  }
3287
3316
  }
3288
- function nearbyTextNode(startNode, startOffset, side) {
3289
- if (side <= 0)
3290
- for (let node = startNode, offset = startOffset;;) {
3291
- if (node.nodeType == 3)
3292
- return { node: node, offset: offset };
3293
- if (node.nodeType == 1 && offset > 0) {
3294
- node = node.childNodes[offset - 1];
3295
- offset = maxOffset(node);
3296
- }
3297
- else {
3298
- break;
3299
- }
3300
- }
3301
- if (side >= 0)
3302
- for (let node = startNode, offset = startOffset;;) {
3303
- if (node.nodeType == 3)
3304
- return { node: node, offset: offset };
3305
- if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
3306
- node = node.childNodes[offset];
3307
- offset = 0;
3308
- }
3309
- else {
3310
- break;
3311
- }
3312
- }
3313
- return null;
3314
- }
3315
3317
  function nextToUneditable(node, offset) {
3316
3318
  if (node.nodeType != 1)
3317
3319
  return 0;
@@ -4475,10 +4477,6 @@ observers.compositionstart = observers.compositionupdate = view => {
4475
4477
  if (view.inputState.composing < 0) {
4476
4478
  // FIXME possibly set a timeout to clear it again on Android
4477
4479
  view.inputState.composing = 0;
4478
- if (view.docView.maybeCreateCompositionBarrier()) {
4479
- view.update([]);
4480
- view.docView.clearCompositionBarrier();
4481
- }
4482
4480
  }
4483
4481
  };
4484
4482
  observers.compositionend = view => {
@@ -4540,6 +4538,10 @@ handlers.beforeinput = (view, event) => {
4540
4538
  // keyboard.
4541
4539
  view.observer.flushSoon();
4542
4540
  }
4541
+ // Safari will occasionally forget to fire compositionend at the end of a dead-key composition
4542
+ if (browser.safari && event.inputType == "insertText" && view.inputState.composing >= 0) {
4543
+ setTimeout(() => observers.compositionend(view, event), 20);
4544
+ }
4543
4545
  return false;
4544
4546
  };
4545
4547
  const appliedFirefoxHack = new Set;
@@ -4902,7 +4904,8 @@ class HeightMapGap extends HeightMap {
4902
4904
  blockAt(height, oracle, top, offset) {
4903
4905
  let { firstLine, lastLine, perLine, perChar } = this.heightMetrics(oracle, offset);
4904
4906
  if (oracle.lineWrapping) {
4905
- let guess = offset + Math.round(Math.max(0, Math.min(1, (height - top) / this.height)) * this.length);
4907
+ let guess = offset + (height < oracle.lineHeight ? 0
4908
+ : Math.round(Math.max(0, Math.min(1, (height - top) / this.height)) * this.length));
4906
4909
  let line = oracle.doc.lineAt(guess), lineHeight = perLine + line.length * perChar;
4907
4910
  let lineTop = Math.max(top, height - lineHeight / 2);
4908
4911
  return new BlockInfo(line.from, line.length, lineTop, lineHeight, 0);
@@ -6238,9 +6241,6 @@ function isAtEnd(parent, node, offset) {
6238
6241
  node = node.parentNode;
6239
6242
  }
6240
6243
  }
6241
- function isBlockElement(node) {
6242
- return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
6243
- }
6244
6244
  class DOMPoint {
6245
6245
  constructor(node, offset) {
6246
6246
  this.node = node;
@@ -6676,9 +6676,12 @@ class DOMObserver {
6676
6676
  let { view } = this;
6677
6677
  // The Selection object is broken in shadow roots in Safari. See
6678
6678
  // https://github.com/codemirror/dev/issues/414
6679
+ let selection = getSelection(view.root);
6680
+ if (!selection)
6681
+ return false;
6679
6682
  let range = browser.safari && view.root.nodeType == 11 &&
6680
6683
  deepActiveElement(this.dom.ownerDocument) == this.dom &&
6681
- safariSelectionRangeHack(this.view) || getSelection(view.root);
6684
+ safariSelectionRangeHack(this.view, selection) || selection;
6682
6685
  if (!range || this.selectionRange.eq(range))
6683
6686
  return false;
6684
6687
  let local = hasSelection(this.dom, range);
@@ -6950,8 +6953,24 @@ function findChild(cView, dom, dir) {
6950
6953
  }
6951
6954
  return null;
6952
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
+ }
6953
6967
  // Used to work around a Safari Selection/shadow DOM bug (#414)
6954
- 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
+ }
6955
6974
  let found = null;
6956
6975
  // Because Safari (at least in 2018-2021) doesn't provide regular
6957
6976
  // access to the selection inside a shadowroot, we have to perform a
@@ -6966,17 +6985,7 @@ function safariSelectionRangeHack(view) {
6966
6985
  view.contentDOM.addEventListener("beforeinput", read, true);
6967
6986
  view.dom.ownerDocument.execCommand("indent");
6968
6987
  view.contentDOM.removeEventListener("beforeinput", read, true);
6969
- if (!found)
6970
- return null;
6971
- let anchorNode = found.startContainer, anchorOffset = found.startOffset;
6972
- let focusNode = found.endContainer, focusOffset = found.endOffset;
6973
- let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
6974
- // Since such a range doesn't distinguish between anchor and head,
6975
- // use a heuristic that flips it around if its end matches the
6976
- // current anchor.
6977
- if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
6978
- [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
6979
- return { anchorNode, anchorOffset, focusNode, focusOffset };
6988
+ return found ? buildSelectionRangeFromRange(view, found) : null;
6980
6989
  }
6981
6990
 
6982
6991
  // The editor's update state machine looks something like this:
@@ -9067,7 +9076,7 @@ const plugin = ViewPlugin.fromClass(class {
9067
9076
  }
9068
9077
  update(update) {
9069
9078
  let { view } = update;
9070
- let height = view.viewState.editorHeight * view.scaleY -
9079
+ let height = view.viewState.editorHeight -
9071
9080
  view.defaultLineHeight - view.documentPadding.top - 0.5;
9072
9081
  if (height >= 0 && height != this.height) {
9073
9082
  this.height = height;
package/dist/index.js CHANGED
@@ -59,6 +59,9 @@ function domIndex(node) {
59
59
  return index;
60
60
  }
61
61
  }
62
+ function isBlockElement(node) {
63
+ return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
64
+ }
62
65
  function scanFor(node, off, targetNode, targetOff, dir) {
63
66
  for (;;) {
64
67
  if (node == targetNode && off == targetOff)
@@ -340,6 +343,46 @@ function atElementStart(doc, selection) {
340
343
  function isScrolledToBottom(elt) {
341
344
  return elt.scrollTop > Math.max(1, elt.scrollHeight - elt.clientHeight - 4);
342
345
  }
346
+ function textNodeBefore(startNode, startOffset) {
347
+ for (let node = startNode, offset = startOffset;;) {
348
+ if (node.nodeType == 3 && offset > 0) {
349
+ return { node: node, offset: offset };
350
+ }
351
+ else if (node.nodeType == 1 && offset > 0) {
352
+ if (node.contentEditable == "false")
353
+ return null;
354
+ node = node.childNodes[offset - 1];
355
+ offset = maxOffset(node);
356
+ }
357
+ else if (node.parentNode && !isBlockElement(node)) {
358
+ offset = domIndex(node);
359
+ node = node.parentNode;
360
+ }
361
+ else {
362
+ return null;
363
+ }
364
+ }
365
+ }
366
+ function textNodeAfter(startNode, startOffset) {
367
+ for (let node = startNode, offset = startOffset;;) {
368
+ if (node.nodeType == 3 && offset < node.nodeValue.length) {
369
+ return { node: node, offset: offset };
370
+ }
371
+ else if (node.nodeType == 1 && offset < node.childNodes.length) {
372
+ if (node.contentEditable == "false")
373
+ return null;
374
+ node = node.childNodes[offset];
375
+ offset = 0;
376
+ }
377
+ else if (node.parentNode && !isBlockElement(node)) {
378
+ offset = domIndex(node) + 1;
379
+ node = node.parentNode;
380
+ }
381
+ else {
382
+ return null;
383
+ }
384
+ }
385
+ }
343
386
 
344
387
  class DOMPos {
345
388
  constructor(node, offset, precise = true) {
@@ -2657,11 +2700,11 @@ class DocView extends ContentView {
2657
2700
  super();
2658
2701
  this.view = view;
2659
2702
  this.decorations = [];
2660
- this.dynamicDecorationMap = [false];
2703
+ this.dynamicDecorationMap = [];
2661
2704
  this.domChanged = null;
2662
2705
  this.hasComposition = null;
2663
2706
  this.markedForComposition = new Set;
2664
- this.compositionBarrier = Decoration.none;
2707
+ this.lastCompositionAfterCursor = false;
2665
2708
  // Track a minimum width for the editor. When measuring sizes in
2666
2709
  // measureVisibleLineHeights, this is updated to point at the width
2667
2710
  // of a given element and its extent in the document. When a change
@@ -2878,7 +2921,7 @@ class DocView extends ContentView {
2878
2921
  if (browser.gecko) {
2879
2922
  let nextTo = nextToUneditable(anchor.node, anchor.offset);
2880
2923
  if (nextTo && nextTo != (1 /* NextTo.Before */ | 2 /* NextTo.After */)) {
2881
- let text = nearbyTextNode(anchor.node, anchor.offset, nextTo == 1 /* NextTo.Before */ ? 1 : -1);
2924
+ let text = (nextTo == 1 /* NextTo.Before */ ? textNodeBefore : textNodeAfter)(anchor.node, anchor.offset);
2882
2925
  if (text)
2883
2926
  anchor = new DOMPos(text.node, text.offset);
2884
2927
  }
@@ -2925,7 +2968,7 @@ class DocView extends ContentView {
2925
2968
  // composition, avoid moving it across it and disrupting the
2926
2969
  // composition.
2927
2970
  suppressWidgetCursorChange(sel, cursor) {
2928
- return this.hasComposition && cursor.empty && !this.compositionBarrier.size &&
2971
+ return this.hasComposition && cursor.empty &&
2929
2972
  isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset) &&
2930
2973
  this.posFromDOM(sel.focusNode, sel.focusOffset) == cursor.head;
2931
2974
  }
@@ -3133,7 +3176,7 @@ class DocView extends ContentView {
3133
3176
  return Decoration.set(deco);
3134
3177
  }
3135
3178
  updateDeco() {
3136
- let i = 1;
3179
+ let i = 0;
3137
3180
  let allDeco = this.view.state.facet(decorations).map(d => {
3138
3181
  let dynamic = this.dynamicDecorationMap[i++] = typeof d == "function";
3139
3182
  return dynamic ? d(this.view) : d;
@@ -3149,7 +3192,6 @@ class DocView extends ContentView {
3149
3192
  allDeco.push(RangeSet.join(outerDeco));
3150
3193
  }
3151
3194
  this.decorations = [
3152
- this.compositionBarrier,
3153
3195
  ...allDeco,
3154
3196
  this.computeBlockGapDeco(),
3155
3197
  this.view.viewState.lineGapDeco
@@ -3158,34 +3200,6 @@ class DocView extends ContentView {
3158
3200
  this.dynamicDecorationMap[i++] = false;
3159
3201
  return this.decorations;
3160
3202
  }
3161
- // Starting a composition will style the inserted text with the
3162
- // style of the text before it, and this is only cleared when the
3163
- // composition ends, because touching it before that will abort it.
3164
- // This (called from compositionstart handler) tries to notice when
3165
- // the cursor is after a non-inclusive mark, where the styling could
3166
- // be jarring, and insert an ad-hoc widget before the cursor to
3167
- // isolate it from the style before it.
3168
- maybeCreateCompositionBarrier() {
3169
- let { main: { head, empty } } = this.view.state.selection;
3170
- if (!empty)
3171
- return false;
3172
- let found = null;
3173
- for (let set of this.decorations) {
3174
- set.between(head, head, (from, to, value) => {
3175
- if (value.point)
3176
- found = false;
3177
- else if (value.endSide < 0 && from < head && to == head)
3178
- found = true;
3179
- });
3180
- if (found === false)
3181
- break;
3182
- }
3183
- this.compositionBarrier = found ? Decoration.set(compositionBarrierWidget.range(head)) : Decoration.none;
3184
- return !!found;
3185
- }
3186
- clearCompositionBarrier() {
3187
- this.compositionBarrier = Decoration.none;
3188
- }
3189
3203
  scrollIntoView(target) {
3190
3204
  if (target.isSnapshot) {
3191
3205
  let ref = this.view.viewState.lineBlockAt(target.range.head);
@@ -3218,7 +3232,6 @@ class DocView extends ContentView {
3218
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);
3219
3233
  }
3220
3234
  }
3221
- const compositionBarrierWidget = /*@__PURE__*/Decoration.widget({ side: -1, widget: NullWidget.inline });
3222
3235
  function betweenUneditable(pos) {
3223
3236
  return pos.node.nodeType == 1 && pos.node.firstChild &&
3224
3237
  (pos.offset == 0 || pos.node.childNodes[pos.offset - 1].contentEditable == "false") &&
@@ -3246,7 +3259,23 @@ class BlockGapWidget extends WidgetType {
3246
3259
  }
3247
3260
  function findCompositionNode(view, headPos) {
3248
3261
  let sel = view.observer.selectionRange;
3249
- let textNode = sel.focusNode && nearbyTextNode(sel.focusNode, sel.focusOffset, 0);
3262
+ if (!sel.focusNode)
3263
+ return null;
3264
+ let textBefore = textNodeBefore(sel.focusNode, sel.focusOffset);
3265
+ let textAfter = textNodeAfter(sel.focusNode, sel.focusOffset);
3266
+ let textNode = textBefore || textAfter;
3267
+ if (textAfter && textBefore && textAfter.node != textBefore.node) {
3268
+ let descAfter = ContentView.get(textAfter.node);
3269
+ if (!descAfter || descAfter instanceof TextView && descAfter.text != textAfter.node.nodeValue) {
3270
+ textNode = textAfter;
3271
+ }
3272
+ else if (view.docView.lastCompositionAfterCursor) {
3273
+ let descBefore = ContentView.get(textBefore.node);
3274
+ if (!(!descBefore || descBefore instanceof TextView && descBefore.text != textBefore.node.nodeValue))
3275
+ textNode = textAfter;
3276
+ }
3277
+ }
3278
+ view.docView.lastCompositionAfterCursor = textNode != textBefore;
3250
3279
  if (!textNode)
3251
3280
  return null;
3252
3281
  let from = headPos - textNode.offset;
@@ -3281,33 +3310,6 @@ function findCompositionRange(view, changes, headPos) {
3281
3310
  return null;
3282
3311
  }
3283
3312
  }
3284
- function nearbyTextNode(startNode, startOffset, side) {
3285
- if (side <= 0)
3286
- for (let node = startNode, offset = startOffset;;) {
3287
- if (node.nodeType == 3)
3288
- return { node: node, offset: offset };
3289
- if (node.nodeType == 1 && offset > 0) {
3290
- node = node.childNodes[offset - 1];
3291
- offset = maxOffset(node);
3292
- }
3293
- else {
3294
- break;
3295
- }
3296
- }
3297
- if (side >= 0)
3298
- for (let node = startNode, offset = startOffset;;) {
3299
- if (node.nodeType == 3)
3300
- return { node: node, offset: offset };
3301
- if (node.nodeType == 1 && offset < node.childNodes.length && side >= 0) {
3302
- node = node.childNodes[offset];
3303
- offset = 0;
3304
- }
3305
- else {
3306
- break;
3307
- }
3308
- }
3309
- return null;
3310
- }
3311
3313
  function nextToUneditable(node, offset) {
3312
3314
  if (node.nodeType != 1)
3313
3315
  return 0;
@@ -4471,10 +4473,6 @@ observers.compositionstart = observers.compositionupdate = view => {
4471
4473
  if (view.inputState.composing < 0) {
4472
4474
  // FIXME possibly set a timeout to clear it again on Android
4473
4475
  view.inputState.composing = 0;
4474
- if (view.docView.maybeCreateCompositionBarrier()) {
4475
- view.update([]);
4476
- view.docView.clearCompositionBarrier();
4477
- }
4478
4476
  }
4479
4477
  };
4480
4478
  observers.compositionend = view => {
@@ -4536,6 +4534,10 @@ handlers.beforeinput = (view, event) => {
4536
4534
  // keyboard.
4537
4535
  view.observer.flushSoon();
4538
4536
  }
4537
+ // Safari will occasionally forget to fire compositionend at the end of a dead-key composition
4538
+ if (browser.safari && event.inputType == "insertText" && view.inputState.composing >= 0) {
4539
+ setTimeout(() => observers.compositionend(view, event), 20);
4540
+ }
4539
4541
  return false;
4540
4542
  };
4541
4543
  const appliedFirefoxHack = /*@__PURE__*/new Set;
@@ -4897,7 +4899,8 @@ class HeightMapGap extends HeightMap {
4897
4899
  blockAt(height, oracle, top, offset) {
4898
4900
  let { firstLine, lastLine, perLine, perChar } = this.heightMetrics(oracle, offset);
4899
4901
  if (oracle.lineWrapping) {
4900
- let guess = offset + Math.round(Math.max(0, Math.min(1, (height - top) / this.height)) * this.length);
4902
+ let guess = offset + (height < oracle.lineHeight ? 0
4903
+ : Math.round(Math.max(0, Math.min(1, (height - top) / this.height)) * this.length));
4901
4904
  let line = oracle.doc.lineAt(guess), lineHeight = perLine + line.length * perChar;
4902
4905
  let lineTop = Math.max(top, height - lineHeight / 2);
4903
4906
  return new BlockInfo(line.from, line.length, lineTop, lineHeight, 0);
@@ -6233,9 +6236,6 @@ function isAtEnd(parent, node, offset) {
6233
6236
  node = node.parentNode;
6234
6237
  }
6235
6238
  }
6236
- function isBlockElement(node) {
6237
- return node.nodeType == 1 && /^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(node.nodeName);
6238
- }
6239
6239
  class DOMPoint {
6240
6240
  constructor(node, offset) {
6241
6241
  this.node = node;
@@ -6671,9 +6671,12 @@ class DOMObserver {
6671
6671
  let { view } = this;
6672
6672
  // The Selection object is broken in shadow roots in Safari. See
6673
6673
  // https://github.com/codemirror/dev/issues/414
6674
+ let selection = getSelection(view.root);
6675
+ if (!selection)
6676
+ return false;
6674
6677
  let range = browser.safari && view.root.nodeType == 11 &&
6675
6678
  deepActiveElement(this.dom.ownerDocument) == this.dom &&
6676
- safariSelectionRangeHack(this.view) || getSelection(view.root);
6679
+ safariSelectionRangeHack(this.view, selection) || selection;
6677
6680
  if (!range || this.selectionRange.eq(range))
6678
6681
  return false;
6679
6682
  let local = hasSelection(this.dom, range);
@@ -6945,8 +6948,24 @@ function findChild(cView, dom, dir) {
6945
6948
  }
6946
6949
  return null;
6947
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
+ }
6948
6962
  // Used to work around a Safari Selection/shadow DOM bug (#414)
6949
- 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
+ }
6950
6969
  let found = null;
6951
6970
  // Because Safari (at least in 2018-2021) doesn't provide regular
6952
6971
  // access to the selection inside a shadowroot, we have to perform a
@@ -6961,17 +6980,7 @@ function safariSelectionRangeHack(view) {
6961
6980
  view.contentDOM.addEventListener("beforeinput", read, true);
6962
6981
  view.dom.ownerDocument.execCommand("indent");
6963
6982
  view.contentDOM.removeEventListener("beforeinput", read, true);
6964
- if (!found)
6965
- return null;
6966
- let anchorNode = found.startContainer, anchorOffset = found.startOffset;
6967
- let focusNode = found.endContainer, focusOffset = found.endOffset;
6968
- let curAnchor = view.docView.domAtPos(view.state.selection.main.anchor);
6969
- // Since such a range doesn't distinguish between anchor and head,
6970
- // use a heuristic that flips it around if its end matches the
6971
- // current anchor.
6972
- if (isEquivalentPosition(curAnchor.node, curAnchor.offset, focusNode, focusOffset))
6973
- [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset];
6974
- return { anchorNode, anchorOffset, focusNode, focusOffset };
6983
+ return found ? buildSelectionRangeFromRange(view, found) : null;
6975
6984
  }
6976
6985
 
6977
6986
  // The editor's update state machine looks something like this:
@@ -9062,7 +9071,7 @@ const plugin = /*@__PURE__*/ViewPlugin.fromClass(class {
9062
9071
  }
9063
9072
  update(update) {
9064
9073
  let { view } = update;
9065
- let height = view.viewState.editorHeight * view.scaleY -
9074
+ let height = view.viewState.editorHeight -
9066
9075
  view.defaultLineHeight - view.documentPadding.top - 0.5;
9067
9076
  if (height >= 0 && height != this.height) {
9068
9077
  this.height = height;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.26.0",
3
+ "version": "6.26.2",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",