Skip to content

Commit 7dfd700

Browse files
NicolappsConvex, Inc.
authored andcommitted
docs: zoom on images when clicking them (#42340)
GitOrigin-RevId: 7efeb8d398789be674987715085871b859a0e869
1 parent 29b7aaf commit 7dfd700

File tree

6 files changed

+340
-0
lines changed

6 files changed

+340
-0
lines changed

npm-packages/docs/docusaurus.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ const config: Config = {
427427
],
428428
"./src/plugins/metrics",
429429
"./src/plugins/prefixIds",
430+
"./src/plugins/imageZoomPlugin",
430431
async function tailwindPlugin() {
431432
return {
432433
name: "docusaurus-tailwindcss",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import "./zoom-vanilla/zoom-vanilla";
2+
import "./zoom-vanilla/zoom-vanilla.css";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import path from "path";
2+
3+
export default () => ({
4+
name: "imageZoom",
5+
getClientModules() {
6+
return [path.resolve(__dirname, "./imageZoom")];
7+
},
8+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Forked from https://github.com/spinningarrow/zoom-vanilla.js
2+
3+
## License
4+
5+
The MIT License
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy of
8+
this software and associated documentation files (the "Software"), to deal in
9+
the Software without restriction, including without limitation the rights to
10+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11+
the Software, and to permit persons to whom the Software is furnished to do so,
12+
subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all
15+
copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
img[data-action="zoom"],
2+
p > img,
3+
.image-center img {
4+
cursor: zoom-in;
5+
}
6+
7+
.zoom-img,
8+
.zoom-img-wrap {
9+
position: relative;
10+
z-index: 666;
11+
transition: all 300ms;
12+
}
13+
14+
img.zoom-img {
15+
cursor: zoom-out;
16+
}
17+
18+
.zoom-overlay {
19+
cursor: zoom-out;
20+
z-index: 420;
21+
background: rgba(255, 255, 255, 0.9);
22+
position: fixed;
23+
top: 0;
24+
left: 0;
25+
right: 0;
26+
bottom: 0;
27+
opacity: 0;
28+
transition: opacity 300ms;
29+
}
30+
31+
html[data-theme="dark"] .zoom-overlay {
32+
background: rgba(0, 0, 0, 0.9);
33+
}
34+
35+
.zoom-overlay-open .zoom-overlay {
36+
opacity: 1;
37+
}
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
+(function () {
2+
"use strict";
3+
var OFFSET = 80;
4+
5+
const SELECTOR = "p > img, .image-center img";
6+
7+
if (typeof document === "undefined") return;
8+
9+
// From http://youmightnotneedjquery.com/#offset
10+
function offset(element) {
11+
var rect = element.getBoundingClientRect();
12+
var scrollTop =
13+
window.pageYOffset ||
14+
document.documentElement.scrollTop ||
15+
document.body.scrollTop ||
16+
0;
17+
var scrollLeft =
18+
window.pageXOffset ||
19+
document.documentElement.scrollLeft ||
20+
document.body.scrollLeft ||
21+
0;
22+
return {
23+
top: rect.top + scrollTop,
24+
left: rect.left + scrollLeft,
25+
};
26+
}
27+
28+
function zoomListener() {
29+
var activeZoom = null;
30+
var initialScrollPosition = null;
31+
var initialTouchPosition = null;
32+
33+
function listen() {
34+
document.body.addEventListener("click", function (event) {
35+
if (!event.target.matches(SELECTOR)) return;
36+
37+
zoom(event);
38+
});
39+
}
40+
41+
function zoom(event) {
42+
event.stopPropagation();
43+
44+
if (document.body.classList.contains("zoom-overlay-open")) return;
45+
46+
if (event.metaKey || event.ctrlKey) return openInNewWindow();
47+
48+
closeActiveZoom({ forceDispose: true });
49+
50+
activeZoom = vanillaZoom(event.target);
51+
activeZoom.zoomImage();
52+
53+
addCloseActiveZoomListeners();
54+
}
55+
56+
function openInNewWindow() {
57+
window.open(
58+
event.target.getAttribute("data-original") ||
59+
event.target.currentSrc ||
60+
event.target.src,
61+
"_blank",
62+
);
63+
}
64+
65+
function closeActiveZoom(options) {
66+
options = options || { forceDispose: false };
67+
if (!activeZoom) return;
68+
69+
activeZoom[options.forceDispose ? "dispose" : "close"]();
70+
removeCloseActiveZoomListeners();
71+
activeZoom = null;
72+
}
73+
74+
function addCloseActiveZoomListeners() {
75+
// todo(fat): probably worth throttling this
76+
window.addEventListener("scroll", handleScroll);
77+
document.addEventListener("click", handleClick);
78+
document.addEventListener("keyup", handleEscPressed);
79+
document.addEventListener("touchstart", handleTouchStart);
80+
document.addEventListener("touchend", handleClick);
81+
}
82+
83+
function removeCloseActiveZoomListeners() {
84+
window.removeEventListener("scroll", handleScroll);
85+
document.removeEventListener("keyup", handleEscPressed);
86+
document.removeEventListener("click", handleClick);
87+
document.removeEventListener("touchstart", handleTouchStart);
88+
document.removeEventListener("touchend", handleClick);
89+
}
90+
91+
function handleScroll() {
92+
if (initialScrollPosition === null)
93+
initialScrollPosition = window.pageYOffset;
94+
var deltaY = initialScrollPosition - window.pageYOffset;
95+
if (Math.abs(deltaY) >= 40) closeActiveZoom();
96+
}
97+
98+
function handleEscPressed(event) {
99+
if (event.keyCode === 27) closeActiveZoom();
100+
}
101+
102+
function handleClick(event) {
103+
event.stopPropagation();
104+
event.preventDefault();
105+
closeActiveZoom();
106+
}
107+
108+
function handleTouchStart(event) {
109+
initialTouchPosition = event.touches[0].pageY;
110+
event.target.addEventListener("touchmove", handleTouchMove);
111+
}
112+
113+
function handleTouchMove(event) {
114+
if (Math.abs(event.touches[0].pageY - initialTouchPosition) <= 10) return;
115+
closeActiveZoom();
116+
event.target.removeEventListener("touchmove", handleTouchMove);
117+
}
118+
119+
return { listen: listen };
120+
}
121+
122+
var vanillaZoom = (function () {
123+
var fullHeight = null;
124+
var fullWidth = null;
125+
var overlay = null;
126+
var imgScaleFactor = null;
127+
128+
var targetImage = null;
129+
var targetImageWrap = null;
130+
var targetImageClone = null;
131+
132+
function zoomImage() {
133+
var img = document.createElement("img");
134+
img.onload = function () {
135+
fullHeight = Number(img.height);
136+
fullWidth = Number(img.width);
137+
zoomOriginal();
138+
};
139+
img.src = targetImage.currentSrc || targetImage.src;
140+
}
141+
142+
function zoomOriginal() {
143+
targetImageWrap = document.createElement("div");
144+
targetImageWrap.className = "zoom-img-wrap";
145+
targetImageWrap.style.position = "absolute";
146+
targetImageWrap.style.top = offset(targetImage).top + "px";
147+
targetImageWrap.style.left = offset(targetImage).left + "px";
148+
149+
targetImageClone = targetImage.cloneNode();
150+
targetImageClone.style.visibility = "hidden";
151+
152+
targetImage.style.width = targetImage.offsetWidth + "px";
153+
targetImage.parentNode.replaceChild(targetImageClone, targetImage);
154+
155+
document.body.appendChild(targetImageWrap);
156+
targetImageWrap.appendChild(targetImage);
157+
158+
targetImage.classList.add("zoom-img");
159+
targetImage.setAttribute("data-action", "zoom-out");
160+
161+
overlay = document.createElement("div");
162+
overlay.className = "zoom-overlay";
163+
164+
document.body.appendChild(overlay);
165+
166+
calculateZoom();
167+
triggerAnimation();
168+
}
169+
170+
function calculateZoom() {
171+
targetImage.offsetWidth; // repaint before animating
172+
173+
var originalFullImageWidth = fullWidth;
174+
var originalFullImageHeight = fullHeight;
175+
176+
var maxScaleFactor = originalFullImageWidth / targetImage.width;
177+
178+
var viewportHeight = window.innerHeight - OFFSET;
179+
var viewportWidth = window.innerWidth - OFFSET;
180+
181+
var imageAspectRatio = originalFullImageWidth / originalFullImageHeight;
182+
var viewportAspectRatio = viewportWidth / viewportHeight;
183+
184+
if (
185+
originalFullImageWidth < viewportWidth &&
186+
originalFullImageHeight < viewportHeight
187+
) {
188+
imgScaleFactor = maxScaleFactor;
189+
} else if (imageAspectRatio < viewportAspectRatio) {
190+
imgScaleFactor =
191+
(viewportHeight / originalFullImageHeight) * maxScaleFactor;
192+
} else {
193+
imgScaleFactor =
194+
(viewportWidth / originalFullImageWidth) * maxScaleFactor;
195+
}
196+
}
197+
198+
function triggerAnimation() {
199+
targetImage.offsetWidth; // repaint before animating
200+
201+
var imageOffset = offset(targetImage);
202+
var scrollTop = window.pageYOffset;
203+
204+
var viewportY = scrollTop + window.innerHeight / 2;
205+
var viewportX = window.innerWidth / 2;
206+
207+
var imageCenterY = imageOffset.top + targetImage.height / 2;
208+
var imageCenterX = imageOffset.left + targetImage.width / 2;
209+
210+
var translateY = Math.round(viewportY - imageCenterY);
211+
var translateX = Math.round(viewportX - imageCenterX);
212+
213+
var targetImageTransform = "scale(" + imgScaleFactor + ")";
214+
var targetImageWrapTransform =
215+
"translate(" + translateX + "px, " + translateY + "px) translateZ(0)";
216+
217+
targetImage.style.webkitTransform = targetImageTransform;
218+
targetImage.style.msTransform = targetImageTransform;
219+
targetImage.style.transform = targetImageTransform;
220+
221+
targetImageWrap.style.webkitTransform = targetImageWrapTransform;
222+
targetImageWrap.style.msTransform = targetImageWrapTransform;
223+
targetImageWrap.style.transform = targetImageWrapTransform;
224+
225+
document.body.classList.add("zoom-overlay-open");
226+
}
227+
228+
function close() {
229+
document.body.classList.remove("zoom-overlay-open");
230+
document.body.classList.add("zoom-overlay-transitioning");
231+
232+
targetImage.style.webkitTransform = "";
233+
targetImage.style.msTransform = "";
234+
targetImage.style.transform = "";
235+
236+
targetImageWrap.style.webkitTransform = "";
237+
targetImageWrap.style.msTransform = "";
238+
targetImageWrap.style.transform = "";
239+
240+
if ((!"transition") in document.body.style) return dispose();
241+
242+
targetImageWrap.addEventListener("transitionend", dispose);
243+
targetImageWrap.addEventListener("webkitTransitionEnd", dispose);
244+
}
245+
246+
function dispose() {
247+
targetImage.removeEventListener("transitionend", dispose);
248+
targetImage.removeEventListener("webkitTransitionEnd", dispose);
249+
250+
if (!targetImageWrap || !targetImageWrap.parentNode) return;
251+
252+
targetImage.classList.remove("zoom-img");
253+
targetImage.style.width = "";
254+
targetImage.setAttribute("data-action", "zoom");
255+
256+
targetImageClone.parentNode.replaceChild(targetImage, targetImageClone);
257+
targetImageWrap.parentNode.removeChild(targetImageWrap);
258+
overlay.parentNode.removeChild(overlay);
259+
260+
document.body.classList.remove("zoom-overlay-transitioning");
261+
}
262+
263+
return function (target) {
264+
targetImage = target;
265+
return { zoomImage: zoomImage, close: close, dispose: dispose };
266+
};
267+
})();
268+
269+
zoomListener().listen();
270+
})();

0 commit comments

Comments
 (0)