[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/blocks/image/ -> view.js (source)

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


Generated : Sat Apr 27 08:20:02 2024 Cross-referenced by PHPXref