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 / interactivity / image.js

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

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