PluginProbe
Gutenberg / 17.8.1
Gutenberg v17.8.1
24.0.0 23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 All 403 releases
gutenberg / build / interactivity / image.js

image.js in Gutenberg 17.8.1, at build/interactivity/image.js

420 lines 16.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import * as __WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__ from "@wordpress/interactivity";
2 /******/ // The require scope
3 /******/ var __webpack_require__ = {};
4 /******/
5 /************************************************************************/
6 /******/ /* webpack/runtime/define property getters */
7 /******/ (() => {
8 /******/ // define getter functions for harmony exports
9 /******/ __webpack_require__.d = (exports, definition) => {
10 /******/ for(var key in definition) {
11 /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
12 /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
13 /******/ }
14 /******/ }
15 /******/ };
16 /******/ })();
17 /******/
18 /******/ /* webpack/runtime/hasOwnProperty shorthand */
19 /******/ (() => {
20 /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
21 /******/ })();
22 /******/
23 /************************************************************************/
24 var __webpack_exports__ = {};
25
26 ;// CONCATENATED MODULE: external "@wordpress/interactivity"
27 var x = y => { var x = {}; __webpack_require__.d(x, y); return x; }
28 var y = x => () => x
29 const interactivity_namespaceObject = x({ ["getContext"]: () => __WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__.getContext, ["getElement"]: () => __WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__.getElement, ["store"]: () => __WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__.store });
30 ;// CONCATENATED MODULE: ./packages/block-library/src/image/view.js
31 /**
32 * WordPress dependencies
33 */
34
35
36 /**
37 * Tracks whether user is touching screen; used to differentiate behavior for
38 * touch and mouse input.
39 *
40 * @type {boolean}
41 */
42 let isTouching = false;
43
44 /**
45 * Tracks the last time the screen was touched; used to differentiate behavior
46 * for touch and mouse input.
47 *
48 * @type {number}
49 */
50 let lastTouchTime = 0;
51
52 /**
53 * Stores the image reference of the currently opened lightbox.
54 *
55 * @type {HTMLElement}
56 */
57 let imageRef;
58
59 /**
60 * Stores the button reference of the currently opened lightbox.
61 *
62 * @type {HTMLElement}
63 */
64 let buttonRef;
65 const {
66 state,
67 actions,
68 callbacks
69 } = (0,interactivity_namespaceObject.store)('core/image', {
70 state: {
71 currentImage: {},
72 get overlayOpened() {
73 return state.currentImage.currentSrc;
74 },
75 get roleAttribute() {
76 return state.overlayOpened ? 'dialog' : null;
77 },
78 get ariaModal() {
79 return state.overlayOpened ? 'true' : null;
80 },
81 get enlargedSrc() {
82 return state.currentImage.uploadedSrc || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
83 },
84 get imgStyles() {
85 return state.overlayOpened && `${state.currentImage.imgStyles?.replace(/;$/, '')}; object-fit:cover;`;
86 }
87 },
88 actions: {
89 showLightbox() {
90 const ctx = (0,interactivity_namespaceObject.getContext)();
91
92 // Bails out if the image has not loaded yet.
93 if (!ctx.imageRef?.complete) {
94 return;
95 }
96
97 // Stores the positons of the scroll to fix it until the overlay is
98 // closed.
99 state.scrollTopReset = document.documentElement.scrollTop;
100 state.scrollLeftReset = document.documentElement.scrollLeft;
101
102 // Moves the information of the expaned image to the state.
103 ctx.currentSrc = ctx.imageRef.currentSrc;
104 imageRef = ctx.imageRef;
105 buttonRef = ctx.buttonRef;
106 state.currentImage = ctx;
107 state.overlayEnabled = true;
108
109 // Computes the styles of the overlay for the animation.
110 callbacks.setOverlayStyles();
111 },
112 hideLightbox() {
113 if (state.overlayEnabled) {
114 // Waits until the close animation has completed before allowing a
115 // user to scroll again. The duration of this animation is defined in
116 // the `styles.scss` file, but in any case we should wait a few
117 // milliseconds longer than the duration, otherwise a user may scroll
118 // too soon and cause the animation to look sloppy.
119 setTimeout(function () {
120 // Delays before changing the focus. Otherwise the focus ring will
121 // appear on Firefox before the image has finished animating, which
122 // looks broken.
123 buttonRef.focus({
124 preventScroll: true
125 });
126
127 // Resets the current image to mark the overlay as closed.
128 state.currentImage = {};
129 imageRef = null;
130 buttonRef = null;
131 }, 450);
132
133 // Starts the overlay closing animation. The showClosingAnimation
134 // class is used to avoid showing it on page load.
135 state.showClosingAnimation = true;
136 state.overlayEnabled = false;
137 }
138 },
139 handleKeydown(event) {
140 if (state.overlayEnabled) {
141 // Focuses the close button when the user presses the tab key.
142 if (event.key === 'Tab') {
143 event.preventDefault();
144 const {
145 ref
146 } = (0,interactivity_namespaceObject.getElement)();
147 ref.querySelector('button').focus();
148 }
149 // Closes the lightbox when the user presses the escape key.
150 if (event.key === 'Escape') {
151 actions.hideLightbox();
152 }
153 }
154 },
155 handleTouchMove(event) {
156 // On mobile devices, prevents triggering the scroll event because
157 // otherwise the page jumps around when it resets the scroll position.
158 // This also means that closing the lightbox requires that a user
159 // perform a simple tap. This may be changed in the future if there is a
160 // better alternative to override or reset the scroll position during
161 // swipe actions.
162 if (state.overlayEnabled) {
163 event.preventDefault();
164 }
165 },
166 handleTouchStart() {
167 isTouching = true;
168 },
169 handleTouchEnd() {
170 // Waits a few milliseconds before resetting to ensure that pinch to
171 // zoom works consistently on mobile devices when the lightbox is open.
172 lastTouchTime = Date.now();
173 isTouching = false;
174 },
175 handleScroll() {
176 // Prevents scrolling behaviors that trigger content shift while the
177 // lightbox is open. It would be better to accomplish through CSS alone,
178 // but using overflow: hidden is currently the only way to do so and
179 // that causes a layout to shift and prevents the zoom animation from
180 // working in some cases because it's not possible to account for the
181 // layout shift when doing the animation calculations. Instead, it uses
182 // JavaScript to prevent and reset the scrolling behavior.
183 if (state.overlayOpened) {
184 // Avoids overriding the scroll behavior on mobile devices because
185 // doing so breaks the pinch to zoom functionality, and users should
186 // be able to zoom in further on the high-res image.
187 if (!isTouching && Date.now() - lastTouchTime > 450) {
188 // It doesn't rely on `event.preventDefault()` to prevent scrolling
189 // because the scroll event can't be canceled, so it resets the
190 // position instead.
191 window.scrollTo(state.scrollLeftReset, state.scrollTopReset);
192 }
193 }
194 }
195 },
196 callbacks: {
197 setOverlayStyles() {
198 if (!imageRef) return;
199 let {
200 naturalWidth,
201 naturalHeight,
202 offsetWidth: originalWidth,
203 offsetHeight: originalHeight
204 } = imageRef;
205 let {
206 x: screenPosX,
207 y: screenPosY
208 } = imageRef.getBoundingClientRect();
209
210 // Natural ratio of the image clicked to open the lightbox.
211 const naturalRatio = naturalWidth / naturalHeight;
212 // Original ratio of the image clicked to open the lightbox.
213 let originalRatio = originalWidth / originalHeight;
214
215 // If it has object-fit: contain, recalculates the original sizes
216 // and the screen position without the blank spaces.
217 if (state.currentImage.scaleAttr === 'contain') {
218 if (naturalRatio > originalRatio) {
219 const heightWithoutSpace = originalWidth / naturalRatio;
220 // Recalculates screen position without the top space.
221 screenPosY += (originalHeight - heightWithoutSpace) / 2;
222 originalHeight = heightWithoutSpace;
223 } else {
224 const widthWithoutSpace = originalHeight * naturalRatio;
225 // Recalculates screen position without the left space.
226 screenPosX += (originalWidth - widthWithoutSpace) / 2;
227 originalWidth = widthWithoutSpace;
228 }
229 }
230 originalRatio = originalWidth / originalHeight;
231
232 // Typically, it uses the image's full-sized dimensions. If those
233 // dimensions have not been set (i.e. an external image with only one
234 // size), the image's dimensions in the lightbox are the same
235 // as those of the image in the content.
236 let imgMaxWidth = parseFloat(state.currentImage.targetWidth !== 'none' ? state.currentImage.targetWidth : naturalWidth);
237 let imgMaxHeight = parseFloat(state.currentImage.targetHeight !== 'none' ? state.currentImage.targetHeight : naturalHeight);
238
239 // Ratio of the biggest image stored in the database.
240 let imgRatio = imgMaxWidth / imgMaxHeight;
241 let containerMaxWidth = imgMaxWidth;
242 let containerMaxHeight = imgMaxHeight;
243 let containerWidth = imgMaxWidth;
244 let containerHeight = imgMaxHeight;
245 // Checks if the target image has a different ratio than the original
246 // one (thumbnail). Recalculates the width and height.
247 if (naturalRatio.toFixed(2) !== imgRatio.toFixed(2)) {
248 if (naturalRatio > imgRatio) {
249 // If the width is reached before the height, it keeps the maxWidth
250 // and recalculates the height unless the difference between the
251 // maxHeight and the reducedHeight is higher than the maxWidth,
252 // where it keeps the reducedHeight and recalculate the width.
253 const reducedHeight = imgMaxWidth / naturalRatio;
254 if (imgMaxHeight - reducedHeight > imgMaxWidth) {
255 imgMaxHeight = reducedHeight;
256 imgMaxWidth = reducedHeight * naturalRatio;
257 } else {
258 imgMaxHeight = imgMaxWidth / naturalRatio;
259 }
260 } else {
261 // If the height is reached before the width, it keeps the maxHeight
262 // and recalculate the width unlesss the difference between the
263 // maxWidth and the reducedWidth is higher than the maxHeight, where
264 // it keeps the reducedWidth and recalculate the height.
265 const reducedWidth = imgMaxHeight * naturalRatio;
266 if (imgMaxWidth - reducedWidth > imgMaxHeight) {
267 imgMaxWidth = reducedWidth;
268 imgMaxHeight = reducedWidth / naturalRatio;
269 } else {
270 imgMaxWidth = imgMaxHeight * naturalRatio;
271 }
272 }
273 containerWidth = imgMaxWidth;
274 containerHeight = imgMaxHeight;
275 imgRatio = imgMaxWidth / imgMaxHeight;
276
277 // Calculates the max size of the container.
278 if (originalRatio > imgRatio) {
279 containerMaxWidth = imgMaxWidth;
280 containerMaxHeight = containerMaxWidth / originalRatio;
281 } else {
282 containerMaxHeight = imgMaxHeight;
283 containerMaxWidth = containerMaxHeight * originalRatio;
284 }
285 }
286
287 // If the image has been pixelated on purpose, it keeps that size.
288 if (originalWidth > containerWidth || originalHeight > containerHeight) {
289 containerWidth = originalWidth;
290 containerHeight = originalHeight;
291 }
292
293 // Calculates the final lightbox image size and the scale factor.
294 // MaxWidth is either the window container (accounting for padding) or
295 // the image resolution.
296 let horizontalPadding = 0;
297 if (window.innerWidth > 480) {
298 horizontalPadding = 80;
299 } else if (window.innerWidth > 1920) {
300 horizontalPadding = 160;
301 }
302 const verticalPadding = 80;
303 const targetMaxWidth = Math.min(window.innerWidth - horizontalPadding, containerWidth);
304 const targetMaxHeight = Math.min(window.innerHeight - verticalPadding, containerHeight);
305 const targetContainerRatio = targetMaxWidth / targetMaxHeight;
306 if (originalRatio > targetContainerRatio) {
307 // If targetMaxWidth is reached before targetMaxHeight.
308 containerWidth = targetMaxWidth;
309 containerHeight = containerWidth / originalRatio;
310 } else {
311 // If targetMaxHeight is reached before targetMaxWidth.
312 containerHeight = targetMaxHeight;
313 containerWidth = containerHeight * originalRatio;
314 }
315 const containerScale = originalWidth / containerWidth;
316 const lightboxImgWidth = imgMaxWidth * (containerWidth / containerMaxWidth);
317 const lightboxImgHeight = imgMaxHeight * (containerHeight / containerMaxHeight);
318
319 // As of this writing, using the calculations above will render the
320 // lightbox with a small, erroneous whitespace on the left side of the
321 // image in iOS Safari, perhaps due to an inconsistency in how browsers
322 // handle absolute positioning and CSS transformation. In any case,
323 // adding 1 pixel to the container width and height solves the problem,
324 // though this can be removed if the issue is fixed in the future.
325 state.overlayStyles = `
326 :root {
327 --wp--lightbox-initial-top-position: ${screenPosY}px;
328 --wp--lightbox-initial-left-position: ${screenPosX}px;
329 --wp--lightbox-container-width: ${containerWidth + 1}px;
330 --wp--lightbox-container-height: ${containerHeight + 1}px;
331 --wp--lightbox-image-width: ${lightboxImgWidth}px;
332 --wp--lightbox-image-height: ${lightboxImgHeight}px;
333 --wp--lightbox-scale: ${containerScale};
334 --wp--lightbox-scrollbar-width: ${window.innerWidth - document.documentElement.clientWidth}px;
335 }
336 `;
337 },
338 setButtonStyles() {
339 const ctx = (0,interactivity_namespaceObject.getContext)();
340 const {
341 ref
342 } = (0,interactivity_namespaceObject.getElement)();
343 ctx.imageRef = ref;
344 const {
345 naturalWidth,
346 naturalHeight,
347 offsetWidth,
348 offsetHeight
349 } = ref;
350
351 // If the image isn't loaded yet, it can't calculate where the button
352 // should be.
353 if (naturalWidth === 0 || naturalHeight === 0) {
354 return;
355 }
356 const figure = ref.parentElement;
357 const figureWidth = ref.parentElement.clientWidth;
358
359 // It needs special handling for the height because a caption will cause
360 // the figure to be taller than the image, which means it needs to
361 // account for that when calculating the placement of the button in the
362 // top right corner of the image.
363 let figureHeight = ref.parentElement.clientHeight;
364 const caption = figure.querySelector('figcaption');
365 if (caption) {
366 const captionComputedStyle = window.getComputedStyle(caption);
367 if (!['absolute', 'fixed'].includes(captionComputedStyle.position)) {
368 figureHeight = figureHeight - caption.offsetHeight - parseFloat(captionComputedStyle.marginTop) - parseFloat(captionComputedStyle.marginBottom);
369 }
370 }
371 const buttonOffsetTop = figureHeight - offsetHeight;
372 const buttonOffsetRight = figureWidth - offsetWidth;
373
374 // In the case of an image with object-fit: contain, the size of the
375 // <img> element can be larger than the image itself, so it needs to
376 // calculate where to place the button.
377 if (ctx.scaleAttr === 'contain') {
378 // Natural ratio of the image.
379 const naturalRatio = naturalWidth / naturalHeight;
380 // Offset ratio of the image.
381 const offsetRatio = offsetWidth / offsetHeight;
382 if (naturalRatio >= offsetRatio) {
383 // If it reaches the width first, it keeps the width and compute the
384 // height.
385 const referenceHeight = offsetWidth / naturalRatio;
386 ctx.imageButtonTop = (offsetHeight - referenceHeight) / 2 + buttonOffsetTop + 16;
387 ctx.imageButtonRight = buttonOffsetRight + 16;
388 } else {
389 // If it reaches the height first, it keeps the height and compute
390 // the width.
391 const referenceWidth = offsetHeight * naturalRatio;
392 ctx.imageButtonTop = buttonOffsetTop + 16;
393 ctx.imageButtonRight = (offsetWidth - referenceWidth) / 2 + buttonOffsetRight + 16;
394 }
395 } else {
396 ctx.imageButtonTop = buttonOffsetTop + 16;
397 ctx.imageButtonRight = buttonOffsetRight + 16;
398 }
399 },
400 setOverlayFocus() {
401 if (state.overlayEnabled) {
402 // Moves the focus to the dialog when it opens.
403 const {
404 ref
405 } = (0,interactivity_namespaceObject.getElement)();
406 ref.focus();
407 }
408 },
409 initTriggerButton() {
410 const ctx = (0,interactivity_namespaceObject.getContext)();
411 const {
412 ref
413 } = (0,interactivity_namespaceObject.getElement)();
414 ctx.buttonRef = ref;
415 }
416 }
417 }, {
418 lock: true
419 });
420