From 5bbdc704205f55afbbc61b9730416fede57dbd57 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 26 Apr 2025 13:45:51 +0800 Subject: [PATCH] fix: preserve selection anchor when spanning paginated content, closes #873 (#968) --- .../reader/components/annotator/Annotator.tsx | 51 +++++++++++++++++++ apps/readest-app/src/types/view.ts | 1 + 2 files changed, 52 insertions(+) diff --git a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx index 5c177f29..71085e40 100644 --- a/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx +++ b/apps/readest-app/src/app/reader/components/annotator/Annotator.tsx @@ -53,6 +53,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const isTextSelected = useRef(false); const isUpToShowPopup = useRef(false); const isTouchstarted = useRef(false); + const selectionStartRef = useRef(null); + const selectionAnchorRef = useRef<{ node: Node; offset: number } | null>(null); const [selection, setSelection] = useState(); const [showAnnotPopup, setShowAnnotPopup] = useState(false); const [showWiktionaryPopup, setShowWiktionaryPopup] = useState(false); @@ -120,6 +122,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { }, 40); }, 0); }; + const handleSelectstart = () => { + selectionAnchorRef.current = null; + }; const handleSelectionchange = () => { // Available on iOS, Android and Desktop, fired when the selection is changed // Ideally the popup only shows when the selection is done, @@ -130,6 +135,27 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { const sel = doc.getSelection(); if (isValidSelection(sel)) { if (osPlatform === 'android' && isTouchstarted.current) { + if (!selectionAnchorRef.current) { + const range = sel.getRangeAt(0); + selectionAnchorRef.current = { + node: range.startContainer, + offset: range.startOffset, + }; + } + if (selectionAnchorRef.current) { + const currentRange = sel.getRangeAt(0); + if ( + currentRange.startContainer !== selectionAnchorRef.current.node || + currentRange.startOffset !== selectionAnchorRef.current.offset + ) { + const newRange = doc.createRange(); + newRange.setStart(selectionAnchorRef.current.node, selectionAnchorRef.current.offset); + newRange.setEnd(currentRange.endContainer, currentRange.endOffset); + + sel.removeAllRanges(); + sel.addRange(newRange); + } + } makeSelection(sel, false); } } else if (!isUpToShowPopup.current) { @@ -165,11 +191,26 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { // Available on iOS, on Android fired after an additional touch event isTouchstarted.current = false; }; + const handleScroll = () => { + // Prevent the container from scrolling when text is selected in paginated mode + // This is a workaround for the issue #873 + // TODO: support text selection across pages + if ( + !viewSettings?.scrolled && + isTextSelected.current && + view?.renderer?.containerPosition && + selectionStartRef.current + ) { + view.renderer.containerPosition = selectionStartRef.current; + } + }; if (bookData.book?.format !== 'PDF') { + view?.renderer?.addEventListener('scroll', handleScroll); detail.doc?.addEventListener('pointerup', handlePointerup); detail.doc?.addEventListener('touchstart', handleTouchstart); detail.doc?.addEventListener('touchmove', handleTouchmove); detail.doc?.addEventListener('touchend', handleTouchend); + detail.doc?.addEventListener('selectstart', handleSelectstart); detail.doc?.addEventListener('selectionchange', handleSelectionchange); // Disable the default context menu on mobile devices, @@ -241,6 +282,16 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => { isTextSelected.current = false; }; + useEffect(() => { + if (isTextSelected.current && !selectionStartRef.current) { + selectionStartRef.current = view?.renderer?.start || null; + } else if (!isTextSelected.current) { + selectionStartRef.current = null; + selectionAnchorRef.current = null; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isTextSelected.current]); + useEffect(() => { const handleSingleClick = (): boolean => { if (isUpToShowPopup.current) { diff --git a/apps/readest-app/src/types/view.ts b/apps/readest-app/src/types/view.ts index d2285402..fce1767a 100644 --- a/apps/readest-app/src/types/view.ts +++ b/apps/readest-app/src/types/view.ts @@ -40,6 +40,7 @@ export interface FoliateView extends HTMLElement { viewSize: number; // whole document view height start: number; end: number; + containerPosition: number; setAttribute: (name: string, value: string | number) => void; removeAttribute: (name: string) => void; next: () => Promise;