Massively zoom in on images, adds panning

This commit is contained in:
Josh Perez 2021-09-28 16:27:35 -04:00 committed by GitHub
parent 68b5064cb1
commit 68cef84c29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 155 additions and 16 deletions

View file

@ -72,18 +72,23 @@
}
}
&__shadow-container {
display: flex;
height: 100%;
padding: 0 16px;
position: absolute;
width: 100%;
z-index: 0;
}
&__object {
&--container {
display: inline-flex;
flex-grow: 1;
justify-content: center;
margin: 0 40px;
overflow: hidden;
position: relative;
&--zoomed {
margin: 0;
}
z-index: 1;
}
height: auto;
@ -92,15 +97,32 @@
max-width: 100%;
object-fit: contain;
outline: none;
padding: 0 40px;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: auto;
}
&__object--container--zoomed &__object {
width: 100%;
&__shadow-container &__object {
max-height: 200%;
max-width: 200%;
padding: 0;
visibility: hidden;
}
&__object--container--fill &__object {
height: 100%;
padding: 0;
width: 100%;
}
&__object--container--zoom &__object {
left: 0;
max-height: 200%;
max-width: 200%;
padding: 0;
top: 0;
}
&__unsupported {
@ -135,7 +157,8 @@
cursor: zoom-in;
}
&__object--container--zoomed {
&__object--container--zoom,
&__object--container--fill {
.Lightbox__zoom-button {
cursor: zoom-out;
}

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, {
CSSProperties,
ReactNode,
useCallback,
useEffect,
@ -40,6 +41,12 @@ export type PropsType = {
selectedIndex?: number;
};
enum ZoomType {
None,
FillScreen,
ZoomAndPan,
}
export function Lightbox({
children,
close,
@ -60,9 +67,15 @@ export function Lightbox({
null
);
const [videoTime, setVideoTime] = useState<number | undefined>();
const [zoomed, setZoomed] = useState(false);
const [zoomType, setZoomType] = useState<ZoomType>(ZoomType.None);
const containerRef = useRef<HTMLDivElement | null>(null);
const [focusRef] = useRestoreFocus();
const imageRef = useRef<HTMLImageElement | null>(null);
const [imagePanStyle, setImagePanStyle] = useState<CSSProperties>({});
const zoomCoordsRef = useRef<
| { screenWidth: number; screenHeight: number; x: number; y: number }
| undefined
>();
const onPrevious = useCallback(
(
@ -123,13 +136,14 @@ export function Lightbox({
const onKeyDown = useCallback(
(event: KeyboardEvent) => {
switch (event.key) {
case 'Escape':
case 'Escape': {
close();
event.preventDefault();
event.stopPropagation();
break;
}
case 'ArrowLeft':
onPrevious(event);
@ -207,9 +221,62 @@ export function Lightbox({
};
}, [isViewOnce, isAttachmentGIF, onTimeUpdate, playVideo, videoElement]);
const positionImage = useCallback((ev?: MouseEvent) => {
const imageNode = imageRef.current;
const zoomCoords = zoomCoordsRef.current;
if (!imageNode || !zoomCoords) {
return;
}
if (ev) {
zoomCoords.x = ev.clientX;
zoomCoords.y = ev.clientY;
}
const scaleX =
(-1 / zoomCoords.screenWidth) *
(imageNode.offsetWidth - zoomCoords.screenWidth);
const scaleY =
(-1 / zoomCoords.screenHeight) *
(imageNode.offsetHeight - zoomCoords.screenHeight);
setImagePanStyle({
transform: `translate(${zoomCoords.x * scaleX}px, ${
zoomCoords.y * scaleY
}px)`,
});
}, []);
function canPanImage(): boolean {
const imageNode = imageRef.current;
return Boolean(
imageNode &&
(imageNode.naturalWidth > document.documentElement.clientWidth ||
imageNode.naturalHeight > document.documentElement.clientHeight)
);
}
useEffect(() => {
const imageNode = imageRef.current;
let hasListener = false;
if (imageNode && zoomType !== ZoomType.None && canPanImage()) {
hasListener = true;
document.addEventListener('mousemove', positionImage);
}
return () => {
if (hasListener) {
document.removeEventListener('mousemove', positionImage);
}
};
}, [positionImage, zoomType]);
const caption = attachment?.caption;
let content: JSX.Element;
let shadowImage: JSX.Element | undefined;
if (!contentType) {
content = <>{children}</>;
} else {
@ -222,6 +289,19 @@ export function Lightbox({
if (isImageTypeSupported) {
if (objectURL) {
shadowImage = (
<div className="Lightbox__shadow-container">
<div className="Lightbox__object--container">
<img
alt={i18n('lightboxImageAlt')}
className="Lightbox__object"
ref={imageRef}
src={objectURL}
tabIndex={-1}
/>
</div>
</div>
);
content = (
<button
className="Lightbox__zoom-button"
@ -231,7 +311,22 @@ export function Lightbox({
event.preventDefault();
event.stopPropagation();
setZoomed(!zoomed);
if (zoomType === ZoomType.None) {
if (canPanImage()) {
setZoomType(ZoomType.ZoomAndPan);
zoomCoordsRef.current = {
screenWidth: document.documentElement.clientWidth,
screenHeight: document.documentElement.clientHeight,
x: event.clientX,
y: event.clientY,
};
positionImage();
} else {
setZoomType(ZoomType.FillScreen);
}
} else {
setZoomType(ZoomType.None);
}
}}
type="button"
>
@ -249,6 +344,7 @@ export function Lightbox({
}
}}
src={objectURL}
style={zoomType === ZoomType.ZoomAndPan ? imagePanStyle : {}}
/>
</button>
);
@ -308,8 +404,10 @@ export function Lightbox({
}
}
const hasNext = !zoomed && selectedIndex < media.length - 1;
const hasPrevious = !zoomed && selectedIndex > 0;
const isZoomed = zoomType !== ZoomType.None;
const hasNext = isZoomed && selectedIndex < media.length - 1;
const hasPrevious = isZoomed && selectedIndex > 0;
return root
? createPortal(
@ -339,7 +437,7 @@ export function Lightbox({
tabIndex={-1}
ref={focusRef}
>
{!zoomed && (
{!isZoomed && (
<div className="Lightbox__header">
{getConversation ? (
<LightboxHeader
@ -378,11 +476,15 @@ export function Lightbox({
)}
<div
className={classNames('Lightbox__object--container', {
'Lightbox__object--container--zoomed': zoomed,
'Lightbox__object--container--fill':
zoomType === ZoomType.FillScreen,
'Lightbox__object--container--zoom':
zoomType === ZoomType.ZoomAndPan,
})}
>
{content}
</div>
{shadowImage}
{hasPrevious && (
<div className="Lightbox__nav-prev">
<button
@ -404,7 +506,7 @@ export function Lightbox({
</div>
)}
</div>
{!zoomed && (
{!isZoomed && (
<div className="Lightbox__footer">
{isViewOnce && videoTime ? (
<div className="Lightbox__timestamp">

View file

@ -12564,6 +12564,20 @@
"reasonCategory": "usageTrusted",
"updated": "2021-08-23T18:39:37.081Z"
},
{
"rule": "React-useRef",
"path": "ts/components/Lightbox.tsx",
"line": " const imageRef = useRef<HTMLImageElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-09-24T00:03:36.061Z"
},
{
"rule": "React-useRef",
"path": "ts/components/Lightbox.tsx",
"line": " const zoomCoordsRef = useRef<",
"reasonCategory": "usageTrusted",
"updated": "2021-09-24T00:03:36.061Z"
},
{
"rule": "React-createRef",
"path": "ts/components/MainHeader.js",