PluginProbe
Gutenberg / 18.9.0
Gutenberg v18.9.0
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 / block-library / blocks / image / view.js

view.js in Gutenberg 18.9.0, at build/block-library/blocks/image/view.js

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