PluginProbe
Gutenberg / 19.6.0
Gutenberg v19.6.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-module / block-library / image / view.js

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

441 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 :root {
341 --wp--lightbox-initial-top-position: ${screenPosY}px;
342 --wp--lightbox-initial-left-position: ${screenPosX}px;
343 --wp--lightbox-container-width: ${containerWidth + 1}px;
344 --wp--lightbox-container-height: ${containerHeight + 1}px;
345 --wp--lightbox-image-width: ${lightboxImgWidth}px;
346 --wp--lightbox-image-height: ${lightboxImgHeight}px;
347 --wp--lightbox-scale: ${containerScale};
348 --wp--lightbox-scrollbar-width: ${window.innerWidth - document.documentElement.clientWidth}px;
349 }
350 `;
351 },
352 setButtonStyles() {
353 const {
354 imageId
355 } = (0,interactivity_namespaceObject.getContext)();
356 const {
357 ref
358 } = (0,interactivity_namespaceObject.getElement)();
359 state.metadata[imageId].imageRef = ref;
360 state.metadata[imageId].currentSrc = ref.currentSrc;
361 const {
362 naturalWidth,
363 naturalHeight,
364 offsetWidth,
365 offsetHeight
366 } = ref;
367
368 // If the image isn't loaded yet, it can't calculate where the button
369 // should be.
370 if (naturalWidth === 0 || naturalHeight === 0) {
371 return;
372 }
373 const figure = ref.parentElement;
374 const figureWidth = ref.parentElement.clientWidth;
375
376 // It needs special handling for the height because a caption will cause
377 // the figure to be taller than the image, which means it needs to
378 // account for that when calculating the placement of the button in the
379 // top right corner of the image.
380 let figureHeight = ref.parentElement.clientHeight;
381 const caption = figure.querySelector('figcaption');
382 if (caption) {
383 const captionComputedStyle = window.getComputedStyle(caption);
384 if (!['absolute', 'fixed'].includes(captionComputedStyle.position)) {
385 figureHeight = figureHeight - caption.offsetHeight - parseFloat(captionComputedStyle.marginTop) - parseFloat(captionComputedStyle.marginBottom);
386 }
387 }
388 const buttonOffsetTop = figureHeight - offsetHeight;
389 const buttonOffsetRight = figureWidth - offsetWidth;
390 let imageButtonTop = buttonOffsetTop + 16;
391 let imageButtonRight = buttonOffsetRight + 16;
392
393 // In the case of an image with object-fit: contain, the size of the
394 // <img> element can be larger than the image itself, so it needs to
395 // calculate where to place the button.
396 if (state.metadata[imageId].scaleAttr === 'contain') {
397 // Natural ratio of the image.
398 const naturalRatio = naturalWidth / naturalHeight;
399 // Offset ratio of the image.
400 const offsetRatio = offsetWidth / offsetHeight;
401 if (naturalRatio >= offsetRatio) {
402 // If it reaches the width first, it keeps the width and compute the
403 // height.
404 const referenceHeight = offsetWidth / naturalRatio;
405 imageButtonTop = (offsetHeight - referenceHeight) / 2 + buttonOffsetTop + 16;
406 imageButtonRight = buttonOffsetRight + 16;
407 } else {
408 // If it reaches the height first, it keeps the height and compute
409 // the width.
410 const referenceWidth = offsetHeight * naturalRatio;
411 imageButtonTop = buttonOffsetTop + 16;
412 imageButtonRight = (offsetWidth - referenceWidth) / 2 + buttonOffsetRight + 16;
413 }
414 }
415 state.metadata[imageId].imageButtonTop = imageButtonTop;
416 state.metadata[imageId].imageButtonRight = imageButtonRight;
417 },
418 setOverlayFocus() {
419 if (state.overlayEnabled) {
420 // Moves the focus to the dialog when it opens.
421 const {
422 ref
423 } = (0,interactivity_namespaceObject.getElement)();
424 ref.focus();
425 }
426 },
427 initTriggerButton() {
428 const {
429 imageId
430 } = (0,interactivity_namespaceObject.getContext)();
431 const {
432 ref
433 } = (0,interactivity_namespaceObject.getElement)();
434 state.metadata[imageId].buttonRef = ref;
435 }
436 }
437 }, {
438 lock: true
439 });
440
441