@codemirror/view 6.39.0-beta.2 → 6.39.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1750,8 +1750,8 @@ class Tile {
1750
1750
  get posAtEnd() {
1751
1751
  return this.posAtStart + this.length;
1752
1752
  }
1753
- posBefore(tile) {
1754
- let pos = this.posAtStart;
1753
+ posBefore(tile, start = this.posAtStart) {
1754
+ let pos = start;
1755
1755
  for (let child of this.children) {
1756
1756
  if (child == tile)
1757
1757
  return pos;
@@ -3540,199 +3540,6 @@ function groupAt(state$1, pos, bias = 1) {
3540
3540
  }
3541
3541
  return state.EditorSelection.range(from + line.from, to + line.from);
3542
3542
  }
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
3543
  function posAtCoordsImprecise(view, contentRect, block, x, y) {
3737
3544
  let into = Math.round((x - contentRect.left) * view.defaultCharacterWidth);
3738
3545
  if (view.lineWrapping && block.height > view.defaultLineHeight * 1.5) {
@@ -3743,49 +3550,6 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3743
3550
  let content = view.state.sliceDoc(block.from, block.to);
3744
3551
  return block.from + state.findColumn(content, into, view.state.tabSize);
3745
3552
  }
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
3553
  function blockAt(view, pos, side) {
3790
3554
  let line = view.lineBlockAt(pos);
3791
3555
  if (Array.isArray(line.type)) {
@@ -3875,11 +3639,7 @@ function moveVertically(view, start, forward, distance) {
3875
3639
  for (let extra = 0;; extra += 10) {
3876
3640
  let curY = startY + (dist + extra) * dir;
3877
3641
  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
- }
3642
+ return state.EditorSelection.cursor(pos.pos, pos.assoc, undefined, goal);
3883
3643
  }
3884
3644
  }
3885
3645
  function skipAtomicRanges(atoms, pos, bias) {
@@ -3925,6 +3685,121 @@ function skipAtoms(view, oldPos, pos) {
3925
3685
  let newPos = skipAtomicRanges(view.state.facet(atomicRanges).map(f => f(view)), pos.from, oldPos.head > pos.from ? -1 : 1);
3926
3686
  return newPos == pos.from ? pos : state.EditorSelection.cursor(newPos, newPos < pos.from ? 1 : -1);
3927
3687
  }
3688
+ class PosAssoc {
3689
+ constructor(pos, assoc) {
3690
+ this.pos = pos;
3691
+ this.assoc = assoc;
3692
+ }
3693
+ }
3694
+ function posAtCoords(view, coords, precise, scanY) {
3695
+ let content = view.contentDOM.getBoundingClientRect(), docTop = content.top + view.viewState.paddingTop;
3696
+ let { x, y } = coords, yOffset = y - docTop, block;
3697
+ // First find the block at the given Y position, if any. If scanY is
3698
+ // given (used for vertical cursor motion), try to skip widgets.
3699
+ for (;;) {
3700
+ if (yOffset < 0)
3701
+ return new PosAssoc(0, 1);
3702
+ if (yOffset > view.viewState.docHeight)
3703
+ return new PosAssoc(view.state.doc.length, -1);
3704
+ block = view.elementAtHeight(yOffset);
3705
+ if (scanY == null || block.type == exports.BlockType.Text)
3706
+ break;
3707
+ let halfLine = view.viewState.heightOracle.textHeight / 2;
3708
+ yOffset = scanY > 0 ? block.bottom + halfLine : block.top - halfLine;
3709
+ }
3710
+ // If outside the viewport, return null if precise==true, an
3711
+ // estimate otherwise.
3712
+ if (view.viewport.from >= block.to || view.viewport.to <= block.from) {
3713
+ if (precise)
3714
+ return null;
3715
+ if (block.type == exports.BlockType.Text) {
3716
+ let pos = posAtCoordsImprecise(view, content, block, x, y);
3717
+ return new PosAssoc(pos, pos == block.from ? 1 : -1);
3718
+ }
3719
+ }
3720
+ if (block.type != exports.BlockType.Text)
3721
+ return yOffset < (block.top + block.bottom) / 2 ? new PosAssoc(block.from, 1) : new PosAssoc(block.to, -1);
3722
+ // Here we know we're in a line, so run the logic for inline layout
3723
+ return posAtCoordsInline(view, view.docView.lineAt(block.from), block.from, x, y);
3724
+ }
3725
+ // Scan through the rectangles for the content of a tile, finding the
3726
+ // one closest to the given coordinates, prefering closeness in Y over
3727
+ // closeness in X.
3728
+ //
3729
+ // If this is a text tile, go character-by-character. For line or mark
3730
+ // tiles, check each non-point-widget child, and descend text or mark
3731
+ // tiles with a recursive call.
3732
+ //
3733
+ // For non-wrapped, purely left-to-right text, this could use a binary
3734
+ // search. But because this seems to be fast enough, for how often it
3735
+ // is called, there's not currently a specialized implementation for
3736
+ // that.
3737
+ function posAtCoordsInline(view, tile, offset, x, y) {
3738
+ let closest = -1, closestRect = null;
3739
+ let dxClosest = 1e9, dyClosest = 1e9;
3740
+ let rowTop = y, rowBot = y;
3741
+ let checkRects = (rects, index) => {
3742
+ for (let i = 0; i < rects.length; i++) {
3743
+ let rect = rects[i];
3744
+ if (rect.top == rect.bottom)
3745
+ continue;
3746
+ let dx = rect.left > x ? rect.left - x : rect.right < x ? x - rect.right : 0;
3747
+ let dy = rect.top > y ? rect.top - y : rect.bottom < y ? y - rect.bottom : 0;
3748
+ if (rect.top <= rowBot && rect.bottom >= rowTop) {
3749
+ // Rectangle is in the current row
3750
+ rowTop = Math.min(rect.top, rowTop);
3751
+ rowBot = Math.max(rect.bottom, rowBot);
3752
+ dy = 0;
3753
+ }
3754
+ if (closest < 0 || (dy - dyClosest || dx - dxClosest) < 0) {
3755
+ if (closest >= 0 && dyClosest && dxClosest < dx &&
3756
+ closestRect.top <= rowBot - 2 && closestRect.bottom >= rowTop + 2) {
3757
+ // Retroactively set dy to 0 if the current match is in this row.
3758
+ dyClosest = 0;
3759
+ }
3760
+ else {
3761
+ closest = index;
3762
+ dxClosest = dx;
3763
+ dyClosest = dy;
3764
+ closestRect = rect;
3765
+ }
3766
+ }
3767
+ }
3768
+ };
3769
+ if (tile.isText()) {
3770
+ for (let i = 0; i < tile.length;) {
3771
+ let next = state.findClusterBreak(tile.text, i);
3772
+ checkRects(textRange(tile.dom, i, next).getClientRects(), i);
3773
+ if (!dxClosest && !dyClosest)
3774
+ break;
3775
+ i = next;
3776
+ }
3777
+ let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == exports.Direction.LTR);
3778
+ return after ? new PosAssoc(offset + state.findClusterBreak(tile.text, closest), -1) : new PosAssoc(offset + closest, 1);
3779
+ }
3780
+ else {
3781
+ if (!tile.length)
3782
+ return new PosAssoc(offset, 1);
3783
+ for (let i = 0; i < tile.children.length; i++) {
3784
+ let child = tile.children[i];
3785
+ if (child.flags & 48 /* TileFlag.PointWidget */)
3786
+ continue;
3787
+ let rects = (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3788
+ checkRects(rects, i);
3789
+ if (!dxClosest && !dyClosest)
3790
+ break;
3791
+ }
3792
+ let inner = tile.children[closest], innerOff = tile.posBefore(inner, offset);
3793
+ if (inner.isComposite() || inner.isText())
3794
+ return posAtCoordsInline(view, inner, innerOff, Math.max(closestRect.left, Math.min(closestRect.right, x)), y);
3795
+ let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == exports.Direction.LTR);
3796
+ return after ? new PosAssoc(innerOff + inner.length, -1) : new PosAssoc(innerOff, 1);
3797
+ }
3798
+ }
3799
+ function dirAt(view, pos) {
3800
+ let line = view.state.doc.lineAt(pos), spans = view.bidiSpans(line);
3801
+ return spans[BidiSpan.find(view.bidiSpans(line), pos - line.from, -1, 1)].dir;
3802
+ }
3928
3803
 
3929
3804
  const LineBreakPlaceholder = "\uffff";
3930
3805
  class DOMReader {
@@ -8354,6 +8229,11 @@ class EditorView {
8354
8229
  return this.docView.posFromDOM(node, offset);
8355
8230
  }
8356
8231
  posAtCoords(coords, precise = true) {
8232
+ this.readMeasured();
8233
+ let found = posAtCoords(this, coords, precise);
8234
+ return found && found.pos;
8235
+ }
8236
+ posAndSideAtCoords(coords, precise = true) {
8357
8237
  this.readMeasured();
8358
8238
  return posAtCoords(this, coords, precise);
8359
8239
  }
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
@@ -1746,8 +1746,8 @@ class Tile {
1746
1746
  get posAtEnd() {
1747
1747
  return this.posAtStart + this.length;
1748
1748
  }
1749
- posBefore(tile) {
1750
- let pos = this.posAtStart;
1749
+ posBefore(tile, start = this.posAtStart) {
1750
+ let pos = start;
1751
1751
  for (let child of this.children) {
1752
1752
  if (child == tile)
1753
1753
  return pos;
@@ -3536,199 +3536,6 @@ function groupAt(state, pos, bias = 1) {
3536
3536
  }
3537
3537
  return EditorSelection.range(from + line.from, to + line.from);
3538
3538
  }
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
3539
  function posAtCoordsImprecise(view, contentRect, block, x, y) {
3733
3540
  let into = Math.round((x - contentRect.left) * view.defaultCharacterWidth);
3734
3541
  if (view.lineWrapping && block.height > view.defaultLineHeight * 1.5) {
@@ -3739,49 +3546,6 @@ function posAtCoordsImprecise(view, contentRect, block, x, y) {
3739
3546
  let content = view.state.sliceDoc(block.from, block.to);
3740
3547
  return block.from + findColumn(content, into, view.state.tabSize);
3741
3548
  }
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
3549
  function blockAt(view, pos, side) {
3786
3550
  let line = view.lineBlockAt(pos);
3787
3551
  if (Array.isArray(line.type)) {
@@ -3871,11 +3635,7 @@ function moveVertically(view, start, forward, distance) {
3871
3635
  for (let extra = 0;; extra += 10) {
3872
3636
  let curY = startY + (dist + extra) * dir;
3873
3637
  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
- }
3638
+ return EditorSelection.cursor(pos.pos, pos.assoc, undefined, goal);
3879
3639
  }
3880
3640
  }
3881
3641
  function skipAtomicRanges(atoms, pos, bias) {
@@ -3921,6 +3681,121 @@ function skipAtoms(view, oldPos, pos) {
3921
3681
  let newPos = skipAtomicRanges(view.state.facet(atomicRanges).map(f => f(view)), pos.from, oldPos.head > pos.from ? -1 : 1);
3922
3682
  return newPos == pos.from ? pos : EditorSelection.cursor(newPos, newPos < pos.from ? 1 : -1);
3923
3683
  }
3684
+ class PosAssoc {
3685
+ constructor(pos, assoc) {
3686
+ this.pos = pos;
3687
+ this.assoc = assoc;
3688
+ }
3689
+ }
3690
+ function posAtCoords(view, coords, precise, scanY) {
3691
+ let content = view.contentDOM.getBoundingClientRect(), docTop = content.top + view.viewState.paddingTop;
3692
+ let { x, y } = coords, yOffset = y - docTop, block;
3693
+ // First find the block at the given Y position, if any. If scanY is
3694
+ // given (used for vertical cursor motion), try to skip widgets.
3695
+ for (;;) {
3696
+ if (yOffset < 0)
3697
+ return new PosAssoc(0, 1);
3698
+ if (yOffset > view.viewState.docHeight)
3699
+ return new PosAssoc(view.state.doc.length, -1);
3700
+ block = view.elementAtHeight(yOffset);
3701
+ if (scanY == null || block.type == BlockType.Text)
3702
+ break;
3703
+ let halfLine = view.viewState.heightOracle.textHeight / 2;
3704
+ yOffset = scanY > 0 ? block.bottom + halfLine : block.top - halfLine;
3705
+ }
3706
+ // If outside the viewport, return null if precise==true, an
3707
+ // estimate otherwise.
3708
+ if (view.viewport.from >= block.to || view.viewport.to <= block.from) {
3709
+ if (precise)
3710
+ return null;
3711
+ if (block.type == BlockType.Text) {
3712
+ let pos = posAtCoordsImprecise(view, content, block, x, y);
3713
+ return new PosAssoc(pos, pos == block.from ? 1 : -1);
3714
+ }
3715
+ }
3716
+ if (block.type != BlockType.Text)
3717
+ return yOffset < (block.top + block.bottom) / 2 ? new PosAssoc(block.from, 1) : new PosAssoc(block.to, -1);
3718
+ // Here we know we're in a line, so run the logic for inline layout
3719
+ return posAtCoordsInline(view, view.docView.lineAt(block.from), block.from, x, y);
3720
+ }
3721
+ // Scan through the rectangles for the content of a tile, finding the
3722
+ // one closest to the given coordinates, prefering closeness in Y over
3723
+ // closeness in X.
3724
+ //
3725
+ // If this is a text tile, go character-by-character. For line or mark
3726
+ // tiles, check each non-point-widget child, and descend text or mark
3727
+ // tiles with a recursive call.
3728
+ //
3729
+ // For non-wrapped, purely left-to-right text, this could use a binary
3730
+ // search. But because this seems to be fast enough, for how often it
3731
+ // is called, there's not currently a specialized implementation for
3732
+ // that.
3733
+ function posAtCoordsInline(view, tile, offset, x, y) {
3734
+ let closest = -1, closestRect = null;
3735
+ let dxClosest = 1e9, dyClosest = 1e9;
3736
+ let rowTop = y, rowBot = y;
3737
+ let checkRects = (rects, index) => {
3738
+ for (let i = 0; i < rects.length; i++) {
3739
+ let rect = rects[i];
3740
+ if (rect.top == rect.bottom)
3741
+ continue;
3742
+ let dx = rect.left > x ? rect.left - x : rect.right < x ? x - rect.right : 0;
3743
+ let dy = rect.top > y ? rect.top - y : rect.bottom < y ? y - rect.bottom : 0;
3744
+ if (rect.top <= rowBot && rect.bottom >= rowTop) {
3745
+ // Rectangle is in the current row
3746
+ rowTop = Math.min(rect.top, rowTop);
3747
+ rowBot = Math.max(rect.bottom, rowBot);
3748
+ dy = 0;
3749
+ }
3750
+ if (closest < 0 || (dy - dyClosest || dx - dxClosest) < 0) {
3751
+ if (closest >= 0 && dyClosest && dxClosest < dx &&
3752
+ closestRect.top <= rowBot - 2 && closestRect.bottom >= rowTop + 2) {
3753
+ // Retroactively set dy to 0 if the current match is in this row.
3754
+ dyClosest = 0;
3755
+ }
3756
+ else {
3757
+ closest = index;
3758
+ dxClosest = dx;
3759
+ dyClosest = dy;
3760
+ closestRect = rect;
3761
+ }
3762
+ }
3763
+ }
3764
+ };
3765
+ if (tile.isText()) {
3766
+ for (let i = 0; i < tile.length;) {
3767
+ let next = findClusterBreak(tile.text, i);
3768
+ checkRects(textRange(tile.dom, i, next).getClientRects(), i);
3769
+ if (!dxClosest && !dyClosest)
3770
+ break;
3771
+ i = next;
3772
+ }
3773
+ let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == Direction.LTR);
3774
+ return after ? new PosAssoc(offset + findClusterBreak(tile.text, closest), -1) : new PosAssoc(offset + closest, 1);
3775
+ }
3776
+ else {
3777
+ if (!tile.length)
3778
+ return new PosAssoc(offset, 1);
3779
+ for (let i = 0; i < tile.children.length; i++) {
3780
+ let child = tile.children[i];
3781
+ if (child.flags & 48 /* TileFlag.PointWidget */)
3782
+ continue;
3783
+ let rects = (child.dom.nodeType == 1 ? child.dom : textRange(child.dom, 0, child.length)).getClientRects();
3784
+ checkRects(rects, i);
3785
+ if (!dxClosest && !dyClosest)
3786
+ break;
3787
+ }
3788
+ let inner = tile.children[closest], innerOff = tile.posBefore(inner, offset);
3789
+ if (inner.isComposite() || inner.isText())
3790
+ return posAtCoordsInline(view, inner, innerOff, Math.max(closestRect.left, Math.min(closestRect.right, x)), y);
3791
+ let after = (x > (closestRect.left + closestRect.right) / 2) == (dirAt(view, closest + offset) == Direction.LTR);
3792
+ return after ? new PosAssoc(innerOff + inner.length, -1) : new PosAssoc(innerOff, 1);
3793
+ }
3794
+ }
3795
+ function dirAt(view, pos) {
3796
+ let line = view.state.doc.lineAt(pos), spans = view.bidiSpans(line);
3797
+ return spans[BidiSpan.find(view.bidiSpans(line), pos - line.from, -1, 1)].dir;
3798
+ }
3924
3799
 
3925
3800
  const LineBreakPlaceholder = "\uffff";
3926
3801
  class DOMReader {
@@ -8349,6 +8224,11 @@ class EditorView {
8349
8224
  return this.docView.posFromDOM(node, offset);
8350
8225
  }
8351
8226
  posAtCoords(coords, precise = true) {
8227
+ this.readMeasured();
8228
+ let found = posAtCoords(this, coords, precise);
8229
+ return found && found.pos;
8230
+ }
8231
+ posAndSideAtCoords(coords, precise = true) {
8352
8232
  this.readMeasured();
8353
8233
  return posAtCoords(this, coords, precise);
8354
8234
  }
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.3",
4
4
  "description": "DOM view component for the CodeMirror code editor",
5
5
  "scripts": {
6
6
  "test": "cm-runtests",