@codemirror/view 6.39.0-beta.2 → 6.39.0-beta.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/dist/index.cjs CHANGED
@@ -1598,6 +1598,8 @@ class ChangedRange {
1598
1598
  let end = ranges[rI + 1];
1599
1599
  rI += 2;
1600
1600
  toB = Math.max(toB, end);
1601
+ for (let i = dI; i < diff.length && diff[i].fromB <= toB; i++)
1602
+ off = diff[i].toA - diff[i].toB;
1601
1603
  toA = Math.max(toA, end + off);
1602
1604
  }
1603
1605
  else if (dI < diff.length && diff[dI].fromB <= toB) {
@@ -1750,8 +1752,8 @@ class Tile {
1750
1752
  get posAtEnd() {
1751
1753
  return this.posAtStart + this.length;
1752
1754
  }
1753
- posBefore(tile) {
1754
- let pos = this.posAtStart;
1755
+ posBefore(tile, start = this.posAtStart) {
1756
+ let pos = start;
1755
1757
  for (let child of this.children) {
1756
1758
  if (child == tile)
1757
1759
  return pos;
@@ -3540,199 +3542,6 @@ function groupAt(state$1, pos, bias = 1) {
3540
3542
  }
3541
3543
  return state.EditorSelection.range(from + line.from, to + line.from);
3542
3544
  }
3543
- // Search the DOM for the {node, offset} position closest to the given
3544
- // coordinates. Very inefficient and crude, but can usually be avoided
3545
- // by calling caret(Position|Range)FromPoint instead.
3546
- function getdx(x, rect) {
3547
- return rect.left > x ? rect.left - x : Math.max(0, x - rect.right);
3548
- }
3549
- function getdy(y, rect) {
3550
- return rect.top > y ? rect.top - y : Math.max(0, y - rect.bottom);
3551
- }
3552
- function yOverlap(a, b) {
3553
- return a.top < b.bottom - 1 && a.bottom > b.top + 1;
3554
- }
3555
- function upTop(rect, top) {
3556
- return top < rect.top ? { top, left: rect.left, right: rect.right, bottom: rect.bottom } : rect;
3557
- }
3558
- function upBot(rect, bottom) {
3559
- return bottom > rect.bottom ? { top: rect.top, left: rect.left, right: rect.right, bottom } : rect;
3560
- }
3561
- function domPosAtCoords(parent, x, y) {
3562
- let closest, closestRect, closestX, closestY, closestOverlap = false;
3563
- let above, below, aboveRect, belowRect;
3564
- for (let child = parent.firstChild; child; child = child.nextSibling) {
3565
- let rects = clientRectsFor(child);
3566
- for (let i = 0; i < rects.length; i++) {
3567
- let rect = rects[i];
3568
- if (closestRect && yOverlap(closestRect, rect))
3569
- rect = upTop(upBot(rect, closestRect.bottom), closestRect.top);
3570
- let dx = getdx(x, rect), dy = getdy(y, rect);
3571
- if (dx == 0 && dy == 0)
3572
- return child.nodeType == 3 ? domPosInText(child, x, y) : domPosAtCoords(child, x, y);
3573
- if (!closest || closestY > dy || closestY == dy && closestX > dx) {
3574
- closest = child;
3575
- closestRect = rect;
3576
- closestX = dx;
3577
- closestY = dy;
3578
- closestOverlap = !dx ? true : x < rect.left ? i > 0 : i < rects.length - 1;
3579
- }
3580
- if (dx == 0) {
3581
- if (y > rect.bottom && (!aboveRect || aboveRect.bottom < rect.bottom)) {
3582
- above = child;
3583
- aboveRect = rect;
3584
- }
3585
- else if (y < rect.top && (!belowRect || belowRect.top > rect.top)) {
3586
- below = child;
3587
- belowRect = rect;
3588
- }
3589
- }
3590
- else if (aboveRect && yOverlap(aboveRect, rect)) {
3591
- aboveRect = upBot(aboveRect, rect.bottom);
3592
- }
3593
- else if (belowRect && yOverlap(belowRect, rect)) {
3594
- belowRect = upTop(belowRect, rect.top);
3595
- }
3596
- }
3597
- }
3598
- if (aboveRect && aboveRect.bottom >= y) {
3599
- closest = above;
3600
- closestRect = aboveRect;
3601
- }
3602
- else if (belowRect && belowRect.top <= y) {
3603
- closest = below;
3604
- closestRect = belowRect;
3605
- }
3606
- if (!closest)
3607
- return { node: parent, offset: 0 };
3608
- let clipX = Math.max(closestRect.left, Math.min(closestRect.right, x));
3609
- if (closest.nodeType == 3)
3610
- return domPosInText(closest, clipX, y);
3611
- if (closestOverlap && closest.contentEditable != "false")
3612
- return domPosAtCoords(closest, clipX, y);
3613
- let offset = Array.prototype.indexOf.call(parent.childNodes, closest) +
3614
- (x >= (closestRect.left + closestRect.right) / 2 ? 1 : 0);
3615
- return { node: parent, offset };
3616
- }
3617
- function domPosInText(node, x, y) {
3618
- let len = node.nodeValue.length;
3619
- let closestOffset = -1, closestDY = 1e9, generalSide = 0;
3620
- for (let i = 0; i < len; i++) {
3621
- let rects = textRange(node, i, i + 1).getClientRects();
3622
- for (let j = 0; j < rects.length; j++) {
3623
- let rect = rects[j];
3624
- if (rect.top == rect.bottom)
3625
- continue;
3626
- if (!generalSide)
3627
- generalSide = x - rect.left;
3628
- let dy = (rect.top > y ? rect.top - y : y - rect.bottom) - 1;
3629
- if (rect.left - 1 <= x && rect.right + 1 >= x && dy < closestDY) {
3630
- let right = x >= (rect.left + rect.right) / 2, after = right;
3631
- if (browser.chrome || browser.gecko) {
3632
- // Check for RTL on browsers that support getting client
3633
- // rects for empty ranges.
3634
- let rectBefore = textRange(node, i).getBoundingClientRect();
3635
- if (Math.abs(rectBefore.left - rect.right) < 0.1)
3636
- after = !right;
3637
- }
3638
- if (dy <= 0)
3639
- return { node, offset: i + (after ? 1 : 0) };
3640
- closestOffset = i + (after ? 1 : 0);
3641
- closestDY = dy;
3642
- }
3643
- }
3644
- }
3645
- return { node, offset: closestOffset > -1 ? closestOffset : generalSide > 0 ? node.nodeValue.length : 0 };
3646
- }
3647
- function posAtCoords(view, coords, precise, bias = -1) {
3648
- var _a;
3649
- let content = view.contentDOM.getBoundingClientRect(), docTop = content.top + view.viewState.paddingTop;
3650
- let block, { docHeight } = view.viewState;
3651
- let { x, y } = coords, yOffset = y - docTop;
3652
- if (yOffset < 0)
3653
- return 0;
3654
- if (yOffset > docHeight)
3655
- return view.state.doc.length;
3656
- // Scan for a text block near the queried y position
3657
- for (let halfLine = view.viewState.heightOracle.textHeight / 2, bounced = false;;) {
3658
- block = view.elementAtHeight(yOffset);
3659
- if (block.type == exports.BlockType.Text)
3660
- break;
3661
- for (;;) {
3662
- // Move the y position out of this block
3663
- yOffset = bias > 0 ? block.bottom + halfLine : block.top - halfLine;
3664
- if (yOffset >= 0 && yOffset <= docHeight)
3665
- break;
3666
- // If the document consists entirely of replaced widgets, we
3667
- // won't find a text block, so return 0
3668
- if (bounced)
3669
- return precise ? null : 0;
3670
- bounced = true;
3671
- bias = -bias;
3672
- }
3673
- }
3674
- y = docTop + yOffset;
3675
- let lineStart = block.from;
3676
- // If this is outside of the rendered viewport, we can't determine a position
3677
- if (lineStart < view.viewport.from)
3678
- return view.viewport.from == 0 ? 0 : precise ? null : posAtCoordsImprecise(view, content, block, x, y);
3679
- if (lineStart > view.viewport.to)
3680
- return view.viewport.to == view.state.doc.length ? view.state.doc.length :
3681
- precise ? null : posAtCoordsImprecise(view, content, block, x, y);
3682
- // Prefer ShadowRootOrDocument.elementFromPoint if present, fall back to document if not
3683
- let doc = view.dom.ownerDocument;
3684
- let root = view.root.elementFromPoint ? view.root : doc;
3685
- let element = root.elementFromPoint(x, y);
3686
- if (element && !view.contentDOM.contains(element))
3687
- element = null;
3688
- // If the element is unexpected, clip x at the sides of the content area and try again
3689
- if (!element) {
3690
- x = Math.max(content.left + 1, Math.min(content.right - 1, x));
3691
- element = root.elementFromPoint(x, y);
3692
- if (element && !view.contentDOM.contains(element))
3693
- element = null;
3694
- }
3695
- // There's visible editor content under the point, so we can try
3696
- // using caret(Position|Range)FromPoint as a shortcut
3697
- let node, offset = -1;
3698
- if (element && !((_a = view.docView.tile.nearest(element)) === null || _a === void 0 ? void 0 : _a.isWidget())) {
3699
- if (doc.caretPositionFromPoint) {
3700
- let pos = doc.caretPositionFromPoint(x, y);
3701
- if (pos)
3702
- ({ offsetNode: node, offset } = pos);
3703
- }
3704
- else if (doc.caretRangeFromPoint) {
3705
- let range = doc.caretRangeFromPoint(x, y);
3706
- if (range)
3707
- ({ startContainer: node, startOffset: offset } = range);
3708
- }
3709
- if (node && (!view.contentDOM.contains(node) ||
3710
- browser.safari && isSuspiciousSafariCaretResult(node, offset, x) ||
3711
- browser.chrome && isSuspiciousChromeCaretResult(node, offset, x)))
3712
- node = undefined;
3713
- // Chrome will return offsets into <input> elements without child
3714
- // nodes, which will lead to a null deref below, so clip the
3715
- // offset to the node size.
3716
- if (node)
3717
- offset = Math.min(maxOffset(node), offset);
3718
- }
3719
- // No luck, do our own (potentially expensive) search
3720
- if (!node || !view.contentDOM.contains(node)) {
3721
- let line = view.docView.lineAt(lineStart);
3722
- if (!line)
3723
- return yOffset > block.top + block.height / 2 ? block.to : block.from;
3724
- ({ node, offset } = domPosAtCoords(line.dom, x, y));
3725
- }
3726
- let nearest = view.docView.tile.nearest(node);
3727
- if (!nearest)
3728
- return null;
3729
- if (nearest.isWidget()) {
3730
- let rect = nearest.dom.getBoundingClientRect();
3731
- return coords.y < rect.top || coords.y <= rect.bottom && coords.x <= (rect.left + rect.right) / 2
3732
- ? nearest.posAtStart : nearest.posAtEnd;
3733
- }
3734
- return view.docView.posFromDOM(node, offset);
3735
- }
3736
3545
  function posAtCoordsImprecise(view, contentRect, block, x, y) {
3737
3546
  let into = Math.round((x - contentRect.left) * view.defaultCharacterWidth);
3738
3547
  if (view.lineWrapping && block.height > view.defaultLineHeight * 1.5) {
@@ -3743,49 +3552,6 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3743
3552
  let content = view.state.sliceDoc(block.from, block.to);
3744
3553
  return block.from + state.findColumn(content, into, view.state.tabSize);
3745
3554
  }
3746
- function isEndOfLineBefore(node, offset, x) {
3747
- let len, scan = node;
3748
- if (node.nodeType != 3 || offset != (len = node.nodeValue.length))
3749
- return false;
3750
- for (;;) { // Check that there is no content after this node
3751
- let next = scan.nextSibling;
3752
- if (next) {
3753
- if (next.nodeName == "BR")
3754
- break;
3755
- return false;
3756
- }
3757
- else {
3758
- let parent = scan.parentNode;
3759
- if (!parent || parent.nodeName == "DIV")
3760
- break;
3761
- scan = parent;
3762
- }
3763
- }
3764
- return textRange(node, len - 1, len).getBoundingClientRect().right > x;
3765
- }
3766
- // In case of a high line height, Safari's caretRangeFromPoint treats
3767
- // the space between lines as belonging to the last character of the
3768
- // line before. This is used to detect such a result so that it can be
3769
- // ignored (issue #401).
3770
- function isSuspiciousSafariCaretResult(node, offset, x) {
3771
- return isEndOfLineBefore(node, offset, x);
3772
- }
3773
- // Chrome will move positions between lines to the start of the next line
3774
- function isSuspiciousChromeCaretResult(node, offset, x) {
3775
- if (offset != 0)
3776
- return isEndOfLineBefore(node, offset, x);
3777
- for (let cur = node;;) {
3778
- let parent = cur.parentNode;
3779
- if (!parent || parent.nodeType != 1 || parent.firstChild != cur)
3780
- return false;
3781
- if (parent.classList.contains("cm-line"))
3782
- break;
3783
- cur = parent;
3784
- }
3785
- let rect = node.nodeType == 1 ? node.getBoundingClientRect()
3786
- : textRange(node, 0, Math.max(node.nodeValue.length, 1)).getBoundingClientRect();
3787
- return x - rect.left > 5;
3788
- }
3789
3555
  function blockAt(view, pos, side) {
3790
3556
  let line = view.lineBlockAt(pos);
3791
3557
  if (Array.isArray(line.type)) {
@@ -3875,11 +3641,7 @@ function moveVertically(view, start, forward, distance) {
3875
3641
  for (let extra = 0;; extra += 10) {
3876
3642
  let curY = startY + (dist + extra) * dir;
3877
3643
  let pos = posAtCoords(view, { x: resolvedGoal, y: curY }, false, dir);
3878
- if (curY < rect.top || curY > rect.bottom || (dir < 0 ? pos < startPos : pos > startPos)) {
3879
- let charRect = view.docView.coordsForChar(pos);
3880
- let assoc = !charRect || curY < charRect.top ? -1 : 1;
3881
- return state.EditorSelection.cursor(pos, assoc, undefined, goal);
3882
- }
3644
+ return state.EditorSelection.cursor(pos.pos, pos.assoc, undefined, goal);
3883
3645
  }
3884
3646
  }
3885
3647
  function skipAtomicRanges(atoms, pos, bias) {
@@ -3925,6 +3687,121 @@ function skipAtoms(view, oldPos, pos) {
3925
3687
  let newPos = skipAtomicRanges(view.state.facet(atomicRanges).map(f => f(view)), pos.from, oldPos.head > pos.from ? -1 : 1);
3926
3688
  return newPos == pos.from ? pos : state.EditorSelection.cursor(newPos, newPos < pos.from ? 1 : -1);
3927
3689
  }
3690
+ class PosAssoc {
3691
+ constructor(pos, assoc) {
3692
+ this.pos = pos;
3693
+ this.assoc = assoc;
3694
+ }
3695
+ }
3696
+ function posAtCoords(view, coords, precise, scanY) {
3697
+ let content = view.contentDOM.getBoundingClientRect(), docTop = content.top + view.viewState.paddingTop;
3698
+ let { x, y } = coords, yOffset = y - docTop, block;
3699
+ // First find the block at the given Y position, if any. If scanY is
3700
+ // given (used for vertical cursor motion), try to skip widgets.
3701
+ for (;;) {
3702
+ if (yOffset < 0)
3703
+ return new PosAssoc(0, 1);
3704
+ if (yOffset > view.viewState.docHeight)
3705
+ return new PosAssoc(view.state.doc.length, -1);
3706
+ block = view.elementAtHeight(yOffset);
3707
+ if (scanY == null || block.type == exports.BlockType.Text)
3708
+ break;
3709
+ let halfLine = view.viewState.heightOracle.textHeight / 2;
3710
+ yOffset = scanY > 0 ? block.bottom + halfLine : block.top - halfLine;
3711
+ }
3712
+ // If outside the viewport, return null if precise==true, an
3713
+ // estimate otherwise.
3714
+ if (view.viewport.from >= block.to || view.viewport.to <= block.from) {
3715
+ if (precise)
3716
+ return null;
3717
+ if (block.type == exports.BlockType.Text) {
3718
+ let pos = posAtCoordsImprecise(view, content, block, x, y);
3719
+ return new PosAssoc(pos, pos == block.from ? 1 : -1);
3720
+ }
3721
+ }
3722
+ if (block.type != exports.BlockType.Text)
3723
+ return yOffset < (block.top + block.bottom) / 2 ? new PosAssoc(block.from, 1) : new PosAssoc(block.to, -1);
3724
+ // Here we know we're in a line, so run the logic for inline layout
3725
+ return posAtCoordsInline(view, view.docView.lineAt(block.from), block.from, x, y);
3726
+ }
3727
+ // Scan through the rectangles for the content of a tile, finding the
3728
+ // one closest to the given coordinates, prefering closeness in Y over
3729
+ // closeness in X.
3730
+ //
3731
+ // If this is a text tile, go character-by-character. For line or mark
3732
+ // tiles, check each non-point-widget child, and descend text or mark
3733
+ // tiles with a recursive call.
3734
+ //
3735
+ // For non-wrapped, purely left-to-right text, this could use a binary
3736
+ // search. But because this seems to be fast enough, for how often it
3737
+ // is called, there's not currently a specialized implementation for
3738
+ // that.
3739
+ function posAtCoordsInline(view, tile, offset, x, y) {
3740
+ let closest = -1, closestRect = null;
3741
+ let dxClosest = 1e9, dyClosest = 1e9;
3742
+ let rowTop = y, rowBot = y;
3743
+ let checkRects = (rects, index) => {
3744
+ for (let i = 0; i < rects.length; i++) {
3745
+ let rect = rects[i];
3746
+ if (rect.top == rect.bottom)
3747
+ continue;
3748
+ let dx = rect.left > x ? rect.left - x : rect.right < x ? x - rect.right : 0;
3749
+ let dy = rect.top > y ? rect.top - y : rect.bottom < y ? y - rect.bottom : 0;
3750
+ if (rect.top <= rowBot && rect.bottom >= rowTop) {
3751
+ // Rectangle is in the current row
3752
+ rowTop = Math.min(rect.top, rowTop);
3753
+ rowBot = Math.max(rect.bottom, rowBot);
3754
+ dy = 0;
3755
+ }
3756
+ if (closest < 0 || (dy - dyClosest || dx - dxClosest) < 0) {
3757
+ if (closest >= 0 && dyClosest && dxClosest < dx &&
3758
+ closestRect.top <= rowBot - 2 && closestRect.bottom >= rowTop + 2) {
3759
+ // Retroactively set dy to 0 if the current match is in this row.
3760
+ dyClosest = 0;
3761
+ }
3762
+ else {
3763
+ closest = index;
3764
+ dxClosest = dx;
3765
+ dyClosest = dy;
3766
+ closestRect = rect;
3767
+ }
3768
+ }
3769
+ }
3770
+ };
3771
+ if (tile.isText()) {
3772
+ for (let i = 0; i < tile.length;) {
3773
+ let next = state.findClusterBreak(tile.text, i);
3774
+ checkRects(textRange(tile.dom, i, next).getClientRects(), i);
3775
+ if (!dxClosest && !dyClosest)
3776
+ break;
3777
+ i = next;
3778
+ }
3779
+ let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == exports.Direction.LTR);
3780
+ return after ? new PosAssoc(offset + state.findClusterBreak(tile.text, closest), -1) : new PosAssoc(offset + closest, 1);
3781
+ }
3782
+ else {
3783
+ if (!tile.length)
3784
+ return new PosAssoc(offset, 1);
3785
+ for (let i = 0; i < tile.children.length; i++) {
3786
+ let child = tile.children[i];
3787
+ if (child.flags & 48 /* TileFlag.PointWidget */)
3788
+ continue;
3789
+ let rects = (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3790
+ checkRects(rects, i);
3791
+ if (!dxClosest && !dyClosest)
3792
+ break;
3793
+ }
3794
+ let inner = tile.children[closest], innerOff = tile.posBefore(inner, offset);
3795
+ if (inner.isComposite() || inner.isText())
3796
+ return posAtCoordsInline(view, inner, innerOff, Math.max(closestRect.left, Math.min(closestRect.right, x)), y);
3797
+ let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == exports.Direction.LTR);
3798
+ return after ? new PosAssoc(innerOff + inner.length, -1) : new PosAssoc(innerOff, 1);
3799
+ }
3800
+ }
3801
+ function dirAt(view, pos) {
3802
+ let line = view.state.doc.lineAt(pos), spans = view.bidiSpans(line);
3803
+ return spans[BidiSpan.find(view.bidiSpans(line), pos - line.from, -1, 1)].dir;
3804
+ }
3928
3805
 
3929
3806
  const LineBreakPlaceholder = "\uffff";
3930
3807
  class DOMReader {
@@ -8354,6 +8231,11 @@ class EditorView {
8354
8231
  return this.docView.posFromDOM(node, offset);
8355
8232
  }
8356
8233
  posAtCoords(coords, precise = true) {
8234
+ this.readMeasured();
8235
+ let found = posAtCoords(this, coords, precise);
8236
+ return found && found.pos;
8237
+ }
8238
+ posAndSideAtCoords(coords, precise = true) {
8357
8239
  this.readMeasured();
8358
8240
  return posAtCoords(this, coords, precise);
8359
8241
  }
package/dist/index.d.cts CHANGED
@@ -1017,6 +1017,28 @@ declare class EditorView {
1017
1017
  y: number;
1018
1018
  }): number | null;
1019
1019
  /**
1020
+ Like [`posAtCoords`](https://codemirror.net/6/docs/ref/#view.EditorView.posAtCoords), but also
1021
+ returns which side of the position the coordinates are closest
1022
+ to. For example, for coordinates on the left side of a
1023
+ left-to-right character, the position before that letter is
1024
+ returned, with `assoc` 1, whereas on the right side, you'd get
1025
+ the position after the character, with `assoc` -1.
1026
+ */
1027
+ posAndSideAtCoords(coords: {
1028
+ x: number;
1029
+ y: number;
1030
+ }, precise: false): {
1031
+ pos: number;
1032
+ assoc: -1 | 1;
1033
+ };
1034
+ posAndSideAtCoords(coords: {
1035
+ x: number;
1036
+ y: number;
1037
+ }): {
1038
+ pos: number;
1039
+ assoc: -1 | 1;
1040
+ } | null;
1041
+ /**
1020
1042
  Get the screen coordinates at the given document position.
1021
1043
  `side` determines whether the coordinates are based on the
1022
1044
  element before (-1) or after (1) the position (if no element is
package/dist/index.d.ts CHANGED
@@ -1017,6 +1017,28 @@ declare class EditorView {
1017
1017
  y: number;
1018
1018
  }): number | null;
1019
1019
  /**
1020
+ Like [`posAtCoords`](https://codemirror.net/6/docs/ref/#view.EditorView.posAtCoords), but also
1021
+ returns which side of the position the coordinates are closest
1022
+ to. For example, for coordinates on the left side of a
1023
+ left-to-right character, the position before that letter is
1024
+ returned, with `assoc` 1, whereas on the right side, you'd get
1025
+ the position after the character, with `assoc` -1.
1026
+ */
1027
+ posAndSideAtCoords(coords: {
1028
+ x: number;
1029
+ y: number;
1030
+ }, precise: false): {
1031
+ pos: number;
1032
+ assoc: -1 | 1;
1033
+ };
1034
+ posAndSideAtCoords(coords: {
1035
+ x: number;
1036
+ y: number;
1037
+ }): {
1038
+ pos: number;
1039
+ assoc: -1 | 1;
1040
+ } | null;
1041
+ /**
1020
1042
  Get the screen coordinates at the given document position.
1021
1043
  `side` determines whether the coordinates are based on the
1022
1044
  element before (-1) or after (1) the position (if no element is
package/dist/index.js CHANGED
@@ -1594,6 +1594,8 @@ class ChangedRange {
1594
1594
  let end = ranges[rI + 1];
1595
1595
  rI += 2;
1596
1596
  toB = Math.max(toB, end);
1597
+ for (let i = dI; i < diff.length && diff[i].fromB <= toB; i++)
1598
+ off = diff[i].toA - diff[i].toB;
1597
1599
  toA = Math.max(toA, end + off);
1598
1600
  }
1599
1601
  else if (dI < diff.length && diff[dI].fromB <= toB) {
@@ -1746,8 +1748,8 @@ class Tile {
1746
1748
  get posAtEnd() {
1747
1749
  return this.posAtStart + this.length;
1748
1750
  }
1749
- posBefore(tile) {
1750
- let pos = this.posAtStart;
1751
+ posBefore(tile, start = this.posAtStart) {
1752
+ let pos = start;
1751
1753
  for (let child of this.children) {
1752
1754
  if (child == tile)
1753
1755
  return pos;
@@ -3536,199 +3538,6 @@ function groupAt(state, pos, bias = 1) {
3536
3538
  }
3537
3539
  return EditorSelection.range(from + line.from, to + line.from);
3538
3540
  }
3539
- // Search the DOM for the {node, offset} position closest to the given
3540
- // coordinates. Very inefficient and crude, but can usually be avoided
3541
- // by calling caret(Position|Range)FromPoint instead.
3542
- function getdx(x, rect) {
3543
- return rect.left > x ? rect.left - x : Math.max(0, x - rect.right);
3544
- }
3545
- function getdy(y, rect) {
3546
- return rect.top > y ? rect.top - y : Math.max(0, y - rect.bottom);
3547
- }
3548
- function yOverlap(a, b) {
3549
- return a.top < b.bottom - 1 && a.bottom > b.top + 1;
3550
- }
3551
- function upTop(rect, top) {
3552
- return top < rect.top ? { top, left: rect.left, right: rect.right, bottom: rect.bottom } : rect;
3553
- }
3554
- function upBot(rect, bottom) {
3555
- return bottom > rect.bottom ? { top: rect.top, left: rect.left, right: rect.right, bottom } : rect;
3556
- }
3557
- function domPosAtCoords(parent, x, y) {
3558
- let closest, closestRect, closestX, closestY, closestOverlap = false;
3559
- let above, below, aboveRect, belowRect;
3560
- for (let child = parent.firstChild; child; child = child.nextSibling) {
3561
- let rects = clientRectsFor(child);
3562
- for (let i = 0; i < rects.length; i++) {
3563
- let rect = rects[i];
3564
- if (closestRect && yOverlap(closestRect, rect))
3565
- rect = upTop(upBot(rect, closestRect.bottom), closestRect.top);
3566
- let dx = getdx(x, rect), dy = getdy(y, rect);
3567
- if (dx == 0 && dy == 0)
3568
- return child.nodeType == 3 ? domPosInText(child, x, y) : domPosAtCoords(child, x, y);
3569
- if (!closest || closestY > dy || closestY == dy && closestX > dx) {
3570
- closest = child;
3571
- closestRect = rect;
3572
- closestX = dx;
3573
- closestY = dy;
3574
- closestOverlap = !dx ? true : x < rect.left ? i > 0 : i < rects.length - 1;
3575
- }
3576
- if (dx == 0) {
3577
- if (y > rect.bottom && (!aboveRect || aboveRect.bottom < rect.bottom)) {
3578
- above = child;
3579
- aboveRect = rect;
3580
- }
3581
- else if (y < rect.top && (!belowRect || belowRect.top > rect.top)) {
3582
- below = child;
3583
- belowRect = rect;
3584
- }
3585
- }
3586
- else if (aboveRect && yOverlap(aboveRect, rect)) {
3587
- aboveRect = upBot(aboveRect, rect.bottom);
3588
- }
3589
- else if (belowRect && yOverlap(belowRect, rect)) {
3590
- belowRect = upTop(belowRect, rect.top);
3591
- }
3592
- }
3593
- }
3594
- if (aboveRect && aboveRect.bottom >= y) {
3595
- closest = above;
3596
- closestRect = aboveRect;
3597
- }
3598
- else if (belowRect && belowRect.top <= y) {
3599
- closest = below;
3600
- closestRect = belowRect;
3601
- }
3602
- if (!closest)
3603
- return { node: parent, offset: 0 };
3604
- let clipX = Math.max(closestRect.left, Math.min(closestRect.right, x));
3605
- if (closest.nodeType == 3)
3606
- return domPosInText(closest, clipX, y);
3607
- if (closestOverlap && closest.contentEditable != "false")
3608
- return domPosAtCoords(closest, clipX, y);
3609
- let offset = Array.prototype.indexOf.call(parent.childNodes, closest) +
3610
- (x >= (closestRect.left + closestRect.right) / 2 ? 1 : 0);
3611
- return { node: parent, offset };
3612
- }
3613
- function domPosInText(node, x, y) {
3614
- let len = node.nodeValue.length;
3615
- let closestOffset = -1, closestDY = 1e9, generalSide = 0;
3616
- for (let i = 0; i < len; i++) {
3617
- let rects = textRange(node, i, i + 1).getClientRects();
3618
- for (let j = 0; j < rects.length; j++) {
3619
- let rect = rects[j];
3620
- if (rect.top == rect.bottom)
3621
- continue;
3622
- if (!generalSide)
3623
- generalSide = x - rect.left;
3624
- let dy = (rect.top > y ? rect.top - y : y - rect.bottom) - 1;
3625
- if (rect.left - 1 <= x && rect.right + 1 >= x && dy < closestDY) {
3626
- let right = x >= (rect.left + rect.right) / 2, after = right;
3627
- if (browser.chrome || browser.gecko) {
3628
- // Check for RTL on browsers that support getting client
3629
- // rects for empty ranges.
3630
- let rectBefore = textRange(node, i).getBoundingClientRect();
3631
- if (Math.abs(rectBefore.left - rect.right) < 0.1)
3632
- after = !right;
3633
- }
3634
- if (dy <= 0)
3635
- return { node, offset: i + (after ? 1 : 0) };
3636
- closestOffset = i + (after ? 1 : 0);
3637
- closestDY = dy;
3638
- }
3639
- }
3640
- }
3641
- return { node, offset: closestOffset > -1 ? closestOffset : generalSide > 0 ? node.nodeValue.length : 0 };
3642
- }
3643
- function posAtCoords(view, coords, precise, bias = -1) {
3644
- var _a;
3645
- let content = view.contentDOM.getBoundingClientRect(), docTop = content.top + view.viewState.paddingTop;
3646
- let block, { docHeight } = view.viewState;
3647
- let { x, y } = coords, yOffset = y - docTop;
3648
- if (yOffset < 0)
3649
- return 0;
3650
- if (yOffset > docHeight)
3651
- return view.state.doc.length;
3652
- // Scan for a text block near the queried y position
3653
- for (let halfLine = view.viewState.heightOracle.textHeight / 2, bounced = false;;) {
3654
- block = view.elementAtHeight(yOffset);
3655
- if (block.type == BlockType.Text)
3656
- break;
3657
- for (;;) {
3658
- // Move the y position out of this block
3659
- yOffset = bias > 0 ? block.bottom + halfLine : block.top - halfLine;
3660
- if (yOffset >= 0 && yOffset <= docHeight)
3661
- break;
3662
- // If the document consists entirely of replaced widgets, we
3663
- // won't find a text block, so return 0
3664
- if (bounced)
3665
- return precise ? null : 0;
3666
- bounced = true;
3667
- bias = -bias;
3668
- }
3669
- }
3670
- y = docTop + yOffset;
3671
- let lineStart = block.from;
3672
- // If this is outside of the rendered viewport, we can't determine a position
3673
- if (lineStart < view.viewport.from)
3674
- return view.viewport.from == 0 ? 0 : precise ? null : posAtCoordsImprecise(view, content, block, x, y);
3675
- if (lineStart > view.viewport.to)
3676
- return view.viewport.to == view.state.doc.length ? view.state.doc.length :
3677
- precise ? null : posAtCoordsImprecise(view, content, block, x, y);
3678
- // Prefer ShadowRootOrDocument.elementFromPoint if present, fall back to document if not
3679
- let doc = view.dom.ownerDocument;
3680
- let root = view.root.elementFromPoint ? view.root : doc;
3681
- let element = root.elementFromPoint(x, y);
3682
- if (element && !view.contentDOM.contains(element))
3683
- element = null;
3684
- // If the element is unexpected, clip x at the sides of the content area and try again
3685
- if (!element) {
3686
- x = Math.max(content.left + 1, Math.min(content.right - 1, x));
3687
- element = root.elementFromPoint(x, y);
3688
- if (element && !view.contentDOM.contains(element))
3689
- element = null;
3690
- }
3691
- // There's visible editor content under the point, so we can try
3692
- // using caret(Position|Range)FromPoint as a shortcut
3693
- let node, offset = -1;
3694
- if (element && !((_a = view.docView.tile.nearest(element)) === null || _a === void 0 ? void 0 : _a.isWidget())) {
3695
- if (doc.caretPositionFromPoint) {
3696
- let pos = doc.caretPositionFromPoint(x, y);
3697
- if (pos)
3698
- ({ offsetNode: node, offset } = pos);
3699
- }
3700
- else if (doc.caretRangeFromPoint) {
3701
- let range = doc.caretRangeFromPoint(x, y);
3702
- if (range)
3703
- ({ startContainer: node, startOffset: offset } = range);
3704
- }
3705
- if (node && (!view.contentDOM.contains(node) ||
3706
- browser.safari && isSuspiciousSafariCaretResult(node, offset, x) ||
3707
- browser.chrome && isSuspiciousChromeCaretResult(node, offset, x)))
3708
- node = undefined;
3709
- // Chrome will return offsets into <input> elements without child
3710
- // nodes, which will lead to a null deref below, so clip the
3711
- // offset to the node size.
3712
- if (node)
3713
- offset = Math.min(maxOffset(node), offset);
3714
- }
3715
- // No luck, do our own (potentially expensive) search
3716
- if (!node || !view.contentDOM.contains(node)) {
3717
- let line = view.docView.lineAt(lineStart);
3718
- if (!line)
3719
- return yOffset > block.top + block.height / 2 ? block.to : block.from;
3720
- ({ node, offset } = domPosAtCoords(line.dom, x, y));
3721
- }
3722
- let nearest = view.docView.tile.nearest(node);
3723
- if (!nearest)
3724
- return null;
3725
- if (nearest.isWidget()) {
3726
- let rect = nearest.dom.getBoundingClientRect();
3727
- return coords.y < rect.top || coords.y <= rect.bottom && coords.x <= (rect.left + rect.right) / 2
3728
- ? nearest.posAtStart : nearest.posAtEnd;
3729
- }
3730
- return view.docView.posFromDOM(node, offset);
3731
- }
3732
3541
  function posAtCoordsImprecise(view, contentRect, block, x, y) {
3733
3542
  let into = Math.round((x - contentRect.left) * view.defaultCharacterWidth);
3734
3543
  if (view.lineWrapping && block.height > view.defaultLineHeight * 1.5) {
@@ -3739,49 +3548,6 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3739
3548
  let content = view.state.sliceDoc(block.from, block.to);
3740
3549
  return block.from + findColumn(content, into, view.state.tabSize);
3741
3550
  }
3742
- function isEndOfLineBefore(node, offset, x) {
3743
- let len, scan = node;
3744
- if (node.nodeType != 3 || offset != (len = node.nodeValue.length))
3745
- return false;
3746
- for (;;) { // Check that there is no content after this node
3747
- let next = scan.nextSibling;
3748
- if (next) {
3749
- if (next.nodeName == "BR")
3750
- break;
3751
- return false;
3752
- }
3753
- else {
3754
- let parent = scan.parentNode;
3755
- if (!parent || parent.nodeName == "DIV")
3756
- break;
3757
- scan = parent;
3758
- }
3759
- }
3760
- return textRange(node, len - 1, len).getBoundingClientRect().right > x;
3761
- }
3762
- // In case of a high line height, Safari's caretRangeFromPoint treats
3763
- // the space between lines as belonging to the last character of the
3764
- // line before. This is used to detect such a result so that it can be
3765
- // ignored (issue #401).
3766
- function isSuspiciousSafariCaretResult(node, offset, x) {
3767
- return isEndOfLineBefore(node, offset, x);
3768
- }
3769
- // Chrome will move positions between lines to the start of the next line
3770
- function isSuspiciousChromeCaretResult(node, offset, x) {
3771
- if (offset != 0)
3772
- return isEndOfLineBefore(node, offset, x);
3773
- for (let cur = node;;) {
3774
- let parent = cur.parentNode;
3775
- if (!parent || parent.nodeType != 1 || parent.firstChild != cur)
3776
- return false;
3777
- if (parent.classList.contains("cm-line"))
3778
- break;
3779
- cur = parent;
3780
- }
3781
- let rect = node.nodeType == 1 ? node.getBoundingClientRect()
3782
- : textRange(node, 0, Math.max(node.nodeValue.length, 1)).getBoundingClientRect();
3783
- return x - rect.left > 5;
3784
- }
3785
3551
  function blockAt(view, pos, side) {
3786
3552
  let line = view.lineBlockAt(pos);
3787
3553
  if (Array.isArray(line.type)) {
@@ -3871,11 +3637,7 @@ function moveVertically(view, start, forward, distance) {
3871
3637
  for (let extra = 0;; extra += 10) {
3872
3638
  let curY = startY + (dist + extra) * dir;
3873
3639
  let pos = posAtCoords(view, { x: resolvedGoal, y: curY }, false, dir);
3874
- if (curY < rect.top || curY > rect.bottom || (dir < 0 ? pos < startPos : pos > startPos)) {
3875
- let charRect = view.docView.coordsForChar(pos);
3876
- let assoc = !charRect || curY < charRect.top ? -1 : 1;
3877
- return EditorSelection.cursor(pos, assoc, undefined, goal);
3878
- }
3640
+ return EditorSelection.cursor(pos.pos, pos.assoc, undefined, goal);
3879
3641
  }
3880
3642
  }
3881
3643
  function skipAtomicRanges(atoms, pos, bias) {
@@ -3921,6 +3683,121 @@ function skipAtoms(view, oldPos, pos) {
3921
3683
  let newPos = skipAtomicRanges(view.state.facet(atomicRanges).map(f => f(view)), pos.from, oldPos.head > pos.from ? -1 : 1);
3922
3684
  return newPos == pos.from ? pos : EditorSelection.cursor(newPos, newPos < pos.from ? 1 : -1);
3923
3685
  }
3686
+ class PosAssoc {
3687
+ constructor(pos, assoc) {
3688
+ this.pos = pos;
3689
+ this.assoc = assoc;
3690
+ }
3691
+ }
3692
+ function posAtCoords(view, coords, precise, scanY) {
3693
+ let content = view.contentDOM.getBoundingClientRect(), docTop = content.top + view.viewState.paddingTop;
3694
+ let { x, y } = coords, yOffset = y - docTop, block;
3695
+ // First find the block at the given Y position, if any. If scanY is
3696
+ // given (used for vertical cursor motion), try to skip widgets.
3697
+ for (;;) {
3698
+ if (yOffset < 0)
3699
+ return new PosAssoc(0, 1);
3700
+ if (yOffset > view.viewState.docHeight)
3701
+ return new PosAssoc(view.state.doc.length, -1);
3702
+ block = view.elementAtHeight(yOffset);
3703
+ if (scanY == null || block.type == BlockType.Text)
3704
+ break;
3705
+ let halfLine = view.viewState.heightOracle.textHeight / 2;
3706
+ yOffset = scanY > 0 ? block.bottom + halfLine : block.top - halfLine;
3707
+ }
3708
+ // If outside the viewport, return null if precise==true, an
3709
+ // estimate otherwise.
3710
+ if (view.viewport.from >= block.to || view.viewport.to <= block.from) {
3711
+ if (precise)
3712
+ return null;
3713
+ if (block.type == BlockType.Text) {
3714
+ let pos = posAtCoordsImprecise(view, content, block, x, y);
3715
+ return new PosAssoc(pos, pos == block.from ? 1 : -1);
3716
+ }
3717
+ }
3718
+ if (block.type != BlockType.Text)
3719
+ return yOffset < (block.top + block.bottom) / 2 ? new PosAssoc(block.from, 1) : new PosAssoc(block.to, -1);
3720
+ // Here we know we're in a line, so run the logic for inline layout
3721
+ return posAtCoordsInline(view, view.docView.lineAt(block.from), block.from, x, y);
3722
+ }
3723
+ // Scan through the rectangles for the content of a tile, finding the
3724
+ // one closest to the given coordinates, prefering closeness in Y over
3725
+ // closeness in X.
3726
+ //
3727
+ // If this is a text tile, go character-by-character. For line or mark
3728
+ // tiles, check each non-point-widget child, and descend text or mark
3729
+ // tiles with a recursive call.
3730
+ //
3731
+ // For non-wrapped, purely left-to-right text, this could use a binary
3732
+ // search. But because this seems to be fast enough, for how often it
3733
+ // is called, there's not currently a specialized implementation for
3734
+ // that.
3735
+ function posAtCoordsInline(view, tile, offset, x, y) {
3736
+ let closest = -1, closestRect = null;
3737
+ let dxClosest = 1e9, dyClosest = 1e9;
3738
+ let rowTop = y, rowBot = y;
3739
+ let checkRects = (rects, index) => {
3740
+ for (let i = 0; i < rects.length; i++) {
3741
+ let rect = rects[i];
3742
+ if (rect.top == rect.bottom)
3743
+ continue;
3744
+ let dx = rect.left > x ? rect.left - x : rect.right < x ? x - rect.right : 0;
3745
+ let dy = rect.top > y ? rect.top - y : rect.bottom < y ? y - rect.bottom : 0;
3746
+ if (rect.top <= rowBot && rect.bottom >= rowTop) {
3747
+ // Rectangle is in the current row
3748
+ rowTop = Math.min(rect.top, rowTop);
3749
+ rowBot = Math.max(rect.bottom, rowBot);
3750
+ dy = 0;
3751
+ }
3752
+ if (closest < 0 || (dy - dyClosest || dx - dxClosest) < 0) {
3753
+ if (closest >= 0 && dyClosest && dxClosest < dx &&
3754
+ closestRect.top <= rowBot - 2 && closestRect.bottom >= rowTop + 2) {
3755
+ // Retroactively set dy to 0 if the current match is in this row.
3756
+ dyClosest = 0;
3757
+ }
3758
+ else {
3759
+ closest = index;
3760
+ dxClosest = dx;
3761
+ dyClosest = dy;
3762
+ closestRect = rect;
3763
+ }
3764
+ }
3765
+ }
3766
+ };
3767
+ if (tile.isText()) {
3768
+ for (let i = 0; i < tile.length;) {
3769
+ let next = findClusterBreak(tile.text, i);
3770
+ checkRects(textRange(tile.dom, i, next).getClientRects(), i);
3771
+ if (!dxClosest && !dyClosest)
3772
+ break;
3773
+ i = next;
3774
+ }
3775
+ let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == Direction.LTR);
3776
+ return after ? new PosAssoc(offset + findClusterBreak(tile.text, closest), -1) : new PosAssoc(offset + closest, 1);
3777
+ }
3778
+ else {
3779
+ if (!tile.length)
3780
+ return new PosAssoc(offset, 1);
3781
+ for (let i = 0; i < tile.children.length; i++) {
3782
+ let child = tile.children[i];
3783
+ if (child.flags & 48 /* TileFlag.PointWidget */)
3784
+ continue;
3785
+ let rects = (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3786
+ checkRects(rects, i);
3787
+ if (!dxClosest && !dyClosest)
3788
+ break;
3789
+ }
3790
+ let inner = tile.children[closest], innerOff = tile.posBefore(inner, offset);
3791
+ if (inner.isComposite() || inner.isText())
3792
+ return posAtCoordsInline(view, inner, innerOff, Math.max(closestRect.left, Math.min(closestRect.right, x)), y);
3793
+ let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == Direction.LTR);
3794
+ return after ? new PosAssoc(innerOff + inner.length, -1) : new PosAssoc(innerOff, 1);
3795
+ }
3796
+ }
3797
+ function dirAt(view, pos) {
3798
+ let line = view.state.doc.lineAt(pos), spans = view.bidiSpans(line);
3799
+ return spans[BidiSpan.find(view.bidiSpans(line), pos - line.from, -1, 1)].dir;
3800
+ }
3924
3801
 
3925
3802
  const LineBreakPlaceholder = "\uffff";
3926
3803
  class DOMReader {
@@ -8349,6 +8226,11 @@ class EditorView {
8349
8226
  return this.docView.posFromDOM(node, offset);
8350
8227
  }
8351
8228
  posAtCoords(coords, precise = true) {
8229
+ this.readMeasured();
8230
+ let found = posAtCoords(this, coords, precise);
8231
+ return found && found.pos;
8232
+ }
8233
+ posAndSideAtCoords(coords, precise = true) {
8352
8234
  this.readMeasured();
8353
8235
  return posAtCoords(this, coords, precise);
8354
8236
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemirror/view",
3
- "version": "6.39.0-beta.2",
3
+ "version": "6.39.0-beta.4",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",