PluginProbe
Gutenberg / 17.6.2
Gutenberg v17.6.2
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.6.2, at build/interactivity/image.js

522 lines 20.4 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 const focusableSelectors = ['a[href]', 'area[href]', 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 'select:not([disabled]):not([aria-hidden])', 'textarea:not([disabled]):not([aria-hidden])', 'button:not([disabled]):not([aria-hidden])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])'];
36
37 /**
38 * Stores a context-bound scroll handler.
39 *
40 * This callback could be defined inline inside of the store
41 * object but it's created externally to avoid confusion about
42 * how its logic is called. This logic is not referenced directly
43 * by the directives in the markup because the scroll event we
44 * need to listen to is triggered on the window; so by defining it
45 * outside of the store, we signal that the behavior here is different.
46 * If we find a compelling reason to move it to the store, feel free.
47 *
48 * @type {Function}
49 */
50 let scrollCallback;
51
52 /**
53 * Tracks whether user is touching screen; used to
54 * differentiate behavior for touch and mouse input.
55 *
56 * @type {boolean}
57 */
58 let isTouching = false;
59
60 /**
61 * Tracks the last time the screen was touched; used to
62 * differentiate behavior for touch and mouse input.
63 *
64 * @type {number}
65 */
66 let lastTouchTime = 0;
67
68 /**
69 * Lightbox page-scroll handler: prevents scrolling.
70 *
71 * This handler is added to prevent scrolling behaviors that
72 * trigger content shift while the lightbox is open.
73 *
74 * It would be better to accomplish this through CSS alone, but
75 * using overflow: hidden is currently the only way to do so, and
76 * that causes the layout to shift and prevents the zoom animation
77 * from working in some cases because we're unable to account for
78 * the layout shift when doing the animation calculations. Instead,
79 * here we use JavaScript to prevent and reset the scrolling
80 * behavior. In the future, we may be able to use CSS or overflow: hidden
81 * instead to not rely on JavaScript, but this seems to be the best approach
82 * for now that provides the best visual experience.
83 *
84 * @param {Object} ctx Context object with the `core/image` namespace.
85 */
86 function handleScroll(ctx) {
87 // We can't override the scroll behavior on mobile devices
88 // because doing so breaks the pinch to zoom functionality, and we
89 // want to allow users to zoom in further on the high-res image.
90 if (!isTouching && Date.now() - lastTouchTime > 450) {
91 // We are unable to use event.preventDefault() to prevent scrolling
92 // because the scroll event can't be canceled, so we reset the position instead.
93 window.scrollTo(ctx.scrollLeftReset, ctx.scrollTopReset);
94 }
95 }
96 const {
97 state,
98 actions,
99 callbacks
100 } = (0,interactivity_namespaceObject.store)('core/image', {
101 state: {
102 windowWidth: window.innerWidth,
103 windowHeight: window.innerHeight,
104 get roleAttribute() {
105 const ctx = (0,interactivity_namespaceObject.getContext)();
106 return ctx.lightboxEnabled ? 'dialog' : null;
107 },
108 get ariaModal() {
109 const ctx = (0,interactivity_namespaceObject.getContext)();
110 return ctx.lightboxEnabled ? 'true' : null;
111 },
112 get dialogLabel() {
113 const ctx = (0,interactivity_namespaceObject.getContext)();
114 return ctx.lightboxEnabled ? ctx.dialogLabel : null;
115 },
116 get lightboxObjectFit() {
117 const ctx = (0,interactivity_namespaceObject.getContext)();
118 if (ctx.initialized) {
119 return 'cover';
120 }
121 },
122 get enlargedImgSrc() {
123 const ctx = (0,interactivity_namespaceObject.getContext)();
124 return ctx.initialized ? ctx.imageUploadedSrc : 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
125 }
126 },
127 actions: {
128 showLightbox(event) {
129 const ctx = (0,interactivity_namespaceObject.getContext)();
130 // We can't initialize the lightbox until the reference
131 // image is loaded, otherwise the UX is broken.
132 if (!ctx.imageLoaded) {
133 return;
134 }
135 ctx.initialized = true;
136 ctx.lastFocusedElement = window.document.activeElement;
137 ctx.scrollDelta = 0;
138 ctx.pointerType = event.pointerType;
139 ctx.lightboxEnabled = true;
140 setStyles(ctx, ctx.imageRef);
141 ctx.scrollTopReset = window.pageYOffset || document.documentElement.scrollTop;
142
143 // In most cases, this value will be 0, but this is included
144 // in case a user has created a page with horizontal scrolling.
145 ctx.scrollLeftReset = window.pageXOffset || document.documentElement.scrollLeft;
146
147 // We define and bind the scroll callback here so
148 // that we can pass the context and as an argument.
149 // We may be able to change this in the future if we
150 // define the scroll callback in the store instead, but
151 // this approach seems to tbe clearest for now.
152 scrollCallback = handleScroll.bind(null, ctx);
153
154 // We need to add a scroll event listener to the window
155 // here because we are unable to otherwise access it via
156 // the Interactivity API directives. If we add a native way
157 // to access the window, we can remove this.
158 window.addEventListener('scroll', scrollCallback, false);
159 },
160 hideLightbox() {
161 const ctx = (0,interactivity_namespaceObject.getContext)();
162 ctx.hideAnimationEnabled = true;
163 if (ctx.lightboxEnabled) {
164 // We want to wait until the close animation is completed
165 // before allowing a user to scroll again. The duration of this
166 // animation is defined in the styles.scss and depends on if the
167 // animation is 'zoom' or 'fade', but in any case we should wait
168 // a few milliseconds longer than the duration, otherwise a user
169 // may scroll too soon and cause the animation to look sloppy.
170 setTimeout(function () {
171 window.removeEventListener('scroll', scrollCallback);
172 // If we don't delay before changing the focus,
173 // the focus ring will appear on Firefox before
174 // the image has finished animating, which looks broken.
175 ctx.lightboxTriggerRef.focus({
176 preventScroll: true
177 });
178 }, 450);
179 ctx.lightboxEnabled = false;
180 }
181 },
182 handleKeydown(event) {
183 const ctx = (0,interactivity_namespaceObject.getContext)();
184 if (ctx.lightboxEnabled) {
185 if (event.key === 'Tab' || event.keyCode === 9) {
186 // If shift + tab it change the direction
187 if (event.shiftKey && window.document.activeElement === ctx.firstFocusableElement) {
188 event.preventDefault();
189 ctx.lastFocusableElement.focus();
190 } else if (!event.shiftKey && window.document.activeElement === ctx.lastFocusableElement) {
191 event.preventDefault();
192 ctx.firstFocusableElement.focus();
193 }
194 }
195 if (event.key === 'Escape' || event.keyCode === 27) {
196 actions.hideLightbox(event);
197 }
198 }
199 },
200 // This is fired just by lazily loaded
201 // images on the page, not all images.
202 handleLoad() {
203 const ctx = (0,interactivity_namespaceObject.getContext)();
204 const {
205 ref
206 } = (0,interactivity_namespaceObject.getElement)();
207 ctx.imageLoaded = true;
208 ctx.imageCurrentSrc = ref.currentSrc;
209 callbacks.setButtonStyles();
210 },
211 handleTouchStart() {
212 isTouching = true;
213 },
214 handleTouchMove(event) {
215 const ctx = (0,interactivity_namespaceObject.getContext)();
216 // On mobile devices, we want to prevent triggering the
217 // scroll event because otherwise the page jumps around as
218 // we reset the scroll position. This also means that closing
219 // the lightbox requires that a user perform a simple tap. This
220 // may be changed in the future if we find a better alternative
221 // to override or reset the scroll position during swipe actions.
222 if (ctx.lightboxEnabled) {
223 event.preventDefault();
224 }
225 },
226 handleTouchEnd() {
227 // We need to wait a few milliseconds before resetting
228 // to ensure that pinch to zoom works consistently
229 // on mobile devices when the lightbox is open.
230 lastTouchTime = Date.now();
231 isTouching = false;
232 }
233 },
234 callbacks: {
235 initOriginImage() {
236 const ctx = (0,interactivity_namespaceObject.getContext)();
237 const {
238 ref
239 } = (0,interactivity_namespaceObject.getElement)();
240 ctx.imageRef = ref;
241 if (ref.complete) {
242 ctx.imageLoaded = true;
243 ctx.imageCurrentSrc = ref.currentSrc;
244 }
245 },
246 initTriggerButton() {
247 const ctx = (0,interactivity_namespaceObject.getContext)();
248 const {
249 ref
250 } = (0,interactivity_namespaceObject.getElement)();
251 ctx.lightboxTriggerRef = ref;
252 },
253 initLightbox() {
254 const ctx = (0,interactivity_namespaceObject.getContext)();
255 const {
256 ref
257 } = (0,interactivity_namespaceObject.getElement)();
258 if (ctx.lightboxEnabled) {
259 const focusableElements = ref.querySelectorAll(focusableSelectors);
260 ctx.firstFocusableElement = focusableElements[0];
261 ctx.lastFocusableElement = focusableElements[focusableElements.length - 1];
262
263 // Move focus to the dialog when opening it.
264 ref.focus();
265 }
266 },
267 setButtonStyles() {
268 const {
269 ref
270 } = (0,interactivity_namespaceObject.getElement)();
271 const {
272 naturalWidth,
273 naturalHeight,
274 offsetWidth,
275 offsetHeight
276 } = ref;
277
278 // If the image isn't loaded yet, we can't
279 // calculate where the button should be.
280 if (naturalWidth === 0 || naturalHeight === 0) {
281 return;
282 }
283 const figure = ref.parentElement;
284 const figureWidth = ref.parentElement.clientWidth;
285
286 // We need special handling for the height because
287 // a caption will cause the figure to be taller than
288 // the image, which means we need to account for that
289 // when calculating the placement of the button in the
290 // top right corner of the image.
291 let figureHeight = ref.parentElement.clientHeight;
292 const caption = figure.querySelector('figcaption');
293 if (caption) {
294 const captionComputedStyle = window.getComputedStyle(caption);
295 if (!['absolute', 'fixed'].includes(captionComputedStyle.position)) {
296 figureHeight = figureHeight - caption.offsetHeight - parseFloat(captionComputedStyle.marginTop) - parseFloat(captionComputedStyle.marginBottom);
297 }
298 }
299 const buttonOffsetTop = figureHeight - offsetHeight;
300 const buttonOffsetRight = figureWidth - offsetWidth;
301 const ctx = (0,interactivity_namespaceObject.getContext)();
302
303 // In the case of an image with object-fit: contain, the
304 // size of the <img> element can be larger than the image itself,
305 // so we need to calculate where to place the button.
306 if (ctx.scaleAttr === 'contain') {
307 // Natural ratio of the image.
308 const naturalRatio = naturalWidth / naturalHeight;
309 // Offset ratio of the image.
310 const offsetRatio = offsetWidth / offsetHeight;
311 if (naturalRatio >= offsetRatio) {
312 // If it reaches the width first, keep
313 // the width and compute the height.
314 const referenceHeight = offsetWidth / naturalRatio;
315 ctx.imageButtonTop = (offsetHeight - referenceHeight) / 2 + buttonOffsetTop + 16;
316 ctx.imageButtonRight = buttonOffsetRight + 16;
317 } else {
318 // If it reaches the height first, keep
319 // the height and compute the width.
320 const referenceWidth = offsetHeight * naturalRatio;
321 ctx.imageButtonTop = buttonOffsetTop + 16;
322 ctx.imageButtonRight = (offsetWidth - referenceWidth) / 2 + buttonOffsetRight + 16;
323 }
324 } else {
325 ctx.imageButtonTop = buttonOffsetTop + 16;
326 ctx.imageButtonRight = buttonOffsetRight + 16;
327 }
328 },
329 setStylesOnResize() {
330 const ctx = (0,interactivity_namespaceObject.getContext)();
331 const {
332 ref
333 } = (0,interactivity_namespaceObject.getElement)();
334 if (ctx.lightboxEnabled && (state.windowWidth || state.windowHeight)) {
335 setStyles(ctx, ref);
336 }
337 }
338 }
339 });
340 window.addEventListener('resize', debounce(() => {
341 state.windowWidth = window.innerWidth;
342 state.windowHeight = window.innerHeight;
343 }));
344
345 /**
346 * Computes styles for the lightbox and adds them to the document.
347 *
348 * @function
349 * @param {Object} ctx - Context for the `core/image` namespace.
350 * @param {Object} ref - The element reference.
351 */
352 function setStyles(ctx, ref) {
353 // The reference img element lies adjacent
354 // to the event target button in the DOM.
355 let {
356 naturalWidth,
357 naturalHeight,
358 offsetWidth: originalWidth,
359 offsetHeight: originalHeight
360 } = ref;
361 let {
362 x: screenPosX,
363 y: screenPosY
364 } = ref.getBoundingClientRect();
365
366 // Natural ratio of the image clicked to open the lightbox.
367 const naturalRatio = naturalWidth / naturalHeight;
368 // Original ratio of the image clicked to open the lightbox.
369 let originalRatio = originalWidth / originalHeight;
370
371 // If it has object-fit: contain, recalculate the original sizes
372 // and the screen position without the blank spaces.
373 if (ctx.scaleAttr === 'contain') {
374 if (naturalRatio > originalRatio) {
375 const heightWithoutSpace = originalWidth / naturalRatio;
376 // Recalculate screen position without the top space.
377 screenPosY += (originalHeight - heightWithoutSpace) / 2;
378 originalHeight = heightWithoutSpace;
379 } else {
380 const widthWithoutSpace = originalHeight * naturalRatio;
381 // Recalculate screen position without the left space.
382 screenPosX += (originalWidth - widthWithoutSpace) / 2;
383 originalWidth = widthWithoutSpace;
384 }
385 }
386 originalRatio = originalWidth / originalHeight;
387
388 // Typically, we use the image's full-sized dimensions. If those
389 // dimensions have not been set (i.e. an external image with only one size),
390 // the image's dimensions in the lightbox are the same
391 // as those of the image in the content.
392 let imgMaxWidth = parseFloat(ctx.targetWidth !== 'none' ? ctx.targetWidth : naturalWidth);
393 let imgMaxHeight = parseFloat(ctx.targetHeight !== 'none' ? ctx.targetHeight : naturalHeight);
394
395 // Ratio of the biggest image stored in the database.
396 let imgRatio = imgMaxWidth / imgMaxHeight;
397 let containerMaxWidth = imgMaxWidth;
398 let containerMaxHeight = imgMaxHeight;
399 let containerWidth = imgMaxWidth;
400 let containerHeight = imgMaxHeight;
401 // Check if the target image has a different ratio than the original one (thumbnail).
402 // Recalculate the width and height.
403 if (naturalRatio.toFixed(2) !== imgRatio.toFixed(2)) {
404 if (naturalRatio > imgRatio) {
405 // If the width is reached before the height, we keep the maxWidth
406 // and recalculate the height.
407 // Unless the difference between the maxHeight and the reducedHeight
408 // is higher than the maxWidth, where we keep the reducedHeight and
409 // recalculate the width.
410 const reducedHeight = imgMaxWidth / naturalRatio;
411 if (imgMaxHeight - reducedHeight > imgMaxWidth) {
412 imgMaxHeight = reducedHeight;
413 imgMaxWidth = reducedHeight * naturalRatio;
414 } else {
415 imgMaxHeight = imgMaxWidth / naturalRatio;
416 }
417 } else {
418 // If the height is reached before the width, we keep the maxHeight
419 // and recalculate the width.
420 // Unless the difference between the maxWidth and the reducedWidth
421 // is higher than the maxHeight, where we keep the reducedWidth and
422 // recalculate the height.
423 const reducedWidth = imgMaxHeight * naturalRatio;
424 if (imgMaxWidth - reducedWidth > imgMaxHeight) {
425 imgMaxWidth = reducedWidth;
426 imgMaxHeight = reducedWidth / naturalRatio;
427 } else {
428 imgMaxWidth = imgMaxHeight * naturalRatio;
429 }
430 }
431 containerWidth = imgMaxWidth;
432 containerHeight = imgMaxHeight;
433 imgRatio = imgMaxWidth / imgMaxHeight;
434
435 // Calculate the max size of the container.
436 if (originalRatio > imgRatio) {
437 containerMaxWidth = imgMaxWidth;
438 containerMaxHeight = containerMaxWidth / originalRatio;
439 } else {
440 containerMaxHeight = imgMaxHeight;
441 containerMaxWidth = containerMaxHeight * originalRatio;
442 }
443 }
444
445 // If the image has been pixelated on purpose, keep that size.
446 if (originalWidth > containerWidth || originalHeight > containerHeight) {
447 containerWidth = originalWidth;
448 containerHeight = originalHeight;
449 }
450
451 // Calculate the final lightbox image size and the
452 // scale factor. MaxWidth is either the window container
453 // (accounting for padding) or the image resolution.
454 let horizontalPadding = 0;
455 if (window.innerWidth > 480) {
456 horizontalPadding = 80;
457 } else if (window.innerWidth > 1920) {
458 horizontalPadding = 160;
459 }
460 const verticalPadding = 80;
461 const targetMaxWidth = Math.min(window.innerWidth - horizontalPadding, containerWidth);
462 const targetMaxHeight = Math.min(window.innerHeight - verticalPadding, containerHeight);
463 const targetContainerRatio = targetMaxWidth / targetMaxHeight;
464 if (originalRatio > targetContainerRatio) {
465 // If targetMaxWidth is reached before targetMaxHeight
466 containerWidth = targetMaxWidth;
467 containerHeight = containerWidth / originalRatio;
468 } else {
469 // If targetMaxHeight is reached before targetMaxWidth
470 containerHeight = targetMaxHeight;
471 containerWidth = containerHeight * originalRatio;
472 }
473 const containerScale = originalWidth / containerWidth;
474 const lightboxImgWidth = imgMaxWidth * (containerWidth / containerMaxWidth);
475 const lightboxImgHeight = imgMaxHeight * (containerHeight / containerMaxHeight);
476
477 // Add the CSS variables needed.
478 let styleTag = document.getElementById('wp-lightbox-styles');
479 if (!styleTag) {
480 styleTag = document.createElement('style');
481 styleTag.id = 'wp-lightbox-styles';
482 document.head.appendChild(styleTag);
483 }
484
485 // As of this writing, using the calculations above will render the lightbox
486 // with a small, erroneous whitespace on the left side of the image in iOS Safari,
487 // perhaps due to an inconsistency in how browsers handle absolute positioning and CSS
488 // transformation. In any case, adding 1 pixel to the container width and height solves
489 // the problem, though this can be removed if the issue is fixed in the future.
490 styleTag.innerHTML = `
491 :root {
492 --wp--lightbox-initial-top-position: ${screenPosY}px;
493 --wp--lightbox-initial-left-position: ${screenPosX}px;
494 --wp--lightbox-container-width: ${containerWidth + 1}px;
495 --wp--lightbox-container-height: ${containerHeight + 1}px;
496 --wp--lightbox-image-width: ${lightboxImgWidth}px;
497 --wp--lightbox-image-height: ${lightboxImgHeight}px;
498 --wp--lightbox-scale: ${containerScale};
499 --wp--lightbox-scrollbar-width: ${window.innerWidth - document.documentElement.clientWidth}px;
500 }
501 `;
502 }
503
504 /**
505 * Debounces a function call.
506 *
507 * @function
508 * @param {Function} func - A function to be called
509 * @param {number} wait - The time to wait before calling the function
510 */
511 function debounce(func, wait = 50) {
512 let timeout;
513 return () => {
514 const later = () => {
515 timeout = null;
516 func();
517 };
518 clearTimeout(timeout);
519 timeout = setTimeout(later, wait);
520 };
521 }
522