Skip to content

Commit 23606da

Browse files
authored
Move image zooming back to unvendorized lib (outline#6980)
* Move image zooming back to unvendorized lib * refactor * perf: Avoid mounting zoom dialog until interacted * Add captions to lightbox * lightbox
1 parent 62ebba1 commit 23606da

15 files changed

+207
-1260
lines changed

.jestconfig.json

+8-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"roots": ["<rootDir>/server", "<rootDir>/plugins"],
88
"moduleNameMapper": {
99
"^@server/(.*)$": "<rootDir>/server/$1",
10-
"^@shared/(.*)$": "<rootDir>/shared/$1"
10+
"^@shared/(.*)$": "<rootDir>/shared/$1",
11+
"react-medium-image-zoom": "<rootDir>/__mocks__/react-medium-image-zoom.js"
1112
},
1213
"setupFiles": ["<rootDir>/__mocks__/console.js"],
1314
"setupFilesAfterEnv": ["<rootDir>/server/test/setup.ts"],
@@ -22,7 +23,8 @@
2223
"^~/(.*)$": "<rootDir>/app/$1",
2324
"^@shared/(.*)$": "<rootDir>/shared/$1",
2425
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js",
25-
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js"
26+
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js",
27+
"react-medium-image-zoom": "<rootDir>/__mocks__/react-medium-image-zoom.js"
2628
},
2729
"modulePaths": ["<rootDir>/app"],
2830
"setupFiles": ["<rootDir>/__mocks__/window.js"],
@@ -37,7 +39,8 @@
3739
"roots": ["<rootDir>/shared"],
3840
"moduleNameMapper": {
3941
"^@server/(.*)$": "<rootDir>/server/$1",
40-
"^@shared/(.*)$": "<rootDir>/shared/$1"
42+
"^@shared/(.*)$": "<rootDir>/shared/$1",
43+
"react-medium-image-zoom": "<rootDir>/__mocks__/react-medium-image-zoom.js"
4144
},
4245
"setupFiles": ["<rootDir>/__mocks__/console.js"],
4346
"setupFilesAfterEnv": ["<rootDir>/shared/test/setup.ts"],
@@ -50,7 +53,8 @@
5053
"^~/(.*)$": "<rootDir>/app/$1",
5154
"^@shared/(.*)$": "<rootDir>/shared/$1",
5255
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js",
53-
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js"
56+
"^uuid$": "<rootDir>/node_modules/uuid/dist/index.js",
57+
"react-medium-image-zoom": "<rootDir>/__mocks__/react-medium-image-zoom.js"
5458
},
5559
"setupFiles": ["<rootDir>/__mocks__/window.js"],
5660
"testEnvironment": "jsdom",

__mocks__/react-medium-image-zoom.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default null;

app/editor/index.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,13 @@ export class Editor extends React.PureComponent<
618618
*/
619619
public getHeadings = () => ProsemirrorHelper.getHeadings(this.view.state.doc);
620620

621+
/**
622+
* Return the images in the current editor.
623+
*
624+
* @returns A list of images in the document
625+
*/
626+
public getImages = () => ProsemirrorHelper.getImages(this.view.state.doc);
627+
621628
/**
622629
* Return the tasks/checkmarks in the current editor.
623630
*

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@
197197
"react-helmet-async": "^2.0.5",
198198
"react-hook-form": "^7.41.5",
199199
"react-i18next": "^12.3.1",
200+
"react-medium-image-zoom": "^5.2.4",
200201
"react-merge-refs": "^2.0.2",
201202
"react-portal": "^4.2.2",
202203
"react-router-dom": "^5.3.4",

shared/editor/components/Image.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import styled from "styled-components";
55
import { s } from "../../styles";
66
import { sanitizeUrl } from "../../utils/urls";
77
import { ComponentProps } from "../types";
8-
import ImageZoom from "./ImageZoom";
8+
import { ImageZoom } from "./ImageZoom";
99
import { ResizeLeft, ResizeRight } from "./ResizeHandle";
1010
import useDragResize from "./hooks/useDragResize";
1111

@@ -70,7 +70,7 @@ const Image = (props: Props) => {
7070
<DownloadIcon />
7171
</Button>
7272
)}
73-
<ImageZoom zoomMargin={24}>
73+
<ImageZoom caption={props.node.attrs.alt}>
7474
<img
7575
style={{
7676
...widthStyle,
+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { transparentize } from "polished";
2+
import * as React from "react";
3+
import styled, { createGlobalStyle } from "styled-components";
4+
import { s } from "../../styles";
5+
import { EditorStyleHelper } from "../styles/EditorStyleHelper";
6+
7+
type Props = {
8+
/** An optional caption to display below the image */
9+
caption?: string;
10+
children: React.ReactNode;
11+
};
12+
13+
/**
14+
* Component that wraps an image with the ability to zoom in
15+
*/
16+
export const ImageZoom = ({ caption, children }: Props) => {
17+
const Zoom = React.lazy(() => import("react-medium-image-zoom"));
18+
const [isActivated, setIsActivated] = React.useState(false);
19+
20+
const handleActivated = React.useCallback(() => {
21+
setIsActivated(true);
22+
}, []);
23+
24+
const fallback = (
25+
<span onPointerEnter={handleActivated} onFocus={handleActivated}>
26+
{children}
27+
</span>
28+
);
29+
30+
if (!isActivated) {
31+
return fallback;
32+
}
33+
34+
return (
35+
<React.Suspense fallback={fallback}>
36+
<Styles />
37+
<Zoom
38+
zoomMargin={EditorStyleHelper.padding}
39+
ZoomContent={(props) => <Lightbox caption={caption} {...props} />}
40+
>
41+
<div>{children}</div>
42+
</Zoom>
43+
</React.Suspense>
44+
);
45+
};
46+
47+
const Lightbox = ({
48+
caption,
49+
modalState,
50+
img,
51+
}: {
52+
caption: string | undefined;
53+
modalState: string;
54+
img: React.ReactNode;
55+
}) => (
56+
<figure>
57+
{img}
58+
<Caption $loaded={modalState === "LOADED"}>{caption}</Caption>
59+
</figure>
60+
);
61+
62+
const Caption = styled("figcaption")<{ $loaded: boolean }>`
63+
position: absolute;
64+
bottom: 0;
65+
left: 50%;
66+
transform: translateX(-50%);
67+
margin-bottom: ${EditorStyleHelper.padding}px;
68+
font-size: 15px;
69+
opacity: ${(props) => (props.$loaded ? 1 : 0)};
70+
transition: opacity 250ms;
71+
72+
font-weight: normal;
73+
color: ${s("textSecondary")};
74+
`;
75+
76+
const Styles = createGlobalStyle`
77+
[data-rmiz] {
78+
position: relative;
79+
}
80+
[data-rmiz-ghost] {
81+
position: absolute;
82+
pointer-events: none;
83+
}
84+
[data-rmiz-btn-zoom],
85+
[data-rmiz-btn-unzoom] {
86+
display: none;
87+
}
88+
[data-rmiz-btn-zoom]:not(:focus):not(:active) {
89+
position: absolute;
90+
clip: rect(0 0 0 0);
91+
clip-path: inset(50%);
92+
height: 1px;
93+
overflow: hidden;
94+
pointer-events: none;
95+
white-space: nowrap;
96+
width: 1px;
97+
}
98+
[data-rmiz-btn-zoom] {
99+
position: absolute;
100+
inset: 10px 10px auto auto;
101+
cursor: zoom-in;
102+
}
103+
[data-rmiz-btn-unzoom] {
104+
position: absolute;
105+
inset: 20px 20px auto auto;
106+
cursor: zoom-out;
107+
z-index: 1;
108+
}
109+
[data-rmiz-content="found"] img,
110+
[data-rmiz-content="found"] svg,
111+
[data-rmiz-content="found"] [role="img"],
112+
[data-rmiz-content="found"] [data-zoom] {
113+
cursor: zoom-in;
114+
}
115+
[data-rmiz-modal]::backdrop {
116+
display: none;
117+
}
118+
[data-rmiz-modal][open] {
119+
position: fixed;
120+
width: 100vw;
121+
width: 100dvw;
122+
height: 100vh;
123+
height: 100dvh;
124+
max-width: none;
125+
max-height: none;
126+
margin: 0;
127+
padding: 0;
128+
border: 0;
129+
background: transparent;
130+
overflow: hidden;
131+
}
132+
[data-rmiz-modal-overlay] {
133+
position: absolute;
134+
inset: 0;
135+
transition: background-color 0.3s;
136+
}
137+
[data-rmiz-modal-overlay="hidden"] {
138+
background-color: ${(props) => transparentize(1, props.theme.background)};
139+
}
140+
[data-rmiz-modal-overlay="visible"] {
141+
background-color: ${s("background")};
142+
}
143+
[data-rmiz-modal-content] {
144+
position: relative;
145+
width: 100%;
146+
height: 100%;
147+
}
148+
[data-rmiz-modal-img] {
149+
position: absolute;
150+
cursor: zoom-out;
151+
image-rendering: high-quality;
152+
transform-origin: top left;
153+
transition: transform 0.3s;
154+
}
155+
@media (prefers-reduced-motion: reduce) {
156+
[data-rmiz-modal-overlay],
157+
[data-rmiz-modal-img] {
158+
transition-duration: 0.01ms !important;
159+
}
160+
}
161+
`;

0 commit comments

Comments
 (0)