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 (
+
+
+
+ );
+}
+
+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 {