From e1fa4698c6e109dbe379aba6f2328fc8c425e2b4 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Sun, 13 Sep 2026 18:07:36 -0400 Subject: [PATCH] Clamp touch selection endpoints to the visible page Chrome word-snapping during a touch selection drag can map the endpoint past the visible column into the next (off-screen) page, selecting the entire visible page at once. Re-map the endpoint via caretRangeFromPoint at the same line height inside the visible column whenever it lands outside the last visible range. --- paginator.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/paginator.js b/paginator.js index 07fe8db..69f4b73 100644 --- a/paginator.js +++ b/paginator.js @@ -599,6 +599,10 @@ export class Paginator extends HTMLElement { }) doc.addEventListener('pointerup', () => isPointerSelecting = false) doc.addEventListener('pointercancel', () => isPointerSelecting = false) + let touchSelecting = false + doc.addEventListener('pointerdown', e => { + touchSelecting = e.pointerType === 'touch' + }) let isKeyboardSelecting = false doc.addEventListener('keydown', () => isKeyboardSelecting = true) doc.addEventListener('keyup', () => isKeyboardSelecting = false) @@ -608,6 +612,11 @@ export class Paginator extends HTMLElement { if (!range) return const sel = doc.getSelection() if (!sel.rangeCount) return + if (touchSelecting) { + if (sel.isCollapsed) touchSelecting = false + else if (sel.type === 'Range') + this.#clampTouchSelection(sel, doc) + } if (isPointerSelecting && sel.type === 'Range') checkPointerSelection(range, sel) else if (isKeyboardSelecting) { @@ -952,6 +961,25 @@ export class Paginator extends HTMLElement { return getVisibleRange(this.#view.document, this.start - size, this.end - size, this.#getRectMapper()) } + #clampTouchSelection(sel, doc) { + const range = this.#lastVisibleRange + if (!range || !sel.rangeCount) return + const backward = selectionIsBackward(sel) + const probe = sel.getRangeAt(0).cloneRange() + probe.collapse(!backward) + if (backward + ? range.compareBoundaryPoints(Range.START_TO_START, probe) <= 0 + : range.compareBoundaryPoints(Range.END_TO_END, probe) >= 0) return + const rect = probe.getBoundingClientRect() + if (!rect) return + const x = backward ? this.start + 1 : this.end - 1 + const caret = doc.caretRangeFromPoint?.(x, rect.top) + ?? doc.caretPositionFromPoint?.(x, rect.top) + if (!caret) return + const node = caret.startContainer ?? caret.offsetNode + const offset = caret.startOffset ?? caret.offset + if (node) sel.extend(node, offset) + } #afterScroll(reason) { const range = this.#getVisibleRange() this.#lastVisibleRange = range