diff --git a/src/common/components/reader-ui.js b/src/common/components/reader-ui.js index 299e2fd7..3974d966 100644 --- a/src/common/components/reader-ui.js +++ b/src/common/components/reader-ui.js @@ -35,6 +35,10 @@ function View(props) { props.onFindPrevious(primary); } + function handleOverlayPopupClose() { + props.onCloseOverlayPopup(primary); + } + return (
} {state[name + 'ViewFindState'].popupOpen && diff --git a/src/common/components/view-popup/overlay-popup/image-popup.js b/src/common/components/view-popup/overlay-popup/image-popup.js new file mode 100644 index 00000000..ad6a98ad --- /dev/null +++ b/src/common/components/view-popup/overlay-popup/image-popup.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { useIntl } from "react-intl"; + +function ImagePopup({ params, onClose }) { + let { src, title, alt } = params; + + const intl = useIntl(); + + return ( +
+ {alt} +
+ ); +} + +export default ImagePopup; diff --git a/src/common/components/view-popup/overlay-popup/index.js b/src/common/components/view-popup/overlay-popup/index.js index e3c618f9..21fe0310 100644 --- a/src/common/components/view-popup/overlay-popup/index.js +++ b/src/common/components/view-popup/overlay-popup/index.js @@ -4,6 +4,7 @@ import LinkPopup from './link-popup'; import FootnotePopup from './footnote-popup'; import ReferencePopup from './reference/reference-popup'; import CitationPopup from './reference/citation-popup'; +import ImagePopup from "./image-popup"; function OverlayPopup(props) { @@ -22,6 +23,9 @@ function OverlayPopup(props) { else if (props.params.type === 'reference') { return ; } + else if (props.params.type === 'image') { + return ; + } } export default OverlayPopup; diff --git a/src/common/reader.js b/src/common/reader.js index f84d311a..1a996967 100644 --- a/src/common/reader.js +++ b/src/common/reader.js @@ -319,6 +319,7 @@ class Reader { onFindPrevious={this.findPrevious.bind(this)} onToggleContextPane={this._onToggleContextPane} onChangeTextSelectionAnnotationMode={this.setTextSelectionAnnotationMode.bind(this)} + onCloseOverlayPopup={this._handleOverlayPopupClose.bind(this)} ref={this._readerRef} tools={this._tools} /> @@ -682,6 +683,10 @@ class Reader { this._updateState({ [primary ? 'primaryViewFindState' : 'secondaryViewFindState']: params }); } + _handleOverlayPopupClose(primary) { + this._updateState({ [primary ? 'primaryViewOverlayPopup' : 'secondaryViewOverlayPopup']: null }); + } + setTextSelectionAnnotationMode(mode) { if (!['highlight', 'underline'].includes(mode)) { throw new Error(`Invalid 'textSelectionAnnotationMode' value '${mode}'`); diff --git a/src/common/stylesheets/components/_view-popup.scss b/src/common/stylesheets/components/_view-popup.scss index ef644e52..79707139 100644 --- a/src/common/stylesheets/components/_view-popup.scss +++ b/src/common/stylesheets/components/_view-popup.scss @@ -301,3 +301,18 @@ max-height: 300px; } } + +.image-popup { + z-index: 1; + padding: 5px; + position: absolute; + inset: 0; + background: var(--color-background); + cursor: zoom-out; + + img { + object-fit: contain; + width: 100%; + height: 100%; + } +} diff --git a/src/common/types.ts b/src/common/types.ts index ac848fe2..78fde32a 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -122,15 +122,27 @@ export type SelectionPopupParams = { annotation?: NewAnnotation | null; } - -export type OverlayPopupParams = { - type: string; - url?: string; - css?: string; - content?: string; +type FootnotePopupParams = { + type: 'footnote'; + content: string; + css: string; rect: ArrayRect; ref: Node; -}; +} + +type LinkPopupParams = { + type: 'link'; + url: string; +} + +type ImagePopupParams = { + type: 'image'; + src: string; + title?: string; + alt?: string; +} + +export type OverlayPopupParams = FootnotePopupParams | LinkPopupParams | ImagePopupParams export type ArrayRect = [left: number, top: number, right: number, bottom: number]; diff --git a/src/dom/epub/epub-view.ts b/src/dom/epub/epub-view.ts index fc1ed901..f9de8208 100644 --- a/src/dom/epub/epub-view.ts +++ b/src/dom/epub/epub-view.ts @@ -676,6 +676,26 @@ class EPUBView extends DOMView { this.navigate({ href }); } + protected override _handlePointerDown(event: PointerEvent) { + super._handlePointerDown(event); + + if (event.defaultPrevented) { + return; + } + + let target = event.target as Element; + if (target.tagName === 'IMG' && target.classList.contains('clickable-image')) { + let img = target as HTMLImageElement; + this._options.onSetOverlayPopup({ + type: 'image', + src: img.currentSrc || img.src, + title: img.title, + alt: img.alt, + }); + event.preventDefault(); + } + } + protected override _handleKeyDown(event: KeyboardEvent) { let { key } = event; diff --git a/src/dom/epub/lib/sanitize-and-render.ts b/src/dom/epub/lib/sanitize-and-render.ts index 04703d89..a5b39f84 100644 --- a/src/dom/epub/lib/sanitize-and-render.ts +++ b/src/dom/epub/lib/sanitize-and-render.ts @@ -68,6 +68,11 @@ export async function sanitizeAndRender(xhtml: string, options: { let img = elem as HTMLImageElement; img.loading = 'eager'; img.decoding = 'sync'; + if (!img.closest('a')) { + // TODO: Localize? No access to strings here + img.setAttribute('aria-label', 'Zoom In'); + img.classList.add('clickable-image'); + } break; } default: diff --git a/src/dom/epub/stylesheets/_content.scss b/src/dom/epub/stylesheets/_content.scss index 82cbbb1f..5f85a62c 100644 --- a/src/dom/epub/stylesheets/_content.scss +++ b/src/dom/epub/stylesheets/_content.scss @@ -194,6 +194,10 @@ replaced-body { display: none; } } + + img.clickable-image { + cursor: zoom-in; + } } body.footnote-popup-content {