PluginProbe
Gutenberg / 20.0.1
Gutenberg v20.0.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-module / block-library / image / view.js

view.js in Gutenberg 20.0.1, at build-module/block-library/image/view.js

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