| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 // packages/block-library/build-module/image/view.mjs 2 import { 3 store, 4 getContext, 5 getElement, 6 getConfig, 7 withSyncEvent, 8 withScope, 9 splitTask 10 } from "@wordpress/interactivity"; 11 12 // packages/block-library/build-module/utils/reduced-motion.mjs 13 function prefersReducedMotion() { 14 return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true; 15 } 16 17 // packages/block-library/build-module/image/constants.mjs 18 var IMAGE_PRELOAD_DELAY = 200; 19 20 // packages/block-library/build-module/image/view.mjs 21 var isTouching = false; 22 var lastTouchTime = 0; 23 var touchStartEvent = { 24 startX: 0, 25 startY: 0, 26 startTime: 0 27 }; 28 var inertElements = []; 29 var touchDrag = { 30 isDragging: false, 31 direction: null, 32 // The elements the gesture moves. The drag offset is written on these 33 // rather than on the overlay, whose `style` attribute is bound to 34 // `state.overlayStyles`: the framework rewrites that attribute whole 35 // whenever the bound value changes, so a resize part way through a gesture 36 // used to wipe the drag out from under the finger. Nothing is bound to the 37 // containers, so what is written here stays written. 38 slideEls: [], 39 // Animations currently running on `slideEls`, so an interrupted gesture can 40 // stop them. 41 animations: [], 42 // Finishes a committed swipe once its slide is over. Held so that a new 43 // gesture can settle a swipe still in flight rather than stranding it. 44 settle: null 45 }; 46 var SLIDE_DURATION_MS = 220; 47 var FADE_DURATION_MS = 300; 48 var SLIDE_EASING = "cubic-bezier(0.2, 0.7, 0.2, 1)"; 49 var DIRECTION_LATCH_THRESHOLD_PX = 10; 50 var HORIZONTAL_DOMINANCE_RATIO = 1.5; 51 var COMMIT_DISTANCE_RATIO = 0.2; 52 var FLICK_DISTANCE_PX = 50; 53 var FLICK_MAX_DURATION_MS = 300; 54 function getSlidePitch() { 55 return window.innerWidth; 56 } 57 function slideTransform(offset) { 58 return `translate(calc(-50% + $offset}px), -50%)`; 59 } 60 function setDragOffset(els, offset) { 61 els.forEach((el) => { 62 if (offset) { 63 el.style.setProperty( 64 "--wp--lightbox-drag-offset", 65 `$offset}px` 66 ); 67 } else { 68 el.style.removeProperty("--wp--lightbox-drag-offset"); 69 } 70 }); 71 } 72 function stopDragAnimations() { 73 touchDrag.animations.forEach((animation) => animation.cancel()); 74 touchDrag.animations = []; 75 } 76 function resetTouchDrag() { 77 touchDrag.isDragging = false; 78 touchDrag.direction = null; 79 touchDrag.slideEls = []; 80 } 81 async function waitForImage(img) { 82 await splitTask(); 83 try { 84 await img?.decode(); 85 } catch { 86 } 87 } 88 var focusableSelectors = [ 89 ".wp-lightbox-close-button", 90 ".wp-lightbox-navigation-button" 91 ]; 92 function getImageSrc({ uploadedSrc }) { 93 return uploadedSrc || "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="; 94 } 95 function getImageSrcset({ lightboxSrcset }) { 96 return lightboxSrcset || ""; 97 } 98 function getImageMetadata(imageId) { 99 const metadata = state.metadata[imageId]; 100 return Object.hasOwn(state.metadata, imageId) ? metadata : void 0; 101 } 102 var { state, actions, callbacks } = store( 103 "core/image", 104 { 105 state: { 106 selectedImageId: null, 107 selectedGalleryId: null, 108 preloadTimers: /* @__PURE__ */ new Map(), 109 preloadedImageIds: /* @__PURE__ */ new Set(), 110 get galleryImages() { 111 if (!state.selectedGalleryId) { 112 return [state.selectedImageId]; 113 } 114 return Object.entries(state.metadata).filter( 115 ([, value]) => value.galleryId === state.selectedGalleryId 116 ).sort(([, a], [, b]) => { 117 const orderA = a.order ?? 0; 118 const orderB = b.order ?? 0; 119 return orderA - orderB; 120 }).map(([key]) => key); 121 }, 122 get selectedImageIndex() { 123 return state.galleryImages.findIndex( 124 (id) => id === state.selectedImageId 125 ); 126 }, 127 get selectedImage() { 128 return getImageMetadata(state.selectedImageId); 129 }, 130 get hasNavigationIcon() { 131 const { navigationButtonType } = state.selectedImage; 132 return navigationButtonType === "icon" || navigationButtonType === "both"; 133 }, 134 get hasNavigationText() { 135 const { navigationButtonType } = state.selectedImage; 136 return navigationButtonType === "text" || navigationButtonType === "both"; 137 }, 138 get thisImage() { 139 const { imageId } = getContext(); 140 return getImageMetadata(imageId); 141 }, 142 get hasNavigation() { 143 return state.galleryImages.length > 1; 144 }, 145 get hasNextImage() { 146 return state.selectedImageIndex + 1 < state.galleryImages.length; 147 }, 148 get hasPreviousImage() { 149 return state.selectedImageIndex - 1 >= 0; 150 }, 151 get overlayOpened() { 152 return state.selectedImageId !== null; 153 }, 154 get roleAttribute() { 155 return state.overlayOpened ? "dialog" : null; 156 }, 157 get ariaModal() { 158 return state.overlayOpened ? "true" : null; 159 }, 160 get ariaLabel() { 161 return state.selectedImage.customAriaLabel || getConfig().defaultAriaLabel; 162 }, 163 get closeButtonAriaLabel() { 164 return state.hasNavigationText ? void 0 : getConfig().closeButtonText; 165 }, 166 get prevButtonAriaLabel() { 167 return state.hasNavigationText ? void 0 : getConfig().prevButtonText; 168 }, 169 get nextButtonAriaLabel() { 170 return state.hasNavigationText ? void 0 : getConfig().nextButtonText; 171 }, 172 get enlargedSrc() { 173 return getImageSrc(state.selectedImage); 174 }, 175 get enlargedSrcset() { 176 return getImageSrcset(state.selectedImage); 177 }, 178 get figureStyles() { 179 return state.overlayOpened && `$state.selectedImage.figureStyles?.replace( 180 /margin[^;]*;?/g, 181 "" 182 )};`; 183 }, 184 get imgStyles() { 185 return state.overlayOpened && `$state.selectedImage.imgStyles?.replace( 186 /;$/, 187 "" 188 )}; object-fit:cover;`; 189 }, 190 get isContentHidden() { 191 const ctx = getContext(); 192 return state.overlayEnabled && state.selectedImageId === ctx.imageId; 193 }, 194 get isContentVisible() { 195 const ctx = getContext(); 196 return !state.overlayEnabled && state.selectedImageId === ctx.imageId; 197 } 198 }, 199 actions: { 200 showLightbox() { 201 const { imageId } = getContext(); 202 if (!getImageMetadata(imageId)?.imageRef?.complete) { 203 return; 204 } 205 state.scrollTopReset = document.documentElement.scrollTop; 206 state.scrollLeftReset = document.documentElement.scrollLeft; 207 state.selectedImageId = imageId; 208 const { galleryId } = getContext("core/gallery") || {}; 209 state.selectedGalleryId = galleryId || null; 210 state.overlayEnabled = true; 211 callbacks.setOverlayStyles(); 212 }, 213 hideLightbox() { 214 if (state.overlayEnabled) { 215 state.overlayEnabled = false; 216 setTimeout(function() { 217 state.selectedImage.buttonRef.focus({ 218 preventScroll: true 219 }); 220 state.selectedImageId = null; 221 state.selectedGalleryId = null; 222 }, 450); 223 } 224 }, 225 showPreviousImage: withSyncEvent((event) => { 226 event.stopPropagation(); 227 const nextIndex = state.hasPreviousImage ? state.selectedImageIndex - 1 : state.galleryImages.length - 1; 228 state.selectedImageId = state.galleryImages[nextIndex]; 229 callbacks.setOverlayStyles(); 230 }), 231 showNextImage: withSyncEvent((event) => { 232 event.stopPropagation(); 233 const nextIndex = state.hasNextImage ? state.selectedImageIndex + 1 : 0; 234 state.selectedImageId = state.galleryImages[nextIndex]; 235 callbacks.setOverlayStyles(); 236 }), 237 handleKeydown: withSyncEvent((event) => { 238 if (state.overlayEnabled) { 239 if (event.key === "Escape") { 240 actions.hideLightbox(); 241 } else if (event.key === "ArrowLeft") { 242 actions.showPreviousImage(event); 243 } else if (event.key === "ArrowRight") { 244 actions.showNextImage(event); 245 } else if (event.key === "Tab") { 246 const focusableElements = Array.from( 247 document.querySelectorAll(focusableSelectors) 248 ); 249 const firstFocusableElement = focusableElements[0]; 250 const lastFocusableElement = focusableElements[focusableElements.length - 1]; 251 if (event.shiftKey && event.target === firstFocusableElement) { 252 event.preventDefault(); 253 lastFocusableElement.focus(); 254 } else if (!event.shiftKey && event.target === lastFocusableElement) { 255 event.preventDefault(); 256 firstFocusableElement.focus(); 257 } 258 } 259 } 260 }), 261 handleTouchMove: withSyncEvent((event) => { 262 if (!state.overlayEnabled) { 263 return; 264 } 265 event.preventDefault(); 266 const t = event.touches && event.touches[0]; 267 if (!t || !state.hasNavigation) { 268 return; 269 } 270 const deltaX = t.clientX - touchStartEvent.startX; 271 const deltaY = t.clientY - touchStartEvent.startY; 272 const absDeltaX = Math.abs(deltaX); 273 const absDeltaY = Math.abs(deltaY); 274 if (!touchDrag.direction && (absDeltaX > DIRECTION_LATCH_THRESHOLD_PX || absDeltaY > DIRECTION_LATCH_THRESHOLD_PX)) { 275 touchDrag.direction = absDeltaX > absDeltaY ? "horizontal" : "vertical"; 276 touchDrag.slideEls = Array.from( 277 event.currentTarget.querySelectorAll( 278 ".lightbox-image-container" 279 ) 280 ); 281 } 282 if (touchDrag.direction === "horizontal" && touchDrag.slideEls.length) { 283 touchDrag.isDragging = true; 284 setDragOffset(touchDrag.slideEls, deltaX); 285 } 286 }), 287 handleTouchStart(event) { 288 isTouching = true; 289 if (event.touches?.length > 1) { 290 return; 291 } 292 const t = event.touches && event.touches[0]; 293 if (t) { 294 touchStartEvent.startX = t.clientX; 295 touchStartEvent.startY = t.clientY; 296 touchStartEvent.startTime = Date.now(); 297 } 298 touchDrag.settle?.(); 299 stopDragAnimations(); 300 resetTouchDrag(); 301 }, 302 handleTouchCancel() { 303 stopDragAnimations(); 304 setDragOffset(touchDrag.slideEls, 0); 305 resetTouchDrag(); 306 lastTouchTime = Date.now(); 307 isTouching = false; 308 }, 309 handleTouchEnd: withSyncEvent((event) => { 310 if (event.touches?.length) { 311 return; 312 } 313 const touchEndEvent = event.changedTouches && event.changedTouches[0] || event.touches && event.touches[0]; 314 const now = Date.now(); 315 if (touchEndEvent && state.overlayEnabled && touchDrag.isDragging) { 316 event.preventDefault(); 317 const deltaX = touchEndEvent.clientX - touchStartEvent.startX; 318 const deltaY = touchEndEvent.clientY - touchStartEvent.startY; 319 const absDeltaX = Math.abs(deltaX); 320 const absDeltaY = Math.abs(deltaY); 321 const elapsedMs = now - touchStartEvent.startTime; 322 const slidePitch = getSlidePitch(); 323 const isHorizontalDominant = absDeltaX > absDeltaY * HORIZONTAL_DOMINANCE_RATIO; 324 const pastDistanceThreshold = absDeltaX > slidePitch * COMMIT_DISTANCE_RATIO; 325 const isFastFlick = absDeltaX > FLICK_DISTANCE_PX && elapsedMs < FLICK_MAX_DURATION_MS; 326 const shouldCommit = isHorizontalDominant && (pastDistanceThreshold || isFastFlick); 327 const showsNextImage = deltaX < 0; 328 const slideEls = touchDrag.slideEls; 329 const reducedMotion = prefersReducedMotion(); 330 const committedOffset = showsNextImage ? -slidePitch : slidePitch; 331 const endOffset = shouldCommit ? committedOffset : 0; 332 const settle = () => { 333 if (touchDrag.settle !== settle) { 334 return; 335 } 336 touchDrag.settle = null; 337 stopDragAnimations(); 338 setDragOffset(slideEls, 0); 339 if (!shouldCommit) { 340 return; 341 } 342 if (showsNextImage) { 343 actions.showNextImage(event); 344 } else { 345 actions.showPreviousImage(event); 346 } 347 if (reducedMotion) { 348 return; 349 } 350 const hold = slideEls.map( 351 (el) => el.animate([{ opacity: 0 }, { opacity: 0 }], { 352 fill: "forwards" 353 }) 354 ); 355 touchDrag.animations = hold; 356 const enlargedEl = slideEls[slideEls.length - 1]; 357 waitForImage(enlargedEl.querySelector("img")).then( 358 () => { 359 if (touchDrag.animations !== hold) { 360 return; 361 } 362 stopDragAnimations(); 363 touchDrag.animations = slideEls.map( 364 (el) => el.animate( 365 [ 366 { opacity: 0 }, 367 { 368 opacity: el === enlargedEl ? 1 : 0 369 } 370 ], 371 { 372 duration: FADE_DURATION_MS, 373 easing: "ease-out" 374 } 375 ) 376 ); 377 } 378 ); 379 }; 380 touchDrag.settle = settle; 381 if (reducedMotion) { 382 settle(); 383 } else { 384 touchDrag.animations = slideEls.map( 385 (el) => el.animate( 386 [ 387 { transform: slideTransform(deltaX) }, 388 { 389 transform: slideTransform(endOffset) 390 } 391 ], 392 { 393 duration: SLIDE_DURATION_MS, 394 easing: SLIDE_EASING, 395 // Holds the end position until `settle()` has 396 // moved the slides there itself. 397 fill: "forwards" 398 } 399 ) 400 ); 401 touchDrag.animations[0].onfinish = withScope(settle); 402 } 403 } 404 lastTouchTime = now; 405 isTouching = false; 406 resetTouchDrag(); 407 }), 408 handleScroll() { 409 if (state.overlayOpened) { 410 if (!isTouching && Date.now() - lastTouchTime > 450) { 411 window.scrollTo( 412 state.scrollLeftReset, 413 state.scrollTopReset 414 ); 415 } 416 } 417 }, 418 preloadImage() { 419 const { imageId } = getContext(); 420 if (state.preloadedImageIds.has(imageId)) { 421 return; 422 } 423 const imageMetadata = getImageMetadata(imageId); 424 if (!imageMetadata) { 425 return; 426 } 427 const imageLink = document.createElement("link"); 428 imageLink.rel = "preload"; 429 imageLink.as = "image"; 430 imageLink.href = getImageSrc(imageMetadata); 431 const srcset = getImageSrcset(imageMetadata); 432 if (srcset) { 433 imageLink.setAttribute("imagesrcset", srcset); 434 imageLink.setAttribute("imagesizes", "100vw"); 435 } 436 document.head.appendChild(imageLink); 437 state.preloadedImageIds.add(imageId); 438 }, 439 preloadImageWithDelay() { 440 const { imageId } = getContext(); 441 actions.cancelPreload(); 442 const timerId = setTimeout( 443 withScope(() => { 444 actions.preloadImage(); 445 state.preloadTimers.delete(imageId); 446 }), 447 IMAGE_PRELOAD_DELAY 448 ); 449 state.preloadTimers.set(imageId, timerId); 450 }, 451 cancelPreload() { 452 const { imageId } = getContext(); 453 if (state.preloadTimers.has(imageId)) { 454 clearTimeout(state.preloadTimers.get(imageId)); 455 state.preloadTimers.delete(imageId); 456 } 457 } 458 }, 459 callbacks: { 460 setOverlayStyles() { 461 if (!state.overlayEnabled) { 462 return; 463 } 464 let { 465 naturalWidth, 466 naturalHeight, 467 offsetWidth: originalWidth, 468 offsetHeight: originalHeight 469 } = state.selectedImage.imageRef; 470 let { x: screenPosX, y: screenPosY } = state.selectedImage.imageRef.getBoundingClientRect(); 471 const naturalRatio = naturalWidth / naturalHeight; 472 const originalRatio = originalWidth / originalHeight; 473 if (state.selectedImage.scaleAttr === "contain") { 474 if (naturalRatio > originalRatio) { 475 const heightWithoutSpace = originalWidth / naturalRatio; 476 screenPosY += (originalHeight - heightWithoutSpace) / 2; 477 originalHeight = heightWithoutSpace; 478 } else { 479 const widthWithoutSpace = originalHeight * naturalRatio; 480 screenPosX += (originalWidth - widthWithoutSpace) / 2; 481 originalWidth = widthWithoutSpace; 482 } 483 } 484 const imgMaxWidth = parseFloat( 485 state.selectedImage.targetWidth && state.selectedImage.targetWidth !== "none" ? state.selectedImage.targetWidth : naturalWidth 486 ); 487 const imgMaxHeight = parseFloat( 488 state.selectedImage.targetHeight && state.selectedImage.targetHeight !== "none" ? state.selectedImage.targetHeight : naturalHeight 489 ); 490 const fullSizeRatio = imgMaxWidth / imgMaxHeight; 491 let containerWidth = imgMaxWidth; 492 let containerHeight = imgMaxHeight; 493 if (originalWidth > containerWidth || originalHeight > containerHeight) { 494 containerWidth = originalWidth; 495 containerHeight = originalHeight; 496 } 497 let horizontalPadding = 0; 498 let verticalPadding = 160; 499 if (480 < window.innerWidth) { 500 horizontalPadding = 80; 501 verticalPadding = 160; 502 } 503 if (960 < window.innerWidth) { 504 horizontalPadding = state.hasNavigation ? 320 : 80; 505 verticalPadding = 80; 506 } 507 const targetMaxWidth = Math.min( 508 window.innerWidth - horizontalPadding, 509 containerWidth 510 ); 511 const targetMaxHeight = Math.min( 512 window.innerHeight - verticalPadding, 513 containerHeight 514 ); 515 const targetContainerRatio = targetMaxWidth / targetMaxHeight; 516 if (fullSizeRatio > targetContainerRatio) { 517 containerWidth = targetMaxWidth; 518 containerHeight = containerWidth / fullSizeRatio; 519 } else { 520 containerHeight = targetMaxHeight; 521 containerWidth = containerHeight * fullSizeRatio; 522 } 523 const hasCroppedSource = naturalRatio.toFixed(2) !== fullSizeRatio.toFixed(2); 524 const sourceRatio = hasCroppedSource ? naturalRatio : fullSizeRatio; 525 const containerScale = Math.max( 526 originalWidth / containerWidth, 527 originalHeight / containerHeight, 528 originalWidth / (containerHeight * sourceRatio), 529 originalHeight * sourceRatio / containerWidth 530 ); 531 const thumbnailWidth = originalWidth / containerScale; 532 const thumbnailHeight = originalHeight / containerScale; 533 const cropX = (containerWidth - thumbnailWidth) / 2; 534 const cropY = (containerHeight - thumbnailHeight) / 2; 535 screenPosX -= cropX * containerScale; 536 screenPosY -= cropY * containerScale; 537 state.overlayStyles = ` 538 --wp--lightbox-initial-top-position: $screenPosY}px; 539 --wp--lightbox-initial-left-position: $screenPosX}px; 540 --wp--lightbox-container-width: $containerWidth + 1}px; 541 --wp--lightbox-container-height: $containerHeight + 1}px; 542 --wp--lightbox-image-width: $containerWidth}px; 543 --wp--lightbox-image-height: $containerHeight}px; 544 --wp--lightbox-initial-clip: inset($cropY}px $cropX}px); 545 --wp--lightbox-thumbnail-width: $hasCroppedSource ? thumbnailWidth : containerWidth}px; 546 --wp--lightbox-thumbnail-height: $hasCroppedSource ? thumbnailHeight : containerHeight}px; 547 --wp--lightbox-scale: $containerScale}; 548 --wp--lightbox-scrollbar-width: $window.innerWidth - document.documentElement.clientWidth}px; 549 `; 550 }, 551 setButtonStyles() { 552 const { ref } = getElement(); 553 if (!ref) { 554 return; 555 } 556 const { imageId } = getContext(); 557 const imageMetadata = getImageMetadata(imageId); 558 if (!imageMetadata) { 559 return; 560 } 561 imageMetadata.imageRef = ref; 562 imageMetadata.currentSrc = ref.currentSrc; 563 const { 564 naturalWidth, 565 naturalHeight, 566 offsetWidth, 567 offsetHeight 568 } = ref; 569 if (naturalWidth === 0 || naturalHeight === 0) { 570 return; 571 } 572 const figure = ref.parentElement; 573 const figureWidth = ref.parentElement.clientWidth; 574 let figureHeight = ref.parentElement.clientHeight; 575 const caption = figure.querySelector("figcaption"); 576 if (caption) { 577 const captionComputedStyle = window.getComputedStyle(caption); 578 if (!["absolute", "fixed"].includes( 579 captionComputedStyle.position 580 )) { 581 figureHeight = figureHeight - caption.offsetHeight - parseFloat(captionComputedStyle.marginTop) - parseFloat(captionComputedStyle.marginBottom); 582 } 583 } 584 const buttonOffsetTop = figureHeight - offsetHeight; 585 const buttonOffsetRight = figureWidth - offsetWidth; 586 let buttonTop = buttonOffsetTop + 16; 587 let buttonRight = buttonOffsetRight + 16; 588 if (imageMetadata.scaleAttr === "contain") { 589 const naturalRatio = naturalWidth / naturalHeight; 590 const offsetRatio = offsetWidth / offsetHeight; 591 if (naturalRatio >= offsetRatio) { 592 const referenceHeight = offsetWidth / naturalRatio; 593 buttonTop = (offsetHeight - referenceHeight) / 2 + buttonOffsetTop + 16; 594 buttonRight = buttonOffsetRight + 16; 595 } else { 596 const referenceWidth = offsetHeight * naturalRatio; 597 buttonTop = buttonOffsetTop + 16; 598 buttonRight = (offsetWidth - referenceWidth) / 2 + buttonOffsetRight + 16; 599 } 600 } 601 imageMetadata.buttonTop = buttonTop; 602 imageMetadata.buttonRight = buttonRight; 603 }, 604 setOverlayFocus() { 605 if (state.overlayEnabled) { 606 const { ref } = getElement(); 607 ref.focus(); 608 } 609 }, 610 setInertElements() { 611 if (!state.overlayEnabled) { 612 inertElements.forEach( 613 (el) => el.removeAttribute("inert") 614 ); 615 inertElements = []; 616 return; 617 } 618 const { ref } = getElement(); 619 let node = ref; 620 while (node && node !== document.body && node.parentElement) { 621 for (const sibling of node.parentElement.children) { 622 if (sibling !== node && !sibling.hasAttribute("inert")) { 623 sibling.setAttribute("inert", ""); 624 inertElements.push(sibling); 625 } 626 } 627 node = node.parentElement; 628 } 629 }, 630 initTriggerButton() { 631 const { imageId } = getContext(); 632 const { ref } = getElement(); 633 const imageMetadata = getImageMetadata(imageId); 634 if (imageMetadata) { 635 imageMetadata.buttonRef = ref; 636 } 637 } 638 } 639 }, 640 { lock: true } 641 );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Oct 31 08:20:35 2026 | Cross-referenced by PHPXref |