diff -r 7b1b88e27a20 -r 48c4eec2b7e6 wp/wp-includes/blocks/image/view.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wp/wp-includes/blocks/image/view.js Fri Sep 05 18:40:08 2025 +0200 @@ -0,0 +1,424 @@ +import * as __WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__ from "@wordpress/interactivity"; +/******/ // The require scope +/******/ var __webpack_require__ = {}; +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; + +;// CONCATENATED MODULE: external "@wordpress/interactivity" +var x = (y) => { + var x = {}; __webpack_require__.d(x, y); return x +} +var y = (x) => (() => (x)) +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) }); +;// CONCATENATED MODULE: ./node_modules/@wordpress/block-library/build-module/image/view.js +/** + * WordPress dependencies + */ + + +/** + * Tracks whether user is touching screen; used to differentiate behavior for + * touch and mouse input. + * + * @type {boolean} + */ +let isTouching = false; + +/** + * Tracks the last time the screen was touched; used to differentiate behavior + * for touch and mouse input. + * + * @type {number} + */ +let lastTouchTime = 0; + +/** + * Stores the image reference of the currently opened lightbox. + * + * @type {HTMLElement} + */ +let imageRef; + +/** + * Stores the button reference of the currently opened lightbox. + * + * @type {HTMLElement} + */ +let buttonRef; +const { + state, + actions, + callbacks +} = (0,interactivity_namespaceObject.store)('core/image', { + state: { + currentImage: {}, + get overlayOpened() { + return state.currentImage.currentSrc; + }, + get roleAttribute() { + return state.overlayOpened ? 'dialog' : null; + }, + get ariaModal() { + return state.overlayOpened ? 'true' : null; + }, + get enlargedSrc() { + return state.currentImage.uploadedSrc || 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='; + }, + get imgStyles() { + return state.overlayOpened && `${state.currentImage.imgStyles?.replace(/;$/, '')}; object-fit:cover;`; + } + }, + actions: { + showLightbox() { + const ctx = (0,interactivity_namespaceObject.getContext)(); + + // Bails out if the image has not loaded yet. + if (!ctx.imageRef?.complete) { + return; + } + + // Stores the positons of the scroll to fix it until the overlay is + // closed. + state.scrollTopReset = document.documentElement.scrollTop; + state.scrollLeftReset = document.documentElement.scrollLeft; + + // Moves the information of the expaned image to the state. + ctx.currentSrc = ctx.imageRef.currentSrc; + imageRef = ctx.imageRef; + buttonRef = ctx.buttonRef; + state.currentImage = ctx; + state.overlayEnabled = true; + + // Computes the styles of the overlay for the animation. + callbacks.setOverlayStyles(); + }, + hideLightbox() { + if (state.overlayEnabled) { + // Waits until the close animation has completed before allowing a + // user to scroll again. The duration of this animation is defined in + // the `styles.scss` file, but in any case we should wait a few + // milliseconds longer than the duration, otherwise a user may scroll + // too soon and cause the animation to look sloppy. + setTimeout(function () { + // Delays before changing the focus. Otherwise the focus ring will + // appear on Firefox before the image has finished animating, which + // looks broken. + buttonRef.focus({ + preventScroll: true + }); + + // Resets the current image to mark the overlay as closed. + state.currentImage = {}; + imageRef = null; + buttonRef = null; + }, 450); + + // Starts the overlay closing animation. The showClosingAnimation + // class is used to avoid showing it on page load. + state.showClosingAnimation = true; + state.overlayEnabled = false; + } + }, + handleKeydown(event) { + if (state.overlayEnabled) { + // Focuses the close button when the user presses the tab key. + if (event.key === 'Tab') { + event.preventDefault(); + const { + ref + } = (0,interactivity_namespaceObject.getElement)(); + ref.querySelector('button').focus(); + } + // Closes the lightbox when the user presses the escape key. + if (event.key === 'Escape') { + actions.hideLightbox(); + } + } + }, + handleTouchMove(event) { + // On mobile devices, prevents triggering the scroll event because + // otherwise the page jumps around when it resets the scroll position. + // This also means that closing the lightbox requires that a user + // perform a simple tap. This may be changed in the future if there is a + // better alternative to override or reset the scroll position during + // swipe actions. + if (state.overlayEnabled) { + event.preventDefault(); + } + }, + handleTouchStart() { + isTouching = true; + }, + handleTouchEnd() { + // Waits a few milliseconds before resetting to ensure that pinch to + // zoom works consistently on mobile devices when the lightbox is open. + lastTouchTime = Date.now(); + isTouching = false; + }, + handleScroll() { + // Prevents scrolling behaviors that trigger content shift while the + // lightbox is open. It would be better to accomplish through CSS alone, + // but using overflow: hidden is currently the only way to do so and + // that causes a layout to shift and prevents the zoom animation from + // working in some cases because it's not possible to account for the + // layout shift when doing the animation calculations. Instead, it uses + // JavaScript to prevent and reset the scrolling behavior. + if (state.overlayOpened) { + // Avoids overriding the scroll behavior on mobile devices because + // doing so breaks the pinch to zoom functionality, and users should + // be able to zoom in further on the high-res image. + if (!isTouching && Date.now() - lastTouchTime > 450) { + // It doesn't rely on `event.preventDefault()` to prevent scrolling + // because the scroll event can't be canceled, so it resets the + // position instead. + window.scrollTo(state.scrollLeftReset, state.scrollTopReset); + } + } + } + }, + callbacks: { + setOverlayStyles() { + if (!imageRef) { + return; + } + let { + naturalWidth, + naturalHeight, + offsetWidth: originalWidth, + offsetHeight: originalHeight + } = imageRef; + let { + x: screenPosX, + y: screenPosY + } = imageRef.getBoundingClientRect(); + + // Natural ratio of the image clicked to open the lightbox. + const naturalRatio = naturalWidth / naturalHeight; + // Original ratio of the image clicked to open the lightbox. + let originalRatio = originalWidth / originalHeight; + + // If it has object-fit: contain, recalculates the original sizes + // and the screen position without the blank spaces. + if (state.currentImage.scaleAttr === 'contain') { + if (naturalRatio > originalRatio) { + const heightWithoutSpace = originalWidth / naturalRatio; + // Recalculates screen position without the top space. + screenPosY += (originalHeight - heightWithoutSpace) / 2; + originalHeight = heightWithoutSpace; + } else { + const widthWithoutSpace = originalHeight * naturalRatio; + // Recalculates screen position without the left space. + screenPosX += (originalWidth - widthWithoutSpace) / 2; + originalWidth = widthWithoutSpace; + } + } + originalRatio = originalWidth / originalHeight; + + // Typically, it uses the image's full-sized dimensions. If those + // dimensions have not been set (i.e. an external image with only one + // size), the image's dimensions in the lightbox are the same + // as those of the image in the content. + let imgMaxWidth = parseFloat(state.currentImage.targetWidth !== 'none' ? state.currentImage.targetWidth : naturalWidth); + let imgMaxHeight = parseFloat(state.currentImage.targetHeight !== 'none' ? state.currentImage.targetHeight : naturalHeight); + + // Ratio of the biggest image stored in the database. + let imgRatio = imgMaxWidth / imgMaxHeight; + let containerMaxWidth = imgMaxWidth; + let containerMaxHeight = imgMaxHeight; + let containerWidth = imgMaxWidth; + let containerHeight = imgMaxHeight; + // Checks if the target image has a different ratio than the original + // one (thumbnail). Recalculates the width and height. + if (naturalRatio.toFixed(2) !== imgRatio.toFixed(2)) { + if (naturalRatio > imgRatio) { + // If the width is reached before the height, it keeps the maxWidth + // and recalculates the height unless the difference between the + // maxHeight and the reducedHeight is higher than the maxWidth, + // where it keeps the reducedHeight and recalculate the width. + const reducedHeight = imgMaxWidth / naturalRatio; + if (imgMaxHeight - reducedHeight > imgMaxWidth) { + imgMaxHeight = reducedHeight; + imgMaxWidth = reducedHeight * naturalRatio; + } else { + imgMaxHeight = imgMaxWidth / naturalRatio; + } + } else { + // If the height is reached before the width, it keeps the maxHeight + // and recalculate the width unlesss the difference between the + // maxWidth and the reducedWidth is higher than the maxHeight, where + // it keeps the reducedWidth and recalculate the height. + const reducedWidth = imgMaxHeight * naturalRatio; + if (imgMaxWidth - reducedWidth > imgMaxHeight) { + imgMaxWidth = reducedWidth; + imgMaxHeight = reducedWidth / naturalRatio; + } else { + imgMaxWidth = imgMaxHeight * naturalRatio; + } + } + containerWidth = imgMaxWidth; + containerHeight = imgMaxHeight; + imgRatio = imgMaxWidth / imgMaxHeight; + + // Calculates the max size of the container. + if (originalRatio > imgRatio) { + containerMaxWidth = imgMaxWidth; + containerMaxHeight = containerMaxWidth / originalRatio; + } else { + containerMaxHeight = imgMaxHeight; + containerMaxWidth = containerMaxHeight * originalRatio; + } + } + + // If the image has been pixelated on purpose, it keeps that size. + if (originalWidth > containerWidth || originalHeight > containerHeight) { + containerWidth = originalWidth; + containerHeight = originalHeight; + } + + // Calculates the final lightbox image size and the scale factor. + // MaxWidth is either the window container (accounting for padding) or + // the image resolution. + let horizontalPadding = 0; + if (window.innerWidth > 480) { + horizontalPadding = 80; + } else if (window.innerWidth > 1920) { + horizontalPadding = 160; + } + const verticalPadding = 80; + const targetMaxWidth = Math.min(window.innerWidth - horizontalPadding, containerWidth); + const targetMaxHeight = Math.min(window.innerHeight - verticalPadding, containerHeight); + const targetContainerRatio = targetMaxWidth / targetMaxHeight; + if (originalRatio > targetContainerRatio) { + // If targetMaxWidth is reached before targetMaxHeight. + containerWidth = targetMaxWidth; + containerHeight = containerWidth / originalRatio; + } else { + // If targetMaxHeight is reached before targetMaxWidth. + containerHeight = targetMaxHeight; + containerWidth = containerHeight * originalRatio; + } + const containerScale = originalWidth / containerWidth; + const lightboxImgWidth = imgMaxWidth * (containerWidth / containerMaxWidth); + const lightboxImgHeight = imgMaxHeight * (containerHeight / containerMaxHeight); + + // As of this writing, using the calculations above will render the + // lightbox with a small, erroneous whitespace on the left side of the + // image in iOS Safari, perhaps due to an inconsistency in how browsers + // handle absolute positioning and CSS transformation. In any case, + // adding 1 pixel to the container width and height solves the problem, + // though this can be removed if the issue is fixed in the future. + state.overlayStyles = ` + :root { + --wp--lightbox-initial-top-position: ${screenPosY}px; + --wp--lightbox-initial-left-position: ${screenPosX}px; + --wp--lightbox-container-width: ${containerWidth + 1}px; + --wp--lightbox-container-height: ${containerHeight + 1}px; + --wp--lightbox-image-width: ${lightboxImgWidth}px; + --wp--lightbox-image-height: ${lightboxImgHeight}px; + --wp--lightbox-scale: ${containerScale}; + --wp--lightbox-scrollbar-width: ${window.innerWidth - document.documentElement.clientWidth}px; + } + `; + }, + setButtonStyles() { + const ctx = (0,interactivity_namespaceObject.getContext)(); + const { + ref + } = (0,interactivity_namespaceObject.getElement)(); + ctx.imageRef = ref; + const { + naturalWidth, + naturalHeight, + offsetWidth, + offsetHeight + } = ref; + + // If the image isn't loaded yet, it can't calculate where the button + // should be. + if (naturalWidth === 0 || naturalHeight === 0) { + return; + } + const figure = ref.parentElement; + const figureWidth = ref.parentElement.clientWidth; + + // It needs special handling for the height because a caption will cause + // the figure to be taller than the image, which means it needs to + // account for that when calculating the placement of the button in the + // top right corner of the image. + let figureHeight = ref.parentElement.clientHeight; + const caption = figure.querySelector('figcaption'); + if (caption) { + const captionComputedStyle = window.getComputedStyle(caption); + if (!['absolute', 'fixed'].includes(captionComputedStyle.position)) { + figureHeight = figureHeight - caption.offsetHeight - parseFloat(captionComputedStyle.marginTop) - parseFloat(captionComputedStyle.marginBottom); + } + } + const buttonOffsetTop = figureHeight - offsetHeight; + const buttonOffsetRight = figureWidth - offsetWidth; + + // In the case of an image with object-fit: contain, the size of the + // element can be larger than the image itself, so it needs to + // calculate where to place the button. + if (ctx.scaleAttr === 'contain') { + // Natural ratio of the image. + const naturalRatio = naturalWidth / naturalHeight; + // Offset ratio of the image. + const offsetRatio = offsetWidth / offsetHeight; + if (naturalRatio >= offsetRatio) { + // If it reaches the width first, it keeps the width and compute the + // height. + const referenceHeight = offsetWidth / naturalRatio; + ctx.imageButtonTop = (offsetHeight - referenceHeight) / 2 + buttonOffsetTop + 16; + ctx.imageButtonRight = buttonOffsetRight + 16; + } else { + // If it reaches the height first, it keeps the height and compute + // the width. + const referenceWidth = offsetHeight * naturalRatio; + ctx.imageButtonTop = buttonOffsetTop + 16; + ctx.imageButtonRight = (offsetWidth - referenceWidth) / 2 + buttonOffsetRight + 16; + } + } else { + ctx.imageButtonTop = buttonOffsetTop + 16; + ctx.imageButtonRight = buttonOffsetRight + 16; + } + }, + setOverlayFocus() { + if (state.overlayEnabled) { + // Moves the focus to the dialog when it opens. + const { + ref + } = (0,interactivity_namespaceObject.getElement)(); + ref.focus(); + } + }, + initTriggerButton() { + const ctx = (0,interactivity_namespaceObject.getContext)(); + const { + ref + } = (0,interactivity_namespaceObject.getElement)(); + ctx.buttonRef = ref; + } + } +}, { + lock: true +}); +