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(