feat(reader): open image gallery & table zoom on single tap (#4600)

* feat(reader): open image gallery & table zoom on single tap

In reflowable EPUBs, a single tap on an image or table now opens the same
viewer a long-press opens, so the image gallery / table zoom is reachable by
both gestures. Fixed-layout books (PDF/comics/manga) keep tap-to-turn, and
long-press is unchanged everywhere.

Reuses the existing iframe-long-press -> handleImagePress/handleTablePress
flow via a new shared detectMediaTarget() helper (also adopted by the
long-press path so the two entry points can't drift). handleClick now takes
an isFixedLayout flag; the tap branch sits after the link/footnote/drag/
long-hold/Word-Wise guards so linked images still follow links and a
long-hold or double-tap won't double-trigger.

Context: #4584 (single taps stop registering after picture zoom on some
WebView builds) - this adds a second, independent way into the viewer rather
than fixing that root cause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(reader): rename iframe-long-press message to iframe-open-media

The message is now posted for both a long-press (any book) and a single tap on
an image/table (reflowable books), so the long-press-specific name was
misleading. Rename the message type to `iframe-open-media` and the consumer
hook `useLongPressEvent` -> `useOpenMediaEvent`. The long-press detector
(`addLongPressListeners`/`handleLongPress`) keeps its name since it still
detects a long-press specifically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-06-16 12:58:37 +08:00
committed by GitHub
parent 675ee78bc9
commit e145eb835a
4 changed files with 150 additions and 53 deletions
@@ -155,9 +155,32 @@ export const handleWheel = (bookKey: string, event: WheelEvent) => {
);
};
// A tappable/long-pressable media element under the pointer, resolved to the
// payload the image gallery / table zoom viewers consume. Shared by the
// long-press path and the single-tap path so the two can't drift.
type MediaTarget = { elementType: 'image'; src: string } | { elementType: 'table'; html: string };
const detectMediaTarget = (target: HTMLElement | null): MediaTarget | null => {
if (!target) return null;
if (target.localName === 'img') {
return { elementType: 'image', src: (target as HTMLImageElement).src };
}
const svgImage = target.closest('svg')?.querySelector('image');
if (svgImage) {
const href =
svgImage.getAttribute('href') ||
svgImage.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (href) return { elementType: 'image', src: href };
}
const table = target.localName === 'table' ? target : target.closest('table');
if (table) return { elementType: 'table', html: (table as HTMLElement).outerHTML };
return null;
};
export const handleClick = (
bookKey: string,
doubleClickDisabled: React.MutableRefObject<boolean>,
isFixedLayout: boolean,
event: MouseEvent,
) => {
const now = Date.now();
@@ -234,6 +257,18 @@ export const handleClick = (
return;
}
// In reflowable books a single tap on an image/table opens the same viewer
// a long-press does, so the image gallery / table zoom is reachable by both
// gestures (#4584). Fixed-layout books (PDF/comics/manga) keep tap-to-turn,
// since there the tap is the page-turn gesture.
if (!isFixedLayout) {
const media = detectMediaTarget(element);
if (media) {
window.postMessage({ type: 'iframe-open-media', bookKey, ...media }, '*');
return;
}
}
window.postMessage(
{
type: 'iframe-single-click',
@@ -316,47 +351,9 @@ export const addLongPressListeners = (bookKey: string, doc: Document) => {
return;
}
if (target.localName === 'img') {
const imgTarget = target as HTMLImageElement;
window.postMessage(
{
type: 'iframe-long-press',
bookKey,
elementType: 'image',
src: imgTarget.src,
},
'*',
);
} else if (target.closest('svg')) {
const svg = target.closest('svg')!;
const svgImage = svg.querySelector('image');
const href =
svgImage?.getAttribute('href') ||
svgImage?.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
if (href) {
window.postMessage(
{
type: 'iframe-long-press',
bookKey,
elementType: 'image',
src: href,
},
'*',
);
}
} else if (target.localName === 'table' || target.closest('table')) {
const tableTarget = (
target.localName === 'table' ? target : target.closest('table')
) as HTMLTableElement;
window.postMessage(
{
type: 'iframe-long-press',
bookKey,
elementType: 'table',
html: tableTarget.outerHTML,
},
'*',
);
const media = detectMediaTarget(target);
if (media) {
window.postMessage({ type: 'iframe-open-media', bookKey, ...media }, '*');
}
};