From d963b911c81af63bb3cf385646fdd77934c5b06a Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 24 Jun 2026 23:57:19 +0800 Subject: [PATCH] fix(reader): zoom linked images on single tap (#4757) (#4766) A single tap on an image wrapped in an element followed the link instead of opening the image viewer, because postSingleClick returned early for any element inside an anchor before reaching media detection. Compute the media target up front and let it bypass the anchor guard, so a tapped image/table/svg-image opens the viewer just like long-press already does. Footnotes are excluded so footnote anchors keep their popup and navigation behavior. Co-authored-by: Claude Opus 4.8 (1M context) --- .../reader/utils/iframeEventHandlers.test.ts | 23 ++++++++++++-- .../app/reader/utils/iframeEventHandlers.ts | 30 +++++++++++-------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/apps/readest-app/src/__tests__/reader/utils/iframeEventHandlers.test.ts b/apps/readest-app/src/__tests__/reader/utils/iframeEventHandlers.test.ts index 6d29606f..fd0e998b 100644 --- a/apps/readest-app/src/__tests__/reader/utils/iframeEventHandlers.test.ts +++ b/apps/readest-app/src/__tests__/reader/utils/iframeEventHandlers.test.ts @@ -176,7 +176,7 @@ describe('single-tap opens image gallery / table zoom in reflowable books (#4584 expect(types).not.toContain('iframe-open-media'); }); - test('reflowable: tap on a linked image follows the link (posts neither)', async () => { + test('reflowable: tap on a linked image opens the viewer instead of following the link (#4757)', async () => { const handlers = await importHandlers(); const anchor = document.createElement('a'); anchor.href = 'https://example.com'; @@ -186,8 +186,27 @@ describe('single-tap opens image gallery / table zoom in reflowable books (#4584 tap(handlers, false, img); + const messages = postedMessages(); + const types = messages.map((m) => m['type']); + expect(types).toContain('iframe-open-media'); + expect(types).not.toContain('iframe-single-click'); + const media = messages.find((m) => m['type'] === 'iframe-open-media')!; + expect(media['elementType']).toBe('image'); + expect(media['src']).toBe(img.src); + }); + + test('reflowable: tap on a footnote-link image keeps footnote behavior, not the media viewer', async () => { + const handlers = await importHandlers(); + const anchor = document.createElement('a'); + anchor.className = 'duokan-footnote'; + anchor.href = '#note-1'; + const img = document.createElement('img'); + img.src = 'blob:http://localhost/note'; + anchor.appendChild(img); + + tap(handlers, false, img); + const types = postedMessages().map((m) => m['type']); expect(types).not.toContain('iframe-open-media'); - expect(types).not.toContain('iframe-single-click'); }); }); diff --git a/apps/readest-app/src/app/reader/utils/iframeEventHandlers.ts b/apps/readest-app/src/app/reader/utils/iframeEventHandlers.ts index c1008693..613f06d7 100644 --- a/apps/readest-app/src/app/reader/utils/iframeEventHandlers.ts +++ b/apps/readest-app/src/app/reader/utils/iframeEventHandlers.ts @@ -208,12 +208,6 @@ export const handleClick = ( const postSingleClick = () => { const element = event.target as HTMLElement | null; - if ( - element?.closest('sup, a, audio, video') && - !element?.closest('a.duokan-footnote:not([href])') - ) { - return; - } const footnoteSelector = [ '.js_readerFooterNote', '.zhangyue-footnote', @@ -221,6 +215,19 @@ export const handleClick = ( '.qqreader-footnote', ].join(', '); const footnote = element?.closest(footnoteSelector); + // In reflowable books a single tap on an image/table opens the media + // viewer. A media element wrapped in a plain link (e.g. a figure linking to + // its full-resolution image) should still zoom rather than follow the link, + // matching long-press (#4757). Footnotes are excluded so footnote links keep + // their popup/navigation behavior. + const media = !isFixedLayout && !footnote ? detectMediaTarget(element) : null; + if ( + !media && + element?.closest('sup, a, audio, video') && + !element?.closest('a.duokan-footnote:not([href])') + ) { + return; + } if (footnote) { eventDispatcher.dispatch('footnote-popup', { bookKey, @@ -260,13 +267,10 @@ export const handleClick = ( // 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; - } + // since there the tap is the page-turn gesture (media is null there). + if (media) { + window.postMessage({ type: 'iframe-open-media', bookKey, ...media }, '*'); + return; } window.postMessage(