wp/wp-includes/js/dist/script-modules/block-library/image/view.js
changeset 22 8c2e4d02f4ef
equal deleted inserted replaced
21:48c4eec2b7e6 22:8c2e4d02f4ef
       
     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), ["withSyncEvent"]: () => (__WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__.withSyncEvent) });
       
    32 ;// ./node_modules/@wordpress/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: (0,interactivity_namespaceObject.withSyncEvent)(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: (0,interactivity_namespaceObject.withSyncEvent)(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