feat(reader): select word on double-click and run instant action or toolbar (#4846)
Double-click (mouse) or touch double-tap on a word now selects that word, like a long-press selection, then runs the configured instant quick action or raises the annotation toolbar when none is set. The iframe posted iframe-double-click but nothing consumed it, so a touch double-tap did nothing (Android has no native double-tap word-select; on desktop the browser already selects the word natively via the pointerup path). - sel.ts: getWordRangeAt expands a caret to its word-like segment via Intl.Segmenter (CJK and Latin); getWordRangeFromPoint resolves the caret at a point and delegates. - useTextSelector: handleDoubleClick selects the word and routes through the existing makeSelection flow (guarded so the programmatic selectionchange echo is ignored). It no-ops when a native selection already exists, so the desktop double-click path is not double-fired. - Annotator: consume iframe-double-click, resolve the visible section doc/index, and set pointerDownTimeRef to 0 so the deliberate double-tap bypasses the touch long-press hold gate before the instant action fires. Tests: unit coverage for the word-range helpers and the selection routing (plus the desktop guard), and an Android CDP e2e for the double-tap gesture on a real device. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -322,6 +322,7 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
handleNativeTouchMove,
|
||||
handlePointerCancel,
|
||||
handlePointerUp,
|
||||
handleDoubleClick,
|
||||
handleSelectionchange,
|
||||
handleShowPopup,
|
||||
handleUpToPopup,
|
||||
@@ -612,6 +613,33 @@ const Annotator: React.FC<{ bookKey: string; contentInsets: Insets }> = ({
|
||||
listenToNativeTouchEvents,
|
||||
});
|
||||
|
||||
// A double-click / touch double-tap on a word selects that word and raises the
|
||||
// quick action (if one is configured) or the annotation toolbar — like a
|
||||
// long-press selection. The iframe posts `iframe-double-click` (gated by the
|
||||
// user's double-click setting) with coordinates in the originating section's
|
||||
// viewport; resolve the visible section's doc/index the way the native-touch
|
||||
// bridge does, then select the word under the point.
|
||||
useEffect(() => {
|
||||
const handleDoubleClickMessage = (msg: MessageEvent) => {
|
||||
const data = msg.data;
|
||||
if (!data || data.bookKey !== bookKey || data.type !== 'iframe-double-click') return;
|
||||
const renderer = view?.renderer;
|
||||
const contents = renderer?.getContents?.() ?? [];
|
||||
const content = contents.find((c) => c.index === renderer?.primaryIndex) ?? contents[0];
|
||||
const doc = content?.doc;
|
||||
const index = content?.index;
|
||||
if (!doc || index === undefined) return;
|
||||
// A double-click is a deliberate act-on-word gesture, so let the quick
|
||||
// action fire without the touch long-press hold gate (matching a mouse
|
||||
// selection, which sets this to 0 on pointerdown).
|
||||
pointerDownTimeRef.current = 0;
|
||||
void handleDoubleClick(doc, index, data.clientX, data.clientY);
|
||||
};
|
||||
window.addEventListener('message', handleDoubleClickMessage);
|
||||
return () => window.removeEventListener('message', handleDoubleClickMessage);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [bookKey, view]);
|
||||
|
||||
// Word Lens: open the dictionary popup for a tapped glossed word. The tap is
|
||||
// detected in the iframe click handler (iframeEventHandlers.ts), which sends
|
||||
// the gloss <ruby> element here. We synthesize a selection over the base word
|
||||
|
||||
Reference in New Issue
Block a user