mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-30 23:49:10 -04:00
Repair the anchor word only from the stability-gated deferred path
A selection write at the moment Chrome re-anchors the drag detaches its touch selection controller; the same writes on a settled selection are fine. Record the word synchronously, repair only when the selection has been unchanged for 150ms.
This commit is contained in:
+31
-13
@@ -611,29 +611,38 @@ export class Paginator extends HTMLElement {
|
|||||||
// The word selected by the initial long-press. Chrome can
|
// The word selected by the initial long-press. Chrome can
|
||||||
// re-anchor a touch drag at a rendered line break when the
|
// re-anchor a touch drag at a rendered line break when the
|
||||||
// finger crosses just past a line-start word, silently dropping
|
// finger crosses just past a line-start word, silently dropping
|
||||||
// that word from the selection; keep it covered.
|
// that word from the selection; keep it covered. The repair
|
||||||
|
// must never run while the gesture is still changing the
|
||||||
|
// selection: a write at the re-anchor moment detaches Chrome's
|
||||||
|
// touch selection controller and the selection stops following
|
||||||
|
// the finger. Focus-only writes on a settled selection are
|
||||||
|
// tolerated, so record here and repair only from the deferred,
|
||||||
|
// stability-gated callbacks below.
|
||||||
let anchorWordStart = null
|
let anchorWordStart = null
|
||||||
doc.addEventListener('pointerdown', e => {
|
doc.addEventListener('pointerdown', e => {
|
||||||
if (e.pointerType === 'touch') anchorWordStart = null
|
if (e.pointerType === 'touch') anchorWordStart = null
|
||||||
})
|
})
|
||||||
const preserveAnchorWord = sel => {
|
const recordAnchorWord = sel => {
|
||||||
if (!sel.rangeCount || sel.type !== 'Range') return
|
if (anchorWordStart || !sel.rangeCount) return
|
||||||
const r = sel.getRangeAt(0)
|
const r = sel.getRangeAt(0)
|
||||||
if (!anchorWordStart) {
|
|
||||||
anchorWordStart =
|
anchorWordStart =
|
||||||
{ node: r.startContainer, offset: r.startOffset }
|
{ node: r.startContainer, offset: r.startOffset }
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
const repairAnchorWord = sel => {
|
||||||
|
if (!anchorWordStart || !sel.rangeCount
|
||||||
|
|| sel.type !== 'Range') return false
|
||||||
|
const r = sel.getRangeAt(0)
|
||||||
const probe = doc.createRange()
|
const probe = doc.createRange()
|
||||||
probe.setStart(anchorWordStart.node, anchorWordStart.offset)
|
probe.setStart(anchorWordStart.node, anchorWordStart.offset)
|
||||||
probe.collapse(true)
|
probe.collapse(true)
|
||||||
if (r.compareBoundaryPoints(Range.START_TO_START, probe) <= 0)
|
if (r.compareBoundaryPoints(Range.START_TO_START, probe) <= 0)
|
||||||
return
|
return false
|
||||||
if (selectionIsBackward(sel))
|
if (selectionIsBackward(sel))
|
||||||
sel.extend(probe.startContainer, probe.startOffset)
|
sel.extend(probe.startContainer, probe.startOffset)
|
||||||
else
|
else
|
||||||
sel.setBaseAndExtent(probe.startContainer, probe.startOffset,
|
sel.setBaseAndExtent(probe.startContainer,
|
||||||
sel.focusNode, sel.focusOffset)
|
probe.startOffset, sel.focusNode, sel.focusOffset)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
let isKeyboardSelecting = false
|
let isKeyboardSelecting = false
|
||||||
doc.addEventListener('keydown', () => isKeyboardSelecting = true)
|
doc.addEventListener('keydown', () => isKeyboardSelecting = true)
|
||||||
@@ -645,21 +654,30 @@ export class Paginator extends HTMLElement {
|
|||||||
const sel = doc.getSelection()
|
const sel = doc.getSelection()
|
||||||
if (!sel.rangeCount) return
|
if (!sel.rangeCount) return
|
||||||
if (touchSelecting && sel.type === 'Range') {
|
if (touchSelecting && sel.type === 'Range') {
|
||||||
preserveAnchorWord(sel)
|
recordAnchorWord(sel)
|
||||||
this.#clampTouchSelection(sel, doc)
|
this.#clampTouchSelection(sel, doc)
|
||||||
// Chrome ignores (or re-maps) selection writes made from
|
// Chrome ignores (or re-maps) selection writes made from
|
||||||
// JS while the touch selection gesture is active, and no
|
// JS while the touch selection gesture is active, and no
|
||||||
// pointer or touch event reaches the document when the
|
// pointer or touch event reaches the document when the
|
||||||
// finger lifts after the takeover — the only signal is
|
// finger lifts after the takeover — the only signal is
|
||||||
// the last selectionchange, mid-gesture. Retry after the
|
// the last selectionchange, mid-gesture. Retry after the
|
||||||
// gesture has most likely ended.
|
// gesture has most likely ended, and only while the
|
||||||
|
// selection is unchanged since the event that scheduled
|
||||||
|
// the retry (an actively changing selection supersedes it
|
||||||
|
// and owns the next retry).
|
||||||
|
const snap = [sel.anchorNode, sel.anchorOffset,
|
||||||
|
sel.focusNode, sel.focusOffset]
|
||||||
|
const same = s => s.anchorNode === snap[0]
|
||||||
|
&& s.anchorOffset === snap[1]
|
||||||
|
&& s.focusNode === snap[2]
|
||||||
|
&& s.focusOffset === snap[3]
|
||||||
const clampLater = () => {
|
const clampLater = () => {
|
||||||
const s = doc.getSelection()
|
const s = doc.getSelection()
|
||||||
if (s && s.rangeCount && s.type === 'Range') {
|
if (!s || !s.rangeCount || s.type !== 'Range'
|
||||||
preserveAnchorWord(s)
|
|| !same(s)) return
|
||||||
|
if (repairAnchorWord(s)) return
|
||||||
this.#clampTouchSelection(s, doc)
|
this.#clampTouchSelection(s, doc)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setTimeout(clampLater, 150)
|
setTimeout(clampLater, 150)
|
||||||
setTimeout(clampLater, 500)
|
setTimeout(clampLater, 500)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user