Source code of my website
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

✨ : add photoswipe library for gallery

+9054
+4
layouts/partials/head/scripts.html
··· 14 14 <script defer language="javascript" type="text/javascript" src="{{ $js_bundle.RelPermalink }}"></script> 15 15 16 16 17 + {{ if .Page.Params.hasGallery }} 18 + <script type="module" src="/js/photoswipe/photoswipe-init.js"></script> 19 + {{ end }} 20 + 17 21 {{ if .Site.Params.plausible }} 18 22 <script defer data-domain="{{ .Site.Params.plausible_domain }}" src="{{ .Site.Params.plausible_script }}"></script> 19 23 {{ end }}
+9
static/js/photoswipe/photoswipe-init.js
··· 1 + import PhotoSwipeLightbox from '/js/photoswipe/photoswipe-lightbox.esm.js'; 2 + 3 + const lightbox = new PhotoSwipeLightbox({ 4 + gallery: '.pswp-gallery', 5 + children: 'a', 6 + pswpModule: () => import('/js/photoswipe/photoswipe.esm.js') 7 + }); 8 + 9 + lightbox.init();
+1960
static/js/photoswipe/photoswipe-lightbox.esm.js
··· 1 + /*! 2 + * PhotoSwipe Lightbox 5.4.4 - https://photoswipe.com 3 + * (c) 2024 Dmytro Semenov 4 + */ 5 + /** @typedef {import('../photoswipe.js').Point} Point */ 6 + 7 + /** 8 + * @template {keyof HTMLElementTagNameMap} T 9 + * @param {string} className 10 + * @param {T} tagName 11 + * @param {Node} [appendToEl] 12 + * @returns {HTMLElementTagNameMap[T]} 13 + */ 14 + function createElement(className, tagName, appendToEl) { 15 + const el = document.createElement(tagName); 16 + 17 + if (className) { 18 + el.className = className; 19 + } 20 + 21 + if (appendToEl) { 22 + appendToEl.appendChild(el); 23 + } 24 + 25 + return el; 26 + } 27 + /** 28 + * Get transform string 29 + * 30 + * @param {number} x 31 + * @param {number} [y] 32 + * @param {number} [scale] 33 + * @returns {string} 34 + */ 35 + 36 + function toTransformString(x, y, scale) { 37 + let propValue = `translate3d(${x}px,${y || 0}px,0)`; 38 + 39 + if (scale !== undefined) { 40 + propValue += ` scale3d(${scale},${scale},1)`; 41 + } 42 + 43 + return propValue; 44 + } 45 + /** 46 + * Apply width and height CSS properties to element 47 + * 48 + * @param {HTMLElement} el 49 + * @param {string | number} w 50 + * @param {string | number} h 51 + */ 52 + 53 + function setWidthHeight(el, w, h) { 54 + el.style.width = typeof w === 'number' ? `${w}px` : w; 55 + el.style.height = typeof h === 'number' ? `${h}px` : h; 56 + } 57 + /** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */ 58 + 59 + /** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */ 60 + 61 + const LOAD_STATE = { 62 + IDLE: 'idle', 63 + LOADING: 'loading', 64 + LOADED: 'loaded', 65 + ERROR: 'error' 66 + }; 67 + /** 68 + * Check if click or keydown event was dispatched 69 + * with a special key or via mouse wheel. 70 + * 71 + * @param {MouseEvent | KeyboardEvent} e 72 + * @returns {boolean} 73 + */ 74 + 75 + function specialKeyUsed(e) { 76 + return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey; 77 + } 78 + /** 79 + * Parse `gallery` or `children` options. 80 + * 81 + * @param {import('../photoswipe.js').ElementProvider} [option] 82 + * @param {string} [legacySelector] 83 + * @param {HTMLElement | Document} [parent] 84 + * @returns HTMLElement[] 85 + */ 86 + 87 + function getElementsFromOption(option, legacySelector, parent = document) { 88 + /** @type {HTMLElement[]} */ 89 + let elements = []; 90 + 91 + if (option instanceof Element) { 92 + elements = [option]; 93 + } else if (option instanceof NodeList || Array.isArray(option)) { 94 + elements = Array.from(option); 95 + } else { 96 + const selector = typeof option === 'string' ? option : legacySelector; 97 + 98 + if (selector) { 99 + elements = Array.from(parent.querySelectorAll(selector)); 100 + } 101 + } 102 + 103 + return elements; 104 + } 105 + /** 106 + * Check if variable is PhotoSwipe class 107 + * 108 + * @param {any} fn 109 + * @returns {boolean} 110 + */ 111 + 112 + function isPswpClass(fn) { 113 + return typeof fn === 'function' && fn.prototype && fn.prototype.goTo; 114 + } 115 + /** 116 + * Check if browser is Safari 117 + * 118 + * @returns {boolean} 119 + */ 120 + 121 + function isSafari() { 122 + return !!(navigator.vendor && navigator.vendor.match(/apple/i)); 123 + } 124 + 125 + /** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */ 126 + 127 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 128 + 129 + /** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */ 130 + 131 + /** @typedef {import('../photoswipe.js').DataSource} DataSource */ 132 + 133 + /** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */ 134 + 135 + /** @typedef {import('../slide/content.js').default} ContentDefault */ 136 + 137 + /** @typedef {import('../slide/slide.js').default} Slide */ 138 + 139 + /** @typedef {import('../slide/slide.js').SlideData} SlideData */ 140 + 141 + /** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */ 142 + 143 + /** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */ 144 + 145 + /** 146 + * Allow adding an arbitrary props to the Content 147 + * https://photoswipe.com/custom-content/#using-webp-image-format 148 + * @typedef {ContentDefault & Record<string, any>} Content 149 + */ 150 + 151 + /** @typedef {{ x?: number; y?: number }} Point */ 152 + 153 + /** 154 + * @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/ 155 + * 156 + * 157 + * https://photoswipe.com/adding-ui-elements/ 158 + * 159 + * @prop {undefined} uiRegister 160 + * @prop {{ data: UIElementData }} uiElementCreate 161 + * 162 + * 163 + * https://photoswipe.com/events/#initialization-events 164 + * 165 + * @prop {undefined} beforeOpen 166 + * @prop {undefined} firstUpdate 167 + * @prop {undefined} initialLayout 168 + * @prop {undefined} change 169 + * @prop {undefined} afterInit 170 + * @prop {undefined} bindEvents 171 + * 172 + * 173 + * https://photoswipe.com/events/#opening-or-closing-transition-events 174 + * 175 + * @prop {undefined} openingAnimationStart 176 + * @prop {undefined} openingAnimationEnd 177 + * @prop {undefined} closingAnimationStart 178 + * @prop {undefined} closingAnimationEnd 179 + * 180 + * 181 + * https://photoswipe.com/events/#closing-events 182 + * 183 + * @prop {undefined} close 184 + * @prop {undefined} destroy 185 + * 186 + * 187 + * https://photoswipe.com/events/#pointer-and-gesture-events 188 + * 189 + * @prop {{ originalEvent: PointerEvent }} pointerDown 190 + * @prop {{ originalEvent: PointerEvent }} pointerMove 191 + * @prop {{ originalEvent: PointerEvent }} pointerUp 192 + * @prop {{ bgOpacity: number }} pinchClose can be default prevented 193 + * @prop {{ panY: number }} verticalDrag can be default prevented 194 + * 195 + * 196 + * https://photoswipe.com/events/#slide-content-events 197 + * 198 + * @prop {{ content: Content }} contentInit 199 + * @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented 200 + * @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented 201 + * @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete 202 + * @prop {{ content: Content; slide: Slide }} loadError 203 + * @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented 204 + * @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange 205 + * @prop {{ content: Content }} contentLazyLoad can be default prevented 206 + * @prop {{ content: Content }} contentAppend can be default prevented 207 + * @prop {{ content: Content }} contentActivate can be default prevented 208 + * @prop {{ content: Content }} contentDeactivate can be default prevented 209 + * @prop {{ content: Content }} contentRemove can be default prevented 210 + * @prop {{ content: Content }} contentDestroy can be default prevented 211 + * 212 + * 213 + * undocumented 214 + * 215 + * @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented 216 + * @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented 217 + * @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented 218 + * @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented 219 + * 220 + * @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented 221 + * @prop {{ x: number; dragging: boolean }} moveMainScroll 222 + * @prop {{ slide: Slide }} firstZoomPan 223 + * @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData 224 + * @prop {undefined} beforeResize 225 + * @prop {undefined} resize 226 + * @prop {undefined} viewportSize 227 + * @prop {undefined} updateScrollOffset 228 + * @prop {{ slide: Slide }} slideInit 229 + * @prop {{ slide: Slide }} afterSetContent 230 + * @prop {{ slide: Slide }} slideLoad 231 + * @prop {{ slide: Slide }} appendHeavy can be default prevented 232 + * @prop {{ slide: Slide }} appendHeavyContent 233 + * @prop {{ slide: Slide }} slideActivate 234 + * @prop {{ slide: Slide }} slideDeactivate 235 + * @prop {{ slide: Slide }} slideDestroy 236 + * @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo 237 + * @prop {{ slide: Slide }} zoomPanUpdate 238 + * @prop {{ slide: Slide }} initialZoomPan 239 + * @prop {{ slide: Slide }} calcSlideSize 240 + * @prop {undefined} resolutionChanged 241 + * @prop {{ originalEvent: WheelEvent }} wheel can be default prevented 242 + * @prop {{ content: Content }} contentAppendImage can be default prevented 243 + * @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented 244 + * @prop {undefined} lazyLoad 245 + * @prop {{ slide: Slide }} calcBounds 246 + * @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate 247 + * 248 + * 249 + * legacy 250 + * 251 + * @prop {undefined} init 252 + * @prop {undefined} initialZoomIn 253 + * @prop {undefined} initialZoomOut 254 + * @prop {undefined} initialZoomInEnd 255 + * @prop {undefined} initialZoomOutEnd 256 + * @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems 257 + * @prop {{ itemData: SlideData; index: number }} itemData 258 + * @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds 259 + */ 260 + 261 + /** 262 + * @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/ 263 + * 264 + * @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems 265 + * Modify the total amount of slides. Example on Data sources page. 266 + * https://photoswipe.com/filters/#numitems 267 + * 268 + * @prop {(itemData: SlideData, index: number) => SlideData} itemData 269 + * Modify slide item data. Example on Data sources page. 270 + * https://photoswipe.com/filters/#itemdata 271 + * 272 + * @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData 273 + * Modify item data when it's parsed from DOM element. Example on Data sources page. 274 + * https://photoswipe.com/filters/#domitemdata 275 + * 276 + * @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex 277 + * Modify clicked gallery item index. 278 + * https://photoswipe.com/filters/#clickedindex 279 + * 280 + * @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc 281 + * Modify placeholder image source. 282 + * https://photoswipe.com/filters/#placeholdersrc 283 + * 284 + * @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading 285 + * Modify if the content is currently loading. 286 + * https://photoswipe.com/filters/#iscontentloading 287 + * 288 + * @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable 289 + * Modify if the content can be zoomed. 290 + * https://photoswipe.com/filters/#iscontentzoomable 291 + * 292 + * @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder 293 + * Modify if the placeholder should be used for the content. 294 + * https://photoswipe.com/filters/#usecontentplaceholder 295 + * 296 + * @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder 297 + * Modify if the placeholder should be kept after the content is loaded. 298 + * https://photoswipe.com/filters/#iskeepingplaceholder 299 + * 300 + * 301 + * @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement 302 + * Modify an element when the content has error state (for example, if image cannot be loaded). 303 + * https://photoswipe.com/filters/#contenterrorelement 304 + * 305 + * @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement 306 + * Modify a UI element that's being created. 307 + * https://photoswipe.com/filters/#uielement 308 + * 309 + * @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl 310 + * Modify the thumbnail element from which opening zoom animation starts or ends. 311 + * https://photoswipe.com/filters/#thumbel 312 + * 313 + * @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds 314 + * Modify the thumbnail bounds from which opening zoom animation starts or ends. 315 + * https://photoswipe.com/filters/#thumbbounds 316 + * 317 + * @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth 318 + * 319 + * @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent 320 + * 321 + */ 322 + 323 + /** 324 + * @template {keyof PhotoSwipeFiltersMap} T 325 + * @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter 326 + */ 327 + 328 + /** 329 + * @template {keyof PhotoSwipeEventsMap} T 330 + * @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent<T> : PhotoSwipeEvent<T> & PhotoSwipeEventsMap[T]} AugmentedEvent 331 + */ 332 + 333 + /** 334 + * @template {keyof PhotoSwipeEventsMap} T 335 + * @typedef {(event: AugmentedEvent<T>) => void} EventCallback 336 + */ 337 + 338 + /** 339 + * Base PhotoSwipe event object 340 + * 341 + * @template {keyof PhotoSwipeEventsMap} T 342 + */ 343 + class PhotoSwipeEvent { 344 + /** 345 + * @param {T} type 346 + * @param {PhotoSwipeEventsMap[T]} [details] 347 + */ 348 + constructor(type, details) { 349 + this.type = type; 350 + this.defaultPrevented = false; 351 + 352 + if (details) { 353 + Object.assign(this, details); 354 + } 355 + } 356 + 357 + preventDefault() { 358 + this.defaultPrevented = true; 359 + } 360 + 361 + } 362 + /** 363 + * PhotoSwipe base class that can listen and dispatch for events. 364 + * Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js 365 + */ 366 + 367 + 368 + class Eventable { 369 + constructor() { 370 + /** 371 + * @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent<T>) => void)[] }} 372 + */ 373 + this._listeners = {}; 374 + /** 375 + * @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter<T>[] }} 376 + */ 377 + 378 + this._filters = {}; 379 + /** @type {PhotoSwipe | undefined} */ 380 + 381 + this.pswp = undefined; 382 + /** @type {PhotoSwipeOptions | undefined} */ 383 + 384 + this.options = undefined; 385 + } 386 + /** 387 + * @template {keyof PhotoSwipeFiltersMap} T 388 + * @param {T} name 389 + * @param {PhotoSwipeFiltersMap[T]} fn 390 + * @param {number} priority 391 + */ 392 + 393 + 394 + addFilter(name, fn, priority = 100) { 395 + var _this$_filters$name, _this$_filters$name2, _this$pswp; 396 + 397 + if (!this._filters[name]) { 398 + this._filters[name] = []; 399 + } 400 + 401 + (_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({ 402 + fn, 403 + priority 404 + }); 405 + (_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority); 406 + (_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority); 407 + } 408 + /** 409 + * @template {keyof PhotoSwipeFiltersMap} T 410 + * @param {T} name 411 + * @param {PhotoSwipeFiltersMap[T]} fn 412 + */ 413 + 414 + 415 + removeFilter(name, fn) { 416 + if (this._filters[name]) { 417 + // @ts-expect-error 418 + this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn); 419 + } 420 + 421 + if (this.pswp) { 422 + this.pswp.removeFilter(name, fn); 423 + } 424 + } 425 + /** 426 + * @template {keyof PhotoSwipeFiltersMap} T 427 + * @param {T} name 428 + * @param {Parameters<PhotoSwipeFiltersMap[T]>} args 429 + * @returns {Parameters<PhotoSwipeFiltersMap[T]>[0]} 430 + */ 431 + 432 + 433 + applyFilters(name, ...args) { 434 + var _this$_filters$name3; 435 + 436 + (_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => { 437 + // @ts-expect-error 438 + args[0] = filter.fn.apply(this, args); 439 + }); 440 + return args[0]; 441 + } 442 + /** 443 + * @template {keyof PhotoSwipeEventsMap} T 444 + * @param {T} name 445 + * @param {EventCallback<T>} fn 446 + */ 447 + 448 + 449 + on(name, fn) { 450 + var _this$_listeners$name, _this$pswp2; 451 + 452 + if (!this._listeners[name]) { 453 + this._listeners[name] = []; 454 + } 455 + 456 + (_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox, 457 + // also bind events to PhotoSwipe Core, 458 + // if it's open. 459 + 460 + (_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn); 461 + } 462 + /** 463 + * @template {keyof PhotoSwipeEventsMap} T 464 + * @param {T} name 465 + * @param {EventCallback<T>} fn 466 + */ 467 + 468 + 469 + off(name, fn) { 470 + var _this$pswp3; 471 + 472 + if (this._listeners[name]) { 473 + // @ts-expect-error 474 + this._listeners[name] = this._listeners[name].filter(listener => fn !== listener); 475 + } 476 + 477 + (_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn); 478 + } 479 + /** 480 + * @template {keyof PhotoSwipeEventsMap} T 481 + * @param {T} name 482 + * @param {PhotoSwipeEventsMap[T]} [details] 483 + * @returns {AugmentedEvent<T>} 484 + */ 485 + 486 + 487 + dispatch(name, details) { 488 + var _this$_listeners$name2; 489 + 490 + if (this.pswp) { 491 + return this.pswp.dispatch(name, details); 492 + } 493 + 494 + const event = 495 + /** @type {AugmentedEvent<T>} */ 496 + new PhotoSwipeEvent(name, details); 497 + (_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => { 498 + listener.call(this, event); 499 + }); 500 + return event; 501 + } 502 + 503 + } 504 + 505 + class Placeholder { 506 + /** 507 + * @param {string | false} imageSrc 508 + * @param {HTMLElement} container 509 + */ 510 + constructor(imageSrc, container) { 511 + // Create placeholder 512 + // (stretched thumbnail or simple div behind the main image) 513 + 514 + /** @type {HTMLImageElement | HTMLDivElement | null} */ 515 + this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container); 516 + 517 + if (imageSrc) { 518 + const imgEl = 519 + /** @type {HTMLImageElement} */ 520 + this.element; 521 + imgEl.decoding = 'async'; 522 + imgEl.alt = ''; 523 + imgEl.src = imageSrc; 524 + imgEl.setAttribute('role', 'presentation'); 525 + } 526 + 527 + this.element.setAttribute('aria-hidden', 'true'); 528 + } 529 + /** 530 + * @param {number} width 531 + * @param {number} height 532 + */ 533 + 534 + 535 + setDisplayedSize(width, height) { 536 + if (!this.element) { 537 + return; 538 + } 539 + 540 + if (this.element.tagName === 'IMG') { 541 + // Use transform scale() to modify img placeholder size 542 + // (instead of changing width/height directly). 543 + // This helps with performance, specifically in iOS15 Safari. 544 + setWidthHeight(this.element, 250, 'auto'); 545 + this.element.style.transformOrigin = '0 0'; 546 + this.element.style.transform = toTransformString(0, 0, width / 250); 547 + } else { 548 + setWidthHeight(this.element, width, height); 549 + } 550 + } 551 + 552 + destroy() { 553 + var _this$element; 554 + 555 + if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) { 556 + this.element.remove(); 557 + } 558 + 559 + this.element = null; 560 + } 561 + 562 + } 563 + 564 + /** @typedef {import('./slide.js').default} Slide */ 565 + 566 + /** @typedef {import('./slide.js').SlideData} SlideData */ 567 + 568 + /** @typedef {import('../core/base.js').default} PhotoSwipeBase */ 569 + 570 + /** @typedef {import('../util/util.js').LoadState} LoadState */ 571 + 572 + class Content { 573 + /** 574 + * @param {SlideData} itemData Slide data 575 + * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance 576 + * @param {number} index 577 + */ 578 + constructor(itemData, instance, index) { 579 + this.instance = instance; 580 + this.data = itemData; 581 + this.index = index; 582 + /** @type {HTMLImageElement | HTMLDivElement | undefined} */ 583 + 584 + this.element = undefined; 585 + /** @type {Placeholder | undefined} */ 586 + 587 + this.placeholder = undefined; 588 + /** @type {Slide | undefined} */ 589 + 590 + this.slide = undefined; 591 + this.displayedImageWidth = 0; 592 + this.displayedImageHeight = 0; 593 + this.width = Number(this.data.w) || Number(this.data.width) || 0; 594 + this.height = Number(this.data.h) || Number(this.data.height) || 0; 595 + this.isAttached = false; 596 + this.hasSlide = false; 597 + this.isDecoding = false; 598 + /** @type {LoadState} */ 599 + 600 + this.state = LOAD_STATE.IDLE; 601 + 602 + if (this.data.type) { 603 + this.type = this.data.type; 604 + } else if (this.data.src) { 605 + this.type = 'image'; 606 + } else { 607 + this.type = 'html'; 608 + } 609 + 610 + this.instance.dispatch('contentInit', { 611 + content: this 612 + }); 613 + } 614 + 615 + removePlaceholder() { 616 + if (this.placeholder && !this.keepPlaceholder()) { 617 + // With delay, as image might be loaded, but not rendered 618 + setTimeout(() => { 619 + if (this.placeholder) { 620 + this.placeholder.destroy(); 621 + this.placeholder = undefined; 622 + } 623 + }, 1000); 624 + } 625 + } 626 + /** 627 + * Preload content 628 + * 629 + * @param {boolean} isLazy 630 + * @param {boolean} [reload] 631 + */ 632 + 633 + 634 + load(isLazy, reload) { 635 + if (this.slide && this.usePlaceholder()) { 636 + if (!this.placeholder) { 637 + const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use image-based placeholder only for the first slide, 638 + // as rendering (even small stretched thumbnail) is an expensive operation 639 + this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this); 640 + this.placeholder = new Placeholder(placeholderSrc, this.slide.container); 641 + } else { 642 + const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created 643 + 644 + if (placeholderEl && !placeholderEl.parentElement) { 645 + this.slide.container.prepend(placeholderEl); 646 + } 647 + } 648 + } 649 + 650 + if (this.element && !reload) { 651 + return; 652 + } 653 + 654 + if (this.instance.dispatch('contentLoad', { 655 + content: this, 656 + isLazy 657 + }).defaultPrevented) { 658 + return; 659 + } 660 + 661 + if (this.isImageContent()) { 662 + this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it. 663 + // Due to Safari feature, we must define sizes before srcset. 664 + 665 + if (this.displayedImageWidth) { 666 + this.loadImage(isLazy); 667 + } 668 + } else { 669 + this.element = createElement('pswp__content', 'div'); 670 + this.element.innerHTML = this.data.html || ''; 671 + } 672 + 673 + if (reload && this.slide) { 674 + this.slide.updateContentSize(true); 675 + } 676 + } 677 + /** 678 + * Preload image 679 + * 680 + * @param {boolean} isLazy 681 + */ 682 + 683 + 684 + loadImage(isLazy) { 685 + var _this$data$src, _this$data$alt; 686 + 687 + if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', { 688 + content: this, 689 + isLazy 690 + }).defaultPrevented) { 691 + return; 692 + } 693 + 694 + const imageElement = 695 + /** @type HTMLImageElement */ 696 + this.element; 697 + this.updateSrcsetSizes(); 698 + 699 + if (this.data.srcset) { 700 + imageElement.srcset = this.data.srcset; 701 + } 702 + 703 + imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : ''; 704 + imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : ''; 705 + this.state = LOAD_STATE.LOADING; 706 + 707 + if (imageElement.complete) { 708 + this.onLoaded(); 709 + } else { 710 + imageElement.onload = () => { 711 + this.onLoaded(); 712 + }; 713 + 714 + imageElement.onerror = () => { 715 + this.onError(); 716 + }; 717 + } 718 + } 719 + /** 720 + * Assign slide to content 721 + * 722 + * @param {Slide} slide 723 + */ 724 + 725 + 726 + setSlide(slide) { 727 + this.slide = slide; 728 + this.hasSlide = true; 729 + this.instance = slide.pswp; // todo: do we need to unset slide? 730 + } 731 + /** 732 + * Content load success handler 733 + */ 734 + 735 + 736 + onLoaded() { 737 + this.state = LOAD_STATE.LOADED; 738 + 739 + if (this.slide && this.element) { 740 + this.instance.dispatch('loadComplete', { 741 + slide: this.slide, 742 + content: this 743 + }); // if content is reloaded 744 + 745 + if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) { 746 + this.append(); 747 + this.slide.updateContentSize(true); 748 + } 749 + 750 + if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) { 751 + this.removePlaceholder(); 752 + } 753 + } 754 + } 755 + /** 756 + * Content load error handler 757 + */ 758 + 759 + 760 + onError() { 761 + this.state = LOAD_STATE.ERROR; 762 + 763 + if (this.slide) { 764 + this.displayError(); 765 + this.instance.dispatch('loadComplete', { 766 + slide: this.slide, 767 + isError: true, 768 + content: this 769 + }); 770 + this.instance.dispatch('loadError', { 771 + slide: this.slide, 772 + content: this 773 + }); 774 + } 775 + } 776 + /** 777 + * @returns {Boolean} If the content is currently loading 778 + */ 779 + 780 + 781 + isLoading() { 782 + return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this); 783 + } 784 + /** 785 + * @returns {Boolean} If the content is in error state 786 + */ 787 + 788 + 789 + isError() { 790 + return this.state === LOAD_STATE.ERROR; 791 + } 792 + /** 793 + * @returns {boolean} If the content is image 794 + */ 795 + 796 + 797 + isImageContent() { 798 + return this.type === 'image'; 799 + } 800 + /** 801 + * Update content size 802 + * 803 + * @param {Number} width 804 + * @param {Number} height 805 + */ 806 + 807 + 808 + setDisplayedSize(width, height) { 809 + if (!this.element) { 810 + return; 811 + } 812 + 813 + if (this.placeholder) { 814 + this.placeholder.setDisplayedSize(width, height); 815 + } 816 + 817 + if (this.instance.dispatch('contentResize', { 818 + content: this, 819 + width, 820 + height 821 + }).defaultPrevented) { 822 + return; 823 + } 824 + 825 + setWidthHeight(this.element, width, height); 826 + 827 + if (this.isImageContent() && !this.isError()) { 828 + const isInitialSizeUpdate = !this.displayedImageWidth && width; 829 + this.displayedImageWidth = width; 830 + this.displayedImageHeight = height; 831 + 832 + if (isInitialSizeUpdate) { 833 + this.loadImage(false); 834 + } else { 835 + this.updateSrcsetSizes(); 836 + } 837 + 838 + if (this.slide) { 839 + this.instance.dispatch('imageSizeChange', { 840 + slide: this.slide, 841 + width, 842 + height, 843 + content: this 844 + }); 845 + } 846 + } 847 + } 848 + /** 849 + * @returns {boolean} If the content can be zoomed 850 + */ 851 + 852 + 853 + isZoomable() { 854 + return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this); 855 + } 856 + /** 857 + * Update image srcset sizes attribute based on width and height 858 + */ 859 + 860 + 861 + updateSrcsetSizes() { 862 + // Handle srcset sizes attribute. 863 + // 864 + // Never lower quality, if it was increased previously. 865 + // Chrome does this automatically, Firefox and Safari do not, 866 + // so we store largest used size in dataset. 867 + if (!this.isImageContent() || !this.element || !this.data.srcset) { 868 + return; 869 + } 870 + 871 + const image = 872 + /** @type HTMLImageElement */ 873 + this.element; 874 + const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this); 875 + 876 + if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) { 877 + image.sizes = sizesWidth + 'px'; 878 + image.dataset.largestUsedSize = String(sizesWidth); 879 + } 880 + } 881 + /** 882 + * @returns {boolean} If content should use a placeholder (from msrc by default) 883 + */ 884 + 885 + 886 + usePlaceholder() { 887 + return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this); 888 + } 889 + /** 890 + * Preload content with lazy-loading param 891 + */ 892 + 893 + 894 + lazyLoad() { 895 + if (this.instance.dispatch('contentLazyLoad', { 896 + content: this 897 + }).defaultPrevented) { 898 + return; 899 + } 900 + 901 + this.load(true); 902 + } 903 + /** 904 + * @returns {boolean} If placeholder should be kept after content is loaded 905 + */ 906 + 907 + 908 + keepPlaceholder() { 909 + return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this); 910 + } 911 + /** 912 + * Destroy the content 913 + */ 914 + 915 + 916 + destroy() { 917 + this.hasSlide = false; 918 + this.slide = undefined; 919 + 920 + if (this.instance.dispatch('contentDestroy', { 921 + content: this 922 + }).defaultPrevented) { 923 + return; 924 + } 925 + 926 + this.remove(); 927 + 928 + if (this.placeholder) { 929 + this.placeholder.destroy(); 930 + this.placeholder = undefined; 931 + } 932 + 933 + if (this.isImageContent() && this.element) { 934 + this.element.onload = null; 935 + this.element.onerror = null; 936 + this.element = undefined; 937 + } 938 + } 939 + /** 940 + * Display error message 941 + */ 942 + 943 + 944 + displayError() { 945 + if (this.slide) { 946 + var _this$instance$option, _this$instance$option2; 947 + 948 + let errorMsgEl = createElement('pswp__error-msg', 'div'); 949 + errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : ''; 950 + errorMsgEl = 951 + /** @type {HTMLDivElement} */ 952 + this.instance.applyFilters('contentErrorElement', errorMsgEl, this); 953 + this.element = createElement('pswp__content pswp__error-msg-container', 'div'); 954 + this.element.appendChild(errorMsgEl); 955 + this.slide.container.innerText = ''; 956 + this.slide.container.appendChild(this.element); 957 + this.slide.updateContentSize(true); 958 + this.removePlaceholder(); 959 + } 960 + } 961 + /** 962 + * Append the content 963 + */ 964 + 965 + 966 + append() { 967 + if (this.isAttached || !this.element) { 968 + return; 969 + } 970 + 971 + this.isAttached = true; 972 + 973 + if (this.state === LOAD_STATE.ERROR) { 974 + this.displayError(); 975 + return; 976 + } 977 + 978 + if (this.instance.dispatch('contentAppend', { 979 + content: this 980 + }).defaultPrevented) { 981 + return; 982 + } 983 + 984 + const supportsDecode = ('decode' in this.element); 985 + 986 + if (this.isImageContent()) { 987 + // Use decode() on nearby slides 988 + // 989 + // Nearby slide images are in DOM and not hidden via display:none. 990 + // However, they are placed offscreen (to the left and right side). 991 + // 992 + // Some browsers do not composite the image until it's actually visible, 993 + // using decode() helps. 994 + // 995 + // You might ask "why dont you just decode() and then append all images", 996 + // that's because I want to show image before it's fully loaded, 997 + // as browser can render parts of image while it is loading. 998 + // We do not do this in Safari due to partial loading bug. 999 + if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) { 1000 + this.isDecoding = true; // purposefully using finally instead of then, 1001 + // as if srcset sizes changes dynamically - it may cause decode error 1002 + 1003 + /** @type {HTMLImageElement} */ 1004 + 1005 + this.element.decode().catch(() => {}).finally(() => { 1006 + this.isDecoding = false; 1007 + this.appendImage(); 1008 + }); 1009 + } else { 1010 + this.appendImage(); 1011 + } 1012 + } else if (this.slide && !this.element.parentNode) { 1013 + this.slide.container.appendChild(this.element); 1014 + } 1015 + } 1016 + /** 1017 + * Activate the slide, 1018 + * active slide is generally the current one, 1019 + * meaning the user can see it. 1020 + */ 1021 + 1022 + 1023 + activate() { 1024 + if (this.instance.dispatch('contentActivate', { 1025 + content: this 1026 + }).defaultPrevented || !this.slide) { 1027 + return; 1028 + } 1029 + 1030 + if (this.isImageContent() && this.isDecoding && !isSafari()) { 1031 + // add image to slide when it becomes active, 1032 + // even if it's not finished decoding 1033 + this.appendImage(); 1034 + } else if (this.isError()) { 1035 + this.load(false, true); // try to reload 1036 + } 1037 + 1038 + if (this.slide.holderElement) { 1039 + this.slide.holderElement.setAttribute('aria-hidden', 'false'); 1040 + } 1041 + } 1042 + /** 1043 + * Deactivate the content 1044 + */ 1045 + 1046 + 1047 + deactivate() { 1048 + this.instance.dispatch('contentDeactivate', { 1049 + content: this 1050 + }); 1051 + 1052 + if (this.slide && this.slide.holderElement) { 1053 + this.slide.holderElement.setAttribute('aria-hidden', 'true'); 1054 + } 1055 + } 1056 + /** 1057 + * Remove the content from DOM 1058 + */ 1059 + 1060 + 1061 + remove() { 1062 + this.isAttached = false; 1063 + 1064 + if (this.instance.dispatch('contentRemove', { 1065 + content: this 1066 + }).defaultPrevented) { 1067 + return; 1068 + } 1069 + 1070 + if (this.element && this.element.parentNode) { 1071 + this.element.remove(); 1072 + } 1073 + 1074 + if (this.placeholder && this.placeholder.element) { 1075 + this.placeholder.element.remove(); 1076 + } 1077 + } 1078 + /** 1079 + * Append the image content to slide container 1080 + */ 1081 + 1082 + 1083 + appendImage() { 1084 + if (!this.isAttached) { 1085 + return; 1086 + } 1087 + 1088 + if (this.instance.dispatch('contentAppendImage', { 1089 + content: this 1090 + }).defaultPrevented) { 1091 + return; 1092 + } // ensure that element exists and is not already appended 1093 + 1094 + 1095 + if (this.slide && this.element && !this.element.parentNode) { 1096 + this.slide.container.appendChild(this.element); 1097 + } 1098 + 1099 + if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) { 1100 + this.removePlaceholder(); 1101 + } 1102 + } 1103 + 1104 + } 1105 + 1106 + /** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */ 1107 + 1108 + /** @typedef {import('../core/base.js').default} PhotoSwipeBase */ 1109 + 1110 + /** @typedef {import('../photoswipe.js').Point} Point */ 1111 + 1112 + /** @typedef {import('../slide/slide.js').SlideData} SlideData */ 1113 + 1114 + /** 1115 + * @param {PhotoSwipeOptions} options 1116 + * @param {PhotoSwipeBase} pswp 1117 + * @returns {Point} 1118 + */ 1119 + function getViewportSize(options, pswp) { 1120 + if (options.getViewportSizeFn) { 1121 + const newViewportSize = options.getViewportSizeFn(options, pswp); 1122 + 1123 + if (newViewportSize) { 1124 + return newViewportSize; 1125 + } 1126 + } 1127 + 1128 + return { 1129 + x: document.documentElement.clientWidth, 1130 + // TODO: height on mobile is very incosistent due to toolbar 1131 + // find a way to improve this 1132 + // 1133 + // document.documentElement.clientHeight - doesn't seem to work well 1134 + y: window.innerHeight 1135 + }; 1136 + } 1137 + /** 1138 + * Parses padding option. 1139 + * Supported formats: 1140 + * 1141 + * // Object 1142 + * padding: { 1143 + * top: 0, 1144 + * bottom: 0, 1145 + * left: 0, 1146 + * right: 0 1147 + * } 1148 + * 1149 + * // A function that returns the object 1150 + * paddingFn: (viewportSize, itemData, index) => { 1151 + * return { 1152 + * top: 0, 1153 + * bottom: 0, 1154 + * left: 0, 1155 + * right: 0 1156 + * }; 1157 + * } 1158 + * 1159 + * // Legacy variant 1160 + * paddingLeft: 0, 1161 + * paddingRight: 0, 1162 + * paddingTop: 0, 1163 + * paddingBottom: 0, 1164 + * 1165 + * @param {'left' | 'top' | 'bottom' | 'right'} prop 1166 + * @param {PhotoSwipeOptions} options PhotoSwipe options 1167 + * @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 } 1168 + * @param {SlideData} itemData Data about the slide 1169 + * @param {number} index Slide index 1170 + * @returns {number} 1171 + */ 1172 + 1173 + function parsePaddingOption(prop, options, viewportSize, itemData, index) { 1174 + let paddingValue = 0; 1175 + 1176 + if (options.paddingFn) { 1177 + paddingValue = options.paddingFn(viewportSize, itemData, index)[prop]; 1178 + } else if (options.padding) { 1179 + paddingValue = options.padding[prop]; 1180 + } else { 1181 + const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error 1182 + 1183 + if (options[legacyPropName]) { 1184 + // @ts-expect-error 1185 + paddingValue = options[legacyPropName]; 1186 + } 1187 + } 1188 + 1189 + return Number(paddingValue) || 0; 1190 + } 1191 + /** 1192 + * @param {PhotoSwipeOptions} options 1193 + * @param {Point} viewportSize 1194 + * @param {SlideData} itemData 1195 + * @param {number} index 1196 + * @returns {Point} 1197 + */ 1198 + 1199 + function getPanAreaSize(options, viewportSize, itemData, index) { 1200 + return { 1201 + x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index), 1202 + y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index) 1203 + }; 1204 + } 1205 + 1206 + const MAX_IMAGE_WIDTH = 4000; 1207 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 1208 + 1209 + /** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */ 1210 + 1211 + /** @typedef {import('../photoswipe.js').Point} Point */ 1212 + 1213 + /** @typedef {import('../slide/slide.js').SlideData} SlideData */ 1214 + 1215 + /** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */ 1216 + 1217 + /** 1218 + * Calculates zoom levels for specific slide. 1219 + * Depends on viewport size and image size. 1220 + */ 1221 + 1222 + class ZoomLevel { 1223 + /** 1224 + * @param {PhotoSwipeOptions} options PhotoSwipe options 1225 + * @param {SlideData} itemData Slide data 1226 + * @param {number} index Slide index 1227 + * @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet 1228 + */ 1229 + constructor(options, itemData, index, pswp) { 1230 + this.pswp = pswp; 1231 + this.options = options; 1232 + this.itemData = itemData; 1233 + this.index = index; 1234 + /** @type { Point | null } */ 1235 + 1236 + this.panAreaSize = null; 1237 + /** @type { Point | null } */ 1238 + 1239 + this.elementSize = null; 1240 + this.fit = 1; 1241 + this.fill = 1; 1242 + this.vFill = 1; 1243 + this.initial = 1; 1244 + this.secondary = 1; 1245 + this.max = 1; 1246 + this.min = 1; 1247 + } 1248 + /** 1249 + * Calculate initial, secondary and maximum zoom level for the specified slide. 1250 + * 1251 + * It should be called when either image or viewport size changes. 1252 + * 1253 + * @param {number} maxWidth 1254 + * @param {number} maxHeight 1255 + * @param {Point} panAreaSize 1256 + */ 1257 + 1258 + 1259 + update(maxWidth, maxHeight, panAreaSize) { 1260 + /** @type {Point} */ 1261 + const elementSize = { 1262 + x: maxWidth, 1263 + y: maxHeight 1264 + }; 1265 + this.elementSize = elementSize; 1266 + this.panAreaSize = panAreaSize; 1267 + const hRatio = panAreaSize.x / elementSize.x; 1268 + const vRatio = panAreaSize.y / elementSize.y; 1269 + this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio); 1270 + this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image 1271 + // when it has 100% of viewport vertical space (height) 1272 + 1273 + this.vFill = Math.min(1, vRatio); 1274 + this.initial = this._getInitial(); 1275 + this.secondary = this._getSecondary(); 1276 + this.max = Math.max(this.initial, this.secondary, this._getMax()); 1277 + this.min = Math.min(this.fit, this.initial, this.secondary); 1278 + 1279 + if (this.pswp) { 1280 + this.pswp.dispatch('zoomLevelsUpdate', { 1281 + zoomLevels: this, 1282 + slideData: this.itemData 1283 + }); 1284 + } 1285 + } 1286 + /** 1287 + * Parses user-defined zoom option. 1288 + * 1289 + * @private 1290 + * @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max) 1291 + * @returns { number | undefined } 1292 + */ 1293 + 1294 + 1295 + _parseZoomLevelOption(optionPrefix) { 1296 + const optionName = 1297 + /** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */ 1298 + optionPrefix + 'ZoomLevel'; 1299 + const optionValue = this.options[optionName]; 1300 + 1301 + if (!optionValue) { 1302 + return; 1303 + } 1304 + 1305 + if (typeof optionValue === 'function') { 1306 + return optionValue(this); 1307 + } 1308 + 1309 + if (optionValue === 'fill') { 1310 + return this.fill; 1311 + } 1312 + 1313 + if (optionValue === 'fit') { 1314 + return this.fit; 1315 + } 1316 + 1317 + return Number(optionValue); 1318 + } 1319 + /** 1320 + * Get zoom level to which image will be zoomed after double-tap gesture, 1321 + * or when user clicks on zoom icon, 1322 + * or mouse-click on image itself. 1323 + * If you return 1 image will be zoomed to its original size. 1324 + * 1325 + * @private 1326 + * @return {number} 1327 + */ 1328 + 1329 + 1330 + _getSecondary() { 1331 + let currZoomLevel = this._parseZoomLevelOption('secondary'); 1332 + 1333 + if (currZoomLevel) { 1334 + return currZoomLevel; 1335 + } // 3x of "fit" state, but not larger than original 1336 + 1337 + 1338 + currZoomLevel = Math.min(1, this.fit * 3); 1339 + 1340 + if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) { 1341 + currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x; 1342 + } 1343 + 1344 + return currZoomLevel; 1345 + } 1346 + /** 1347 + * Get initial image zoom level. 1348 + * 1349 + * @private 1350 + * @return {number} 1351 + */ 1352 + 1353 + 1354 + _getInitial() { 1355 + return this._parseZoomLevelOption('initial') || this.fit; 1356 + } 1357 + /** 1358 + * Maximum zoom level when user zooms 1359 + * via zoom/pinch gesture, 1360 + * via cmd/ctrl-wheel or via trackpad. 1361 + * 1362 + * @private 1363 + * @return {number} 1364 + */ 1365 + 1366 + 1367 + _getMax() { 1368 + // max zoom level is x4 from "fit state", 1369 + // used for zoom gesture and ctrl/trackpad zoom 1370 + return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4); 1371 + } 1372 + 1373 + } 1374 + 1375 + /** 1376 + * Lazy-load an image 1377 + * This function is used both by Lightbox and PhotoSwipe core, 1378 + * thus it can be called before dialog is opened. 1379 + * 1380 + * @param {SlideData} itemData Data about the slide 1381 + * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance 1382 + * @param {number} index 1383 + * @returns {Content} Image that is being decoded or false. 1384 + */ 1385 + 1386 + function lazyLoadData(itemData, instance, index) { 1387 + const content = instance.createContentFromData(itemData, index); 1388 + /** @type {ZoomLevel | undefined} */ 1389 + 1390 + let zoomLevel; 1391 + const { 1392 + options 1393 + } = instance; // We need to know dimensions of the image to preload it, 1394 + // as it might use srcset, and we need to define sizes 1395 + 1396 + if (options) { 1397 + zoomLevel = new ZoomLevel(options, itemData, -1); 1398 + let viewportSize; 1399 + 1400 + if (instance.pswp) { 1401 + viewportSize = instance.pswp.viewportSize; 1402 + } else { 1403 + viewportSize = getViewportSize(options, instance); 1404 + } 1405 + 1406 + const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index); 1407 + zoomLevel.update(content.width, content.height, panAreaSize); 1408 + } 1409 + 1410 + content.lazyLoad(); 1411 + 1412 + if (zoomLevel) { 1413 + content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial)); 1414 + } 1415 + 1416 + return content; 1417 + } 1418 + /** 1419 + * Lazy-loads specific slide. 1420 + * This function is used both by Lightbox and PhotoSwipe core, 1421 + * thus it can be called before dialog is opened. 1422 + * 1423 + * By default, it loads image based on viewport size and initial zoom level. 1424 + * 1425 + * @param {number} index Slide index 1426 + * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance 1427 + * @returns {Content | undefined} 1428 + */ 1429 + 1430 + function lazyLoadSlide(index, instance) { 1431 + const itemData = instance.getItemData(index); 1432 + 1433 + if (instance.dispatch('lazyLoadSlide', { 1434 + index, 1435 + itemData 1436 + }).defaultPrevented) { 1437 + return; 1438 + } 1439 + 1440 + return lazyLoadData(itemData, instance, index); 1441 + } 1442 + 1443 + /** @typedef {import("../photoswipe.js").default} PhotoSwipe */ 1444 + 1445 + /** @typedef {import("../slide/slide.js").SlideData} SlideData */ 1446 + 1447 + /** 1448 + * PhotoSwipe base class that can retrieve data about every slide. 1449 + * Shared by PhotoSwipe Core and PhotoSwipe Lightbox 1450 + */ 1451 + 1452 + class PhotoSwipeBase extends Eventable { 1453 + /** 1454 + * Get total number of slides 1455 + * 1456 + * @returns {number} 1457 + */ 1458 + getNumItems() { 1459 + var _this$options; 1460 + 1461 + let numItems = 0; 1462 + const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource; 1463 + 1464 + if (dataSource && 'length' in dataSource) { 1465 + // may be an array or just object with length property 1466 + numItems = dataSource.length; 1467 + } else if (dataSource && 'gallery' in dataSource) { 1468 + // query DOM elements 1469 + if (!dataSource.items) { 1470 + dataSource.items = this._getGalleryDOMElements(dataSource.gallery); 1471 + } 1472 + 1473 + if (dataSource.items) { 1474 + numItems = dataSource.items.length; 1475 + } 1476 + } // legacy event, before filters were introduced 1477 + 1478 + 1479 + const event = this.dispatch('numItems', { 1480 + dataSource, 1481 + numItems 1482 + }); 1483 + return this.applyFilters('numItems', event.numItems, dataSource); 1484 + } 1485 + /** 1486 + * @param {SlideData} slideData 1487 + * @param {number} index 1488 + * @returns {Content} 1489 + */ 1490 + 1491 + 1492 + createContentFromData(slideData, index) { 1493 + return new Content(slideData, this, index); 1494 + } 1495 + /** 1496 + * Get item data by index. 1497 + * 1498 + * "item data" should contain normalized information that PhotoSwipe needs to generate a slide. 1499 + * For example, it may contain properties like 1500 + * `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image. 1501 + * 1502 + * @param {number} index 1503 + * @returns {SlideData} 1504 + */ 1505 + 1506 + 1507 + getItemData(index) { 1508 + var _this$options2; 1509 + 1510 + const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource; 1511 + /** @type {SlideData | HTMLElement} */ 1512 + 1513 + let dataSourceItem = {}; 1514 + 1515 + if (Array.isArray(dataSource)) { 1516 + // Datasource is an array of elements 1517 + dataSourceItem = dataSource[index]; 1518 + } else if (dataSource && 'gallery' in dataSource) { 1519 + // dataSource has gallery property, 1520 + // thus it was created by Lightbox, based on 1521 + // gallery and children options 1522 + // query DOM elements 1523 + if (!dataSource.items) { 1524 + dataSource.items = this._getGalleryDOMElements(dataSource.gallery); 1525 + } 1526 + 1527 + dataSourceItem = dataSource.items[index]; 1528 + } 1529 + 1530 + let itemData = dataSourceItem; 1531 + 1532 + if (itemData instanceof Element) { 1533 + itemData = this._domElementToItemData(itemData); 1534 + } // Dispatching the itemData event, 1535 + // it's a legacy verion before filters were introduced 1536 + 1537 + 1538 + const event = this.dispatch('itemData', { 1539 + itemData: itemData || {}, 1540 + index 1541 + }); 1542 + return this.applyFilters('itemData', event.itemData, index); 1543 + } 1544 + /** 1545 + * Get array of gallery DOM elements, 1546 + * based on childSelector and gallery element. 1547 + * 1548 + * @param {HTMLElement} galleryElement 1549 + * @returns {HTMLElement[]} 1550 + */ 1551 + 1552 + 1553 + _getGalleryDOMElements(galleryElement) { 1554 + var _this$options3, _this$options4; 1555 + 1556 + if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) { 1557 + return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || []; 1558 + } 1559 + 1560 + return [galleryElement]; 1561 + } 1562 + /** 1563 + * Converts DOM element to item data object. 1564 + * 1565 + * @param {HTMLElement} element DOM element 1566 + * @returns {SlideData} 1567 + */ 1568 + 1569 + 1570 + _domElementToItemData(element) { 1571 + /** @type {SlideData} */ 1572 + const itemData = { 1573 + element 1574 + }; 1575 + const linkEl = 1576 + /** @type {HTMLAnchorElement} */ 1577 + element.tagName === 'A' ? element : element.querySelector('a'); 1578 + 1579 + if (linkEl) { 1580 + // src comes from data-pswp-src attribute, 1581 + // if it's empty link href is used 1582 + itemData.src = linkEl.dataset.pswpSrc || linkEl.href; 1583 + 1584 + if (linkEl.dataset.pswpSrcset) { 1585 + itemData.srcset = linkEl.dataset.pswpSrcset; 1586 + } 1587 + 1588 + itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0; 1589 + itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties 1590 + 1591 + itemData.w = itemData.width; 1592 + itemData.h = itemData.height; 1593 + 1594 + if (linkEl.dataset.pswpType) { 1595 + itemData.type = linkEl.dataset.pswpType; 1596 + } 1597 + 1598 + const thumbnailEl = element.querySelector('img'); 1599 + 1600 + if (thumbnailEl) { 1601 + var _thumbnailEl$getAttri; 1602 + 1603 + // msrc is URL to placeholder image that's displayed before large image is loaded 1604 + // by default it's displayed only for the first slide 1605 + itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src; 1606 + itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : ''; 1607 + } 1608 + 1609 + if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) { 1610 + itemData.thumbCropped = true; 1611 + } 1612 + } 1613 + 1614 + return this.applyFilters('domItemData', itemData, element, linkEl); 1615 + } 1616 + /** 1617 + * Lazy-load by slide data 1618 + * 1619 + * @param {SlideData} itemData Data about the slide 1620 + * @param {number} index 1621 + * @returns {Content} Image that is being decoded or false. 1622 + */ 1623 + 1624 + 1625 + lazyLoadData(itemData, index) { 1626 + return lazyLoadData(itemData, this, index); 1627 + } 1628 + 1629 + } 1630 + 1631 + /** 1632 + * @template T 1633 + * @typedef {import('../types.js').Type<T>} Type<T> 1634 + */ 1635 + 1636 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 1637 + 1638 + /** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */ 1639 + 1640 + /** @typedef {import('../photoswipe.js').DataSource} DataSource */ 1641 + 1642 + /** @typedef {import('../photoswipe.js').Point} Point */ 1643 + 1644 + /** @typedef {import('../slide/content.js').default} Content */ 1645 + 1646 + /** @typedef {import('../core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */ 1647 + 1648 + /** @typedef {import('../core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */ 1649 + 1650 + /** 1651 + * @template {keyof PhotoSwipeEventsMap} T 1652 + * @typedef {import('../core/eventable.js').EventCallback<T>} EventCallback<T> 1653 + */ 1654 + 1655 + /** 1656 + * PhotoSwipe Lightbox 1657 + * 1658 + * - If user has unsupported browser it falls back to default browser action (just opens URL) 1659 + * - Binds click event to links that should open PhotoSwipe 1660 + * - parses DOM strcture for PhotoSwipe (retrieves large image URLs and sizes) 1661 + * - Initializes PhotoSwipe 1662 + * 1663 + * 1664 + * Loader options use the same object as PhotoSwipe, and supports such options: 1665 + * 1666 + * gallery - Element | Element[] | NodeList | string selector for the gallery element 1667 + * children - Element | Element[] | NodeList | string selector for the gallery children 1668 + * 1669 + */ 1670 + 1671 + class PhotoSwipeLightbox extends PhotoSwipeBase { 1672 + /** 1673 + * @param {PhotoSwipeOptions} [options] 1674 + */ 1675 + constructor(options) { 1676 + super(); 1677 + /** @type {PhotoSwipeOptions} */ 1678 + 1679 + this.options = options || {}; 1680 + this._uid = 0; 1681 + this.shouldOpen = false; 1682 + /** 1683 + * @private 1684 + * @type {Content | undefined} 1685 + */ 1686 + 1687 + this._preloadedContent = undefined; 1688 + this.onThumbnailsClick = this.onThumbnailsClick.bind(this); 1689 + } 1690 + /** 1691 + * Initialize lightbox, should be called only once. 1692 + * It's not included in the main constructor, so you may bind events before it. 1693 + */ 1694 + 1695 + 1696 + init() { 1697 + // Bind click events to each gallery 1698 + getElementsFromOption(this.options.gallery, this.options.gallerySelector).forEach(galleryElement => { 1699 + galleryElement.addEventListener('click', this.onThumbnailsClick, false); 1700 + }); 1701 + } 1702 + /** 1703 + * @param {MouseEvent} e 1704 + */ 1705 + 1706 + 1707 + onThumbnailsClick(e) { 1708 + // Exit and allow default browser action if: 1709 + if (specialKeyUsed(e) // ... if clicked with a special key (ctrl/cmd...) 1710 + || window.pswp) { 1711 + // ... if PhotoSwipe is already open 1712 + return; 1713 + } // If both clientX and clientY are 0 or not defined, 1714 + // the event is likely triggered by keyboard, 1715 + // so we do not pass the initialPoint 1716 + // 1717 + // Note that some screen readers emulate the mouse position, 1718 + // so it's not the ideal way to detect them. 1719 + // 1720 + 1721 + /** @type {Point | null} */ 1722 + 1723 + 1724 + let initialPoint = { 1725 + x: e.clientX, 1726 + y: e.clientY 1727 + }; 1728 + 1729 + if (!initialPoint.x && !initialPoint.y) { 1730 + initialPoint = null; 1731 + } 1732 + 1733 + let clickedIndex = this.getClickedIndex(e); 1734 + clickedIndex = this.applyFilters('clickedIndex', clickedIndex, e, this); 1735 + /** @type {DataSource} */ 1736 + 1737 + const dataSource = { 1738 + gallery: 1739 + /** @type {HTMLElement} */ 1740 + e.currentTarget 1741 + }; 1742 + 1743 + if (clickedIndex >= 0) { 1744 + e.preventDefault(); 1745 + this.loadAndOpen(clickedIndex, dataSource, initialPoint); 1746 + } 1747 + } 1748 + /** 1749 + * Get index of gallery item that was clicked. 1750 + * 1751 + * @param {MouseEvent} e click event 1752 + * @returns {number} 1753 + */ 1754 + 1755 + 1756 + getClickedIndex(e) { 1757 + // legacy option 1758 + if (this.options.getClickedIndexFn) { 1759 + return this.options.getClickedIndexFn.call(this, e); 1760 + } 1761 + 1762 + const clickedTarget = 1763 + /** @type {HTMLElement} */ 1764 + e.target; 1765 + const childElements = getElementsFromOption(this.options.children, this.options.childSelector, 1766 + /** @type {HTMLElement} */ 1767 + e.currentTarget); 1768 + const clickedChildIndex = childElements.findIndex(child => child === clickedTarget || child.contains(clickedTarget)); 1769 + 1770 + if (clickedChildIndex !== -1) { 1771 + return clickedChildIndex; 1772 + } else if (this.options.children || this.options.childSelector) { 1773 + // click wasn't on a child element 1774 + return -1; 1775 + } // There is only one item (which is the gallery) 1776 + 1777 + 1778 + return 0; 1779 + } 1780 + /** 1781 + * Load and open PhotoSwipe 1782 + * 1783 + * @param {number} index 1784 + * @param {DataSource} [dataSource] 1785 + * @param {Point | null} [initialPoint] 1786 + * @returns {boolean} 1787 + */ 1788 + 1789 + 1790 + loadAndOpen(index, dataSource, initialPoint) { 1791 + // Check if the gallery is already open 1792 + if (window.pswp || !this.options) { 1793 + return false; 1794 + } // Use the first gallery element if dataSource is not provided 1795 + 1796 + 1797 + if (!dataSource && this.options.gallery && this.options.children) { 1798 + const galleryElements = getElementsFromOption(this.options.gallery); 1799 + 1800 + if (galleryElements[0]) { 1801 + dataSource = { 1802 + gallery: galleryElements[0] 1803 + }; 1804 + } 1805 + } // set initial index 1806 + 1807 + 1808 + this.options.index = index; // define options for PhotoSwipe constructor 1809 + 1810 + this.options.initialPointerPos = initialPoint; 1811 + this.shouldOpen = true; 1812 + this.preload(index, dataSource); 1813 + return true; 1814 + } 1815 + /** 1816 + * Load the main module and the slide content by index 1817 + * 1818 + * @param {number} index 1819 + * @param {DataSource} [dataSource] 1820 + */ 1821 + 1822 + 1823 + preload(index, dataSource) { 1824 + const { 1825 + options 1826 + } = this; 1827 + 1828 + if (dataSource) { 1829 + options.dataSource = dataSource; 1830 + } // Add the main module 1831 + 1832 + /** @type {Promise<Type<PhotoSwipe>>[]} */ 1833 + 1834 + 1835 + const promiseArray = []; 1836 + const pswpModuleType = typeof options.pswpModule; 1837 + 1838 + if (isPswpClass(options.pswpModule)) { 1839 + promiseArray.push(Promise.resolve( 1840 + /** @type {Type<PhotoSwipe>} */ 1841 + options.pswpModule)); 1842 + } else if (pswpModuleType === 'string') { 1843 + throw new Error('pswpModule as string is no longer supported'); 1844 + } else if (pswpModuleType === 'function') { 1845 + promiseArray.push( 1846 + /** @type {() => Promise<Type<PhotoSwipe>>} */ 1847 + options.pswpModule()); 1848 + } else { 1849 + throw new Error('pswpModule is not valid'); 1850 + } // Add custom-defined promise, if any 1851 + 1852 + 1853 + if (typeof options.openPromise === 'function') { 1854 + // allow developers to perform some task before opening 1855 + promiseArray.push(options.openPromise()); 1856 + } 1857 + 1858 + if (options.preloadFirstSlide !== false && index >= 0) { 1859 + this._preloadedContent = lazyLoadSlide(index, this); 1860 + } // Wait till all promises resolve and open PhotoSwipe 1861 + 1862 + 1863 + const uid = ++this._uid; 1864 + Promise.all(promiseArray).then(iterableModules => { 1865 + if (this.shouldOpen) { 1866 + const mainModule = iterableModules[0]; 1867 + 1868 + this._openPhotoswipe(mainModule, uid); 1869 + } 1870 + }); 1871 + } 1872 + /** 1873 + * @private 1874 + * @param {Type<PhotoSwipe> | { default: Type<PhotoSwipe> }} module 1875 + * @param {number} uid 1876 + */ 1877 + 1878 + 1879 + _openPhotoswipe(module, uid) { 1880 + // Cancel opening if UID doesn't match the current one 1881 + // (if user clicked on another gallery item before current was loaded). 1882 + // 1883 + // Or if shouldOpen flag is set to false 1884 + // (developer may modify it via public API) 1885 + if (uid !== this._uid && this.shouldOpen) { 1886 + return; 1887 + } 1888 + 1889 + this.shouldOpen = false; // PhotoSwipe is already open 1890 + 1891 + if (window.pswp) { 1892 + return; 1893 + } 1894 + /** 1895 + * Pass data to PhotoSwipe and open init 1896 + * 1897 + * @type {PhotoSwipe} 1898 + */ 1899 + 1900 + 1901 + const pswp = typeof module === 'object' ? new module.default(this.options) // eslint-disable-line 1902 + : new module(this.options); // eslint-disable-line 1903 + 1904 + this.pswp = pswp; 1905 + window.pswp = pswp; // map listeners from Lightbox to PhotoSwipe Core 1906 + 1907 + /** @type {(keyof PhotoSwipeEventsMap)[]} */ 1908 + 1909 + Object.keys(this._listeners).forEach(name => { 1910 + var _this$_listeners$name; 1911 + 1912 + (_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.forEach(fn => { 1913 + pswp.on(name, 1914 + /** @type {EventCallback<typeof name>} */ 1915 + fn); 1916 + }); 1917 + }); // same with filters 1918 + 1919 + /** @type {(keyof PhotoSwipeFiltersMap)[]} */ 1920 + 1921 + Object.keys(this._filters).forEach(name => { 1922 + var _this$_filters$name; 1923 + 1924 + (_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.forEach(filter => { 1925 + pswp.addFilter(name, filter.fn, filter.priority); 1926 + }); 1927 + }); 1928 + 1929 + if (this._preloadedContent) { 1930 + pswp.contentLoader.addToCache(this._preloadedContent); 1931 + this._preloadedContent = undefined; 1932 + } 1933 + 1934 + pswp.on('destroy', () => { 1935 + // clean up public variables 1936 + this.pswp = undefined; 1937 + delete window.pswp; 1938 + }); 1939 + pswp.init(); 1940 + } 1941 + /** 1942 + * Unbinds all events, closes PhotoSwipe if it's open. 1943 + */ 1944 + 1945 + 1946 + destroy() { 1947 + var _this$pswp; 1948 + 1949 + (_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.destroy(); 1950 + this.shouldOpen = false; 1951 + this._listeners = {}; 1952 + getElementsFromOption(this.options.gallery, this.options.gallerySelector).forEach(galleryElement => { 1953 + galleryElement.removeEventListener('click', this.onThumbnailsClick, false); 1954 + }); 1955 + } 1956 + 1957 + } 1958 + 1959 + export { PhotoSwipeLightbox as default }; 1960 + //# sourceMappingURL=photoswipe-lightbox.esm.js.map
+7081
static/js/photoswipe/photoswipe.esm.js
··· 1 + /*! 2 + * PhotoSwipe 5.4.4 - https://photoswipe.com 3 + * (c) 2024 Dmytro Semenov 4 + */ 5 + /** @typedef {import('../photoswipe.js').Point} Point */ 6 + 7 + /** 8 + * @template {keyof HTMLElementTagNameMap} T 9 + * @param {string} className 10 + * @param {T} tagName 11 + * @param {Node} [appendToEl] 12 + * @returns {HTMLElementTagNameMap[T]} 13 + */ 14 + function createElement(className, tagName, appendToEl) { 15 + const el = document.createElement(tagName); 16 + 17 + if (className) { 18 + el.className = className; 19 + } 20 + 21 + if (appendToEl) { 22 + appendToEl.appendChild(el); 23 + } 24 + 25 + return el; 26 + } 27 + /** 28 + * @param {Point} p1 29 + * @param {Point} p2 30 + * @returns {Point} 31 + */ 32 + 33 + function equalizePoints(p1, p2) { 34 + p1.x = p2.x; 35 + p1.y = p2.y; 36 + 37 + if (p2.id !== undefined) { 38 + p1.id = p2.id; 39 + } 40 + 41 + return p1; 42 + } 43 + /** 44 + * @param {Point} p 45 + */ 46 + 47 + function roundPoint(p) { 48 + p.x = Math.round(p.x); 49 + p.y = Math.round(p.y); 50 + } 51 + /** 52 + * Returns distance between two points. 53 + * 54 + * @param {Point} p1 55 + * @param {Point} p2 56 + * @returns {number} 57 + */ 58 + 59 + function getDistanceBetween(p1, p2) { 60 + const x = Math.abs(p1.x - p2.x); 61 + const y = Math.abs(p1.y - p2.y); 62 + return Math.sqrt(x * x + y * y); 63 + } 64 + /** 65 + * Whether X and Y positions of points are equal 66 + * 67 + * @param {Point} p1 68 + * @param {Point} p2 69 + * @returns {boolean} 70 + */ 71 + 72 + function pointsEqual(p1, p2) { 73 + return p1.x === p2.x && p1.y === p2.y; 74 + } 75 + /** 76 + * The float result between the min and max values. 77 + * 78 + * @param {number} val 79 + * @param {number} min 80 + * @param {number} max 81 + * @returns {number} 82 + */ 83 + 84 + function clamp(val, min, max) { 85 + return Math.min(Math.max(val, min), max); 86 + } 87 + /** 88 + * Get transform string 89 + * 90 + * @param {number} x 91 + * @param {number} [y] 92 + * @param {number} [scale] 93 + * @returns {string} 94 + */ 95 + 96 + function toTransformString(x, y, scale) { 97 + let propValue = `translate3d(${x}px,${y || 0}px,0)`; 98 + 99 + if (scale !== undefined) { 100 + propValue += ` scale3d(${scale},${scale},1)`; 101 + } 102 + 103 + return propValue; 104 + } 105 + /** 106 + * Apply transform:translate(x, y) scale(scale) to element 107 + * 108 + * @param {HTMLElement} el 109 + * @param {number} x 110 + * @param {number} [y] 111 + * @param {number} [scale] 112 + */ 113 + 114 + function setTransform(el, x, y, scale) { 115 + el.style.transform = toTransformString(x, y, scale); 116 + } 117 + const defaultCSSEasing = 'cubic-bezier(.4,0,.22,1)'; 118 + /** 119 + * Apply CSS transition to element 120 + * 121 + * @param {HTMLElement} el 122 + * @param {string} [prop] CSS property to animate 123 + * @param {number} [duration] in ms 124 + * @param {string} [ease] CSS easing function 125 + */ 126 + 127 + function setTransitionStyle(el, prop, duration, ease) { 128 + // inOut: 'cubic-bezier(.4, 0, .22, 1)', // for "toggle state" transitions 129 + // out: 'cubic-bezier(0, 0, .22, 1)', // for "show" transitions 130 + // in: 'cubic-bezier(.4, 0, 1, 1)'// for "hide" transitions 131 + el.style.transition = prop ? `${prop} ${duration}ms ${ease || defaultCSSEasing}` : 'none'; 132 + } 133 + /** 134 + * Apply width and height CSS properties to element 135 + * 136 + * @param {HTMLElement} el 137 + * @param {string | number} w 138 + * @param {string | number} h 139 + */ 140 + 141 + function setWidthHeight(el, w, h) { 142 + el.style.width = typeof w === 'number' ? `${w}px` : w; 143 + el.style.height = typeof h === 'number' ? `${h}px` : h; 144 + } 145 + /** 146 + * @param {HTMLElement} el 147 + */ 148 + 149 + function removeTransitionStyle(el) { 150 + setTransitionStyle(el); 151 + } 152 + /** 153 + * @param {HTMLImageElement} img 154 + * @returns {Promise<HTMLImageElement | void>} 155 + */ 156 + 157 + function decodeImage(img) { 158 + if ('decode' in img) { 159 + return img.decode().catch(() => {}); 160 + } 161 + 162 + if (img.complete) { 163 + return Promise.resolve(img); 164 + } 165 + 166 + return new Promise((resolve, reject) => { 167 + img.onload = () => resolve(img); 168 + 169 + img.onerror = reject; 170 + }); 171 + } 172 + /** @typedef {LOAD_STATE[keyof LOAD_STATE]} LoadState */ 173 + 174 + /** @type {{ IDLE: 'idle'; LOADING: 'loading'; LOADED: 'loaded'; ERROR: 'error' }} */ 175 + 176 + const LOAD_STATE = { 177 + IDLE: 'idle', 178 + LOADING: 'loading', 179 + LOADED: 'loaded', 180 + ERROR: 'error' 181 + }; 182 + /** 183 + * Check if click or keydown event was dispatched 184 + * with a special key or via mouse wheel. 185 + * 186 + * @param {MouseEvent | KeyboardEvent} e 187 + * @returns {boolean} 188 + */ 189 + 190 + function specialKeyUsed(e) { 191 + return 'button' in e && e.button === 1 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey; 192 + } 193 + /** 194 + * Parse `gallery` or `children` options. 195 + * 196 + * @param {import('../photoswipe.js').ElementProvider} [option] 197 + * @param {string} [legacySelector] 198 + * @param {HTMLElement | Document} [parent] 199 + * @returns HTMLElement[] 200 + */ 201 + 202 + function getElementsFromOption(option, legacySelector, parent = document) { 203 + /** @type {HTMLElement[]} */ 204 + let elements = []; 205 + 206 + if (option instanceof Element) { 207 + elements = [option]; 208 + } else if (option instanceof NodeList || Array.isArray(option)) { 209 + elements = Array.from(option); 210 + } else { 211 + const selector = typeof option === 'string' ? option : legacySelector; 212 + 213 + if (selector) { 214 + elements = Array.from(parent.querySelectorAll(selector)); 215 + } 216 + } 217 + 218 + return elements; 219 + } 220 + /** 221 + * Check if browser is Safari 222 + * 223 + * @returns {boolean} 224 + */ 225 + 226 + function isSafari() { 227 + return !!(navigator.vendor && navigator.vendor.match(/apple/i)); 228 + } 229 + 230 + // Detect passive event listener support 231 + let supportsPassive = false; 232 + /* eslint-disable */ 233 + 234 + try { 235 + /* @ts-ignore */ 236 + window.addEventListener('test', null, Object.defineProperty({}, 'passive', { 237 + get: () => { 238 + supportsPassive = true; 239 + } 240 + })); 241 + } catch (e) {} 242 + /* eslint-enable */ 243 + 244 + /** 245 + * @typedef {Object} PoolItem 246 + * @prop {HTMLElement | Window | Document | undefined | null} target 247 + * @prop {string} type 248 + * @prop {EventListenerOrEventListenerObject} listener 249 + * @prop {boolean} [passive] 250 + */ 251 + 252 + 253 + class DOMEvents { 254 + constructor() { 255 + /** 256 + * @type {PoolItem[]} 257 + * @private 258 + */ 259 + this._pool = []; 260 + } 261 + /** 262 + * Adds event listeners 263 + * 264 + * @param {PoolItem['target']} target 265 + * @param {PoolItem['type']} type Can be multiple, separated by space. 266 + * @param {PoolItem['listener']} listener 267 + * @param {PoolItem['passive']} [passive] 268 + */ 269 + 270 + 271 + add(target, type, listener, passive) { 272 + this._toggleListener(target, type, listener, passive); 273 + } 274 + /** 275 + * Removes event listeners 276 + * 277 + * @param {PoolItem['target']} target 278 + * @param {PoolItem['type']} type 279 + * @param {PoolItem['listener']} listener 280 + * @param {PoolItem['passive']} [passive] 281 + */ 282 + 283 + 284 + remove(target, type, listener, passive) { 285 + this._toggleListener(target, type, listener, passive, true); 286 + } 287 + /** 288 + * Removes all bound events 289 + */ 290 + 291 + 292 + removeAll() { 293 + this._pool.forEach(poolItem => { 294 + this._toggleListener(poolItem.target, poolItem.type, poolItem.listener, poolItem.passive, true, true); 295 + }); 296 + 297 + this._pool = []; 298 + } 299 + /** 300 + * Adds or removes event 301 + * 302 + * @private 303 + * @param {PoolItem['target']} target 304 + * @param {PoolItem['type']} type 305 + * @param {PoolItem['listener']} listener 306 + * @param {PoolItem['passive']} [passive] 307 + * @param {boolean} [unbind] Whether the event should be added or removed 308 + * @param {boolean} [skipPool] Whether events pool should be skipped 309 + */ 310 + 311 + 312 + _toggleListener(target, type, listener, passive, unbind, skipPool) { 313 + if (!target) { 314 + return; 315 + } 316 + 317 + const methodName = unbind ? 'removeEventListener' : 'addEventListener'; 318 + const types = type.split(' '); 319 + types.forEach(eType => { 320 + if (eType) { 321 + // Events pool is used to easily unbind all events when PhotoSwipe is closed, 322 + // so developer doesn't need to do this manually 323 + if (!skipPool) { 324 + if (unbind) { 325 + // Remove from the events pool 326 + this._pool = this._pool.filter(poolItem => { 327 + return poolItem.type !== eType || poolItem.listener !== listener || poolItem.target !== target; 328 + }); 329 + } else { 330 + // Add to the events pool 331 + this._pool.push({ 332 + target, 333 + type: eType, 334 + listener, 335 + passive 336 + }); 337 + } 338 + } // most PhotoSwipe events call preventDefault, 339 + // and we do not need browser to scroll the page 340 + 341 + 342 + const eventOptions = supportsPassive ? { 343 + passive: passive || false 344 + } : false; 345 + target[methodName](eType, listener, eventOptions); 346 + } 347 + }); 348 + } 349 + 350 + } 351 + 352 + /** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */ 353 + 354 + /** @typedef {import('../core/base.js').default} PhotoSwipeBase */ 355 + 356 + /** @typedef {import('../photoswipe.js').Point} Point */ 357 + 358 + /** @typedef {import('../slide/slide.js').SlideData} SlideData */ 359 + 360 + /** 361 + * @param {PhotoSwipeOptions} options 362 + * @param {PhotoSwipeBase} pswp 363 + * @returns {Point} 364 + */ 365 + function getViewportSize(options, pswp) { 366 + if (options.getViewportSizeFn) { 367 + const newViewportSize = options.getViewportSizeFn(options, pswp); 368 + 369 + if (newViewportSize) { 370 + return newViewportSize; 371 + } 372 + } 373 + 374 + return { 375 + x: document.documentElement.clientWidth, 376 + // TODO: height on mobile is very incosistent due to toolbar 377 + // find a way to improve this 378 + // 379 + // document.documentElement.clientHeight - doesn't seem to work well 380 + y: window.innerHeight 381 + }; 382 + } 383 + /** 384 + * Parses padding option. 385 + * Supported formats: 386 + * 387 + * // Object 388 + * padding: { 389 + * top: 0, 390 + * bottom: 0, 391 + * left: 0, 392 + * right: 0 393 + * } 394 + * 395 + * // A function that returns the object 396 + * paddingFn: (viewportSize, itemData, index) => { 397 + * return { 398 + * top: 0, 399 + * bottom: 0, 400 + * left: 0, 401 + * right: 0 402 + * }; 403 + * } 404 + * 405 + * // Legacy variant 406 + * paddingLeft: 0, 407 + * paddingRight: 0, 408 + * paddingTop: 0, 409 + * paddingBottom: 0, 410 + * 411 + * @param {'left' | 'top' | 'bottom' | 'right'} prop 412 + * @param {PhotoSwipeOptions} options PhotoSwipe options 413 + * @param {Point} viewportSize PhotoSwipe viewport size, for example: { x:800, y:600 } 414 + * @param {SlideData} itemData Data about the slide 415 + * @param {number} index Slide index 416 + * @returns {number} 417 + */ 418 + 419 + function parsePaddingOption(prop, options, viewportSize, itemData, index) { 420 + let paddingValue = 0; 421 + 422 + if (options.paddingFn) { 423 + paddingValue = options.paddingFn(viewportSize, itemData, index)[prop]; 424 + } else if (options.padding) { 425 + paddingValue = options.padding[prop]; 426 + } else { 427 + const legacyPropName = 'padding' + prop[0].toUpperCase() + prop.slice(1); // @ts-expect-error 428 + 429 + if (options[legacyPropName]) { 430 + // @ts-expect-error 431 + paddingValue = options[legacyPropName]; 432 + } 433 + } 434 + 435 + return Number(paddingValue) || 0; 436 + } 437 + /** 438 + * @param {PhotoSwipeOptions} options 439 + * @param {Point} viewportSize 440 + * @param {SlideData} itemData 441 + * @param {number} index 442 + * @returns {Point} 443 + */ 444 + 445 + function getPanAreaSize(options, viewportSize, itemData, index) { 446 + return { 447 + x: viewportSize.x - parsePaddingOption('left', options, viewportSize, itemData, index) - parsePaddingOption('right', options, viewportSize, itemData, index), 448 + y: viewportSize.y - parsePaddingOption('top', options, viewportSize, itemData, index) - parsePaddingOption('bottom', options, viewportSize, itemData, index) 449 + }; 450 + } 451 + 452 + /** @typedef {import('./slide.js').default} Slide */ 453 + 454 + /** @typedef {Record<Axis, number>} Point */ 455 + 456 + /** @typedef {'x' | 'y'} Axis */ 457 + 458 + /** 459 + * Calculates minimum, maximum and initial (center) bounds of a slide 460 + */ 461 + 462 + class PanBounds { 463 + /** 464 + * @param {Slide} slide 465 + */ 466 + constructor(slide) { 467 + this.slide = slide; 468 + this.currZoomLevel = 1; 469 + this.center = 470 + /** @type {Point} */ 471 + { 472 + x: 0, 473 + y: 0 474 + }; 475 + this.max = 476 + /** @type {Point} */ 477 + { 478 + x: 0, 479 + y: 0 480 + }; 481 + this.min = 482 + /** @type {Point} */ 483 + { 484 + x: 0, 485 + y: 0 486 + }; 487 + } 488 + /** 489 + * _getItemBounds 490 + * 491 + * @param {number} currZoomLevel 492 + */ 493 + 494 + 495 + update(currZoomLevel) { 496 + this.currZoomLevel = currZoomLevel; 497 + 498 + if (!this.slide.width) { 499 + this.reset(); 500 + } else { 501 + this._updateAxis('x'); 502 + 503 + this._updateAxis('y'); 504 + 505 + this.slide.pswp.dispatch('calcBounds', { 506 + slide: this.slide 507 + }); 508 + } 509 + } 510 + /** 511 + * _calculateItemBoundsForAxis 512 + * 513 + * @param {Axis} axis 514 + */ 515 + 516 + 517 + _updateAxis(axis) { 518 + const { 519 + pswp 520 + } = this.slide; 521 + const elSize = this.slide[axis === 'x' ? 'width' : 'height'] * this.currZoomLevel; 522 + const paddingProp = axis === 'x' ? 'left' : 'top'; 523 + const padding = parsePaddingOption(paddingProp, pswp.options, pswp.viewportSize, this.slide.data, this.slide.index); 524 + const panAreaSize = this.slide.panAreaSize[axis]; // Default position of element. 525 + // By default, it is center of viewport: 526 + 527 + this.center[axis] = Math.round((panAreaSize - elSize) / 2) + padding; // maximum pan position 528 + 529 + this.max[axis] = elSize > panAreaSize ? Math.round(panAreaSize - elSize) + padding : this.center[axis]; // minimum pan position 530 + 531 + this.min[axis] = elSize > panAreaSize ? padding : this.center[axis]; 532 + } // _getZeroBounds 533 + 534 + 535 + reset() { 536 + this.center.x = 0; 537 + this.center.y = 0; 538 + this.max.x = 0; 539 + this.max.y = 0; 540 + this.min.x = 0; 541 + this.min.y = 0; 542 + } 543 + /** 544 + * Correct pan position if it's beyond the bounds 545 + * 546 + * @param {Axis} axis x or y 547 + * @param {number} panOffset 548 + * @returns {number} 549 + */ 550 + 551 + 552 + correctPan(axis, panOffset) { 553 + // checkPanBounds 554 + return clamp(panOffset, this.max[axis], this.min[axis]); 555 + } 556 + 557 + } 558 + 559 + const MAX_IMAGE_WIDTH = 4000; 560 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 561 + 562 + /** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */ 563 + 564 + /** @typedef {import('../photoswipe.js').Point} Point */ 565 + 566 + /** @typedef {import('../slide/slide.js').SlideData} SlideData */ 567 + 568 + /** @typedef {'fit' | 'fill' | number | ((zoomLevelObject: ZoomLevel) => number)} ZoomLevelOption */ 569 + 570 + /** 571 + * Calculates zoom levels for specific slide. 572 + * Depends on viewport size and image size. 573 + */ 574 + 575 + class ZoomLevel { 576 + /** 577 + * @param {PhotoSwipeOptions} options PhotoSwipe options 578 + * @param {SlideData} itemData Slide data 579 + * @param {number} index Slide index 580 + * @param {PhotoSwipe} [pswp] PhotoSwipe instance, can be undefined if not initialized yet 581 + */ 582 + constructor(options, itemData, index, pswp) { 583 + this.pswp = pswp; 584 + this.options = options; 585 + this.itemData = itemData; 586 + this.index = index; 587 + /** @type { Point | null } */ 588 + 589 + this.panAreaSize = null; 590 + /** @type { Point | null } */ 591 + 592 + this.elementSize = null; 593 + this.fit = 1; 594 + this.fill = 1; 595 + this.vFill = 1; 596 + this.initial = 1; 597 + this.secondary = 1; 598 + this.max = 1; 599 + this.min = 1; 600 + } 601 + /** 602 + * Calculate initial, secondary and maximum zoom level for the specified slide. 603 + * 604 + * It should be called when either image or viewport size changes. 605 + * 606 + * @param {number} maxWidth 607 + * @param {number} maxHeight 608 + * @param {Point} panAreaSize 609 + */ 610 + 611 + 612 + update(maxWidth, maxHeight, panAreaSize) { 613 + /** @type {Point} */ 614 + const elementSize = { 615 + x: maxWidth, 616 + y: maxHeight 617 + }; 618 + this.elementSize = elementSize; 619 + this.panAreaSize = panAreaSize; 620 + const hRatio = panAreaSize.x / elementSize.x; 621 + const vRatio = panAreaSize.y / elementSize.y; 622 + this.fit = Math.min(1, hRatio < vRatio ? hRatio : vRatio); 623 + this.fill = Math.min(1, hRatio > vRatio ? hRatio : vRatio); // zoom.vFill defines zoom level of the image 624 + // when it has 100% of viewport vertical space (height) 625 + 626 + this.vFill = Math.min(1, vRatio); 627 + this.initial = this._getInitial(); 628 + this.secondary = this._getSecondary(); 629 + this.max = Math.max(this.initial, this.secondary, this._getMax()); 630 + this.min = Math.min(this.fit, this.initial, this.secondary); 631 + 632 + if (this.pswp) { 633 + this.pswp.dispatch('zoomLevelsUpdate', { 634 + zoomLevels: this, 635 + slideData: this.itemData 636 + }); 637 + } 638 + } 639 + /** 640 + * Parses user-defined zoom option. 641 + * 642 + * @private 643 + * @param {'initial' | 'secondary' | 'max'} optionPrefix Zoom level option prefix (initial, secondary, max) 644 + * @returns { number | undefined } 645 + */ 646 + 647 + 648 + _parseZoomLevelOption(optionPrefix) { 649 + const optionName = 650 + /** @type {'initialZoomLevel' | 'secondaryZoomLevel' | 'maxZoomLevel'} */ 651 + optionPrefix + 'ZoomLevel'; 652 + const optionValue = this.options[optionName]; 653 + 654 + if (!optionValue) { 655 + return; 656 + } 657 + 658 + if (typeof optionValue === 'function') { 659 + return optionValue(this); 660 + } 661 + 662 + if (optionValue === 'fill') { 663 + return this.fill; 664 + } 665 + 666 + if (optionValue === 'fit') { 667 + return this.fit; 668 + } 669 + 670 + return Number(optionValue); 671 + } 672 + /** 673 + * Get zoom level to which image will be zoomed after double-tap gesture, 674 + * or when user clicks on zoom icon, 675 + * or mouse-click on image itself. 676 + * If you return 1 image will be zoomed to its original size. 677 + * 678 + * @private 679 + * @return {number} 680 + */ 681 + 682 + 683 + _getSecondary() { 684 + let currZoomLevel = this._parseZoomLevelOption('secondary'); 685 + 686 + if (currZoomLevel) { 687 + return currZoomLevel; 688 + } // 3x of "fit" state, but not larger than original 689 + 690 + 691 + currZoomLevel = Math.min(1, this.fit * 3); 692 + 693 + if (this.elementSize && currZoomLevel * this.elementSize.x > MAX_IMAGE_WIDTH) { 694 + currZoomLevel = MAX_IMAGE_WIDTH / this.elementSize.x; 695 + } 696 + 697 + return currZoomLevel; 698 + } 699 + /** 700 + * Get initial image zoom level. 701 + * 702 + * @private 703 + * @return {number} 704 + */ 705 + 706 + 707 + _getInitial() { 708 + return this._parseZoomLevelOption('initial') || this.fit; 709 + } 710 + /** 711 + * Maximum zoom level when user zooms 712 + * via zoom/pinch gesture, 713 + * via cmd/ctrl-wheel or via trackpad. 714 + * 715 + * @private 716 + * @return {number} 717 + */ 718 + 719 + 720 + _getMax() { 721 + // max zoom level is x4 from "fit state", 722 + // used for zoom gesture and ctrl/trackpad zoom 723 + return this._parseZoomLevelOption('max') || Math.max(1, this.fit * 4); 724 + } 725 + 726 + } 727 + 728 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 729 + /** 730 + * Renders and allows to control a single slide 731 + */ 732 + 733 + class Slide { 734 + /** 735 + * @param {SlideData} data 736 + * @param {number} index 737 + * @param {PhotoSwipe} pswp 738 + */ 739 + constructor(data, index, pswp) { 740 + this.data = data; 741 + this.index = index; 742 + this.pswp = pswp; 743 + this.isActive = index === pswp.currIndex; 744 + this.currentResolution = 0; 745 + /** @type {Point} */ 746 + 747 + this.panAreaSize = { 748 + x: 0, 749 + y: 0 750 + }; 751 + /** @type {Point} */ 752 + 753 + this.pan = { 754 + x: 0, 755 + y: 0 756 + }; 757 + this.isFirstSlide = this.isActive && !pswp.opener.isOpen; 758 + this.zoomLevels = new ZoomLevel(pswp.options, data, index, pswp); 759 + this.pswp.dispatch('gettingData', { 760 + slide: this, 761 + data: this.data, 762 + index 763 + }); 764 + this.content = this.pswp.contentLoader.getContentBySlide(this); 765 + this.container = createElement('pswp__zoom-wrap', 'div'); 766 + /** @type {HTMLElement | null} */ 767 + 768 + this.holderElement = null; 769 + this.currZoomLevel = 1; 770 + /** @type {number} */ 771 + 772 + this.width = this.content.width; 773 + /** @type {number} */ 774 + 775 + this.height = this.content.height; 776 + this.heavyAppended = false; 777 + this.bounds = new PanBounds(this); 778 + this.prevDisplayedWidth = -1; 779 + this.prevDisplayedHeight = -1; 780 + this.pswp.dispatch('slideInit', { 781 + slide: this 782 + }); 783 + } 784 + /** 785 + * If this slide is active/current/visible 786 + * 787 + * @param {boolean} isActive 788 + */ 789 + 790 + 791 + setIsActive(isActive) { 792 + if (isActive && !this.isActive) { 793 + // slide just became active 794 + this.activate(); 795 + } else if (!isActive && this.isActive) { 796 + // slide just became non-active 797 + this.deactivate(); 798 + } 799 + } 800 + /** 801 + * Appends slide content to DOM 802 + * 803 + * @param {HTMLElement} holderElement 804 + */ 805 + 806 + 807 + append(holderElement) { 808 + this.holderElement = holderElement; 809 + this.container.style.transformOrigin = '0 0'; // Slide appended to DOM 810 + 811 + if (!this.data) { 812 + return; 813 + } 814 + 815 + this.calculateSize(); 816 + this.load(); 817 + this.updateContentSize(); 818 + this.appendHeavy(); 819 + this.holderElement.appendChild(this.container); 820 + this.zoomAndPanToInitial(); 821 + this.pswp.dispatch('firstZoomPan', { 822 + slide: this 823 + }); 824 + this.applyCurrentZoomPan(); 825 + this.pswp.dispatch('afterSetContent', { 826 + slide: this 827 + }); 828 + 829 + if (this.isActive) { 830 + this.activate(); 831 + } 832 + } 833 + 834 + load() { 835 + this.content.load(false); 836 + this.pswp.dispatch('slideLoad', { 837 + slide: this 838 + }); 839 + } 840 + /** 841 + * Append "heavy" DOM elements 842 + * 843 + * This may depend on a type of slide, 844 + * but generally these are large images. 845 + */ 846 + 847 + 848 + appendHeavy() { 849 + const { 850 + pswp 851 + } = this; 852 + const appendHeavyNearby = true; // todo 853 + // Avoid appending heavy elements during animations 854 + 855 + if (this.heavyAppended || !pswp.opener.isOpen || pswp.mainScroll.isShifted() || !this.isActive && !appendHeavyNearby) { 856 + return; 857 + } 858 + 859 + if (this.pswp.dispatch('appendHeavy', { 860 + slide: this 861 + }).defaultPrevented) { 862 + return; 863 + } 864 + 865 + this.heavyAppended = true; 866 + this.content.append(); 867 + this.pswp.dispatch('appendHeavyContent', { 868 + slide: this 869 + }); 870 + } 871 + /** 872 + * Triggered when this slide is active (selected). 873 + * 874 + * If it's part of opening/closing transition - 875 + * activate() will trigger after the transition is ended. 876 + */ 877 + 878 + 879 + activate() { 880 + this.isActive = true; 881 + this.appendHeavy(); 882 + this.content.activate(); 883 + this.pswp.dispatch('slideActivate', { 884 + slide: this 885 + }); 886 + } 887 + /** 888 + * Triggered when this slide becomes inactive. 889 + * 890 + * Slide can become inactive only after it was active. 891 + */ 892 + 893 + 894 + deactivate() { 895 + this.isActive = false; 896 + this.content.deactivate(); 897 + 898 + if (this.currZoomLevel !== this.zoomLevels.initial) { 899 + // allow filtering 900 + this.calculateSize(); 901 + } // reset zoom level 902 + 903 + 904 + this.currentResolution = 0; 905 + this.zoomAndPanToInitial(); 906 + this.applyCurrentZoomPan(); 907 + this.updateContentSize(); 908 + this.pswp.dispatch('slideDeactivate', { 909 + slide: this 910 + }); 911 + } 912 + /** 913 + * The slide should destroy itself, it will never be used again. 914 + * (unbind all events and destroy internal components) 915 + */ 916 + 917 + 918 + destroy() { 919 + this.content.hasSlide = false; 920 + this.content.remove(); 921 + this.container.remove(); 922 + this.pswp.dispatch('slideDestroy', { 923 + slide: this 924 + }); 925 + } 926 + 927 + resize() { 928 + if (this.currZoomLevel === this.zoomLevels.initial || !this.isActive) { 929 + // Keep initial zoom level if it was before the resize, 930 + // as well as when this slide is not active 931 + // Reset position and scale to original state 932 + this.calculateSize(); 933 + this.currentResolution = 0; 934 + this.zoomAndPanToInitial(); 935 + this.applyCurrentZoomPan(); 936 + this.updateContentSize(); 937 + } else { 938 + // readjust pan position if it's beyond the bounds 939 + this.calculateSize(); 940 + this.bounds.update(this.currZoomLevel); 941 + this.panTo(this.pan.x, this.pan.y); 942 + } 943 + } 944 + /** 945 + * Apply size to current slide content, 946 + * based on the current resolution and scale. 947 + * 948 + * @param {boolean} [force] if size should be updated even if dimensions weren't changed 949 + */ 950 + 951 + 952 + updateContentSize(force) { 953 + // Use initial zoom level 954 + // if resolution is not defined (user didn't zoom yet) 955 + const scaleMultiplier = this.currentResolution || this.zoomLevels.initial; 956 + 957 + if (!scaleMultiplier) { 958 + return; 959 + } 960 + 961 + const width = Math.round(this.width * scaleMultiplier) || this.pswp.viewportSize.x; 962 + const height = Math.round(this.height * scaleMultiplier) || this.pswp.viewportSize.y; 963 + 964 + if (!this.sizeChanged(width, height) && !force) { 965 + return; 966 + } 967 + 968 + this.content.setDisplayedSize(width, height); 969 + } 970 + /** 971 + * @param {number} width 972 + * @param {number} height 973 + */ 974 + 975 + 976 + sizeChanged(width, height) { 977 + if (width !== this.prevDisplayedWidth || height !== this.prevDisplayedHeight) { 978 + this.prevDisplayedWidth = width; 979 + this.prevDisplayedHeight = height; 980 + return true; 981 + } 982 + 983 + return false; 984 + } 985 + /** @returns {HTMLImageElement | HTMLDivElement | null | undefined} */ 986 + 987 + 988 + getPlaceholderElement() { 989 + var _this$content$placeho; 990 + 991 + return (_this$content$placeho = this.content.placeholder) === null || _this$content$placeho === void 0 ? void 0 : _this$content$placeho.element; 992 + } 993 + /** 994 + * Zoom current slide image to... 995 + * 996 + * @param {number} destZoomLevel Destination zoom level. 997 + * @param {Point} [centerPoint] 998 + * Transform origin center point, or false if viewport center should be used. 999 + * @param {number | false} [transitionDuration] Transition duration, may be set to 0. 1000 + * @param {boolean} [ignoreBounds] Minimum and maximum zoom levels will be ignored. 1001 + */ 1002 + 1003 + 1004 + zoomTo(destZoomLevel, centerPoint, transitionDuration, ignoreBounds) { 1005 + const { 1006 + pswp 1007 + } = this; 1008 + 1009 + if (!this.isZoomable() || pswp.mainScroll.isShifted()) { 1010 + return; 1011 + } 1012 + 1013 + pswp.dispatch('beforeZoomTo', { 1014 + destZoomLevel, 1015 + centerPoint, 1016 + transitionDuration 1017 + }); // stop all pan and zoom transitions 1018 + 1019 + pswp.animations.stopAllPan(); // if (!centerPoint) { 1020 + // centerPoint = pswp.getViewportCenterPoint(); 1021 + // } 1022 + 1023 + const prevZoomLevel = this.currZoomLevel; 1024 + 1025 + if (!ignoreBounds) { 1026 + destZoomLevel = clamp(destZoomLevel, this.zoomLevels.min, this.zoomLevels.max); 1027 + } // if (transitionDuration === undefined) { 1028 + // transitionDuration = this.pswp.options.zoomAnimationDuration; 1029 + // } 1030 + 1031 + 1032 + this.setZoomLevel(destZoomLevel); 1033 + this.pan.x = this.calculateZoomToPanOffset('x', centerPoint, prevZoomLevel); 1034 + this.pan.y = this.calculateZoomToPanOffset('y', centerPoint, prevZoomLevel); 1035 + roundPoint(this.pan); 1036 + 1037 + const finishTransition = () => { 1038 + this._setResolution(destZoomLevel); 1039 + 1040 + this.applyCurrentZoomPan(); 1041 + }; 1042 + 1043 + if (!transitionDuration) { 1044 + finishTransition(); 1045 + } else { 1046 + pswp.animations.startTransition({ 1047 + isPan: true, 1048 + name: 'zoomTo', 1049 + target: this.container, 1050 + transform: this.getCurrentTransform(), 1051 + onComplete: finishTransition, 1052 + duration: transitionDuration, 1053 + easing: pswp.options.easing 1054 + }); 1055 + } 1056 + } 1057 + /** 1058 + * @param {Point} [centerPoint] 1059 + */ 1060 + 1061 + 1062 + toggleZoom(centerPoint) { 1063 + this.zoomTo(this.currZoomLevel === this.zoomLevels.initial ? this.zoomLevels.secondary : this.zoomLevels.initial, centerPoint, this.pswp.options.zoomAnimationDuration); 1064 + } 1065 + /** 1066 + * Updates zoom level property and recalculates new pan bounds, 1067 + * unlike zoomTo it does not apply transform (use applyCurrentZoomPan) 1068 + * 1069 + * @param {number} currZoomLevel 1070 + */ 1071 + 1072 + 1073 + setZoomLevel(currZoomLevel) { 1074 + this.currZoomLevel = currZoomLevel; 1075 + this.bounds.update(this.currZoomLevel); 1076 + } 1077 + /** 1078 + * Get pan position after zoom at a given `point`. 1079 + * 1080 + * Always call setZoomLevel(newZoomLevel) beforehand to recalculate 1081 + * pan bounds according to the new zoom level. 1082 + * 1083 + * @param {'x' | 'y'} axis 1084 + * @param {Point} [point] 1085 + * point based on which zoom is performed, usually refers to the current mouse position, 1086 + * if false - viewport center will be used. 1087 + * @param {number} [prevZoomLevel] Zoom level before new zoom was applied. 1088 + * @returns {number} 1089 + */ 1090 + 1091 + 1092 + calculateZoomToPanOffset(axis, point, prevZoomLevel) { 1093 + const totalPanDistance = this.bounds.max[axis] - this.bounds.min[axis]; 1094 + 1095 + if (totalPanDistance === 0) { 1096 + return this.bounds.center[axis]; 1097 + } 1098 + 1099 + if (!point) { 1100 + point = this.pswp.getViewportCenterPoint(); 1101 + } 1102 + 1103 + if (!prevZoomLevel) { 1104 + prevZoomLevel = this.zoomLevels.initial; 1105 + } 1106 + 1107 + const zoomFactor = this.currZoomLevel / prevZoomLevel; 1108 + return this.bounds.correctPan(axis, (this.pan[axis] - point[axis]) * zoomFactor + point[axis]); 1109 + } 1110 + /** 1111 + * Apply pan and keep it within bounds. 1112 + * 1113 + * @param {number} panX 1114 + * @param {number} panY 1115 + */ 1116 + 1117 + 1118 + panTo(panX, panY) { 1119 + this.pan.x = this.bounds.correctPan('x', panX); 1120 + this.pan.y = this.bounds.correctPan('y', panY); 1121 + this.applyCurrentZoomPan(); 1122 + } 1123 + /** 1124 + * If the slide in the current state can be panned by the user 1125 + * @returns {boolean} 1126 + */ 1127 + 1128 + 1129 + isPannable() { 1130 + return Boolean(this.width) && this.currZoomLevel > this.zoomLevels.fit; 1131 + } 1132 + /** 1133 + * If the slide can be zoomed 1134 + * @returns {boolean} 1135 + */ 1136 + 1137 + 1138 + isZoomable() { 1139 + return Boolean(this.width) && this.content.isZoomable(); 1140 + } 1141 + /** 1142 + * Apply transform and scale based on 1143 + * the current pan position (this.pan) and zoom level (this.currZoomLevel) 1144 + */ 1145 + 1146 + 1147 + applyCurrentZoomPan() { 1148 + this._applyZoomTransform(this.pan.x, this.pan.y, this.currZoomLevel); 1149 + 1150 + if (this === this.pswp.currSlide) { 1151 + this.pswp.dispatch('zoomPanUpdate', { 1152 + slide: this 1153 + }); 1154 + } 1155 + } 1156 + 1157 + zoomAndPanToInitial() { 1158 + this.currZoomLevel = this.zoomLevels.initial; // pan according to the zoom level 1159 + 1160 + this.bounds.update(this.currZoomLevel); 1161 + equalizePoints(this.pan, this.bounds.center); 1162 + this.pswp.dispatch('initialZoomPan', { 1163 + slide: this 1164 + }); 1165 + } 1166 + /** 1167 + * Set translate and scale based on current resolution 1168 + * 1169 + * @param {number} x 1170 + * @param {number} y 1171 + * @param {number} zoom 1172 + * @private 1173 + */ 1174 + 1175 + 1176 + _applyZoomTransform(x, y, zoom) { 1177 + zoom /= this.currentResolution || this.zoomLevels.initial; 1178 + setTransform(this.container, x, y, zoom); 1179 + } 1180 + 1181 + calculateSize() { 1182 + const { 1183 + pswp 1184 + } = this; 1185 + equalizePoints(this.panAreaSize, getPanAreaSize(pswp.options, pswp.viewportSize, this.data, this.index)); 1186 + this.zoomLevels.update(this.width, this.height, this.panAreaSize); 1187 + pswp.dispatch('calcSlideSize', { 1188 + slide: this 1189 + }); 1190 + } 1191 + /** @returns {string} */ 1192 + 1193 + 1194 + getCurrentTransform() { 1195 + const scale = this.currZoomLevel / (this.currentResolution || this.zoomLevels.initial); 1196 + return toTransformString(this.pan.x, this.pan.y, scale); 1197 + } 1198 + /** 1199 + * Set resolution and re-render the image. 1200 + * 1201 + * For example, if the real image size is 2000x1500, 1202 + * and resolution is 0.5 - it will be rendered as 1000x750. 1203 + * 1204 + * Image with zoom level 2 and resolution 0.5 is 1205 + * the same as image with zoom level 1 and resolution 1. 1206 + * 1207 + * Used to optimize animations and make 1208 + * sure that browser renders image in the highest quality. 1209 + * Also used by responsive images to load the correct one. 1210 + * 1211 + * @param {number} newResolution 1212 + */ 1213 + 1214 + 1215 + _setResolution(newResolution) { 1216 + if (newResolution === this.currentResolution) { 1217 + return; 1218 + } 1219 + 1220 + this.currentResolution = newResolution; 1221 + this.updateContentSize(); 1222 + this.pswp.dispatch('resolutionChanged'); 1223 + } 1224 + 1225 + } 1226 + 1227 + /** @typedef {import('../photoswipe.js').Point} Point */ 1228 + 1229 + /** @typedef {import('./gestures.js').default} Gestures */ 1230 + 1231 + const PAN_END_FRICTION = 0.35; 1232 + const VERTICAL_DRAG_FRICTION = 0.6; // 1 corresponds to the third of viewport height 1233 + 1234 + const MIN_RATIO_TO_CLOSE = 0.4; // Minimum speed required to navigate 1235 + // to next or previous slide 1236 + 1237 + const MIN_NEXT_SLIDE_SPEED = 0.5; 1238 + /** 1239 + * @param {number} initialVelocity 1240 + * @param {number} decelerationRate 1241 + * @returns {number} 1242 + */ 1243 + 1244 + function project(initialVelocity, decelerationRate) { 1245 + return initialVelocity * decelerationRate / (1 - decelerationRate); 1246 + } 1247 + /** 1248 + * Handles single pointer dragging 1249 + */ 1250 + 1251 + 1252 + class DragHandler { 1253 + /** 1254 + * @param {Gestures} gestures 1255 + */ 1256 + constructor(gestures) { 1257 + this.gestures = gestures; 1258 + this.pswp = gestures.pswp; 1259 + /** @type {Point} */ 1260 + 1261 + this.startPan = { 1262 + x: 0, 1263 + y: 0 1264 + }; 1265 + } 1266 + 1267 + start() { 1268 + if (this.pswp.currSlide) { 1269 + equalizePoints(this.startPan, this.pswp.currSlide.pan); 1270 + } 1271 + 1272 + this.pswp.animations.stopAll(); 1273 + } 1274 + 1275 + change() { 1276 + const { 1277 + p1, 1278 + prevP1, 1279 + dragAxis 1280 + } = this.gestures; 1281 + const { 1282 + currSlide 1283 + } = this.pswp; 1284 + 1285 + if (dragAxis === 'y' && this.pswp.options.closeOnVerticalDrag && currSlide && currSlide.currZoomLevel <= currSlide.zoomLevels.fit && !this.gestures.isMultitouch) { 1286 + // Handle vertical drag to close 1287 + const panY = currSlide.pan.y + (p1.y - prevP1.y); 1288 + 1289 + if (!this.pswp.dispatch('verticalDrag', { 1290 + panY 1291 + }).defaultPrevented) { 1292 + this._setPanWithFriction('y', panY, VERTICAL_DRAG_FRICTION); 1293 + 1294 + const bgOpacity = 1 - Math.abs(this._getVerticalDragRatio(currSlide.pan.y)); 1295 + this.pswp.applyBgOpacity(bgOpacity); 1296 + currSlide.applyCurrentZoomPan(); 1297 + } 1298 + } else { 1299 + const mainScrollChanged = this._panOrMoveMainScroll('x'); 1300 + 1301 + if (!mainScrollChanged) { 1302 + this._panOrMoveMainScroll('y'); 1303 + 1304 + if (currSlide) { 1305 + roundPoint(currSlide.pan); 1306 + currSlide.applyCurrentZoomPan(); 1307 + } 1308 + } 1309 + } 1310 + } 1311 + 1312 + end() { 1313 + const { 1314 + velocity 1315 + } = this.gestures; 1316 + const { 1317 + mainScroll, 1318 + currSlide 1319 + } = this.pswp; 1320 + let indexDiff = 0; 1321 + this.pswp.animations.stopAll(); // Handle main scroll if it's shifted 1322 + 1323 + if (mainScroll.isShifted()) { 1324 + // Position of the main scroll relative to the viewport 1325 + const mainScrollShiftDiff = mainScroll.x - mainScroll.getCurrSlideX(); // Ratio between 0 and 1: 1326 + // 0 - slide is not visible at all, 1327 + // 0.5 - half of the slide is visible 1328 + // 1 - slide is fully visible 1329 + 1330 + const currentSlideVisibilityRatio = mainScrollShiftDiff / this.pswp.viewportSize.x; // Go next slide. 1331 + // 1332 + // - if velocity and its direction is matched, 1333 + // and we see at least tiny part of the next slide 1334 + // 1335 + // - or if we see less than 50% of the current slide 1336 + // and velocity is close to 0 1337 + // 1338 + 1339 + if (velocity.x < -MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio < 0 || velocity.x < 0.1 && currentSlideVisibilityRatio < -0.5) { 1340 + // Go to next slide 1341 + indexDiff = 1; 1342 + velocity.x = Math.min(velocity.x, 0); 1343 + } else if (velocity.x > MIN_NEXT_SLIDE_SPEED && currentSlideVisibilityRatio > 0 || velocity.x > -0.1 && currentSlideVisibilityRatio > 0.5) { 1344 + // Go to prev slide 1345 + indexDiff = -1; 1346 + velocity.x = Math.max(velocity.x, 0); 1347 + } 1348 + 1349 + mainScroll.moveIndexBy(indexDiff, true, velocity.x); 1350 + } // Restore zoom level 1351 + 1352 + 1353 + if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.max || this.gestures.isMultitouch) { 1354 + this.gestures.zoomLevels.correctZoomPan(true); 1355 + } else { 1356 + // we run two animations instead of one, 1357 + // as each axis has own pan boundaries and thus different spring function 1358 + // (correctZoomPan does not have this functionality, 1359 + // it animates all properties with single timing function) 1360 + this._finishPanGestureForAxis('x'); 1361 + 1362 + this._finishPanGestureForAxis('y'); 1363 + } 1364 + } 1365 + /** 1366 + * @private 1367 + * @param {'x' | 'y'} axis 1368 + */ 1369 + 1370 + 1371 + _finishPanGestureForAxis(axis) { 1372 + const { 1373 + velocity 1374 + } = this.gestures; 1375 + const { 1376 + currSlide 1377 + } = this.pswp; 1378 + 1379 + if (!currSlide) { 1380 + return; 1381 + } 1382 + 1383 + const { 1384 + pan, 1385 + bounds 1386 + } = currSlide; 1387 + const panPos = pan[axis]; 1388 + const restoreBgOpacity = this.pswp.bgOpacity < 1 && axis === 'y'; // 0.995 means - scroll view loses 0.5% of its velocity per millisecond 1389 + // Increasing this number will reduce travel distance 1390 + 1391 + const decelerationRate = 0.995; // 0.99 1392 + // Pan position if there is no bounds 1393 + 1394 + const projectedPosition = panPos + project(velocity[axis], decelerationRate); 1395 + 1396 + if (restoreBgOpacity) { 1397 + const vDragRatio = this._getVerticalDragRatio(panPos); 1398 + 1399 + const projectedVDragRatio = this._getVerticalDragRatio(projectedPosition); // If we are above and moving upwards, 1400 + // or if we are below and moving downwards 1401 + 1402 + 1403 + if (vDragRatio < 0 && projectedVDragRatio < -MIN_RATIO_TO_CLOSE || vDragRatio > 0 && projectedVDragRatio > MIN_RATIO_TO_CLOSE) { 1404 + this.pswp.close(); 1405 + return; 1406 + } 1407 + } // Pan position with corrected bounds 1408 + 1409 + 1410 + const correctedPanPosition = bounds.correctPan(axis, projectedPosition); // Exit if pan position should not be changed 1411 + // or if speed it too low 1412 + 1413 + if (panPos === correctedPanPosition) { 1414 + return; 1415 + } // Overshoot if the final position is out of pan bounds 1416 + 1417 + 1418 + const dampingRatio = correctedPanPosition === projectedPosition ? 1 : 0.82; 1419 + const initialBgOpacity = this.pswp.bgOpacity; 1420 + const totalPanDist = correctedPanPosition - panPos; 1421 + this.pswp.animations.startSpring({ 1422 + name: 'panGesture' + axis, 1423 + isPan: true, 1424 + start: panPos, 1425 + end: correctedPanPosition, 1426 + velocity: velocity[axis], 1427 + dampingRatio, 1428 + onUpdate: pos => { 1429 + // Animate opacity of background relative to Y pan position of an image 1430 + if (restoreBgOpacity && this.pswp.bgOpacity < 1) { 1431 + // 0 - start of animation, 1 - end of animation 1432 + const animationProgressRatio = 1 - (correctedPanPosition - pos) / totalPanDist; // We clamp opacity to keep it between 0 and 1. 1433 + // As progress ratio can be larger than 1 due to overshoot, 1434 + // and we do not want to bounce opacity. 1435 + 1436 + this.pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * animationProgressRatio, 0, 1)); 1437 + } 1438 + 1439 + pan[axis] = Math.floor(pos); 1440 + currSlide.applyCurrentZoomPan(); 1441 + } 1442 + }); 1443 + } 1444 + /** 1445 + * Update position of the main scroll, 1446 + * or/and update pan position of the current slide. 1447 + * 1448 + * Should return true if it changes (or can change) main scroll. 1449 + * 1450 + * @private 1451 + * @param {'x' | 'y'} axis 1452 + * @returns {boolean} 1453 + */ 1454 + 1455 + 1456 + _panOrMoveMainScroll(axis) { 1457 + const { 1458 + p1, 1459 + dragAxis, 1460 + prevP1, 1461 + isMultitouch 1462 + } = this.gestures; 1463 + const { 1464 + currSlide, 1465 + mainScroll 1466 + } = this.pswp; 1467 + const delta = p1[axis] - prevP1[axis]; 1468 + const newMainScrollX = mainScroll.x + delta; 1469 + 1470 + if (!delta || !currSlide) { 1471 + return false; 1472 + } // Always move main scroll if image can not be panned 1473 + 1474 + 1475 + if (axis === 'x' && !currSlide.isPannable() && !isMultitouch) { 1476 + mainScroll.moveTo(newMainScrollX, true); 1477 + return true; // changed main scroll 1478 + } 1479 + 1480 + const { 1481 + bounds 1482 + } = currSlide; 1483 + const newPan = currSlide.pan[axis] + delta; 1484 + 1485 + if (this.pswp.options.allowPanToNext && dragAxis === 'x' && axis === 'x' && !isMultitouch) { 1486 + const currSlideMainScrollX = mainScroll.getCurrSlideX(); // Position of the main scroll relative to the viewport 1487 + 1488 + const mainScrollShiftDiff = mainScroll.x - currSlideMainScrollX; 1489 + const isLeftToRight = delta > 0; 1490 + const isRightToLeft = !isLeftToRight; 1491 + 1492 + if (newPan > bounds.min[axis] && isLeftToRight) { 1493 + // Panning from left to right, beyond the left edge 1494 + // Wether the image was at minimum pan position (or less) 1495 + // when this drag gesture started. 1496 + // Minimum pan position refers to the left edge of the image. 1497 + const wasAtMinPanPosition = bounds.min[axis] <= this.startPan[axis]; 1498 + 1499 + if (wasAtMinPanPosition) { 1500 + mainScroll.moveTo(newMainScrollX, true); 1501 + return true; 1502 + } else { 1503 + this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan; 1504 + 1505 + } 1506 + } else if (newPan < bounds.max[axis] && isRightToLeft) { 1507 + // Paning from right to left, beyond the right edge 1508 + // Maximum pan position refers to the right edge of the image. 1509 + const wasAtMaxPanPosition = this.startPan[axis] <= bounds.max[axis]; 1510 + 1511 + if (wasAtMaxPanPosition) { 1512 + mainScroll.moveTo(newMainScrollX, true); 1513 + return true; 1514 + } else { 1515 + this._setPanWithFriction(axis, newPan); //currSlide.pan[axis] = newPan; 1516 + 1517 + } 1518 + } else { 1519 + // If main scroll is shifted 1520 + if (mainScrollShiftDiff !== 0) { 1521 + // If main scroll is shifted right 1522 + if (mainScrollShiftDiff > 0 1523 + /*&& isRightToLeft*/ 1524 + ) { 1525 + mainScroll.moveTo(Math.max(newMainScrollX, currSlideMainScrollX), true); 1526 + return true; 1527 + } else if (mainScrollShiftDiff < 0 1528 + /*&& isLeftToRight*/ 1529 + ) { 1530 + // Main scroll is shifted left (Position is less than 0 comparing to the viewport 0) 1531 + mainScroll.moveTo(Math.min(newMainScrollX, currSlideMainScrollX), true); 1532 + return true; 1533 + } 1534 + } else { 1535 + // We are within pan bounds, so just pan 1536 + this._setPanWithFriction(axis, newPan); 1537 + } 1538 + } 1539 + } else { 1540 + if (axis === 'y') { 1541 + // Do not pan vertically if main scroll is shifted o 1542 + if (!mainScroll.isShifted() && bounds.min.y !== bounds.max.y) { 1543 + this._setPanWithFriction(axis, newPan); 1544 + } 1545 + } else { 1546 + this._setPanWithFriction(axis, newPan); 1547 + } 1548 + } 1549 + 1550 + return false; 1551 + } // If we move above - the ratio is negative 1552 + // If we move below the ratio is positive 1553 + 1554 + /** 1555 + * Relation between pan Y position and third of viewport height. 1556 + * 1557 + * When we are at initial position (center bounds) - the ratio is 0, 1558 + * if position is shifted upwards - the ratio is negative, 1559 + * if position is shifted downwards - the ratio is positive. 1560 + * 1561 + * @private 1562 + * @param {number} panY The current pan Y position. 1563 + * @returns {number} 1564 + */ 1565 + 1566 + 1567 + _getVerticalDragRatio(panY) { 1568 + var _this$pswp$currSlide$, _this$pswp$currSlide; 1569 + 1570 + return (panY - ((_this$pswp$currSlide$ = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.bounds.center.y) !== null && _this$pswp$currSlide$ !== void 0 ? _this$pswp$currSlide$ : 0)) / (this.pswp.viewportSize.y / 3); 1571 + } 1572 + /** 1573 + * Set pan position of the current slide. 1574 + * Apply friction if the position is beyond the pan bounds, 1575 + * or if custom friction is defined. 1576 + * 1577 + * @private 1578 + * @param {'x' | 'y'} axis 1579 + * @param {number} potentialPan 1580 + * @param {number} [customFriction] (0.1 - 1) 1581 + */ 1582 + 1583 + 1584 + _setPanWithFriction(axis, potentialPan, customFriction) { 1585 + const { 1586 + currSlide 1587 + } = this.pswp; 1588 + 1589 + if (!currSlide) { 1590 + return; 1591 + } 1592 + 1593 + const { 1594 + pan, 1595 + bounds 1596 + } = currSlide; 1597 + const correctedPan = bounds.correctPan(axis, potentialPan); // If we are out of pan bounds 1598 + 1599 + if (correctedPan !== potentialPan || customFriction) { 1600 + const delta = Math.round(potentialPan - pan[axis]); 1601 + pan[axis] += delta * (customFriction || PAN_END_FRICTION); 1602 + } else { 1603 + pan[axis] = potentialPan; 1604 + } 1605 + } 1606 + 1607 + } 1608 + 1609 + /** @typedef {import('../photoswipe.js').Point} Point */ 1610 + 1611 + /** @typedef {import('./gestures.js').default} Gestures */ 1612 + 1613 + const UPPER_ZOOM_FRICTION = 0.05; 1614 + const LOWER_ZOOM_FRICTION = 0.15; 1615 + /** 1616 + * Get center point between two points 1617 + * 1618 + * @param {Point} p 1619 + * @param {Point} p1 1620 + * @param {Point} p2 1621 + * @returns {Point} 1622 + */ 1623 + 1624 + function getZoomPointsCenter(p, p1, p2) { 1625 + p.x = (p1.x + p2.x) / 2; 1626 + p.y = (p1.y + p2.y) / 2; 1627 + return p; 1628 + } 1629 + 1630 + class ZoomHandler { 1631 + /** 1632 + * @param {Gestures} gestures 1633 + */ 1634 + constructor(gestures) { 1635 + this.gestures = gestures; 1636 + /** 1637 + * @private 1638 + * @type {Point} 1639 + */ 1640 + 1641 + this._startPan = { 1642 + x: 0, 1643 + y: 0 1644 + }; 1645 + /** 1646 + * @private 1647 + * @type {Point} 1648 + */ 1649 + 1650 + this._startZoomPoint = { 1651 + x: 0, 1652 + y: 0 1653 + }; 1654 + /** 1655 + * @private 1656 + * @type {Point} 1657 + */ 1658 + 1659 + this._zoomPoint = { 1660 + x: 0, 1661 + y: 0 1662 + }; 1663 + /** @private */ 1664 + 1665 + this._wasOverFitZoomLevel = false; 1666 + /** @private */ 1667 + 1668 + this._startZoomLevel = 1; 1669 + } 1670 + 1671 + start() { 1672 + const { 1673 + currSlide 1674 + } = this.gestures.pswp; 1675 + 1676 + if (currSlide) { 1677 + this._startZoomLevel = currSlide.currZoomLevel; 1678 + equalizePoints(this._startPan, currSlide.pan); 1679 + } 1680 + 1681 + this.gestures.pswp.animations.stopAllPan(); 1682 + this._wasOverFitZoomLevel = false; 1683 + } 1684 + 1685 + change() { 1686 + const { 1687 + p1, 1688 + startP1, 1689 + p2, 1690 + startP2, 1691 + pswp 1692 + } = this.gestures; 1693 + const { 1694 + currSlide 1695 + } = pswp; 1696 + 1697 + if (!currSlide) { 1698 + return; 1699 + } 1700 + 1701 + const minZoomLevel = currSlide.zoomLevels.min; 1702 + const maxZoomLevel = currSlide.zoomLevels.max; 1703 + 1704 + if (!currSlide.isZoomable() || pswp.mainScroll.isShifted()) { 1705 + return; 1706 + } 1707 + 1708 + getZoomPointsCenter(this._startZoomPoint, startP1, startP2); 1709 + getZoomPointsCenter(this._zoomPoint, p1, p2); 1710 + 1711 + let currZoomLevel = 1 / getDistanceBetween(startP1, startP2) * getDistanceBetween(p1, p2) * this._startZoomLevel; // slightly over the zoom.fit 1712 + 1713 + 1714 + if (currZoomLevel > currSlide.zoomLevels.initial + currSlide.zoomLevels.initial / 15) { 1715 + this._wasOverFitZoomLevel = true; 1716 + } 1717 + 1718 + if (currZoomLevel < minZoomLevel) { 1719 + if (pswp.options.pinchToClose && !this._wasOverFitZoomLevel && this._startZoomLevel <= currSlide.zoomLevels.initial) { 1720 + // fade out background if zooming out 1721 + const bgOpacity = 1 - (minZoomLevel - currZoomLevel) / (minZoomLevel / 1.2); 1722 + 1723 + if (!pswp.dispatch('pinchClose', { 1724 + bgOpacity 1725 + }).defaultPrevented) { 1726 + pswp.applyBgOpacity(bgOpacity); 1727 + } 1728 + } else { 1729 + // Apply the friction if zoom level is below the min 1730 + currZoomLevel = minZoomLevel - (minZoomLevel - currZoomLevel) * LOWER_ZOOM_FRICTION; 1731 + } 1732 + } else if (currZoomLevel > maxZoomLevel) { 1733 + // Apply the friction if zoom level is above the max 1734 + currZoomLevel = maxZoomLevel + (currZoomLevel - maxZoomLevel) * UPPER_ZOOM_FRICTION; 1735 + } 1736 + 1737 + currSlide.pan.x = this._calculatePanForZoomLevel('x', currZoomLevel); 1738 + currSlide.pan.y = this._calculatePanForZoomLevel('y', currZoomLevel); 1739 + currSlide.setZoomLevel(currZoomLevel); 1740 + currSlide.applyCurrentZoomPan(); 1741 + } 1742 + 1743 + end() { 1744 + const { 1745 + pswp 1746 + } = this.gestures; 1747 + const { 1748 + currSlide 1749 + } = pswp; 1750 + 1751 + if ((!currSlide || currSlide.currZoomLevel < currSlide.zoomLevels.initial) && !this._wasOverFitZoomLevel && pswp.options.pinchToClose) { 1752 + pswp.close(); 1753 + } else { 1754 + this.correctZoomPan(); 1755 + } 1756 + } 1757 + /** 1758 + * @private 1759 + * @param {'x' | 'y'} axis 1760 + * @param {number} currZoomLevel 1761 + * @returns {number} 1762 + */ 1763 + 1764 + 1765 + _calculatePanForZoomLevel(axis, currZoomLevel) { 1766 + const zoomFactor = currZoomLevel / this._startZoomLevel; 1767 + return this._zoomPoint[axis] - (this._startZoomPoint[axis] - this._startPan[axis]) * zoomFactor; 1768 + } 1769 + /** 1770 + * Correct currZoomLevel and pan if they are 1771 + * beyond minimum or maximum values. 1772 + * With animation. 1773 + * 1774 + * @param {boolean} [ignoreGesture] 1775 + * Wether gesture coordinates should be ignored when calculating destination pan position. 1776 + */ 1777 + 1778 + 1779 + correctZoomPan(ignoreGesture) { 1780 + const { 1781 + pswp 1782 + } = this.gestures; 1783 + const { 1784 + currSlide 1785 + } = pswp; 1786 + 1787 + if (!(currSlide !== null && currSlide !== void 0 && currSlide.isZoomable())) { 1788 + return; 1789 + } 1790 + 1791 + if (this._zoomPoint.x === 0) { 1792 + ignoreGesture = true; 1793 + } 1794 + 1795 + const prevZoomLevel = currSlide.currZoomLevel; 1796 + /** @type {number} */ 1797 + 1798 + let destinationZoomLevel; 1799 + let currZoomLevelNeedsChange = true; 1800 + 1801 + if (prevZoomLevel < currSlide.zoomLevels.initial) { 1802 + destinationZoomLevel = currSlide.zoomLevels.initial; // zoom to min 1803 + } else if (prevZoomLevel > currSlide.zoomLevels.max) { 1804 + destinationZoomLevel = currSlide.zoomLevels.max; // zoom to max 1805 + } else { 1806 + currZoomLevelNeedsChange = false; 1807 + destinationZoomLevel = prevZoomLevel; 1808 + } 1809 + 1810 + const initialBgOpacity = pswp.bgOpacity; 1811 + const restoreBgOpacity = pswp.bgOpacity < 1; 1812 + const initialPan = equalizePoints({ 1813 + x: 0, 1814 + y: 0 1815 + }, currSlide.pan); 1816 + let destinationPan = equalizePoints({ 1817 + x: 0, 1818 + y: 0 1819 + }, initialPan); 1820 + 1821 + if (ignoreGesture) { 1822 + this._zoomPoint.x = 0; 1823 + this._zoomPoint.y = 0; 1824 + this._startZoomPoint.x = 0; 1825 + this._startZoomPoint.y = 0; 1826 + this._startZoomLevel = prevZoomLevel; 1827 + equalizePoints(this._startPan, initialPan); 1828 + } 1829 + 1830 + if (currZoomLevelNeedsChange) { 1831 + destinationPan = { 1832 + x: this._calculatePanForZoomLevel('x', destinationZoomLevel), 1833 + y: this._calculatePanForZoomLevel('y', destinationZoomLevel) 1834 + }; 1835 + } // set zoom level, so pan bounds are updated according to it 1836 + 1837 + 1838 + currSlide.setZoomLevel(destinationZoomLevel); 1839 + destinationPan = { 1840 + x: currSlide.bounds.correctPan('x', destinationPan.x), 1841 + y: currSlide.bounds.correctPan('y', destinationPan.y) 1842 + }; // return zoom level and its bounds to initial 1843 + 1844 + currSlide.setZoomLevel(prevZoomLevel); 1845 + const panNeedsChange = !pointsEqual(destinationPan, initialPan); 1846 + 1847 + if (!panNeedsChange && !currZoomLevelNeedsChange && !restoreBgOpacity) { 1848 + // update resolution after gesture 1849 + currSlide._setResolution(destinationZoomLevel); 1850 + 1851 + currSlide.applyCurrentZoomPan(); // nothing to animate 1852 + 1853 + return; 1854 + } 1855 + 1856 + pswp.animations.stopAllPan(); 1857 + pswp.animations.startSpring({ 1858 + isPan: true, 1859 + start: 0, 1860 + end: 1000, 1861 + velocity: 0, 1862 + dampingRatio: 1, 1863 + naturalFrequency: 40, 1864 + onUpdate: now => { 1865 + now /= 1000; // 0 - start, 1 - end 1866 + 1867 + if (panNeedsChange || currZoomLevelNeedsChange) { 1868 + if (panNeedsChange) { 1869 + currSlide.pan.x = initialPan.x + (destinationPan.x - initialPan.x) * now; 1870 + currSlide.pan.y = initialPan.y + (destinationPan.y - initialPan.y) * now; 1871 + } 1872 + 1873 + if (currZoomLevelNeedsChange) { 1874 + const newZoomLevel = prevZoomLevel + (destinationZoomLevel - prevZoomLevel) * now; 1875 + currSlide.setZoomLevel(newZoomLevel); 1876 + } 1877 + 1878 + currSlide.applyCurrentZoomPan(); 1879 + } // Restore background opacity 1880 + 1881 + 1882 + if (restoreBgOpacity && pswp.bgOpacity < 1) { 1883 + // We clamp opacity to keep it between 0 and 1. 1884 + // As progress ratio can be larger than 1 due to overshoot, 1885 + // and we do not want to bounce opacity. 1886 + pswp.applyBgOpacity(clamp(initialBgOpacity + (1 - initialBgOpacity) * now, 0, 1)); 1887 + } 1888 + }, 1889 + onComplete: () => { 1890 + // update resolution after transition ends 1891 + currSlide._setResolution(destinationZoomLevel); 1892 + 1893 + currSlide.applyCurrentZoomPan(); 1894 + } 1895 + }); 1896 + } 1897 + 1898 + } 1899 + 1900 + /** 1901 + * @template {string} T 1902 + * @template {string} P 1903 + * @typedef {import('../types.js').AddPostfix<T, P>} AddPostfix<T, P> 1904 + */ 1905 + 1906 + /** @typedef {import('./gestures.js').default} Gestures */ 1907 + 1908 + /** @typedef {import('../photoswipe.js').Point} Point */ 1909 + 1910 + /** @typedef {'imageClick' | 'bgClick' | 'tap' | 'doubleTap'} Actions */ 1911 + 1912 + /** 1913 + * Whether the tap was performed on the main slide 1914 + * (rather than controls or caption). 1915 + * 1916 + * @param {PointerEvent} event 1917 + * @returns {boolean} 1918 + */ 1919 + function didTapOnMainContent(event) { 1920 + return !! 1921 + /** @type {HTMLElement} */ 1922 + event.target.closest('.pswp__container'); 1923 + } 1924 + /** 1925 + * Tap, double-tap handler. 1926 + */ 1927 + 1928 + 1929 + class TapHandler { 1930 + /** 1931 + * @param {Gestures} gestures 1932 + */ 1933 + constructor(gestures) { 1934 + this.gestures = gestures; 1935 + } 1936 + /** 1937 + * @param {Point} point 1938 + * @param {PointerEvent} originalEvent 1939 + */ 1940 + 1941 + 1942 + click(point, originalEvent) { 1943 + const targetClassList = 1944 + /** @type {HTMLElement} */ 1945 + originalEvent.target.classList; 1946 + const isImageClick = targetClassList.contains('pswp__img'); 1947 + const isBackgroundClick = targetClassList.contains('pswp__item') || targetClassList.contains('pswp__zoom-wrap'); 1948 + 1949 + if (isImageClick) { 1950 + this._doClickOrTapAction('imageClick', point, originalEvent); 1951 + } else if (isBackgroundClick) { 1952 + this._doClickOrTapAction('bgClick', point, originalEvent); 1953 + } 1954 + } 1955 + /** 1956 + * @param {Point} point 1957 + * @param {PointerEvent} originalEvent 1958 + */ 1959 + 1960 + 1961 + tap(point, originalEvent) { 1962 + if (didTapOnMainContent(originalEvent)) { 1963 + this._doClickOrTapAction('tap', point, originalEvent); 1964 + } 1965 + } 1966 + /** 1967 + * @param {Point} point 1968 + * @param {PointerEvent} originalEvent 1969 + */ 1970 + 1971 + 1972 + doubleTap(point, originalEvent) { 1973 + if (didTapOnMainContent(originalEvent)) { 1974 + this._doClickOrTapAction('doubleTap', point, originalEvent); 1975 + } 1976 + } 1977 + /** 1978 + * @private 1979 + * @param {Actions} actionName 1980 + * @param {Point} point 1981 + * @param {PointerEvent} originalEvent 1982 + */ 1983 + 1984 + 1985 + _doClickOrTapAction(actionName, point, originalEvent) { 1986 + var _this$gestures$pswp$e; 1987 + 1988 + const { 1989 + pswp 1990 + } = this.gestures; 1991 + const { 1992 + currSlide 1993 + } = pswp; 1994 + const actionFullName = 1995 + /** @type {AddPostfix<Actions, 'Action'>} */ 1996 + actionName + 'Action'; 1997 + const optionValue = pswp.options[actionFullName]; 1998 + 1999 + if (pswp.dispatch(actionFullName, { 2000 + point, 2001 + originalEvent 2002 + }).defaultPrevented) { 2003 + return; 2004 + } 2005 + 2006 + if (typeof optionValue === 'function') { 2007 + optionValue.call(pswp, point, originalEvent); 2008 + return; 2009 + } 2010 + 2011 + switch (optionValue) { 2012 + case 'close': 2013 + case 'next': 2014 + pswp[optionValue](); 2015 + break; 2016 + 2017 + case 'zoom': 2018 + currSlide === null || currSlide === void 0 || currSlide.toggleZoom(point); 2019 + break; 2020 + 2021 + case 'zoom-or-close': 2022 + // by default click zooms current image, 2023 + // if it can not be zoomed - gallery will be closed 2024 + if (currSlide !== null && currSlide !== void 0 && currSlide.isZoomable() && currSlide.zoomLevels.secondary !== currSlide.zoomLevels.initial) { 2025 + currSlide.toggleZoom(point); 2026 + } else if (pswp.options.clickToCloseNonZoomable) { 2027 + pswp.close(); 2028 + } 2029 + 2030 + break; 2031 + 2032 + case 'toggle-controls': 2033 + (_this$gestures$pswp$e = this.gestures.pswp.element) === null || _this$gestures$pswp$e === void 0 || _this$gestures$pswp$e.classList.toggle('pswp--ui-visible'); // if (_controlsVisible) { 2034 + // _ui.hideControls(); 2035 + // } else { 2036 + // _ui.showControls(); 2037 + // } 2038 + 2039 + break; 2040 + } 2041 + } 2042 + 2043 + } 2044 + 2045 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 2046 + 2047 + /** @typedef {import('../photoswipe.js').Point} Point */ 2048 + // How far should user should drag 2049 + // until we can determine that the gesture is swipe and its direction 2050 + 2051 + const AXIS_SWIPE_HYSTERISIS = 10; //const PAN_END_FRICTION = 0.35; 2052 + 2053 + const DOUBLE_TAP_DELAY = 300; // ms 2054 + 2055 + const MIN_TAP_DISTANCE = 25; // px 2056 + 2057 + /** 2058 + * Gestures class bind touch, pointer or mouse events 2059 + * and emits drag to drag-handler and zoom events zoom-handler. 2060 + * 2061 + * Drag and zoom events are emited in requestAnimationFrame, 2062 + * and only when one of pointers was actually changed. 2063 + */ 2064 + 2065 + class Gestures { 2066 + /** 2067 + * @param {PhotoSwipe} pswp 2068 + */ 2069 + constructor(pswp) { 2070 + this.pswp = pswp; 2071 + /** @type {'x' | 'y' | null} */ 2072 + 2073 + this.dragAxis = null; // point objects are defined once and reused 2074 + // PhotoSwipe keeps track only of two pointers, others are ignored 2075 + 2076 + /** @type {Point} */ 2077 + 2078 + this.p1 = { 2079 + x: 0, 2080 + y: 0 2081 + }; // the first pressed pointer 2082 + 2083 + /** @type {Point} */ 2084 + 2085 + this.p2 = { 2086 + x: 0, 2087 + y: 0 2088 + }; // the second pressed pointer 2089 + 2090 + /** @type {Point} */ 2091 + 2092 + this.prevP1 = { 2093 + x: 0, 2094 + y: 0 2095 + }; 2096 + /** @type {Point} */ 2097 + 2098 + this.prevP2 = { 2099 + x: 0, 2100 + y: 0 2101 + }; 2102 + /** @type {Point} */ 2103 + 2104 + this.startP1 = { 2105 + x: 0, 2106 + y: 0 2107 + }; 2108 + /** @type {Point} */ 2109 + 2110 + this.startP2 = { 2111 + x: 0, 2112 + y: 0 2113 + }; 2114 + /** @type {Point} */ 2115 + 2116 + this.velocity = { 2117 + x: 0, 2118 + y: 0 2119 + }; 2120 + /** @type {Point} 2121 + * @private 2122 + */ 2123 + 2124 + this._lastStartP1 = { 2125 + x: 0, 2126 + y: 0 2127 + }; 2128 + /** @type {Point} 2129 + * @private 2130 + */ 2131 + 2132 + this._intervalP1 = { 2133 + x: 0, 2134 + y: 0 2135 + }; 2136 + /** @private */ 2137 + 2138 + this._numActivePoints = 0; 2139 + /** @type {Point[]} 2140 + * @private 2141 + */ 2142 + 2143 + this._ongoingPointers = []; 2144 + /** @private */ 2145 + 2146 + this._touchEventEnabled = 'ontouchstart' in window; 2147 + /** @private */ 2148 + 2149 + this._pointerEventEnabled = !!window.PointerEvent; 2150 + this.supportsTouch = this._touchEventEnabled || this._pointerEventEnabled && navigator.maxTouchPoints > 1; 2151 + /** @private */ 2152 + 2153 + this._numActivePoints = 0; 2154 + /** @private */ 2155 + 2156 + this._intervalTime = 0; 2157 + /** @private */ 2158 + 2159 + this._velocityCalculated = false; 2160 + this.isMultitouch = false; 2161 + this.isDragging = false; 2162 + this.isZooming = false; 2163 + /** @type {number | null} */ 2164 + 2165 + this.raf = null; 2166 + /** @type {NodeJS.Timeout | null} 2167 + * @private 2168 + */ 2169 + 2170 + this._tapTimer = null; 2171 + 2172 + if (!this.supportsTouch) { 2173 + // disable pan to next slide for non-touch devices 2174 + pswp.options.allowPanToNext = false; 2175 + } 2176 + 2177 + this.drag = new DragHandler(this); 2178 + this.zoomLevels = new ZoomHandler(this); 2179 + this.tapHandler = new TapHandler(this); 2180 + pswp.on('bindEvents', () => { 2181 + pswp.events.add(pswp.scrollWrap, 'click', 2182 + /** @type EventListener */ 2183 + this._onClick.bind(this)); 2184 + 2185 + if (this._pointerEventEnabled) { 2186 + this._bindEvents('pointer', 'down', 'up', 'cancel'); 2187 + } else if (this._touchEventEnabled) { 2188 + this._bindEvents('touch', 'start', 'end', 'cancel'); // In previous versions we also bound mouse event here, 2189 + // in case device supports both touch and mouse events, 2190 + // but newer versions of browsers now support PointerEvent. 2191 + // on iOS10 if you bind touchmove/end after touchstart, 2192 + // and you don't preventDefault touchstart (which PhotoSwipe does), 2193 + // preventDefault will have no effect on touchmove and touchend. 2194 + // Unless you bind it previously. 2195 + 2196 + 2197 + if (pswp.scrollWrap) { 2198 + pswp.scrollWrap.ontouchmove = () => {}; 2199 + 2200 + pswp.scrollWrap.ontouchend = () => {}; 2201 + } 2202 + } else { 2203 + this._bindEvents('mouse', 'down', 'up'); 2204 + } 2205 + }); 2206 + } 2207 + /** 2208 + * @private 2209 + * @param {'mouse' | 'touch' | 'pointer'} pref 2210 + * @param {'down' | 'start'} down 2211 + * @param {'up' | 'end'} up 2212 + * @param {'cancel'} [cancel] 2213 + */ 2214 + 2215 + 2216 + _bindEvents(pref, down, up, cancel) { 2217 + const { 2218 + pswp 2219 + } = this; 2220 + const { 2221 + events 2222 + } = pswp; 2223 + const cancelEvent = cancel ? pref + cancel : ''; 2224 + events.add(pswp.scrollWrap, pref + down, 2225 + /** @type EventListener */ 2226 + this.onPointerDown.bind(this)); 2227 + events.add(window, pref + 'move', 2228 + /** @type EventListener */ 2229 + this.onPointerMove.bind(this)); 2230 + events.add(window, pref + up, 2231 + /** @type EventListener */ 2232 + this.onPointerUp.bind(this)); 2233 + 2234 + if (cancelEvent) { 2235 + events.add(pswp.scrollWrap, cancelEvent, 2236 + /** @type EventListener */ 2237 + this.onPointerUp.bind(this)); 2238 + } 2239 + } 2240 + /** 2241 + * @param {PointerEvent} e 2242 + */ 2243 + 2244 + 2245 + onPointerDown(e) { 2246 + // We do not call preventDefault for touch events 2247 + // to allow browser to show native dialog on longpress 2248 + // (the one that allows to save image or open it in new tab). 2249 + // 2250 + // Desktop Safari allows to drag images when preventDefault isn't called on mousedown, 2251 + // even though preventDefault IS called on mousemove. That's why we preventDefault mousedown. 2252 + const isMousePointer = e.type === 'mousedown' || e.pointerType === 'mouse'; // Allow dragging only via left mouse button. 2253 + // http://www.quirksmode.org/js/events_properties.html 2254 + // https://developer.mozilla.org/en-US/docs/Web/API/event.button 2255 + 2256 + if (isMousePointer && e.button > 0) { 2257 + return; 2258 + } 2259 + 2260 + const { 2261 + pswp 2262 + } = this; // if PhotoSwipe is opening or closing 2263 + 2264 + if (!pswp.opener.isOpen) { 2265 + e.preventDefault(); 2266 + return; 2267 + } 2268 + 2269 + if (pswp.dispatch('pointerDown', { 2270 + originalEvent: e 2271 + }).defaultPrevented) { 2272 + return; 2273 + } 2274 + 2275 + if (isMousePointer) { 2276 + pswp.mouseDetected(); // preventDefault mouse event to prevent 2277 + // browser image drag feature 2278 + 2279 + this._preventPointerEventBehaviour(e, 'down'); 2280 + } 2281 + 2282 + pswp.animations.stopAll(); 2283 + 2284 + this._updatePoints(e, 'down'); 2285 + 2286 + if (this._numActivePoints === 1) { 2287 + this.dragAxis = null; // we need to store initial point to determine the main axis, 2288 + // drag is activated only after the axis is determined 2289 + 2290 + equalizePoints(this.startP1, this.p1); 2291 + } 2292 + 2293 + if (this._numActivePoints > 1) { 2294 + // Tap or double tap should not trigger if more than one pointer 2295 + this._clearTapTimer(); 2296 + 2297 + this.isMultitouch = true; 2298 + } else { 2299 + this.isMultitouch = false; 2300 + } 2301 + } 2302 + /** 2303 + * @param {PointerEvent} e 2304 + */ 2305 + 2306 + 2307 + onPointerMove(e) { 2308 + this._preventPointerEventBehaviour(e, 'move'); 2309 + 2310 + if (!this._numActivePoints) { 2311 + return; 2312 + } 2313 + 2314 + this._updatePoints(e, 'move'); 2315 + 2316 + if (this.pswp.dispatch('pointerMove', { 2317 + originalEvent: e 2318 + }).defaultPrevented) { 2319 + return; 2320 + } 2321 + 2322 + if (this._numActivePoints === 1 && !this.isDragging) { 2323 + if (!this.dragAxis) { 2324 + this._calculateDragDirection(); 2325 + } // Drag axis was detected, emit drag.start 2326 + 2327 + 2328 + if (this.dragAxis && !this.isDragging) { 2329 + if (this.isZooming) { 2330 + this.isZooming = false; 2331 + this.zoomLevels.end(); 2332 + } 2333 + 2334 + this.isDragging = true; 2335 + 2336 + this._clearTapTimer(); // Tap can not trigger after drag 2337 + // Adjust starting point 2338 + 2339 + 2340 + this._updateStartPoints(); 2341 + 2342 + this._intervalTime = Date.now(); //this._startTime = this._intervalTime; 2343 + 2344 + this._velocityCalculated = false; 2345 + equalizePoints(this._intervalP1, this.p1); 2346 + this.velocity.x = 0; 2347 + this.velocity.y = 0; 2348 + this.drag.start(); 2349 + 2350 + this._rafStopLoop(); 2351 + 2352 + this._rafRenderLoop(); 2353 + } 2354 + } else if (this._numActivePoints > 1 && !this.isZooming) { 2355 + this._finishDrag(); 2356 + 2357 + this.isZooming = true; // Adjust starting points 2358 + 2359 + this._updateStartPoints(); 2360 + 2361 + this.zoomLevels.start(); 2362 + 2363 + this._rafStopLoop(); 2364 + 2365 + this._rafRenderLoop(); 2366 + } 2367 + } 2368 + /** 2369 + * @private 2370 + */ 2371 + 2372 + 2373 + _finishDrag() { 2374 + if (this.isDragging) { 2375 + this.isDragging = false; // Try to calculate velocity, 2376 + // if it wasn't calculated yet in drag.change 2377 + 2378 + if (!this._velocityCalculated) { 2379 + this._updateVelocity(true); 2380 + } 2381 + 2382 + this.drag.end(); 2383 + this.dragAxis = null; 2384 + } 2385 + } 2386 + /** 2387 + * @param {PointerEvent} e 2388 + */ 2389 + 2390 + 2391 + onPointerUp(e) { 2392 + if (!this._numActivePoints) { 2393 + return; 2394 + } 2395 + 2396 + this._updatePoints(e, 'up'); 2397 + 2398 + if (this.pswp.dispatch('pointerUp', { 2399 + originalEvent: e 2400 + }).defaultPrevented) { 2401 + return; 2402 + } 2403 + 2404 + if (this._numActivePoints === 0) { 2405 + this._rafStopLoop(); 2406 + 2407 + if (this.isDragging) { 2408 + this._finishDrag(); 2409 + } else if (!this.isZooming && !this.isMultitouch) { 2410 + //this.zoomLevels.correctZoomPan(); 2411 + this._finishTap(e); 2412 + } 2413 + } 2414 + 2415 + if (this._numActivePoints < 2 && this.isZooming) { 2416 + this.isZooming = false; 2417 + this.zoomLevels.end(); 2418 + 2419 + if (this._numActivePoints === 1) { 2420 + // Since we have 1 point left, we need to reinitiate drag 2421 + this.dragAxis = null; 2422 + 2423 + this._updateStartPoints(); 2424 + } 2425 + } 2426 + } 2427 + /** 2428 + * @private 2429 + */ 2430 + 2431 + 2432 + _rafRenderLoop() { 2433 + if (this.isDragging || this.isZooming) { 2434 + this._updateVelocity(); 2435 + 2436 + if (this.isDragging) { 2437 + // make sure that pointer moved since the last update 2438 + if (!pointsEqual(this.p1, this.prevP1)) { 2439 + this.drag.change(); 2440 + } 2441 + } else 2442 + /* if (this.isZooming) */ 2443 + { 2444 + if (!pointsEqual(this.p1, this.prevP1) || !pointsEqual(this.p2, this.prevP2)) { 2445 + this.zoomLevels.change(); 2446 + } 2447 + } 2448 + 2449 + this._updatePrevPoints(); 2450 + 2451 + this.raf = requestAnimationFrame(this._rafRenderLoop.bind(this)); 2452 + } 2453 + } 2454 + /** 2455 + * Update velocity at 50ms interval 2456 + * 2457 + * @private 2458 + * @param {boolean} [force] 2459 + */ 2460 + 2461 + 2462 + _updateVelocity(force) { 2463 + const time = Date.now(); 2464 + const duration = time - this._intervalTime; 2465 + 2466 + if (duration < 50 && !force) { 2467 + return; 2468 + } 2469 + 2470 + this.velocity.x = this._getVelocity('x', duration); 2471 + this.velocity.y = this._getVelocity('y', duration); 2472 + this._intervalTime = time; 2473 + equalizePoints(this._intervalP1, this.p1); 2474 + this._velocityCalculated = true; 2475 + } 2476 + /** 2477 + * @private 2478 + * @param {PointerEvent} e 2479 + */ 2480 + 2481 + 2482 + _finishTap(e) { 2483 + const { 2484 + mainScroll 2485 + } = this.pswp; // Do not trigger tap events if main scroll is shifted 2486 + 2487 + if (mainScroll.isShifted()) { 2488 + // restore main scroll position 2489 + // (usually happens if stopped in the middle of animation) 2490 + mainScroll.moveIndexBy(0, true); 2491 + return; 2492 + } // Do not trigger tap for touchcancel or pointercancel 2493 + 2494 + 2495 + if (e.type.indexOf('cancel') > 0) { 2496 + return; 2497 + } // Trigger click instead of tap for mouse events 2498 + 2499 + 2500 + if (e.type === 'mouseup' || e.pointerType === 'mouse') { 2501 + this.tapHandler.click(this.startP1, e); 2502 + return; 2503 + } // Disable delay if there is no doubleTapAction 2504 + 2505 + 2506 + const tapDelay = this.pswp.options.doubleTapAction ? DOUBLE_TAP_DELAY : 0; // If tapTimer is defined - we tapped recently, 2507 + // check if the current tap is close to the previous one, 2508 + // if yes - trigger double tap 2509 + 2510 + if (this._tapTimer) { 2511 + this._clearTapTimer(); // Check if two taps were more or less on the same place 2512 + 2513 + 2514 + if (getDistanceBetween(this._lastStartP1, this.startP1) < MIN_TAP_DISTANCE) { 2515 + this.tapHandler.doubleTap(this.startP1, e); 2516 + } 2517 + } else { 2518 + equalizePoints(this._lastStartP1, this.startP1); 2519 + this._tapTimer = setTimeout(() => { 2520 + this.tapHandler.tap(this.startP1, e); 2521 + 2522 + this._clearTapTimer(); 2523 + }, tapDelay); 2524 + } 2525 + } 2526 + /** 2527 + * @private 2528 + */ 2529 + 2530 + 2531 + _clearTapTimer() { 2532 + if (this._tapTimer) { 2533 + clearTimeout(this._tapTimer); 2534 + this._tapTimer = null; 2535 + } 2536 + } 2537 + /** 2538 + * Get velocity for axis 2539 + * 2540 + * @private 2541 + * @param {'x' | 'y'} axis 2542 + * @param {number} duration 2543 + * @returns {number} 2544 + */ 2545 + 2546 + 2547 + _getVelocity(axis, duration) { 2548 + // displacement is like distance, but can be negative. 2549 + const displacement = this.p1[axis] - this._intervalP1[axis]; 2550 + 2551 + if (Math.abs(displacement) > 1 && duration > 5) { 2552 + return displacement / duration; 2553 + } 2554 + 2555 + return 0; 2556 + } 2557 + /** 2558 + * @private 2559 + */ 2560 + 2561 + 2562 + _rafStopLoop() { 2563 + if (this.raf) { 2564 + cancelAnimationFrame(this.raf); 2565 + this.raf = null; 2566 + } 2567 + } 2568 + /** 2569 + * @private 2570 + * @param {PointerEvent} e 2571 + * @param {'up' | 'down' | 'move'} pointerType Normalized pointer type 2572 + */ 2573 + 2574 + 2575 + _preventPointerEventBehaviour(e, pointerType) { 2576 + const preventPointerEvent = this.pswp.applyFilters('preventPointerEvent', true, e, pointerType); 2577 + 2578 + if (preventPointerEvent) { 2579 + e.preventDefault(); 2580 + } 2581 + } 2582 + /** 2583 + * Parses and normalizes points from the touch, mouse or pointer event. 2584 + * Updates p1 and p2. 2585 + * 2586 + * @private 2587 + * @param {PointerEvent | TouchEvent} e 2588 + * @param {'up' | 'down' | 'move'} pointerType Normalized pointer type 2589 + */ 2590 + 2591 + 2592 + _updatePoints(e, pointerType) { 2593 + if (this._pointerEventEnabled) { 2594 + const pointerEvent = 2595 + /** @type {PointerEvent} */ 2596 + e; // Try to find the current pointer in ongoing pointers by its ID 2597 + 2598 + const pointerIndex = this._ongoingPointers.findIndex(ongoingPointer => { 2599 + return ongoingPointer.id === pointerEvent.pointerId; 2600 + }); 2601 + 2602 + if (pointerType === 'up' && pointerIndex > -1) { 2603 + // release the pointer - remove it from ongoing 2604 + this._ongoingPointers.splice(pointerIndex, 1); 2605 + } else if (pointerType === 'down' && pointerIndex === -1) { 2606 + // add new pointer 2607 + this._ongoingPointers.push(this._convertEventPosToPoint(pointerEvent, { 2608 + x: 0, 2609 + y: 0 2610 + })); 2611 + } else if (pointerIndex > -1) { 2612 + // update existing pointer 2613 + this._convertEventPosToPoint(pointerEvent, this._ongoingPointers[pointerIndex]); 2614 + } 2615 + 2616 + this._numActivePoints = this._ongoingPointers.length; // update points that PhotoSwipe uses 2617 + // to calculate position and scale 2618 + 2619 + if (this._numActivePoints > 0) { 2620 + equalizePoints(this.p1, this._ongoingPointers[0]); 2621 + } 2622 + 2623 + if (this._numActivePoints > 1) { 2624 + equalizePoints(this.p2, this._ongoingPointers[1]); 2625 + } 2626 + } else { 2627 + const touchEvent = 2628 + /** @type {TouchEvent} */ 2629 + e; 2630 + this._numActivePoints = 0; 2631 + 2632 + if (touchEvent.type.indexOf('touch') > -1) { 2633 + // Touch Event 2634 + // https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent 2635 + if (touchEvent.touches && touchEvent.touches.length > 0) { 2636 + this._convertEventPosToPoint(touchEvent.touches[0], this.p1); 2637 + 2638 + this._numActivePoints++; 2639 + 2640 + if (touchEvent.touches.length > 1) { 2641 + this._convertEventPosToPoint(touchEvent.touches[1], this.p2); 2642 + 2643 + this._numActivePoints++; 2644 + } 2645 + } 2646 + } else { 2647 + // Mouse Event 2648 + this._convertEventPosToPoint( 2649 + /** @type {PointerEvent} */ 2650 + e, this.p1); 2651 + 2652 + if (pointerType === 'up') { 2653 + // clear all points on mouseup 2654 + this._numActivePoints = 0; 2655 + } else { 2656 + this._numActivePoints++; 2657 + } 2658 + } 2659 + } 2660 + } 2661 + /** update points that were used during previous rAF tick 2662 + * @private 2663 + */ 2664 + 2665 + 2666 + _updatePrevPoints() { 2667 + equalizePoints(this.prevP1, this.p1); 2668 + equalizePoints(this.prevP2, this.p2); 2669 + } 2670 + /** update points at the start of gesture 2671 + * @private 2672 + */ 2673 + 2674 + 2675 + _updateStartPoints() { 2676 + equalizePoints(this.startP1, this.p1); 2677 + equalizePoints(this.startP2, this.p2); 2678 + 2679 + this._updatePrevPoints(); 2680 + } 2681 + /** @private */ 2682 + 2683 + 2684 + _calculateDragDirection() { 2685 + if (this.pswp.mainScroll.isShifted()) { 2686 + // if main scroll position is shifted – direction is always horizontal 2687 + this.dragAxis = 'x'; 2688 + } else { 2689 + // calculate delta of the last touchmove tick 2690 + const diff = Math.abs(this.p1.x - this.startP1.x) - Math.abs(this.p1.y - this.startP1.y); 2691 + 2692 + if (diff !== 0) { 2693 + // check if pointer was shifted horizontally or vertically 2694 + const axisToCheck = diff > 0 ? 'x' : 'y'; 2695 + 2696 + if (Math.abs(this.p1[axisToCheck] - this.startP1[axisToCheck]) >= AXIS_SWIPE_HYSTERISIS) { 2697 + this.dragAxis = axisToCheck; 2698 + } 2699 + } 2700 + } 2701 + } 2702 + /** 2703 + * Converts touch, pointer or mouse event 2704 + * to PhotoSwipe point. 2705 + * 2706 + * @private 2707 + * @param {Touch | PointerEvent} e 2708 + * @param {Point} p 2709 + * @returns {Point} 2710 + */ 2711 + 2712 + 2713 + _convertEventPosToPoint(e, p) { 2714 + p.x = e.pageX - this.pswp.offset.x; 2715 + p.y = e.pageY - this.pswp.offset.y; 2716 + 2717 + if ('pointerId' in e) { 2718 + p.id = e.pointerId; 2719 + } else if (e.identifier !== undefined) { 2720 + p.id = e.identifier; 2721 + } 2722 + 2723 + return p; 2724 + } 2725 + /** 2726 + * @private 2727 + * @param {PointerEvent} e 2728 + */ 2729 + 2730 + 2731 + _onClick(e) { 2732 + // Do not allow click event to pass through after drag 2733 + if (this.pswp.mainScroll.isShifted()) { 2734 + e.preventDefault(); 2735 + e.stopPropagation(); 2736 + } 2737 + } 2738 + 2739 + } 2740 + 2741 + /** @typedef {import('./photoswipe.js').default} PhotoSwipe */ 2742 + 2743 + /** @typedef {import('./slide/slide.js').default} Slide */ 2744 + 2745 + /** @typedef {{ el: HTMLDivElement; slide?: Slide }} ItemHolder */ 2746 + 2747 + const MAIN_SCROLL_END_FRICTION = 0.35; // const MIN_SWIPE_TRANSITION_DURATION = 250; 2748 + // const MAX_SWIPE_TRABSITION_DURATION = 500; 2749 + // const DEFAULT_SWIPE_TRANSITION_DURATION = 333; 2750 + 2751 + /** 2752 + * Handles movement of the main scrolling container 2753 + * (for example, it repositions when user swipes left or right). 2754 + * 2755 + * Also stores its state. 2756 + */ 2757 + 2758 + class MainScroll { 2759 + /** 2760 + * @param {PhotoSwipe} pswp 2761 + */ 2762 + constructor(pswp) { 2763 + this.pswp = pswp; 2764 + this.x = 0; 2765 + this.slideWidth = 0; 2766 + /** @private */ 2767 + 2768 + this._currPositionIndex = 0; 2769 + /** @private */ 2770 + 2771 + this._prevPositionIndex = 0; 2772 + /** @private */ 2773 + 2774 + this._containerShiftIndex = -1; 2775 + /** @type {ItemHolder[]} */ 2776 + 2777 + this.itemHolders = []; 2778 + } 2779 + /** 2780 + * Position the scroller and slide containers 2781 + * according to viewport size. 2782 + * 2783 + * @param {boolean} [resizeSlides] Whether slides content should resized 2784 + */ 2785 + 2786 + 2787 + resize(resizeSlides) { 2788 + const { 2789 + pswp 2790 + } = this; 2791 + const newSlideWidth = Math.round(pswp.viewportSize.x + pswp.viewportSize.x * pswp.options.spacing); // Mobile browsers might trigger a resize event during a gesture. 2792 + // (due to toolbar appearing or hiding). 2793 + // Avoid re-adjusting main scroll position if width wasn't changed 2794 + 2795 + const slideWidthChanged = newSlideWidth !== this.slideWidth; 2796 + 2797 + if (slideWidthChanged) { 2798 + this.slideWidth = newSlideWidth; 2799 + this.moveTo(this.getCurrSlideX()); 2800 + } 2801 + 2802 + this.itemHolders.forEach((itemHolder, index) => { 2803 + if (slideWidthChanged) { 2804 + setTransform(itemHolder.el, (index + this._containerShiftIndex) * this.slideWidth); 2805 + } 2806 + 2807 + if (resizeSlides && itemHolder.slide) { 2808 + itemHolder.slide.resize(); 2809 + } 2810 + }); 2811 + } 2812 + /** 2813 + * Reset X position of the main scroller to zero 2814 + */ 2815 + 2816 + 2817 + resetPosition() { 2818 + // Position on the main scroller (offset) 2819 + // it is independent from slide index 2820 + this._currPositionIndex = 0; 2821 + this._prevPositionIndex = 0; // This will force recalculation of size on next resize() 2822 + 2823 + this.slideWidth = 0; // _containerShiftIndex*viewportSize will give you amount of transform of the current slide 2824 + 2825 + this._containerShiftIndex = -1; 2826 + } 2827 + /** 2828 + * Create and append array of three items 2829 + * that hold data about slides in DOM 2830 + */ 2831 + 2832 + 2833 + appendHolders() { 2834 + this.itemHolders = []; // append our three slide holders - 2835 + // previous, current, and next 2836 + 2837 + for (let i = 0; i < 3; i++) { 2838 + const el = createElement('pswp__item', 'div', this.pswp.container); 2839 + el.setAttribute('role', 'group'); 2840 + el.setAttribute('aria-roledescription', 'slide'); 2841 + el.setAttribute('aria-hidden', 'true'); // hide nearby item holders until initial zoom animation finishes (to avoid extra Paints) 2842 + 2843 + el.style.display = i === 1 ? 'block' : 'none'; 2844 + this.itemHolders.push({ 2845 + el //index: -1 2846 + 2847 + }); 2848 + } 2849 + } 2850 + /** 2851 + * Whether the main scroll can be horizontally swiped to the next or previous slide. 2852 + * @returns {boolean} 2853 + */ 2854 + 2855 + 2856 + canBeSwiped() { 2857 + return this.pswp.getNumItems() > 1; 2858 + } 2859 + /** 2860 + * Move main scroll by X amount of slides. 2861 + * For example: 2862 + * `-1` will move to the previous slide, 2863 + * `0` will reset the scroll position of the current slide, 2864 + * `3` will move three slides forward 2865 + * 2866 + * If loop option is enabled - index will be automatically looped too, 2867 + * (for example `-1` will move to the last slide of the gallery). 2868 + * 2869 + * @param {number} diff 2870 + * @param {boolean} [animate] 2871 + * @param {number} [velocityX] 2872 + * @returns {boolean} whether index was changed or not 2873 + */ 2874 + 2875 + 2876 + moveIndexBy(diff, animate, velocityX) { 2877 + const { 2878 + pswp 2879 + } = this; 2880 + let newIndex = pswp.potentialIndex + diff; 2881 + const numSlides = pswp.getNumItems(); 2882 + 2883 + if (pswp.canLoop()) { 2884 + newIndex = pswp.getLoopedIndex(newIndex); 2885 + const distance = (diff + numSlides) % numSlides; 2886 + 2887 + if (distance <= numSlides / 2) { 2888 + // go forward 2889 + diff = distance; 2890 + } else { 2891 + // go backwards 2892 + diff = distance - numSlides; 2893 + } 2894 + } else { 2895 + if (newIndex < 0) { 2896 + newIndex = 0; 2897 + } else if (newIndex >= numSlides) { 2898 + newIndex = numSlides - 1; 2899 + } 2900 + 2901 + diff = newIndex - pswp.potentialIndex; 2902 + } 2903 + 2904 + pswp.potentialIndex = newIndex; 2905 + this._currPositionIndex -= diff; 2906 + pswp.animations.stopMainScroll(); 2907 + const destinationX = this.getCurrSlideX(); 2908 + 2909 + if (!animate) { 2910 + this.moveTo(destinationX); 2911 + this.updateCurrItem(); 2912 + } else { 2913 + pswp.animations.startSpring({ 2914 + isMainScroll: true, 2915 + start: this.x, 2916 + end: destinationX, 2917 + velocity: velocityX || 0, 2918 + naturalFrequency: 30, 2919 + dampingRatio: 1, 2920 + //0.7, 2921 + onUpdate: x => { 2922 + this.moveTo(x); 2923 + }, 2924 + onComplete: () => { 2925 + this.updateCurrItem(); 2926 + pswp.appendHeavy(); 2927 + } 2928 + }); 2929 + let currDiff = pswp.potentialIndex - pswp.currIndex; 2930 + 2931 + if (pswp.canLoop()) { 2932 + const currDistance = (currDiff + numSlides) % numSlides; 2933 + 2934 + if (currDistance <= numSlides / 2) { 2935 + // go forward 2936 + currDiff = currDistance; 2937 + } else { 2938 + // go backwards 2939 + currDiff = currDistance - numSlides; 2940 + } 2941 + } // Force-append new slides during transition 2942 + // if difference between slides is more than 1 2943 + 2944 + 2945 + if (Math.abs(currDiff) > 1) { 2946 + this.updateCurrItem(); 2947 + } 2948 + } 2949 + 2950 + return Boolean(diff); 2951 + } 2952 + /** 2953 + * X position of the main scroll for the current slide 2954 + * (ignores position during dragging) 2955 + * @returns {number} 2956 + */ 2957 + 2958 + 2959 + getCurrSlideX() { 2960 + return this.slideWidth * this._currPositionIndex; 2961 + } 2962 + /** 2963 + * Whether scroll position is shifted. 2964 + * For example, it will return true if the scroll is being dragged or animated. 2965 + * @returns {boolean} 2966 + */ 2967 + 2968 + 2969 + isShifted() { 2970 + return this.x !== this.getCurrSlideX(); 2971 + } 2972 + /** 2973 + * Update slides X positions and set their content 2974 + */ 2975 + 2976 + 2977 + updateCurrItem() { 2978 + var _this$itemHolders$; 2979 + 2980 + const { 2981 + pswp 2982 + } = this; 2983 + const positionDifference = this._prevPositionIndex - this._currPositionIndex; 2984 + 2985 + if (!positionDifference) { 2986 + return; 2987 + } 2988 + 2989 + this._prevPositionIndex = this._currPositionIndex; 2990 + pswp.currIndex = pswp.potentialIndex; 2991 + let diffAbs = Math.abs(positionDifference); 2992 + /** @type {ItemHolder | undefined} */ 2993 + 2994 + let tempHolder; 2995 + 2996 + if (diffAbs >= 3) { 2997 + this._containerShiftIndex += positionDifference + (positionDifference > 0 ? -3 : 3); 2998 + diffAbs = 3; // If slides are changed by 3 screens or more - clean up previous slides 2999 + 3000 + this.itemHolders.forEach(itemHolder => { 3001 + var _itemHolder$slide; 3002 + 3003 + (_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.destroy(); 3004 + itemHolder.slide = undefined; 3005 + }); 3006 + } 3007 + 3008 + for (let i = 0; i < diffAbs; i++) { 3009 + if (positionDifference > 0) { 3010 + tempHolder = this.itemHolders.shift(); 3011 + 3012 + if (tempHolder) { 3013 + this.itemHolders[2] = tempHolder; // move first to last 3014 + 3015 + this._containerShiftIndex++; 3016 + setTransform(tempHolder.el, (this._containerShiftIndex + 2) * this.slideWidth); 3017 + pswp.setContent(tempHolder, pswp.currIndex - diffAbs + i + 2); 3018 + } 3019 + } else { 3020 + tempHolder = this.itemHolders.pop(); 3021 + 3022 + if (tempHolder) { 3023 + this.itemHolders.unshift(tempHolder); // move last to first 3024 + 3025 + this._containerShiftIndex--; 3026 + setTransform(tempHolder.el, this._containerShiftIndex * this.slideWidth); 3027 + pswp.setContent(tempHolder, pswp.currIndex + diffAbs - i - 2); 3028 + } 3029 + } 3030 + } // Reset transfrom every 50ish navigations in one direction. 3031 + // 3032 + // Otherwise transform will keep growing indefinitely, 3033 + // which might cause issues as browsers have a maximum transform limit. 3034 + // I wasn't able to reach it, but just to be safe. 3035 + // This should not cause noticable lag. 3036 + 3037 + 3038 + if (Math.abs(this._containerShiftIndex) > 50 && !this.isShifted()) { 3039 + this.resetPosition(); 3040 + this.resize(); 3041 + } // Pan transition might be running (and consntantly updating pan position) 3042 + 3043 + 3044 + pswp.animations.stopAllPan(); 3045 + this.itemHolders.forEach((itemHolder, i) => { 3046 + if (itemHolder.slide) { 3047 + // Slide in the 2nd holder is always active 3048 + itemHolder.slide.setIsActive(i === 1); 3049 + } 3050 + }); 3051 + pswp.currSlide = (_this$itemHolders$ = this.itemHolders[1]) === null || _this$itemHolders$ === void 0 ? void 0 : _this$itemHolders$.slide; 3052 + pswp.contentLoader.updateLazy(positionDifference); 3053 + 3054 + if (pswp.currSlide) { 3055 + pswp.currSlide.applyCurrentZoomPan(); 3056 + } 3057 + 3058 + pswp.dispatch('change'); 3059 + } 3060 + /** 3061 + * Move the X position of the main scroll container 3062 + * 3063 + * @param {number} x 3064 + * @param {boolean} [dragging] 3065 + */ 3066 + 3067 + 3068 + moveTo(x, dragging) { 3069 + if (!this.pswp.canLoop() && dragging) { 3070 + // Apply friction 3071 + let newSlideIndexOffset = (this.slideWidth * this._currPositionIndex - x) / this.slideWidth; 3072 + newSlideIndexOffset += this.pswp.currIndex; 3073 + const delta = Math.round(x - this.x); 3074 + 3075 + if (newSlideIndexOffset < 0 && delta > 0 || newSlideIndexOffset >= this.pswp.getNumItems() - 1 && delta < 0) { 3076 + x = this.x + delta * MAIN_SCROLL_END_FRICTION; 3077 + } 3078 + } 3079 + 3080 + this.x = x; 3081 + 3082 + if (this.pswp.container) { 3083 + setTransform(this.pswp.container, x); 3084 + } 3085 + 3086 + this.pswp.dispatch('moveMainScroll', { 3087 + x, 3088 + dragging: dragging !== null && dragging !== void 0 ? dragging : false 3089 + }); 3090 + } 3091 + 3092 + } 3093 + 3094 + /** @typedef {import('./photoswipe.js').default} PhotoSwipe */ 3095 + 3096 + /** 3097 + * @template T 3098 + * @typedef {import('./types.js').Methods<T>} Methods<T> 3099 + */ 3100 + 3101 + const KeyboardKeyCodesMap = { 3102 + Escape: 27, 3103 + z: 90, 3104 + ArrowLeft: 37, 3105 + ArrowUp: 38, 3106 + ArrowRight: 39, 3107 + ArrowDown: 40, 3108 + Tab: 9 3109 + }; 3110 + /** 3111 + * @template {keyof KeyboardKeyCodesMap} T 3112 + * @param {T} key 3113 + * @param {boolean} isKeySupported 3114 + * @returns {T | number | undefined} 3115 + */ 3116 + 3117 + const getKeyboardEventKey = (key, isKeySupported) => { 3118 + return isKeySupported ? key : KeyboardKeyCodesMap[key]; 3119 + }; 3120 + /** 3121 + * - Manages keyboard shortcuts. 3122 + * - Helps trap focus within photoswipe. 3123 + */ 3124 + 3125 + 3126 + class Keyboard { 3127 + /** 3128 + * @param {PhotoSwipe} pswp 3129 + */ 3130 + constructor(pswp) { 3131 + this.pswp = pswp; 3132 + /** @private */ 3133 + 3134 + this._wasFocused = false; 3135 + pswp.on('bindEvents', () => { 3136 + if (pswp.options.trapFocus) { 3137 + // Dialog was likely opened by keyboard if initial point is not defined 3138 + if (!pswp.options.initialPointerPos) { 3139 + // focus causes layout, 3140 + // which causes lag during the animation, 3141 + // that's why we delay it until the opener transition ends 3142 + this._focusRoot(); 3143 + } 3144 + 3145 + pswp.events.add(document, 'focusin', 3146 + /** @type EventListener */ 3147 + this._onFocusIn.bind(this)); 3148 + } 3149 + 3150 + pswp.events.add(document, 'keydown', 3151 + /** @type EventListener */ 3152 + this._onKeyDown.bind(this)); 3153 + }); 3154 + const lastActiveElement = 3155 + /** @type {HTMLElement} */ 3156 + document.activeElement; 3157 + pswp.on('destroy', () => { 3158 + if (pswp.options.returnFocus && lastActiveElement && this._wasFocused) { 3159 + lastActiveElement.focus(); 3160 + } 3161 + }); 3162 + } 3163 + /** @private */ 3164 + 3165 + 3166 + _focusRoot() { 3167 + if (!this._wasFocused && this.pswp.element) { 3168 + this.pswp.element.focus(); 3169 + this._wasFocused = true; 3170 + } 3171 + } 3172 + /** 3173 + * @private 3174 + * @param {KeyboardEvent} e 3175 + */ 3176 + 3177 + 3178 + _onKeyDown(e) { 3179 + const { 3180 + pswp 3181 + } = this; 3182 + 3183 + if (pswp.dispatch('keydown', { 3184 + originalEvent: e 3185 + }).defaultPrevented) { 3186 + return; 3187 + } 3188 + 3189 + if (specialKeyUsed(e)) { 3190 + // don't do anything if special key pressed 3191 + // to prevent from overriding default browser actions 3192 + // for example, in Chrome on Mac cmd+arrow-left returns to previous page 3193 + return; 3194 + } 3195 + /** @type {Methods<PhotoSwipe> | undefined} */ 3196 + 3197 + 3198 + let keydownAction; 3199 + /** @type {'x' | 'y' | undefined} */ 3200 + 3201 + let axis; 3202 + let isForward = false; 3203 + const isKeySupported = ('key' in e); 3204 + 3205 + switch (isKeySupported ? e.key : e.keyCode) { 3206 + case getKeyboardEventKey('Escape', isKeySupported): 3207 + if (pswp.options.escKey) { 3208 + keydownAction = 'close'; 3209 + } 3210 + 3211 + break; 3212 + 3213 + case getKeyboardEventKey('z', isKeySupported): 3214 + keydownAction = 'toggleZoom'; 3215 + break; 3216 + 3217 + case getKeyboardEventKey('ArrowLeft', isKeySupported): 3218 + axis = 'x'; 3219 + break; 3220 + 3221 + case getKeyboardEventKey('ArrowUp', isKeySupported): 3222 + axis = 'y'; 3223 + break; 3224 + 3225 + case getKeyboardEventKey('ArrowRight', isKeySupported): 3226 + axis = 'x'; 3227 + isForward = true; 3228 + break; 3229 + 3230 + case getKeyboardEventKey('ArrowDown', isKeySupported): 3231 + isForward = true; 3232 + axis = 'y'; 3233 + break; 3234 + 3235 + case getKeyboardEventKey('Tab', isKeySupported): 3236 + this._focusRoot(); 3237 + 3238 + break; 3239 + } // if left/right/top/bottom key 3240 + 3241 + 3242 + if (axis) { 3243 + // prevent page scroll 3244 + e.preventDefault(); 3245 + const { 3246 + currSlide 3247 + } = pswp; 3248 + 3249 + if (pswp.options.arrowKeys && axis === 'x' && pswp.getNumItems() > 1) { 3250 + keydownAction = isForward ? 'next' : 'prev'; 3251 + } else if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.fit) { 3252 + // up/down arrow keys pan the image vertically 3253 + // left/right arrow keys pan horizontally. 3254 + // Unless there is only one image, 3255 + // or arrowKeys option is disabled 3256 + currSlide.pan[axis] += isForward ? -80 : 80; 3257 + currSlide.panTo(currSlide.pan.x, currSlide.pan.y); 3258 + } 3259 + } 3260 + 3261 + if (keydownAction) { 3262 + e.preventDefault(); // @ts-ignore 3263 + 3264 + pswp[keydownAction](); 3265 + } 3266 + } 3267 + /** 3268 + * Trap focus inside photoswipe 3269 + * 3270 + * @private 3271 + * @param {FocusEvent} e 3272 + */ 3273 + 3274 + 3275 + _onFocusIn(e) { 3276 + const { 3277 + template 3278 + } = this.pswp; 3279 + 3280 + if (template && document !== e.target && template !== e.target && !template.contains( 3281 + /** @type {Node} */ 3282 + e.target)) { 3283 + // focus root element 3284 + template.focus(); 3285 + } 3286 + } 3287 + 3288 + } 3289 + 3290 + const DEFAULT_EASING = 'cubic-bezier(.4,0,.22,1)'; 3291 + /** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */ 3292 + 3293 + /** @typedef {Object} DefaultCssAnimationProps 3294 + * 3295 + * @prop {HTMLElement} target 3296 + * @prop {number} [duration] 3297 + * @prop {string} [easing] 3298 + * @prop {string} [transform] 3299 + * @prop {string} [opacity] 3300 + * */ 3301 + 3302 + /** @typedef {SharedAnimationProps & DefaultCssAnimationProps} CssAnimationProps */ 3303 + 3304 + /** 3305 + * Runs CSS transition. 3306 + */ 3307 + 3308 + class CSSAnimation { 3309 + /** 3310 + * onComplete can be unpredictable, be careful about current state 3311 + * 3312 + * @param {CssAnimationProps} props 3313 + */ 3314 + constructor(props) { 3315 + var _props$prop; 3316 + 3317 + this.props = props; 3318 + const { 3319 + target, 3320 + onComplete, 3321 + transform, 3322 + onFinish = () => {}, 3323 + duration = 333, 3324 + easing = DEFAULT_EASING 3325 + } = props; 3326 + this.onFinish = onFinish; // support only transform and opacity 3327 + 3328 + const prop = transform ? 'transform' : 'opacity'; 3329 + const propValue = (_props$prop = props[prop]) !== null && _props$prop !== void 0 ? _props$prop : ''; 3330 + /** @private */ 3331 + 3332 + this._target = target; 3333 + /** @private */ 3334 + 3335 + this._onComplete = onComplete; 3336 + /** @private */ 3337 + 3338 + this._finished = false; 3339 + /** @private */ 3340 + 3341 + this._onTransitionEnd = this._onTransitionEnd.bind(this); // Using timeout hack to make sure that animation 3342 + // starts even if the animated property was changed recently, 3343 + // otherwise transitionend might not fire or transition won't start. 3344 + // https://drafts.csswg.org/css-transitions/#starting 3345 + // 3346 + // ¯\_(ツ)_/¯ 3347 + 3348 + /** @private */ 3349 + 3350 + this._helperTimeout = setTimeout(() => { 3351 + setTransitionStyle(target, prop, duration, easing); 3352 + this._helperTimeout = setTimeout(() => { 3353 + target.addEventListener('transitionend', this._onTransitionEnd, false); 3354 + target.addEventListener('transitioncancel', this._onTransitionEnd, false); // Safari occasionally does not emit transitionend event 3355 + // if element property was modified during the transition, 3356 + // which may be caused by resize or third party component, 3357 + // using timeout as a safety fallback 3358 + 3359 + this._helperTimeout = setTimeout(() => { 3360 + this._finalizeAnimation(); 3361 + }, duration + 500); 3362 + target.style[prop] = propValue; 3363 + }, 30); // Do not reduce this number 3364 + }, 0); 3365 + } 3366 + /** 3367 + * @private 3368 + * @param {TransitionEvent} e 3369 + */ 3370 + 3371 + 3372 + _onTransitionEnd(e) { 3373 + if (e.target === this._target) { 3374 + this._finalizeAnimation(); 3375 + } 3376 + } 3377 + /** 3378 + * @private 3379 + */ 3380 + 3381 + 3382 + _finalizeAnimation() { 3383 + if (!this._finished) { 3384 + this._finished = true; 3385 + this.onFinish(); 3386 + 3387 + if (this._onComplete) { 3388 + this._onComplete(); 3389 + } 3390 + } 3391 + } // Destroy is called automatically onFinish 3392 + 3393 + 3394 + destroy() { 3395 + if (this._helperTimeout) { 3396 + clearTimeout(this._helperTimeout); 3397 + } 3398 + 3399 + removeTransitionStyle(this._target); 3400 + 3401 + this._target.removeEventListener('transitionend', this._onTransitionEnd, false); 3402 + 3403 + this._target.removeEventListener('transitioncancel', this._onTransitionEnd, false); 3404 + 3405 + if (!this._finished) { 3406 + this._finalizeAnimation(); 3407 + } 3408 + } 3409 + 3410 + } 3411 + 3412 + const DEFAULT_NATURAL_FREQUENCY = 12; 3413 + const DEFAULT_DAMPING_RATIO = 0.75; 3414 + /** 3415 + * Spring easing helper 3416 + */ 3417 + 3418 + class SpringEaser { 3419 + /** 3420 + * @param {number} initialVelocity Initial velocity, px per ms. 3421 + * 3422 + * @param {number} [dampingRatio] 3423 + * Determines how bouncy animation will be. 3424 + * From 0 to 1, 0 - always overshoot, 1 - do not overshoot. 3425 + * "overshoot" refers to part of animation that 3426 + * goes beyond the final value. 3427 + * 3428 + * @param {number} [naturalFrequency] 3429 + * Determines how fast animation will slow down. 3430 + * The higher value - the stiffer the transition will be, 3431 + * and the faster it will slow down. 3432 + * Recommended value from 10 to 50 3433 + */ 3434 + constructor(initialVelocity, dampingRatio, naturalFrequency) { 3435 + this.velocity = initialVelocity * 1000; // convert to "pixels per second" 3436 + // https://en.wikipedia.org/wiki/Damping_ratio 3437 + 3438 + this._dampingRatio = dampingRatio || DEFAULT_DAMPING_RATIO; // https://en.wikipedia.org/wiki/Natural_frequency 3439 + 3440 + this._naturalFrequency = naturalFrequency || DEFAULT_NATURAL_FREQUENCY; 3441 + this._dampedFrequency = this._naturalFrequency; 3442 + 3443 + if (this._dampingRatio < 1) { 3444 + this._dampedFrequency *= Math.sqrt(1 - this._dampingRatio * this._dampingRatio); 3445 + } 3446 + } 3447 + /** 3448 + * @param {number} deltaPosition Difference between current and end position of the animation 3449 + * @param {number} deltaTime Frame duration in milliseconds 3450 + * 3451 + * @returns {number} Displacement, relative to the end position. 3452 + */ 3453 + 3454 + 3455 + easeFrame(deltaPosition, deltaTime) { 3456 + // Inspired by Apple Webkit and Android spring function implementation 3457 + // https://en.wikipedia.org/wiki/Oscillation 3458 + // https://en.wikipedia.org/wiki/Damping_ratio 3459 + // we ignore mass (assume that it's 1kg) 3460 + let displacement = 0; 3461 + let coeff; 3462 + deltaTime /= 1000; 3463 + const naturalDumpingPow = Math.E ** (-this._dampingRatio * this._naturalFrequency * deltaTime); 3464 + 3465 + if (this._dampingRatio === 1) { 3466 + coeff = this.velocity + this._naturalFrequency * deltaPosition; 3467 + displacement = (deltaPosition + coeff * deltaTime) * naturalDumpingPow; 3468 + this.velocity = displacement * -this._naturalFrequency + coeff * naturalDumpingPow; 3469 + } else if (this._dampingRatio < 1) { 3470 + coeff = 1 / this._dampedFrequency * (this._dampingRatio * this._naturalFrequency * deltaPosition + this.velocity); 3471 + const dumpedFCos = Math.cos(this._dampedFrequency * deltaTime); 3472 + const dumpedFSin = Math.sin(this._dampedFrequency * deltaTime); 3473 + displacement = naturalDumpingPow * (deltaPosition * dumpedFCos + coeff * dumpedFSin); 3474 + this.velocity = displacement * -this._naturalFrequency * this._dampingRatio + naturalDumpingPow * (-this._dampedFrequency * deltaPosition * dumpedFSin + this._dampedFrequency * coeff * dumpedFCos); 3475 + } // Overdamped (>1) damping ratio is not supported 3476 + 3477 + 3478 + return displacement; 3479 + } 3480 + 3481 + } 3482 + 3483 + /** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */ 3484 + 3485 + /** 3486 + * @typedef {Object} DefaultSpringAnimationProps 3487 + * 3488 + * @prop {number} start 3489 + * @prop {number} end 3490 + * @prop {number} velocity 3491 + * @prop {number} [dampingRatio] 3492 + * @prop {number} [naturalFrequency] 3493 + * @prop {(end: number) => void} onUpdate 3494 + */ 3495 + 3496 + /** @typedef {SharedAnimationProps & DefaultSpringAnimationProps} SpringAnimationProps */ 3497 + 3498 + class SpringAnimation { 3499 + /** 3500 + * @param {SpringAnimationProps} props 3501 + */ 3502 + constructor(props) { 3503 + this.props = props; 3504 + this._raf = 0; 3505 + const { 3506 + start, 3507 + end, 3508 + velocity, 3509 + onUpdate, 3510 + onComplete, 3511 + onFinish = () => {}, 3512 + dampingRatio, 3513 + naturalFrequency 3514 + } = props; 3515 + this.onFinish = onFinish; 3516 + const easer = new SpringEaser(velocity, dampingRatio, naturalFrequency); 3517 + let prevTime = Date.now(); 3518 + let deltaPosition = start - end; 3519 + 3520 + const animationLoop = () => { 3521 + if (this._raf) { 3522 + deltaPosition = easer.easeFrame(deltaPosition, Date.now() - prevTime); // Stop the animation if velocity is low and position is close to end 3523 + 3524 + if (Math.abs(deltaPosition) < 1 && Math.abs(easer.velocity) < 50) { 3525 + // Finalize the animation 3526 + onUpdate(end); 3527 + 3528 + if (onComplete) { 3529 + onComplete(); 3530 + } 3531 + 3532 + this.onFinish(); 3533 + } else { 3534 + prevTime = Date.now(); 3535 + onUpdate(deltaPosition + end); 3536 + this._raf = requestAnimationFrame(animationLoop); 3537 + } 3538 + } 3539 + }; 3540 + 3541 + this._raf = requestAnimationFrame(animationLoop); 3542 + } // Destroy is called automatically onFinish 3543 + 3544 + 3545 + destroy() { 3546 + if (this._raf >= 0) { 3547 + cancelAnimationFrame(this._raf); 3548 + } 3549 + 3550 + this._raf = 0; 3551 + } 3552 + 3553 + } 3554 + 3555 + /** @typedef {import('./css-animation.js').CssAnimationProps} CssAnimationProps */ 3556 + 3557 + /** @typedef {import('./spring-animation.js').SpringAnimationProps} SpringAnimationProps */ 3558 + 3559 + /** @typedef {Object} SharedAnimationProps 3560 + * @prop {string} [name] 3561 + * @prop {boolean} [isPan] 3562 + * @prop {boolean} [isMainScroll] 3563 + * @prop {VoidFunction} [onComplete] 3564 + * @prop {VoidFunction} [onFinish] 3565 + */ 3566 + 3567 + /** @typedef {SpringAnimation | CSSAnimation} Animation */ 3568 + 3569 + /** @typedef {SpringAnimationProps | CssAnimationProps} AnimationProps */ 3570 + 3571 + /** 3572 + * Manages animations 3573 + */ 3574 + 3575 + class Animations { 3576 + constructor() { 3577 + /** @type {Animation[]} */ 3578 + this.activeAnimations = []; 3579 + } 3580 + /** 3581 + * @param {SpringAnimationProps} props 3582 + */ 3583 + 3584 + 3585 + startSpring(props) { 3586 + this._start(props, true); 3587 + } 3588 + /** 3589 + * @param {CssAnimationProps} props 3590 + */ 3591 + 3592 + 3593 + startTransition(props) { 3594 + this._start(props); 3595 + } 3596 + /** 3597 + * @private 3598 + * @param {AnimationProps} props 3599 + * @param {boolean} [isSpring] 3600 + * @returns {Animation} 3601 + */ 3602 + 3603 + 3604 + _start(props, isSpring) { 3605 + const animation = isSpring ? new SpringAnimation( 3606 + /** @type SpringAnimationProps */ 3607 + props) : new CSSAnimation( 3608 + /** @type CssAnimationProps */ 3609 + props); 3610 + this.activeAnimations.push(animation); 3611 + 3612 + animation.onFinish = () => this.stop(animation); 3613 + 3614 + return animation; 3615 + } 3616 + /** 3617 + * @param {Animation} animation 3618 + */ 3619 + 3620 + 3621 + stop(animation) { 3622 + animation.destroy(); 3623 + const index = this.activeAnimations.indexOf(animation); 3624 + 3625 + if (index > -1) { 3626 + this.activeAnimations.splice(index, 1); 3627 + } 3628 + } 3629 + 3630 + stopAll() { 3631 + // _stopAllAnimations 3632 + this.activeAnimations.forEach(animation => { 3633 + animation.destroy(); 3634 + }); 3635 + this.activeAnimations = []; 3636 + } 3637 + /** 3638 + * Stop all pan or zoom transitions 3639 + */ 3640 + 3641 + 3642 + stopAllPan() { 3643 + this.activeAnimations = this.activeAnimations.filter(animation => { 3644 + if (animation.props.isPan) { 3645 + animation.destroy(); 3646 + return false; 3647 + } 3648 + 3649 + return true; 3650 + }); 3651 + } 3652 + 3653 + stopMainScroll() { 3654 + this.activeAnimations = this.activeAnimations.filter(animation => { 3655 + if (animation.props.isMainScroll) { 3656 + animation.destroy(); 3657 + return false; 3658 + } 3659 + 3660 + return true; 3661 + }); 3662 + } 3663 + /** 3664 + * Returns true if main scroll transition is running 3665 + */ 3666 + // isMainScrollRunning() { 3667 + // return this.activeAnimations.some((animation) => { 3668 + // return animation.props.isMainScroll; 3669 + // }); 3670 + // } 3671 + 3672 + /** 3673 + * Returns true if any pan or zoom transition is running 3674 + */ 3675 + 3676 + 3677 + isPanRunning() { 3678 + return this.activeAnimations.some(animation => { 3679 + return animation.props.isPan; 3680 + }); 3681 + } 3682 + 3683 + } 3684 + 3685 + /** @typedef {import('./photoswipe.js').default} PhotoSwipe */ 3686 + 3687 + /** 3688 + * Handles scroll wheel. 3689 + * Can pan and zoom current slide image. 3690 + */ 3691 + class ScrollWheel { 3692 + /** 3693 + * @param {PhotoSwipe} pswp 3694 + */ 3695 + constructor(pswp) { 3696 + this.pswp = pswp; 3697 + pswp.events.add(pswp.element, 'wheel', 3698 + /** @type EventListener */ 3699 + this._onWheel.bind(this)); 3700 + } 3701 + /** 3702 + * @private 3703 + * @param {WheelEvent} e 3704 + */ 3705 + 3706 + 3707 + _onWheel(e) { 3708 + e.preventDefault(); 3709 + const { 3710 + currSlide 3711 + } = this.pswp; 3712 + let { 3713 + deltaX, 3714 + deltaY 3715 + } = e; 3716 + 3717 + if (!currSlide) { 3718 + return; 3719 + } 3720 + 3721 + if (this.pswp.dispatch('wheel', { 3722 + originalEvent: e 3723 + }).defaultPrevented) { 3724 + return; 3725 + } 3726 + 3727 + if (e.ctrlKey || this.pswp.options.wheelToZoom) { 3728 + // zoom 3729 + if (currSlide.isZoomable()) { 3730 + let zoomFactor = -deltaY; 3731 + 3732 + if (e.deltaMode === 1 3733 + /* DOM_DELTA_LINE */ 3734 + ) { 3735 + zoomFactor *= 0.05; 3736 + } else { 3737 + zoomFactor *= e.deltaMode ? 1 : 0.002; 3738 + } 3739 + 3740 + zoomFactor = 2 ** zoomFactor; 3741 + const destZoomLevel = currSlide.currZoomLevel * zoomFactor; 3742 + currSlide.zoomTo(destZoomLevel, { 3743 + x: e.clientX, 3744 + y: e.clientY 3745 + }); 3746 + } 3747 + } else { 3748 + // pan 3749 + if (currSlide.isPannable()) { 3750 + if (e.deltaMode === 1 3751 + /* DOM_DELTA_LINE */ 3752 + ) { 3753 + // 18 - average line height 3754 + deltaX *= 18; 3755 + deltaY *= 18; 3756 + } 3757 + 3758 + currSlide.panTo(currSlide.pan.x - deltaX, currSlide.pan.y - deltaY); 3759 + } 3760 + } 3761 + } 3762 + 3763 + } 3764 + 3765 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 3766 + 3767 + /** 3768 + * @template T 3769 + * @typedef {import('../types.js').Methods<T>} Methods<T> 3770 + */ 3771 + 3772 + /** 3773 + * @typedef {Object} UIElementMarkupProps 3774 + * @prop {boolean} [isCustomSVG] 3775 + * @prop {string} inner 3776 + * @prop {string} [outlineID] 3777 + * @prop {number | string} [size] 3778 + */ 3779 + 3780 + /** 3781 + * @typedef {Object} UIElementData 3782 + * @prop {DefaultUIElements | string} [name] 3783 + * @prop {string} [className] 3784 + * @prop {UIElementMarkup} [html] 3785 + * @prop {boolean} [isButton] 3786 + * @prop {keyof HTMLElementTagNameMap} [tagName] 3787 + * @prop {string} [title] 3788 + * @prop {string} [ariaLabel] 3789 + * @prop {(element: HTMLElement, pswp: PhotoSwipe) => void} [onInit] 3790 + * @prop {Methods<PhotoSwipe> | ((e: MouseEvent, element: HTMLElement, pswp: PhotoSwipe) => void)} [onClick] 3791 + * @prop {'bar' | 'wrapper' | 'root'} [appendTo] 3792 + * @prop {number} [order] 3793 + */ 3794 + 3795 + /** @typedef {'arrowPrev' | 'arrowNext' | 'close' | 'zoom' | 'counter'} DefaultUIElements */ 3796 + 3797 + /** @typedef {string | UIElementMarkupProps} UIElementMarkup */ 3798 + 3799 + /** 3800 + * @param {UIElementMarkup} [htmlData] 3801 + * @returns {string} 3802 + */ 3803 + 3804 + function addElementHTML(htmlData) { 3805 + if (typeof htmlData === 'string') { 3806 + // Allow developers to provide full svg, 3807 + // For example: 3808 + // <svg viewBox="0 0 32 32" width="32" height="32" aria-hidden="true" class="pswp__icn"> 3809 + // <path d="..." /> 3810 + // <circle ... /> 3811 + // </svg> 3812 + // Can also be any HTML string. 3813 + return htmlData; 3814 + } 3815 + 3816 + if (!htmlData || !htmlData.isCustomSVG) { 3817 + return ''; 3818 + } 3819 + 3820 + const svgData = htmlData; 3821 + let out = '<svg aria-hidden="true" class="pswp__icn" viewBox="0 0 %d %d" width="%d" height="%d">'; // replace all %d with size 3822 + 3823 + out = out.split('%d').join( 3824 + /** @type {string} */ 3825 + svgData.size || 32); // Icons may contain outline/shadow, 3826 + // to make it we "clone" base icon shape and add border to it. 3827 + // Icon itself and border are styled via CSS. 3828 + // 3829 + // Property shadowID defines ID of element that should be cloned. 3830 + 3831 + if (svgData.outlineID) { 3832 + out += '<use class="pswp__icn-shadow" xlink:href="#' + svgData.outlineID + '"/>'; 3833 + } 3834 + 3835 + out += svgData.inner; 3836 + out += '</svg>'; 3837 + return out; 3838 + } 3839 + 3840 + class UIElement { 3841 + /** 3842 + * @param {PhotoSwipe} pswp 3843 + * @param {UIElementData} data 3844 + */ 3845 + constructor(pswp, data) { 3846 + var _container; 3847 + 3848 + const name = data.name || data.className; 3849 + let elementHTML = data.html; // @ts-expect-error lookup only by `data.name` maybe? 3850 + 3851 + if (pswp.options[name] === false) { 3852 + // exit if element is disabled from options 3853 + return; 3854 + } // Allow to override SVG icons from options 3855 + // @ts-expect-error lookup only by `data.name` maybe? 3856 + 3857 + 3858 + if (typeof pswp.options[name + 'SVG'] === 'string') { 3859 + // arrowPrevSVG 3860 + // arrowNextSVG 3861 + // closeSVG 3862 + // zoomSVG 3863 + // @ts-expect-error lookup only by `data.name` maybe? 3864 + elementHTML = pswp.options[name + 'SVG']; 3865 + } 3866 + 3867 + pswp.dispatch('uiElementCreate', { 3868 + data 3869 + }); 3870 + let className = ''; 3871 + 3872 + if (data.isButton) { 3873 + className += 'pswp__button '; 3874 + className += data.className || `pswp__button--${data.name}`; 3875 + } else { 3876 + className += data.className || `pswp__${data.name}`; 3877 + } 3878 + 3879 + let tagName = data.isButton ? data.tagName || 'button' : data.tagName || 'div'; 3880 + tagName = 3881 + /** @type {keyof HTMLElementTagNameMap} */ 3882 + tagName.toLowerCase(); 3883 + /** @type {HTMLElement} */ 3884 + 3885 + const element = createElement(className, tagName); 3886 + 3887 + if (data.isButton) { 3888 + if (tagName === 'button') { 3889 + /** @type {HTMLButtonElement} */ 3890 + element.type = 'button'; 3891 + } 3892 + 3893 + let { 3894 + title 3895 + } = data; 3896 + const { 3897 + ariaLabel 3898 + } = data; // @ts-expect-error lookup only by `data.name` maybe? 3899 + 3900 + if (typeof pswp.options[name + 'Title'] === 'string') { 3901 + // @ts-expect-error lookup only by `data.name` maybe? 3902 + title = pswp.options[name + 'Title']; 3903 + } 3904 + 3905 + if (title) { 3906 + element.title = title; 3907 + } 3908 + 3909 + const ariaText = ariaLabel || title; 3910 + 3911 + if (ariaText) { 3912 + element.setAttribute('aria-label', ariaText); 3913 + } 3914 + } 3915 + 3916 + element.innerHTML = addElementHTML(elementHTML); 3917 + 3918 + if (data.onInit) { 3919 + data.onInit(element, pswp); 3920 + } 3921 + 3922 + if (data.onClick) { 3923 + element.onclick = e => { 3924 + if (typeof data.onClick === 'string') { 3925 + // @ts-ignore 3926 + pswp[data.onClick](); 3927 + } else if (typeof data.onClick === 'function') { 3928 + data.onClick(e, element, pswp); 3929 + } 3930 + }; 3931 + } // Top bar is default position 3932 + 3933 + 3934 + const appendTo = data.appendTo || 'bar'; 3935 + /** @type {HTMLElement | undefined} root element by default */ 3936 + 3937 + let container = pswp.element; 3938 + 3939 + if (appendTo === 'bar') { 3940 + if (!pswp.topBar) { 3941 + pswp.topBar = createElement('pswp__top-bar pswp__hide-on-close', 'div', pswp.scrollWrap); 3942 + } 3943 + 3944 + container = pswp.topBar; 3945 + } else { 3946 + // element outside of top bar gets a secondary class 3947 + // that makes element fade out on close 3948 + element.classList.add('pswp__hide-on-close'); 3949 + 3950 + if (appendTo === 'wrapper') { 3951 + container = pswp.scrollWrap; 3952 + } 3953 + } 3954 + 3955 + (_container = container) === null || _container === void 0 || _container.appendChild(pswp.applyFilters('uiElement', element, data)); 3956 + } 3957 + 3958 + } 3959 + 3960 + /* 3961 + Backward and forward arrow buttons 3962 + */ 3963 + 3964 + /** @typedef {import('./ui-element.js').UIElementData} UIElementData */ 3965 + 3966 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 3967 + 3968 + /** 3969 + * 3970 + * @param {HTMLElement} element 3971 + * @param {PhotoSwipe} pswp 3972 + * @param {boolean} [isNextButton] 3973 + */ 3974 + function initArrowButton(element, pswp, isNextButton) { 3975 + element.classList.add('pswp__button--arrow'); // TODO: this should point to a unique id for this instance 3976 + 3977 + element.setAttribute('aria-controls', 'pswp__items'); 3978 + pswp.on('change', () => { 3979 + if (!pswp.options.loop) { 3980 + if (isNextButton) { 3981 + /** @type {HTMLButtonElement} */ 3982 + element.disabled = !(pswp.currIndex < pswp.getNumItems() - 1); 3983 + } else { 3984 + /** @type {HTMLButtonElement} */ 3985 + element.disabled = !(pswp.currIndex > 0); 3986 + } 3987 + } 3988 + }); 3989 + } 3990 + /** @type {UIElementData} */ 3991 + 3992 + 3993 + const arrowPrev = { 3994 + name: 'arrowPrev', 3995 + className: 'pswp__button--arrow--prev', 3996 + title: 'Previous', 3997 + order: 10, 3998 + isButton: true, 3999 + appendTo: 'wrapper', 4000 + html: { 4001 + isCustomSVG: true, 4002 + size: 60, 4003 + inner: '<path d="M29 43l-3 3-16-16 16-16 3 3-13 13 13 13z" id="pswp__icn-arrow"/>', 4004 + outlineID: 'pswp__icn-arrow' 4005 + }, 4006 + onClick: 'prev', 4007 + onInit: initArrowButton 4008 + }; 4009 + /** @type {UIElementData} */ 4010 + 4011 + const arrowNext = { 4012 + name: 'arrowNext', 4013 + className: 'pswp__button--arrow--next', 4014 + title: 'Next', 4015 + order: 11, 4016 + isButton: true, 4017 + appendTo: 'wrapper', 4018 + html: { 4019 + isCustomSVG: true, 4020 + size: 60, 4021 + inner: '<use xlink:href="#pswp__icn-arrow"/>', 4022 + outlineID: 'pswp__icn-arrow' 4023 + }, 4024 + onClick: 'next', 4025 + onInit: (el, pswp) => { 4026 + initArrowButton(el, pswp, true); 4027 + } 4028 + }; 4029 + 4030 + /** @type {import('./ui-element.js').UIElementData} UIElementData */ 4031 + const closeButton = { 4032 + name: 'close', 4033 + title: 'Close', 4034 + order: 20, 4035 + isButton: true, 4036 + html: { 4037 + isCustomSVG: true, 4038 + inner: '<path d="M24 10l-2-2-6 6-6-6-2 2 6 6-6 6 2 2 6-6 6 6 2-2-6-6z" id="pswp__icn-close"/>', 4039 + outlineID: 'pswp__icn-close' 4040 + }, 4041 + onClick: 'close' 4042 + }; 4043 + 4044 + /** @type {import('./ui-element.js').UIElementData} UIElementData */ 4045 + const zoomButton = { 4046 + name: 'zoom', 4047 + title: 'Zoom', 4048 + order: 10, 4049 + isButton: true, 4050 + html: { 4051 + isCustomSVG: true, 4052 + // eslint-disable-next-line max-len 4053 + inner: '<path d="M17.426 19.926a6 6 0 1 1 1.5-1.5L23 22.5 21.5 24l-4.074-4.074z" id="pswp__icn-zoom"/>' + '<path fill="currentColor" class="pswp__zoom-icn-bar-h" d="M11 16v-2h6v2z"/>' + '<path fill="currentColor" class="pswp__zoom-icn-bar-v" d="M13 12h2v6h-2z"/>', 4054 + outlineID: 'pswp__icn-zoom' 4055 + }, 4056 + onClick: 'toggleZoom' 4057 + }; 4058 + 4059 + /** @type {import('./ui-element.js').UIElementData} UIElementData */ 4060 + const loadingIndicator = { 4061 + name: 'preloader', 4062 + appendTo: 'bar', 4063 + order: 7, 4064 + html: { 4065 + isCustomSVG: true, 4066 + // eslint-disable-next-line max-len 4067 + inner: '<path fill-rule="evenodd" clip-rule="evenodd" d="M21.2 16a5.2 5.2 0 1 1-5.2-5.2V8a8 8 0 1 0 8 8h-2.8Z" id="pswp__icn-loading"/>', 4068 + outlineID: 'pswp__icn-loading' 4069 + }, 4070 + onInit: (indicatorElement, pswp) => { 4071 + /** @type {boolean | undefined} */ 4072 + let isVisible; 4073 + /** @type {NodeJS.Timeout | null} */ 4074 + 4075 + let delayTimeout = null; 4076 + /** 4077 + * @param {string} className 4078 + * @param {boolean} add 4079 + */ 4080 + 4081 + const toggleIndicatorClass = (className, add) => { 4082 + indicatorElement.classList.toggle('pswp__preloader--' + className, add); 4083 + }; 4084 + /** 4085 + * @param {boolean} visible 4086 + */ 4087 + 4088 + 4089 + const setIndicatorVisibility = visible => { 4090 + if (isVisible !== visible) { 4091 + isVisible = visible; 4092 + toggleIndicatorClass('active', visible); 4093 + } 4094 + }; 4095 + 4096 + const updatePreloaderVisibility = () => { 4097 + var _pswp$currSlide; 4098 + 4099 + if (!((_pswp$currSlide = pswp.currSlide) !== null && _pswp$currSlide !== void 0 && _pswp$currSlide.content.isLoading())) { 4100 + setIndicatorVisibility(false); 4101 + 4102 + if (delayTimeout) { 4103 + clearTimeout(delayTimeout); 4104 + delayTimeout = null; 4105 + } 4106 + 4107 + return; 4108 + } 4109 + 4110 + if (!delayTimeout) { 4111 + // display loading indicator with delay 4112 + delayTimeout = setTimeout(() => { 4113 + var _pswp$currSlide2; 4114 + 4115 + setIndicatorVisibility(Boolean((_pswp$currSlide2 = pswp.currSlide) === null || _pswp$currSlide2 === void 0 ? void 0 : _pswp$currSlide2.content.isLoading())); 4116 + delayTimeout = null; 4117 + }, pswp.options.preloaderDelay); 4118 + } 4119 + }; 4120 + 4121 + pswp.on('change', updatePreloaderVisibility); 4122 + pswp.on('loadComplete', e => { 4123 + if (pswp.currSlide === e.slide) { 4124 + updatePreloaderVisibility(); 4125 + } 4126 + }); // expose the method 4127 + 4128 + if (pswp.ui) { 4129 + pswp.ui.updatePreloaderVisibility = updatePreloaderVisibility; 4130 + } 4131 + } 4132 + }; 4133 + 4134 + /** @type {import('./ui-element.js').UIElementData} UIElementData */ 4135 + const counterIndicator = { 4136 + name: 'counter', 4137 + order: 5, 4138 + onInit: (counterElement, pswp) => { 4139 + pswp.on('change', () => { 4140 + counterElement.innerText = pswp.currIndex + 1 + pswp.options.indexIndicatorSep + pswp.getNumItems(); 4141 + }); 4142 + } 4143 + }; 4144 + 4145 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 4146 + 4147 + /** @typedef {import('./ui-element.js').UIElementData} UIElementData */ 4148 + 4149 + /** 4150 + * Set special class on element when image is zoomed. 4151 + * 4152 + * By default, it is used to adjust 4153 + * zoom icon and zoom cursor via CSS. 4154 + * 4155 + * @param {HTMLElement} el 4156 + * @param {boolean} isZoomedIn 4157 + */ 4158 + 4159 + function setZoomedIn(el, isZoomedIn) { 4160 + el.classList.toggle('pswp--zoomed-in', isZoomedIn); 4161 + } 4162 + 4163 + class UI { 4164 + /** 4165 + * @param {PhotoSwipe} pswp 4166 + */ 4167 + constructor(pswp) { 4168 + this.pswp = pswp; 4169 + this.isRegistered = false; 4170 + /** @type {UIElementData[]} */ 4171 + 4172 + this.uiElementsData = []; 4173 + /** @type {(UIElement | UIElementData)[]} */ 4174 + 4175 + this.items = []; 4176 + /** @type {() => void} */ 4177 + 4178 + this.updatePreloaderVisibility = () => {}; 4179 + /** 4180 + * @private 4181 + * @type {number | undefined} 4182 + */ 4183 + 4184 + 4185 + this._lastUpdatedZoomLevel = undefined; 4186 + } 4187 + 4188 + init() { 4189 + const { 4190 + pswp 4191 + } = this; 4192 + this.isRegistered = false; 4193 + this.uiElementsData = [closeButton, arrowPrev, arrowNext, zoomButton, loadingIndicator, counterIndicator]; 4194 + pswp.dispatch('uiRegister'); // sort by order 4195 + 4196 + this.uiElementsData.sort((a, b) => { 4197 + // default order is 0 4198 + return (a.order || 0) - (b.order || 0); 4199 + }); 4200 + this.items = []; 4201 + this.isRegistered = true; 4202 + this.uiElementsData.forEach(uiElementData => { 4203 + this.registerElement(uiElementData); 4204 + }); 4205 + pswp.on('change', () => { 4206 + var _pswp$element; 4207 + 4208 + (_pswp$element = pswp.element) === null || _pswp$element === void 0 || _pswp$element.classList.toggle('pswp--one-slide', pswp.getNumItems() === 1); 4209 + }); 4210 + pswp.on('zoomPanUpdate', () => this._onZoomPanUpdate()); 4211 + } 4212 + /** 4213 + * @param {UIElementData} elementData 4214 + */ 4215 + 4216 + 4217 + registerElement(elementData) { 4218 + if (this.isRegistered) { 4219 + this.items.push(new UIElement(this.pswp, elementData)); 4220 + } else { 4221 + this.uiElementsData.push(elementData); 4222 + } 4223 + } 4224 + /** 4225 + * Fired each time zoom or pan position is changed. 4226 + * Update classes that control visibility of zoom button and cursor icon. 4227 + * 4228 + * @private 4229 + */ 4230 + 4231 + 4232 + _onZoomPanUpdate() { 4233 + const { 4234 + template, 4235 + currSlide, 4236 + options 4237 + } = this.pswp; 4238 + 4239 + if (this.pswp.opener.isClosing || !template || !currSlide) { 4240 + return; 4241 + } 4242 + 4243 + let { 4244 + currZoomLevel 4245 + } = currSlide; // if not open yet - check against initial zoom level 4246 + 4247 + if (!this.pswp.opener.isOpen) { 4248 + currZoomLevel = currSlide.zoomLevels.initial; 4249 + } 4250 + 4251 + if (currZoomLevel === this._lastUpdatedZoomLevel) { 4252 + return; 4253 + } 4254 + 4255 + this._lastUpdatedZoomLevel = currZoomLevel; 4256 + const currZoomLevelDiff = currSlide.zoomLevels.initial - currSlide.zoomLevels.secondary; // Initial and secondary zoom levels are almost equal 4257 + 4258 + if (Math.abs(currZoomLevelDiff) < 0.01 || !currSlide.isZoomable()) { 4259 + // disable zoom 4260 + setZoomedIn(template, false); 4261 + template.classList.remove('pswp--zoom-allowed'); 4262 + return; 4263 + } 4264 + 4265 + template.classList.add('pswp--zoom-allowed'); 4266 + const potentialZoomLevel = currZoomLevel === currSlide.zoomLevels.initial ? currSlide.zoomLevels.secondary : currSlide.zoomLevels.initial; 4267 + setZoomedIn(template, potentialZoomLevel <= currZoomLevel); 4268 + 4269 + if (options.imageClickAction === 'zoom' || options.imageClickAction === 'zoom-or-close') { 4270 + template.classList.add('pswp--click-to-zoom'); 4271 + } 4272 + } 4273 + 4274 + } 4275 + 4276 + /** @typedef {import('./slide.js').SlideData} SlideData */ 4277 + 4278 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 4279 + 4280 + /** @typedef {{ x: number; y: number; w: number; innerRect?: { w: number; h: number; x: number; y: number } }} Bounds */ 4281 + 4282 + /** 4283 + * @param {HTMLElement} el 4284 + * @returns Bounds 4285 + */ 4286 + function getBoundsByElement(el) { 4287 + const thumbAreaRect = el.getBoundingClientRect(); 4288 + return { 4289 + x: thumbAreaRect.left, 4290 + y: thumbAreaRect.top, 4291 + w: thumbAreaRect.width 4292 + }; 4293 + } 4294 + /** 4295 + * @param {HTMLElement} el 4296 + * @param {number} imageWidth 4297 + * @param {number} imageHeight 4298 + * @returns Bounds 4299 + */ 4300 + 4301 + 4302 + function getCroppedBoundsByElement(el, imageWidth, imageHeight) { 4303 + const thumbAreaRect = el.getBoundingClientRect(); // fill image into the area 4304 + // (do they same as object-fit:cover does to retrieve coordinates) 4305 + 4306 + const hRatio = thumbAreaRect.width / imageWidth; 4307 + const vRatio = thumbAreaRect.height / imageHeight; 4308 + const fillZoomLevel = hRatio > vRatio ? hRatio : vRatio; 4309 + const offsetX = (thumbAreaRect.width - imageWidth * fillZoomLevel) / 2; 4310 + const offsetY = (thumbAreaRect.height - imageHeight * fillZoomLevel) / 2; 4311 + /** 4312 + * Coordinates of the image, 4313 + * as if it was not cropped, 4314 + * height is calculated automatically 4315 + * 4316 + * @type {Bounds} 4317 + */ 4318 + 4319 + const bounds = { 4320 + x: thumbAreaRect.left + offsetX, 4321 + y: thumbAreaRect.top + offsetY, 4322 + w: imageWidth * fillZoomLevel 4323 + }; // Coordinates of inner crop area 4324 + // relative to the image 4325 + 4326 + bounds.innerRect = { 4327 + w: thumbAreaRect.width, 4328 + h: thumbAreaRect.height, 4329 + x: offsetX, 4330 + y: offsetY 4331 + }; 4332 + return bounds; 4333 + } 4334 + /** 4335 + * Get dimensions of thumbnail image 4336 + * (click on which opens photoswipe or closes photoswipe to) 4337 + * 4338 + * @param {number} index 4339 + * @param {SlideData} itemData 4340 + * @param {PhotoSwipe} instance PhotoSwipe instance 4341 + * @returns {Bounds | undefined} 4342 + */ 4343 + 4344 + 4345 + function getThumbBounds(index, itemData, instance) { 4346 + // legacy event, before filters were introduced 4347 + const event = instance.dispatch('thumbBounds', { 4348 + index, 4349 + itemData, 4350 + instance 4351 + }); // @ts-expect-error 4352 + 4353 + if (event.thumbBounds) { 4354 + // @ts-expect-error 4355 + return event.thumbBounds; 4356 + } 4357 + 4358 + const { 4359 + element 4360 + } = itemData; 4361 + /** @type {Bounds | undefined} */ 4362 + 4363 + let thumbBounds; 4364 + /** @type {HTMLElement | null | undefined} */ 4365 + 4366 + let thumbnail; 4367 + 4368 + if (element && instance.options.thumbSelector !== false) { 4369 + const thumbSelector = instance.options.thumbSelector || 'img'; 4370 + thumbnail = element.matches(thumbSelector) ? element : 4371 + /** @type {HTMLElement | null} */ 4372 + element.querySelector(thumbSelector); 4373 + } 4374 + 4375 + thumbnail = instance.applyFilters('thumbEl', thumbnail, itemData, index); 4376 + 4377 + if (thumbnail) { 4378 + if (!itemData.thumbCropped) { 4379 + thumbBounds = getBoundsByElement(thumbnail); 4380 + } else { 4381 + thumbBounds = getCroppedBoundsByElement(thumbnail, itemData.width || itemData.w || 0, itemData.height || itemData.h || 0); 4382 + } 4383 + } 4384 + 4385 + return instance.applyFilters('thumbBounds', thumbBounds, itemData, index); 4386 + } 4387 + 4388 + /** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */ 4389 + 4390 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 4391 + 4392 + /** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */ 4393 + 4394 + /** @typedef {import('../photoswipe.js').DataSource} DataSource */ 4395 + 4396 + /** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */ 4397 + 4398 + /** @typedef {import('../slide/content.js').default} ContentDefault */ 4399 + 4400 + /** @typedef {import('../slide/slide.js').default} Slide */ 4401 + 4402 + /** @typedef {import('../slide/slide.js').SlideData} SlideData */ 4403 + 4404 + /** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */ 4405 + 4406 + /** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */ 4407 + 4408 + /** 4409 + * Allow adding an arbitrary props to the Content 4410 + * https://photoswipe.com/custom-content/#using-webp-image-format 4411 + * @typedef {ContentDefault & Record<string, any>} Content 4412 + */ 4413 + 4414 + /** @typedef {{ x?: number; y?: number }} Point */ 4415 + 4416 + /** 4417 + * @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/ 4418 + * 4419 + * 4420 + * https://photoswipe.com/adding-ui-elements/ 4421 + * 4422 + * @prop {undefined} uiRegister 4423 + * @prop {{ data: UIElementData }} uiElementCreate 4424 + * 4425 + * 4426 + * https://photoswipe.com/events/#initialization-events 4427 + * 4428 + * @prop {undefined} beforeOpen 4429 + * @prop {undefined} firstUpdate 4430 + * @prop {undefined} initialLayout 4431 + * @prop {undefined} change 4432 + * @prop {undefined} afterInit 4433 + * @prop {undefined} bindEvents 4434 + * 4435 + * 4436 + * https://photoswipe.com/events/#opening-or-closing-transition-events 4437 + * 4438 + * @prop {undefined} openingAnimationStart 4439 + * @prop {undefined} openingAnimationEnd 4440 + * @prop {undefined} closingAnimationStart 4441 + * @prop {undefined} closingAnimationEnd 4442 + * 4443 + * 4444 + * https://photoswipe.com/events/#closing-events 4445 + * 4446 + * @prop {undefined} close 4447 + * @prop {undefined} destroy 4448 + * 4449 + * 4450 + * https://photoswipe.com/events/#pointer-and-gesture-events 4451 + * 4452 + * @prop {{ originalEvent: PointerEvent }} pointerDown 4453 + * @prop {{ originalEvent: PointerEvent }} pointerMove 4454 + * @prop {{ originalEvent: PointerEvent }} pointerUp 4455 + * @prop {{ bgOpacity: number }} pinchClose can be default prevented 4456 + * @prop {{ panY: number }} verticalDrag can be default prevented 4457 + * 4458 + * 4459 + * https://photoswipe.com/events/#slide-content-events 4460 + * 4461 + * @prop {{ content: Content }} contentInit 4462 + * @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented 4463 + * @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented 4464 + * @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete 4465 + * @prop {{ content: Content; slide: Slide }} loadError 4466 + * @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented 4467 + * @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange 4468 + * @prop {{ content: Content }} contentLazyLoad can be default prevented 4469 + * @prop {{ content: Content }} contentAppend can be default prevented 4470 + * @prop {{ content: Content }} contentActivate can be default prevented 4471 + * @prop {{ content: Content }} contentDeactivate can be default prevented 4472 + * @prop {{ content: Content }} contentRemove can be default prevented 4473 + * @prop {{ content: Content }} contentDestroy can be default prevented 4474 + * 4475 + * 4476 + * undocumented 4477 + * 4478 + * @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented 4479 + * @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented 4480 + * @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented 4481 + * @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented 4482 + * 4483 + * @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented 4484 + * @prop {{ x: number; dragging: boolean }} moveMainScroll 4485 + * @prop {{ slide: Slide }} firstZoomPan 4486 + * @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData 4487 + * @prop {undefined} beforeResize 4488 + * @prop {undefined} resize 4489 + * @prop {undefined} viewportSize 4490 + * @prop {undefined} updateScrollOffset 4491 + * @prop {{ slide: Slide }} slideInit 4492 + * @prop {{ slide: Slide }} afterSetContent 4493 + * @prop {{ slide: Slide }} slideLoad 4494 + * @prop {{ slide: Slide }} appendHeavy can be default prevented 4495 + * @prop {{ slide: Slide }} appendHeavyContent 4496 + * @prop {{ slide: Slide }} slideActivate 4497 + * @prop {{ slide: Slide }} slideDeactivate 4498 + * @prop {{ slide: Slide }} slideDestroy 4499 + * @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo 4500 + * @prop {{ slide: Slide }} zoomPanUpdate 4501 + * @prop {{ slide: Slide }} initialZoomPan 4502 + * @prop {{ slide: Slide }} calcSlideSize 4503 + * @prop {undefined} resolutionChanged 4504 + * @prop {{ originalEvent: WheelEvent }} wheel can be default prevented 4505 + * @prop {{ content: Content }} contentAppendImage can be default prevented 4506 + * @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented 4507 + * @prop {undefined} lazyLoad 4508 + * @prop {{ slide: Slide }} calcBounds 4509 + * @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate 4510 + * 4511 + * 4512 + * legacy 4513 + * 4514 + * @prop {undefined} init 4515 + * @prop {undefined} initialZoomIn 4516 + * @prop {undefined} initialZoomOut 4517 + * @prop {undefined} initialZoomInEnd 4518 + * @prop {undefined} initialZoomOutEnd 4519 + * @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems 4520 + * @prop {{ itemData: SlideData; index: number }} itemData 4521 + * @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds 4522 + */ 4523 + 4524 + /** 4525 + * @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/ 4526 + * 4527 + * @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems 4528 + * Modify the total amount of slides. Example on Data sources page. 4529 + * https://photoswipe.com/filters/#numitems 4530 + * 4531 + * @prop {(itemData: SlideData, index: number) => SlideData} itemData 4532 + * Modify slide item data. Example on Data sources page. 4533 + * https://photoswipe.com/filters/#itemdata 4534 + * 4535 + * @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData 4536 + * Modify item data when it's parsed from DOM element. Example on Data sources page. 4537 + * https://photoswipe.com/filters/#domitemdata 4538 + * 4539 + * @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex 4540 + * Modify clicked gallery item index. 4541 + * https://photoswipe.com/filters/#clickedindex 4542 + * 4543 + * @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc 4544 + * Modify placeholder image source. 4545 + * https://photoswipe.com/filters/#placeholdersrc 4546 + * 4547 + * @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading 4548 + * Modify if the content is currently loading. 4549 + * https://photoswipe.com/filters/#iscontentloading 4550 + * 4551 + * @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable 4552 + * Modify if the content can be zoomed. 4553 + * https://photoswipe.com/filters/#iscontentzoomable 4554 + * 4555 + * @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder 4556 + * Modify if the placeholder should be used for the content. 4557 + * https://photoswipe.com/filters/#usecontentplaceholder 4558 + * 4559 + * @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder 4560 + * Modify if the placeholder should be kept after the content is loaded. 4561 + * https://photoswipe.com/filters/#iskeepingplaceholder 4562 + * 4563 + * 4564 + * @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement 4565 + * Modify an element when the content has error state (for example, if image cannot be loaded). 4566 + * https://photoswipe.com/filters/#contenterrorelement 4567 + * 4568 + * @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement 4569 + * Modify a UI element that's being created. 4570 + * https://photoswipe.com/filters/#uielement 4571 + * 4572 + * @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl 4573 + * Modify the thumbnail element from which opening zoom animation starts or ends. 4574 + * https://photoswipe.com/filters/#thumbel 4575 + * 4576 + * @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds 4577 + * Modify the thumbnail bounds from which opening zoom animation starts or ends. 4578 + * https://photoswipe.com/filters/#thumbbounds 4579 + * 4580 + * @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth 4581 + * 4582 + * @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent 4583 + * 4584 + */ 4585 + 4586 + /** 4587 + * @template {keyof PhotoSwipeFiltersMap} T 4588 + * @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter 4589 + */ 4590 + 4591 + /** 4592 + * @template {keyof PhotoSwipeEventsMap} T 4593 + * @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent<T> : PhotoSwipeEvent<T> & PhotoSwipeEventsMap[T]} AugmentedEvent 4594 + */ 4595 + 4596 + /** 4597 + * @template {keyof PhotoSwipeEventsMap} T 4598 + * @typedef {(event: AugmentedEvent<T>) => void} EventCallback 4599 + */ 4600 + 4601 + /** 4602 + * Base PhotoSwipe event object 4603 + * 4604 + * @template {keyof PhotoSwipeEventsMap} T 4605 + */ 4606 + class PhotoSwipeEvent { 4607 + /** 4608 + * @param {T} type 4609 + * @param {PhotoSwipeEventsMap[T]} [details] 4610 + */ 4611 + constructor(type, details) { 4612 + this.type = type; 4613 + this.defaultPrevented = false; 4614 + 4615 + if (details) { 4616 + Object.assign(this, details); 4617 + } 4618 + } 4619 + 4620 + preventDefault() { 4621 + this.defaultPrevented = true; 4622 + } 4623 + 4624 + } 4625 + /** 4626 + * PhotoSwipe base class that can listen and dispatch for events. 4627 + * Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js 4628 + */ 4629 + 4630 + 4631 + class Eventable { 4632 + constructor() { 4633 + /** 4634 + * @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent<T>) => void)[] }} 4635 + */ 4636 + this._listeners = {}; 4637 + /** 4638 + * @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter<T>[] }} 4639 + */ 4640 + 4641 + this._filters = {}; 4642 + /** @type {PhotoSwipe | undefined} */ 4643 + 4644 + this.pswp = undefined; 4645 + /** @type {PhotoSwipeOptions | undefined} */ 4646 + 4647 + this.options = undefined; 4648 + } 4649 + /** 4650 + * @template {keyof PhotoSwipeFiltersMap} T 4651 + * @param {T} name 4652 + * @param {PhotoSwipeFiltersMap[T]} fn 4653 + * @param {number} priority 4654 + */ 4655 + 4656 + 4657 + addFilter(name, fn, priority = 100) { 4658 + var _this$_filters$name, _this$_filters$name2, _this$pswp; 4659 + 4660 + if (!this._filters[name]) { 4661 + this._filters[name] = []; 4662 + } 4663 + 4664 + (_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({ 4665 + fn, 4666 + priority 4667 + }); 4668 + (_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority); 4669 + (_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority); 4670 + } 4671 + /** 4672 + * @template {keyof PhotoSwipeFiltersMap} T 4673 + * @param {T} name 4674 + * @param {PhotoSwipeFiltersMap[T]} fn 4675 + */ 4676 + 4677 + 4678 + removeFilter(name, fn) { 4679 + if (this._filters[name]) { 4680 + // @ts-expect-error 4681 + this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn); 4682 + } 4683 + 4684 + if (this.pswp) { 4685 + this.pswp.removeFilter(name, fn); 4686 + } 4687 + } 4688 + /** 4689 + * @template {keyof PhotoSwipeFiltersMap} T 4690 + * @param {T} name 4691 + * @param {Parameters<PhotoSwipeFiltersMap[T]>} args 4692 + * @returns {Parameters<PhotoSwipeFiltersMap[T]>[0]} 4693 + */ 4694 + 4695 + 4696 + applyFilters(name, ...args) { 4697 + var _this$_filters$name3; 4698 + 4699 + (_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => { 4700 + // @ts-expect-error 4701 + args[0] = filter.fn.apply(this, args); 4702 + }); 4703 + return args[0]; 4704 + } 4705 + /** 4706 + * @template {keyof PhotoSwipeEventsMap} T 4707 + * @param {T} name 4708 + * @param {EventCallback<T>} fn 4709 + */ 4710 + 4711 + 4712 + on(name, fn) { 4713 + var _this$_listeners$name, _this$pswp2; 4714 + 4715 + if (!this._listeners[name]) { 4716 + this._listeners[name] = []; 4717 + } 4718 + 4719 + (_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox, 4720 + // also bind events to PhotoSwipe Core, 4721 + // if it's open. 4722 + 4723 + (_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn); 4724 + } 4725 + /** 4726 + * @template {keyof PhotoSwipeEventsMap} T 4727 + * @param {T} name 4728 + * @param {EventCallback<T>} fn 4729 + */ 4730 + 4731 + 4732 + off(name, fn) { 4733 + var _this$pswp3; 4734 + 4735 + if (this._listeners[name]) { 4736 + // @ts-expect-error 4737 + this._listeners[name] = this._listeners[name].filter(listener => fn !== listener); 4738 + } 4739 + 4740 + (_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn); 4741 + } 4742 + /** 4743 + * @template {keyof PhotoSwipeEventsMap} T 4744 + * @param {T} name 4745 + * @param {PhotoSwipeEventsMap[T]} [details] 4746 + * @returns {AugmentedEvent<T>} 4747 + */ 4748 + 4749 + 4750 + dispatch(name, details) { 4751 + var _this$_listeners$name2; 4752 + 4753 + if (this.pswp) { 4754 + return this.pswp.dispatch(name, details); 4755 + } 4756 + 4757 + const event = 4758 + /** @type {AugmentedEvent<T>} */ 4759 + new PhotoSwipeEvent(name, details); 4760 + (_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => { 4761 + listener.call(this, event); 4762 + }); 4763 + return event; 4764 + } 4765 + 4766 + } 4767 + 4768 + class Placeholder { 4769 + /** 4770 + * @param {string | false} imageSrc 4771 + * @param {HTMLElement} container 4772 + */ 4773 + constructor(imageSrc, container) { 4774 + // Create placeholder 4775 + // (stretched thumbnail or simple div behind the main image) 4776 + 4777 + /** @type {HTMLImageElement | HTMLDivElement | null} */ 4778 + this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container); 4779 + 4780 + if (imageSrc) { 4781 + const imgEl = 4782 + /** @type {HTMLImageElement} */ 4783 + this.element; 4784 + imgEl.decoding = 'async'; 4785 + imgEl.alt = ''; 4786 + imgEl.src = imageSrc; 4787 + imgEl.setAttribute('role', 'presentation'); 4788 + } 4789 + 4790 + this.element.setAttribute('aria-hidden', 'true'); 4791 + } 4792 + /** 4793 + * @param {number} width 4794 + * @param {number} height 4795 + */ 4796 + 4797 + 4798 + setDisplayedSize(width, height) { 4799 + if (!this.element) { 4800 + return; 4801 + } 4802 + 4803 + if (this.element.tagName === 'IMG') { 4804 + // Use transform scale() to modify img placeholder size 4805 + // (instead of changing width/height directly). 4806 + // This helps with performance, specifically in iOS15 Safari. 4807 + setWidthHeight(this.element, 250, 'auto'); 4808 + this.element.style.transformOrigin = '0 0'; 4809 + this.element.style.transform = toTransformString(0, 0, width / 250); 4810 + } else { 4811 + setWidthHeight(this.element, width, height); 4812 + } 4813 + } 4814 + 4815 + destroy() { 4816 + var _this$element; 4817 + 4818 + if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) { 4819 + this.element.remove(); 4820 + } 4821 + 4822 + this.element = null; 4823 + } 4824 + 4825 + } 4826 + 4827 + /** @typedef {import('./slide.js').default} Slide */ 4828 + 4829 + /** @typedef {import('./slide.js').SlideData} SlideData */ 4830 + 4831 + /** @typedef {import('../core/base.js').default} PhotoSwipeBase */ 4832 + 4833 + /** @typedef {import('../util/util.js').LoadState} LoadState */ 4834 + 4835 + class Content { 4836 + /** 4837 + * @param {SlideData} itemData Slide data 4838 + * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance 4839 + * @param {number} index 4840 + */ 4841 + constructor(itemData, instance, index) { 4842 + this.instance = instance; 4843 + this.data = itemData; 4844 + this.index = index; 4845 + /** @type {HTMLImageElement | HTMLDivElement | undefined} */ 4846 + 4847 + this.element = undefined; 4848 + /** @type {Placeholder | undefined} */ 4849 + 4850 + this.placeholder = undefined; 4851 + /** @type {Slide | undefined} */ 4852 + 4853 + this.slide = undefined; 4854 + this.displayedImageWidth = 0; 4855 + this.displayedImageHeight = 0; 4856 + this.width = Number(this.data.w) || Number(this.data.width) || 0; 4857 + this.height = Number(this.data.h) || Number(this.data.height) || 0; 4858 + this.isAttached = false; 4859 + this.hasSlide = false; 4860 + this.isDecoding = false; 4861 + /** @type {LoadState} */ 4862 + 4863 + this.state = LOAD_STATE.IDLE; 4864 + 4865 + if (this.data.type) { 4866 + this.type = this.data.type; 4867 + } else if (this.data.src) { 4868 + this.type = 'image'; 4869 + } else { 4870 + this.type = 'html'; 4871 + } 4872 + 4873 + this.instance.dispatch('contentInit', { 4874 + content: this 4875 + }); 4876 + } 4877 + 4878 + removePlaceholder() { 4879 + if (this.placeholder && !this.keepPlaceholder()) { 4880 + // With delay, as image might be loaded, but not rendered 4881 + setTimeout(() => { 4882 + if (this.placeholder) { 4883 + this.placeholder.destroy(); 4884 + this.placeholder = undefined; 4885 + } 4886 + }, 1000); 4887 + } 4888 + } 4889 + /** 4890 + * Preload content 4891 + * 4892 + * @param {boolean} isLazy 4893 + * @param {boolean} [reload] 4894 + */ 4895 + 4896 + 4897 + load(isLazy, reload) { 4898 + if (this.slide && this.usePlaceholder()) { 4899 + if (!this.placeholder) { 4900 + const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use image-based placeholder only for the first slide, 4901 + // as rendering (even small stretched thumbnail) is an expensive operation 4902 + this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this); 4903 + this.placeholder = new Placeholder(placeholderSrc, this.slide.container); 4904 + } else { 4905 + const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created 4906 + 4907 + if (placeholderEl && !placeholderEl.parentElement) { 4908 + this.slide.container.prepend(placeholderEl); 4909 + } 4910 + } 4911 + } 4912 + 4913 + if (this.element && !reload) { 4914 + return; 4915 + } 4916 + 4917 + if (this.instance.dispatch('contentLoad', { 4918 + content: this, 4919 + isLazy 4920 + }).defaultPrevented) { 4921 + return; 4922 + } 4923 + 4924 + if (this.isImageContent()) { 4925 + this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it. 4926 + // Due to Safari feature, we must define sizes before srcset. 4927 + 4928 + if (this.displayedImageWidth) { 4929 + this.loadImage(isLazy); 4930 + } 4931 + } else { 4932 + this.element = createElement('pswp__content', 'div'); 4933 + this.element.innerHTML = this.data.html || ''; 4934 + } 4935 + 4936 + if (reload && this.slide) { 4937 + this.slide.updateContentSize(true); 4938 + } 4939 + } 4940 + /** 4941 + * Preload image 4942 + * 4943 + * @param {boolean} isLazy 4944 + */ 4945 + 4946 + 4947 + loadImage(isLazy) { 4948 + var _this$data$src, _this$data$alt; 4949 + 4950 + if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', { 4951 + content: this, 4952 + isLazy 4953 + }).defaultPrevented) { 4954 + return; 4955 + } 4956 + 4957 + const imageElement = 4958 + /** @type HTMLImageElement */ 4959 + this.element; 4960 + this.updateSrcsetSizes(); 4961 + 4962 + if (this.data.srcset) { 4963 + imageElement.srcset = this.data.srcset; 4964 + } 4965 + 4966 + imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : ''; 4967 + imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : ''; 4968 + this.state = LOAD_STATE.LOADING; 4969 + 4970 + if (imageElement.complete) { 4971 + this.onLoaded(); 4972 + } else { 4973 + imageElement.onload = () => { 4974 + this.onLoaded(); 4975 + }; 4976 + 4977 + imageElement.onerror = () => { 4978 + this.onError(); 4979 + }; 4980 + } 4981 + } 4982 + /** 4983 + * Assign slide to content 4984 + * 4985 + * @param {Slide} slide 4986 + */ 4987 + 4988 + 4989 + setSlide(slide) { 4990 + this.slide = slide; 4991 + this.hasSlide = true; 4992 + this.instance = slide.pswp; // todo: do we need to unset slide? 4993 + } 4994 + /** 4995 + * Content load success handler 4996 + */ 4997 + 4998 + 4999 + onLoaded() { 5000 + this.state = LOAD_STATE.LOADED; 5001 + 5002 + if (this.slide && this.element) { 5003 + this.instance.dispatch('loadComplete', { 5004 + slide: this.slide, 5005 + content: this 5006 + }); // if content is reloaded 5007 + 5008 + if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) { 5009 + this.append(); 5010 + this.slide.updateContentSize(true); 5011 + } 5012 + 5013 + if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) { 5014 + this.removePlaceholder(); 5015 + } 5016 + } 5017 + } 5018 + /** 5019 + * Content load error handler 5020 + */ 5021 + 5022 + 5023 + onError() { 5024 + this.state = LOAD_STATE.ERROR; 5025 + 5026 + if (this.slide) { 5027 + this.displayError(); 5028 + this.instance.dispatch('loadComplete', { 5029 + slide: this.slide, 5030 + isError: true, 5031 + content: this 5032 + }); 5033 + this.instance.dispatch('loadError', { 5034 + slide: this.slide, 5035 + content: this 5036 + }); 5037 + } 5038 + } 5039 + /** 5040 + * @returns {Boolean} If the content is currently loading 5041 + */ 5042 + 5043 + 5044 + isLoading() { 5045 + return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this); 5046 + } 5047 + /** 5048 + * @returns {Boolean} If the content is in error state 5049 + */ 5050 + 5051 + 5052 + isError() { 5053 + return this.state === LOAD_STATE.ERROR; 5054 + } 5055 + /** 5056 + * @returns {boolean} If the content is image 5057 + */ 5058 + 5059 + 5060 + isImageContent() { 5061 + return this.type === 'image'; 5062 + } 5063 + /** 5064 + * Update content size 5065 + * 5066 + * @param {Number} width 5067 + * @param {Number} height 5068 + */ 5069 + 5070 + 5071 + setDisplayedSize(width, height) { 5072 + if (!this.element) { 5073 + return; 5074 + } 5075 + 5076 + if (this.placeholder) { 5077 + this.placeholder.setDisplayedSize(width, height); 5078 + } 5079 + 5080 + if (this.instance.dispatch('contentResize', { 5081 + content: this, 5082 + width, 5083 + height 5084 + }).defaultPrevented) { 5085 + return; 5086 + } 5087 + 5088 + setWidthHeight(this.element, width, height); 5089 + 5090 + if (this.isImageContent() && !this.isError()) { 5091 + const isInitialSizeUpdate = !this.displayedImageWidth && width; 5092 + this.displayedImageWidth = width; 5093 + this.displayedImageHeight = height; 5094 + 5095 + if (isInitialSizeUpdate) { 5096 + this.loadImage(false); 5097 + } else { 5098 + this.updateSrcsetSizes(); 5099 + } 5100 + 5101 + if (this.slide) { 5102 + this.instance.dispatch('imageSizeChange', { 5103 + slide: this.slide, 5104 + width, 5105 + height, 5106 + content: this 5107 + }); 5108 + } 5109 + } 5110 + } 5111 + /** 5112 + * @returns {boolean} If the content can be zoomed 5113 + */ 5114 + 5115 + 5116 + isZoomable() { 5117 + return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this); 5118 + } 5119 + /** 5120 + * Update image srcset sizes attribute based on width and height 5121 + */ 5122 + 5123 + 5124 + updateSrcsetSizes() { 5125 + // Handle srcset sizes attribute. 5126 + // 5127 + // Never lower quality, if it was increased previously. 5128 + // Chrome does this automatically, Firefox and Safari do not, 5129 + // so we store largest used size in dataset. 5130 + if (!this.isImageContent() || !this.element || !this.data.srcset) { 5131 + return; 5132 + } 5133 + 5134 + const image = 5135 + /** @type HTMLImageElement */ 5136 + this.element; 5137 + const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this); 5138 + 5139 + if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) { 5140 + image.sizes = sizesWidth + 'px'; 5141 + image.dataset.largestUsedSize = String(sizesWidth); 5142 + } 5143 + } 5144 + /** 5145 + * @returns {boolean} If content should use a placeholder (from msrc by default) 5146 + */ 5147 + 5148 + 5149 + usePlaceholder() { 5150 + return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this); 5151 + } 5152 + /** 5153 + * Preload content with lazy-loading param 5154 + */ 5155 + 5156 + 5157 + lazyLoad() { 5158 + if (this.instance.dispatch('contentLazyLoad', { 5159 + content: this 5160 + }).defaultPrevented) { 5161 + return; 5162 + } 5163 + 5164 + this.load(true); 5165 + } 5166 + /** 5167 + * @returns {boolean} If placeholder should be kept after content is loaded 5168 + */ 5169 + 5170 + 5171 + keepPlaceholder() { 5172 + return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this); 5173 + } 5174 + /** 5175 + * Destroy the content 5176 + */ 5177 + 5178 + 5179 + destroy() { 5180 + this.hasSlide = false; 5181 + this.slide = undefined; 5182 + 5183 + if (this.instance.dispatch('contentDestroy', { 5184 + content: this 5185 + }).defaultPrevented) { 5186 + return; 5187 + } 5188 + 5189 + this.remove(); 5190 + 5191 + if (this.placeholder) { 5192 + this.placeholder.destroy(); 5193 + this.placeholder = undefined; 5194 + } 5195 + 5196 + if (this.isImageContent() && this.element) { 5197 + this.element.onload = null; 5198 + this.element.onerror = null; 5199 + this.element = undefined; 5200 + } 5201 + } 5202 + /** 5203 + * Display error message 5204 + */ 5205 + 5206 + 5207 + displayError() { 5208 + if (this.slide) { 5209 + var _this$instance$option, _this$instance$option2; 5210 + 5211 + let errorMsgEl = createElement('pswp__error-msg', 'div'); 5212 + errorMsgEl.innerText = (_this$instance$option = (_this$instance$option2 = this.instance.options) === null || _this$instance$option2 === void 0 ? void 0 : _this$instance$option2.errorMsg) !== null && _this$instance$option !== void 0 ? _this$instance$option : ''; 5213 + errorMsgEl = 5214 + /** @type {HTMLDivElement} */ 5215 + this.instance.applyFilters('contentErrorElement', errorMsgEl, this); 5216 + this.element = createElement('pswp__content pswp__error-msg-container', 'div'); 5217 + this.element.appendChild(errorMsgEl); 5218 + this.slide.container.innerText = ''; 5219 + this.slide.container.appendChild(this.element); 5220 + this.slide.updateContentSize(true); 5221 + this.removePlaceholder(); 5222 + } 5223 + } 5224 + /** 5225 + * Append the content 5226 + */ 5227 + 5228 + 5229 + append() { 5230 + if (this.isAttached || !this.element) { 5231 + return; 5232 + } 5233 + 5234 + this.isAttached = true; 5235 + 5236 + if (this.state === LOAD_STATE.ERROR) { 5237 + this.displayError(); 5238 + return; 5239 + } 5240 + 5241 + if (this.instance.dispatch('contentAppend', { 5242 + content: this 5243 + }).defaultPrevented) { 5244 + return; 5245 + } 5246 + 5247 + const supportsDecode = ('decode' in this.element); 5248 + 5249 + if (this.isImageContent()) { 5250 + // Use decode() on nearby slides 5251 + // 5252 + // Nearby slide images are in DOM and not hidden via display:none. 5253 + // However, they are placed offscreen (to the left and right side). 5254 + // 5255 + // Some browsers do not composite the image until it's actually visible, 5256 + // using decode() helps. 5257 + // 5258 + // You might ask "why dont you just decode() and then append all images", 5259 + // that's because I want to show image before it's fully loaded, 5260 + // as browser can render parts of image while it is loading. 5261 + // We do not do this in Safari due to partial loading bug. 5262 + if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) { 5263 + this.isDecoding = true; // purposefully using finally instead of then, 5264 + // as if srcset sizes changes dynamically - it may cause decode error 5265 + 5266 + /** @type {HTMLImageElement} */ 5267 + 5268 + this.element.decode().catch(() => {}).finally(() => { 5269 + this.isDecoding = false; 5270 + this.appendImage(); 5271 + }); 5272 + } else { 5273 + this.appendImage(); 5274 + } 5275 + } else if (this.slide && !this.element.parentNode) { 5276 + this.slide.container.appendChild(this.element); 5277 + } 5278 + } 5279 + /** 5280 + * Activate the slide, 5281 + * active slide is generally the current one, 5282 + * meaning the user can see it. 5283 + */ 5284 + 5285 + 5286 + activate() { 5287 + if (this.instance.dispatch('contentActivate', { 5288 + content: this 5289 + }).defaultPrevented || !this.slide) { 5290 + return; 5291 + } 5292 + 5293 + if (this.isImageContent() && this.isDecoding && !isSafari()) { 5294 + // add image to slide when it becomes active, 5295 + // even if it's not finished decoding 5296 + this.appendImage(); 5297 + } else if (this.isError()) { 5298 + this.load(false, true); // try to reload 5299 + } 5300 + 5301 + if (this.slide.holderElement) { 5302 + this.slide.holderElement.setAttribute('aria-hidden', 'false'); 5303 + } 5304 + } 5305 + /** 5306 + * Deactivate the content 5307 + */ 5308 + 5309 + 5310 + deactivate() { 5311 + this.instance.dispatch('contentDeactivate', { 5312 + content: this 5313 + }); 5314 + 5315 + if (this.slide && this.slide.holderElement) { 5316 + this.slide.holderElement.setAttribute('aria-hidden', 'true'); 5317 + } 5318 + } 5319 + /** 5320 + * Remove the content from DOM 5321 + */ 5322 + 5323 + 5324 + remove() { 5325 + this.isAttached = false; 5326 + 5327 + if (this.instance.dispatch('contentRemove', { 5328 + content: this 5329 + }).defaultPrevented) { 5330 + return; 5331 + } 5332 + 5333 + if (this.element && this.element.parentNode) { 5334 + this.element.remove(); 5335 + } 5336 + 5337 + if (this.placeholder && this.placeholder.element) { 5338 + this.placeholder.element.remove(); 5339 + } 5340 + } 5341 + /** 5342 + * Append the image content to slide container 5343 + */ 5344 + 5345 + 5346 + appendImage() { 5347 + if (!this.isAttached) { 5348 + return; 5349 + } 5350 + 5351 + if (this.instance.dispatch('contentAppendImage', { 5352 + content: this 5353 + }).defaultPrevented) { 5354 + return; 5355 + } // ensure that element exists and is not already appended 5356 + 5357 + 5358 + if (this.slide && this.element && !this.element.parentNode) { 5359 + this.slide.container.appendChild(this.element); 5360 + } 5361 + 5362 + if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) { 5363 + this.removePlaceholder(); 5364 + } 5365 + } 5366 + 5367 + } 5368 + 5369 + /** @typedef {import('./content.js').default} Content */ 5370 + 5371 + /** @typedef {import('./slide.js').default} Slide */ 5372 + 5373 + /** @typedef {import('./slide.js').SlideData} SlideData */ 5374 + 5375 + /** @typedef {import('../core/base.js').default} PhotoSwipeBase */ 5376 + 5377 + /** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 5378 + 5379 + const MIN_SLIDES_TO_CACHE = 5; 5380 + /** 5381 + * Lazy-load an image 5382 + * This function is used both by Lightbox and PhotoSwipe core, 5383 + * thus it can be called before dialog is opened. 5384 + * 5385 + * @param {SlideData} itemData Data about the slide 5386 + * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance 5387 + * @param {number} index 5388 + * @returns {Content} Image that is being decoded or false. 5389 + */ 5390 + 5391 + function lazyLoadData(itemData, instance, index) { 5392 + const content = instance.createContentFromData(itemData, index); 5393 + /** @type {ZoomLevel | undefined} */ 5394 + 5395 + let zoomLevel; 5396 + const { 5397 + options 5398 + } = instance; // We need to know dimensions of the image to preload it, 5399 + // as it might use srcset, and we need to define sizes 5400 + 5401 + if (options) { 5402 + zoomLevel = new ZoomLevel(options, itemData, -1); 5403 + let viewportSize; 5404 + 5405 + if (instance.pswp) { 5406 + viewportSize = instance.pswp.viewportSize; 5407 + } else { 5408 + viewportSize = getViewportSize(options, instance); 5409 + } 5410 + 5411 + const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index); 5412 + zoomLevel.update(content.width, content.height, panAreaSize); 5413 + } 5414 + 5415 + content.lazyLoad(); 5416 + 5417 + if (zoomLevel) { 5418 + content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial)); 5419 + } 5420 + 5421 + return content; 5422 + } 5423 + /** 5424 + * Lazy-loads specific slide. 5425 + * This function is used both by Lightbox and PhotoSwipe core, 5426 + * thus it can be called before dialog is opened. 5427 + * 5428 + * By default, it loads image based on viewport size and initial zoom level. 5429 + * 5430 + * @param {number} index Slide index 5431 + * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance 5432 + * @returns {Content | undefined} 5433 + */ 5434 + 5435 + function lazyLoadSlide(index, instance) { 5436 + const itemData = instance.getItemData(index); 5437 + 5438 + if (instance.dispatch('lazyLoadSlide', { 5439 + index, 5440 + itemData 5441 + }).defaultPrevented) { 5442 + return; 5443 + } 5444 + 5445 + return lazyLoadData(itemData, instance, index); 5446 + } 5447 + 5448 + class ContentLoader { 5449 + /** 5450 + * @param {PhotoSwipe} pswp 5451 + */ 5452 + constructor(pswp) { 5453 + this.pswp = pswp; // Total amount of cached images 5454 + 5455 + this.limit = Math.max(pswp.options.preload[0] + pswp.options.preload[1] + 1, MIN_SLIDES_TO_CACHE); 5456 + /** @type {Content[]} */ 5457 + 5458 + this._cachedItems = []; 5459 + } 5460 + /** 5461 + * Lazy load nearby slides based on `preload` option. 5462 + * 5463 + * @param {number} [diff] Difference between slide indexes that was changed recently, or 0. 5464 + */ 5465 + 5466 + 5467 + updateLazy(diff) { 5468 + const { 5469 + pswp 5470 + } = this; 5471 + 5472 + if (pswp.dispatch('lazyLoad').defaultPrevented) { 5473 + return; 5474 + } 5475 + 5476 + const { 5477 + preload 5478 + } = pswp.options; 5479 + const isForward = diff === undefined ? true : diff >= 0; 5480 + let i; // preload[1] - num items to preload in forward direction 5481 + 5482 + for (i = 0; i <= preload[1]; i++) { 5483 + this.loadSlideByIndex(pswp.currIndex + (isForward ? i : -i)); 5484 + } // preload[0] - num items to preload in backward direction 5485 + 5486 + 5487 + for (i = 1; i <= preload[0]; i++) { 5488 + this.loadSlideByIndex(pswp.currIndex + (isForward ? -i : i)); 5489 + } 5490 + } 5491 + /** 5492 + * @param {number} initialIndex 5493 + */ 5494 + 5495 + 5496 + loadSlideByIndex(initialIndex) { 5497 + const index = this.pswp.getLoopedIndex(initialIndex); // try to get cached content 5498 + 5499 + let content = this.getContentByIndex(index); 5500 + 5501 + if (!content) { 5502 + // no cached content, so try to load from scratch: 5503 + content = lazyLoadSlide(index, this.pswp); // if content can be loaded, add it to cache: 5504 + 5505 + if (content) { 5506 + this.addToCache(content); 5507 + } 5508 + } 5509 + } 5510 + /** 5511 + * @param {Slide} slide 5512 + * @returns {Content} 5513 + */ 5514 + 5515 + 5516 + getContentBySlide(slide) { 5517 + let content = this.getContentByIndex(slide.index); 5518 + 5519 + if (!content) { 5520 + // create content if not found in cache 5521 + content = this.pswp.createContentFromData(slide.data, slide.index); 5522 + this.addToCache(content); 5523 + } // assign slide to content 5524 + 5525 + 5526 + content.setSlide(slide); 5527 + return content; 5528 + } 5529 + /** 5530 + * @param {Content} content 5531 + */ 5532 + 5533 + 5534 + addToCache(content) { 5535 + // move to the end of array 5536 + this.removeByIndex(content.index); 5537 + 5538 + this._cachedItems.push(content); 5539 + 5540 + if (this._cachedItems.length > this.limit) { 5541 + // Destroy the first content that's not attached 5542 + const indexToRemove = this._cachedItems.findIndex(item => { 5543 + return !item.isAttached && !item.hasSlide; 5544 + }); 5545 + 5546 + if (indexToRemove !== -1) { 5547 + const removedItem = this._cachedItems.splice(indexToRemove, 1)[0]; 5548 + 5549 + removedItem.destroy(); 5550 + } 5551 + } 5552 + } 5553 + /** 5554 + * Removes an image from cache, does not destroy() it, just removes. 5555 + * 5556 + * @param {number} index 5557 + */ 5558 + 5559 + 5560 + removeByIndex(index) { 5561 + const indexToRemove = this._cachedItems.findIndex(item => item.index === index); 5562 + 5563 + if (indexToRemove !== -1) { 5564 + this._cachedItems.splice(indexToRemove, 1); 5565 + } 5566 + } 5567 + /** 5568 + * @param {number} index 5569 + * @returns {Content | undefined} 5570 + */ 5571 + 5572 + 5573 + getContentByIndex(index) { 5574 + return this._cachedItems.find(content => content.index === index); 5575 + } 5576 + 5577 + destroy() { 5578 + this._cachedItems.forEach(content => content.destroy()); 5579 + 5580 + this._cachedItems = []; 5581 + } 5582 + 5583 + } 5584 + 5585 + /** @typedef {import("../photoswipe.js").default} PhotoSwipe */ 5586 + 5587 + /** @typedef {import("../slide/slide.js").SlideData} SlideData */ 5588 + 5589 + /** 5590 + * PhotoSwipe base class that can retrieve data about every slide. 5591 + * Shared by PhotoSwipe Core and PhotoSwipe Lightbox 5592 + */ 5593 + 5594 + class PhotoSwipeBase extends Eventable { 5595 + /** 5596 + * Get total number of slides 5597 + * 5598 + * @returns {number} 5599 + */ 5600 + getNumItems() { 5601 + var _this$options; 5602 + 5603 + let numItems = 0; 5604 + const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource; 5605 + 5606 + if (dataSource && 'length' in dataSource) { 5607 + // may be an array or just object with length property 5608 + numItems = dataSource.length; 5609 + } else if (dataSource && 'gallery' in dataSource) { 5610 + // query DOM elements 5611 + if (!dataSource.items) { 5612 + dataSource.items = this._getGalleryDOMElements(dataSource.gallery); 5613 + } 5614 + 5615 + if (dataSource.items) { 5616 + numItems = dataSource.items.length; 5617 + } 5618 + } // legacy event, before filters were introduced 5619 + 5620 + 5621 + const event = this.dispatch('numItems', { 5622 + dataSource, 5623 + numItems 5624 + }); 5625 + return this.applyFilters('numItems', event.numItems, dataSource); 5626 + } 5627 + /** 5628 + * @param {SlideData} slideData 5629 + * @param {number} index 5630 + * @returns {Content} 5631 + */ 5632 + 5633 + 5634 + createContentFromData(slideData, index) { 5635 + return new Content(slideData, this, index); 5636 + } 5637 + /** 5638 + * Get item data by index. 5639 + * 5640 + * "item data" should contain normalized information that PhotoSwipe needs to generate a slide. 5641 + * For example, it may contain properties like 5642 + * `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image. 5643 + * 5644 + * @param {number} index 5645 + * @returns {SlideData} 5646 + */ 5647 + 5648 + 5649 + getItemData(index) { 5650 + var _this$options2; 5651 + 5652 + const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource; 5653 + /** @type {SlideData | HTMLElement} */ 5654 + 5655 + let dataSourceItem = {}; 5656 + 5657 + if (Array.isArray(dataSource)) { 5658 + // Datasource is an array of elements 5659 + dataSourceItem = dataSource[index]; 5660 + } else if (dataSource && 'gallery' in dataSource) { 5661 + // dataSource has gallery property, 5662 + // thus it was created by Lightbox, based on 5663 + // gallery and children options 5664 + // query DOM elements 5665 + if (!dataSource.items) { 5666 + dataSource.items = this._getGalleryDOMElements(dataSource.gallery); 5667 + } 5668 + 5669 + dataSourceItem = dataSource.items[index]; 5670 + } 5671 + 5672 + let itemData = dataSourceItem; 5673 + 5674 + if (itemData instanceof Element) { 5675 + itemData = this._domElementToItemData(itemData); 5676 + } // Dispatching the itemData event, 5677 + // it's a legacy verion before filters were introduced 5678 + 5679 + 5680 + const event = this.dispatch('itemData', { 5681 + itemData: itemData || {}, 5682 + index 5683 + }); 5684 + return this.applyFilters('itemData', event.itemData, index); 5685 + } 5686 + /** 5687 + * Get array of gallery DOM elements, 5688 + * based on childSelector and gallery element. 5689 + * 5690 + * @param {HTMLElement} galleryElement 5691 + * @returns {HTMLElement[]} 5692 + */ 5693 + 5694 + 5695 + _getGalleryDOMElements(galleryElement) { 5696 + var _this$options3, _this$options4; 5697 + 5698 + if ((_this$options3 = this.options) !== null && _this$options3 !== void 0 && _this$options3.children || (_this$options4 = this.options) !== null && _this$options4 !== void 0 && _this$options4.childSelector) { 5699 + return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || []; 5700 + } 5701 + 5702 + return [galleryElement]; 5703 + } 5704 + /** 5705 + * Converts DOM element to item data object. 5706 + * 5707 + * @param {HTMLElement} element DOM element 5708 + * @returns {SlideData} 5709 + */ 5710 + 5711 + 5712 + _domElementToItemData(element) { 5713 + /** @type {SlideData} */ 5714 + const itemData = { 5715 + element 5716 + }; 5717 + const linkEl = 5718 + /** @type {HTMLAnchorElement} */ 5719 + element.tagName === 'A' ? element : element.querySelector('a'); 5720 + 5721 + if (linkEl) { 5722 + // src comes from data-pswp-src attribute, 5723 + // if it's empty link href is used 5724 + itemData.src = linkEl.dataset.pswpSrc || linkEl.href; 5725 + 5726 + if (linkEl.dataset.pswpSrcset) { 5727 + itemData.srcset = linkEl.dataset.pswpSrcset; 5728 + } 5729 + 5730 + itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0; 5731 + itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties 5732 + 5733 + itemData.w = itemData.width; 5734 + itemData.h = itemData.height; 5735 + 5736 + if (linkEl.dataset.pswpType) { 5737 + itemData.type = linkEl.dataset.pswpType; 5738 + } 5739 + 5740 + const thumbnailEl = element.querySelector('img'); 5741 + 5742 + if (thumbnailEl) { 5743 + var _thumbnailEl$getAttri; 5744 + 5745 + // msrc is URL to placeholder image that's displayed before large image is loaded 5746 + // by default it's displayed only for the first slide 5747 + itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src; 5748 + itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : ''; 5749 + } 5750 + 5751 + if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) { 5752 + itemData.thumbCropped = true; 5753 + } 5754 + } 5755 + 5756 + return this.applyFilters('domItemData', itemData, element, linkEl); 5757 + } 5758 + /** 5759 + * Lazy-load by slide data 5760 + * 5761 + * @param {SlideData} itemData Data about the slide 5762 + * @param {number} index 5763 + * @returns {Content} Image that is being decoded or false. 5764 + */ 5765 + 5766 + 5767 + lazyLoadData(itemData, index) { 5768 + return lazyLoadData(itemData, this, index); 5769 + } 5770 + 5771 + } 5772 + 5773 + /** @typedef {import('./photoswipe.js').default} PhotoSwipe */ 5774 + 5775 + /** @typedef {import('./slide/get-thumb-bounds.js').Bounds} Bounds */ 5776 + 5777 + /** @typedef {import('./util/animations.js').AnimationProps} AnimationProps */ 5778 + // some browsers do not paint 5779 + // elements which opacity is set to 0, 5780 + // since we need to pre-render elements for the animation - 5781 + // we set it to the minimum amount 5782 + 5783 + const MIN_OPACITY = 0.003; 5784 + /** 5785 + * Manages opening and closing transitions of the PhotoSwipe. 5786 + * 5787 + * It can perform zoom, fade or no transition. 5788 + */ 5789 + 5790 + class Opener { 5791 + /** 5792 + * @param {PhotoSwipe} pswp 5793 + */ 5794 + constructor(pswp) { 5795 + this.pswp = pswp; 5796 + this.isClosed = true; 5797 + this.isOpen = false; 5798 + this.isClosing = false; 5799 + this.isOpening = false; 5800 + /** 5801 + * @private 5802 + * @type {number | false | undefined} 5803 + */ 5804 + 5805 + this._duration = undefined; 5806 + /** @private */ 5807 + 5808 + this._useAnimation = false; 5809 + /** @private */ 5810 + 5811 + this._croppedZoom = false; 5812 + /** @private */ 5813 + 5814 + this._animateRootOpacity = false; 5815 + /** @private */ 5816 + 5817 + this._animateBgOpacity = false; 5818 + /** 5819 + * @private 5820 + * @type { HTMLDivElement | HTMLImageElement | null | undefined } 5821 + */ 5822 + 5823 + this._placeholder = undefined; 5824 + /** 5825 + * @private 5826 + * @type { HTMLDivElement | undefined } 5827 + */ 5828 + 5829 + this._opacityElement = undefined; 5830 + /** 5831 + * @private 5832 + * @type { HTMLDivElement | undefined } 5833 + */ 5834 + 5835 + this._cropContainer1 = undefined; 5836 + /** 5837 + * @private 5838 + * @type { HTMLElement | null | undefined } 5839 + */ 5840 + 5841 + this._cropContainer2 = undefined; 5842 + /** 5843 + * @private 5844 + * @type {Bounds | undefined} 5845 + */ 5846 + 5847 + this._thumbBounds = undefined; 5848 + this._prepareOpen = this._prepareOpen.bind(this); // Override initial zoom and pan position 5849 + 5850 + pswp.on('firstZoomPan', this._prepareOpen); 5851 + } 5852 + 5853 + open() { 5854 + this._prepareOpen(); 5855 + 5856 + this._start(); 5857 + } 5858 + 5859 + close() { 5860 + if (this.isClosed || this.isClosing || this.isOpening) { 5861 + // if we close during opening animation 5862 + // for now do nothing, 5863 + // browsers aren't good at changing the direction of the CSS transition 5864 + return; 5865 + } 5866 + 5867 + const slide = this.pswp.currSlide; 5868 + this.isOpen = false; 5869 + this.isOpening = false; 5870 + this.isClosing = true; 5871 + this._duration = this.pswp.options.hideAnimationDuration; 5872 + 5873 + if (slide && slide.currZoomLevel * slide.width >= this.pswp.options.maxWidthToAnimate) { 5874 + this._duration = 0; 5875 + } 5876 + 5877 + this._applyStartProps(); 5878 + 5879 + setTimeout(() => { 5880 + this._start(); 5881 + }, this._croppedZoom ? 30 : 0); 5882 + } 5883 + /** @private */ 5884 + 5885 + 5886 + _prepareOpen() { 5887 + this.pswp.off('firstZoomPan', this._prepareOpen); 5888 + 5889 + if (!this.isOpening) { 5890 + const slide = this.pswp.currSlide; 5891 + this.isOpening = true; 5892 + this.isClosing = false; 5893 + this._duration = this.pswp.options.showAnimationDuration; 5894 + 5895 + if (slide && slide.zoomLevels.initial * slide.width >= this.pswp.options.maxWidthToAnimate) { 5896 + this._duration = 0; 5897 + } 5898 + 5899 + this._applyStartProps(); 5900 + } 5901 + } 5902 + /** @private */ 5903 + 5904 + 5905 + _applyStartProps() { 5906 + const { 5907 + pswp 5908 + } = this; 5909 + const slide = this.pswp.currSlide; 5910 + const { 5911 + options 5912 + } = pswp; 5913 + 5914 + if (options.showHideAnimationType === 'fade') { 5915 + options.showHideOpacity = true; 5916 + this._thumbBounds = undefined; 5917 + } else if (options.showHideAnimationType === 'none') { 5918 + options.showHideOpacity = false; 5919 + this._duration = 0; 5920 + this._thumbBounds = undefined; 5921 + } else if (this.isOpening && pswp._initialThumbBounds) { 5922 + // Use initial bounds if defined 5923 + this._thumbBounds = pswp._initialThumbBounds; 5924 + } else { 5925 + this._thumbBounds = this.pswp.getThumbBounds(); 5926 + } 5927 + 5928 + this._placeholder = slide === null || slide === void 0 ? void 0 : slide.getPlaceholderElement(); 5929 + pswp.animations.stopAll(); // Discard animations when duration is less than 50ms 5930 + 5931 + this._useAnimation = Boolean(this._duration && this._duration > 50); 5932 + this._animateZoom = Boolean(this._thumbBounds) && (slide === null || slide === void 0 ? void 0 : slide.content.usePlaceholder()) && (!this.isClosing || !pswp.mainScroll.isShifted()); 5933 + 5934 + if (!this._animateZoom) { 5935 + this._animateRootOpacity = true; 5936 + 5937 + if (this.isOpening && slide) { 5938 + slide.zoomAndPanToInitial(); 5939 + slide.applyCurrentZoomPan(); 5940 + } 5941 + } else { 5942 + var _options$showHideOpac; 5943 + 5944 + this._animateRootOpacity = (_options$showHideOpac = options.showHideOpacity) !== null && _options$showHideOpac !== void 0 ? _options$showHideOpac : false; 5945 + } 5946 + 5947 + this._animateBgOpacity = !this._animateRootOpacity && this.pswp.options.bgOpacity > MIN_OPACITY; 5948 + this._opacityElement = this._animateRootOpacity ? pswp.element : pswp.bg; 5949 + 5950 + if (!this._useAnimation) { 5951 + this._duration = 0; 5952 + this._animateZoom = false; 5953 + this._animateBgOpacity = false; 5954 + this._animateRootOpacity = true; 5955 + 5956 + if (this.isOpening) { 5957 + if (pswp.element) { 5958 + pswp.element.style.opacity = String(MIN_OPACITY); 5959 + } 5960 + 5961 + pswp.applyBgOpacity(1); 5962 + } 5963 + 5964 + return; 5965 + } 5966 + 5967 + if (this._animateZoom && this._thumbBounds && this._thumbBounds.innerRect) { 5968 + var _this$pswp$currSlide; 5969 + 5970 + // Properties are used when animation from cropped thumbnail 5971 + this._croppedZoom = true; 5972 + this._cropContainer1 = this.pswp.container; 5973 + this._cropContainer2 = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.holderElement; 5974 + 5975 + if (pswp.container) { 5976 + pswp.container.style.overflow = 'hidden'; 5977 + pswp.container.style.width = pswp.viewportSize.x + 'px'; 5978 + } 5979 + } else { 5980 + this._croppedZoom = false; 5981 + } 5982 + 5983 + if (this.isOpening) { 5984 + // Apply styles before opening transition 5985 + if (this._animateRootOpacity) { 5986 + if (pswp.element) { 5987 + pswp.element.style.opacity = String(MIN_OPACITY); 5988 + } 5989 + 5990 + pswp.applyBgOpacity(1); 5991 + } else { 5992 + if (this._animateBgOpacity && pswp.bg) { 5993 + pswp.bg.style.opacity = String(MIN_OPACITY); 5994 + } 5995 + 5996 + if (pswp.element) { 5997 + pswp.element.style.opacity = '1'; 5998 + } 5999 + } 6000 + 6001 + if (this._animateZoom) { 6002 + this._setClosedStateZoomPan(); 6003 + 6004 + if (this._placeholder) { 6005 + // tell browser that we plan to animate the placeholder 6006 + this._placeholder.style.willChange = 'transform'; // hide placeholder to allow hiding of 6007 + // elements that overlap it (such as icons over the thumbnail) 6008 + 6009 + this._placeholder.style.opacity = String(MIN_OPACITY); 6010 + } 6011 + } 6012 + } else if (this.isClosing) { 6013 + // hide nearby slides to make sure that 6014 + // they are not painted during the transition 6015 + if (pswp.mainScroll.itemHolders[0]) { 6016 + pswp.mainScroll.itemHolders[0].el.style.display = 'none'; 6017 + } 6018 + 6019 + if (pswp.mainScroll.itemHolders[2]) { 6020 + pswp.mainScroll.itemHolders[2].el.style.display = 'none'; 6021 + } 6022 + 6023 + if (this._croppedZoom) { 6024 + if (pswp.mainScroll.x !== 0) { 6025 + // shift the main scroller to zero position 6026 + pswp.mainScroll.resetPosition(); 6027 + pswp.mainScroll.resize(); 6028 + } 6029 + } 6030 + } 6031 + } 6032 + /** @private */ 6033 + 6034 + 6035 + _start() { 6036 + if (this.isOpening && this._useAnimation && this._placeholder && this._placeholder.tagName === 'IMG') { 6037 + // To ensure smooth animation 6038 + // we wait till the current slide image placeholder is decoded, 6039 + // but no longer than 250ms, 6040 + // and no shorter than 50ms 6041 + // (just using requestanimationframe is not enough in Firefox, 6042 + // for some reason) 6043 + new Promise(resolve => { 6044 + let decoded = false; 6045 + let isDelaying = true; 6046 + decodeImage( 6047 + /** @type {HTMLImageElement} */ 6048 + this._placeholder).finally(() => { 6049 + decoded = true; 6050 + 6051 + if (!isDelaying) { 6052 + resolve(true); 6053 + } 6054 + }); 6055 + setTimeout(() => { 6056 + isDelaying = false; 6057 + 6058 + if (decoded) { 6059 + resolve(true); 6060 + } 6061 + }, 50); 6062 + setTimeout(resolve, 250); 6063 + }).finally(() => this._initiate()); 6064 + } else { 6065 + this._initiate(); 6066 + } 6067 + } 6068 + /** @private */ 6069 + 6070 + 6071 + _initiate() { 6072 + var _this$pswp$element, _this$pswp$element2; 6073 + 6074 + (_this$pswp$element = this.pswp.element) === null || _this$pswp$element === void 0 || _this$pswp$element.style.setProperty('--pswp-transition-duration', this._duration + 'ms'); 6075 + this.pswp.dispatch(this.isOpening ? 'openingAnimationStart' : 'closingAnimationStart'); // legacy event 6076 + 6077 + this.pswp.dispatch( 6078 + /** @type {'initialZoomIn' | 'initialZoomOut'} */ 6079 + 'initialZoom' + (this.isOpening ? 'In' : 'Out')); 6080 + (_this$pswp$element2 = this.pswp.element) === null || _this$pswp$element2 === void 0 || _this$pswp$element2.classList.toggle('pswp--ui-visible', this.isOpening); 6081 + 6082 + if (this.isOpening) { 6083 + if (this._placeholder) { 6084 + // unhide the placeholder 6085 + this._placeholder.style.opacity = '1'; 6086 + } 6087 + 6088 + this._animateToOpenState(); 6089 + } else if (this.isClosing) { 6090 + this._animateToClosedState(); 6091 + } 6092 + 6093 + if (!this._useAnimation) { 6094 + this._onAnimationComplete(); 6095 + } 6096 + } 6097 + /** @private */ 6098 + 6099 + 6100 + _onAnimationComplete() { 6101 + const { 6102 + pswp 6103 + } = this; 6104 + this.isOpen = this.isOpening; 6105 + this.isClosed = this.isClosing; 6106 + this.isOpening = false; 6107 + this.isClosing = false; 6108 + pswp.dispatch(this.isOpen ? 'openingAnimationEnd' : 'closingAnimationEnd'); // legacy event 6109 + 6110 + pswp.dispatch( 6111 + /** @type {'initialZoomInEnd' | 'initialZoomOutEnd'} */ 6112 + 'initialZoom' + (this.isOpen ? 'InEnd' : 'OutEnd')); 6113 + 6114 + if (this.isClosed) { 6115 + pswp.destroy(); 6116 + } else if (this.isOpen) { 6117 + var _pswp$currSlide; 6118 + 6119 + if (this._animateZoom && pswp.container) { 6120 + pswp.container.style.overflow = 'visible'; 6121 + pswp.container.style.width = '100%'; 6122 + } 6123 + 6124 + (_pswp$currSlide = pswp.currSlide) === null || _pswp$currSlide === void 0 || _pswp$currSlide.applyCurrentZoomPan(); 6125 + } 6126 + } 6127 + /** @private */ 6128 + 6129 + 6130 + _animateToOpenState() { 6131 + const { 6132 + pswp 6133 + } = this; 6134 + 6135 + if (this._animateZoom) { 6136 + if (this._croppedZoom && this._cropContainer1 && this._cropContainer2) { 6137 + this._animateTo(this._cropContainer1, 'transform', 'translate3d(0,0,0)'); 6138 + 6139 + this._animateTo(this._cropContainer2, 'transform', 'none'); 6140 + } 6141 + 6142 + if (pswp.currSlide) { 6143 + pswp.currSlide.zoomAndPanToInitial(); 6144 + 6145 + this._animateTo(pswp.currSlide.container, 'transform', pswp.currSlide.getCurrentTransform()); 6146 + } 6147 + } 6148 + 6149 + if (this._animateBgOpacity && pswp.bg) { 6150 + this._animateTo(pswp.bg, 'opacity', String(pswp.options.bgOpacity)); 6151 + } 6152 + 6153 + if (this._animateRootOpacity && pswp.element) { 6154 + this._animateTo(pswp.element, 'opacity', '1'); 6155 + } 6156 + } 6157 + /** @private */ 6158 + 6159 + 6160 + _animateToClosedState() { 6161 + const { 6162 + pswp 6163 + } = this; 6164 + 6165 + if (this._animateZoom) { 6166 + this._setClosedStateZoomPan(true); 6167 + } // do not animate opacity if it's already at 0 6168 + 6169 + 6170 + if (this._animateBgOpacity && pswp.bgOpacity > 0.01 && pswp.bg) { 6171 + this._animateTo(pswp.bg, 'opacity', '0'); 6172 + } 6173 + 6174 + if (this._animateRootOpacity && pswp.element) { 6175 + this._animateTo(pswp.element, 'opacity', '0'); 6176 + } 6177 + } 6178 + /** 6179 + * @private 6180 + * @param {boolean} [animate] 6181 + */ 6182 + 6183 + 6184 + _setClosedStateZoomPan(animate) { 6185 + if (!this._thumbBounds) return; 6186 + const { 6187 + pswp 6188 + } = this; 6189 + const { 6190 + innerRect 6191 + } = this._thumbBounds; 6192 + const { 6193 + currSlide, 6194 + viewportSize 6195 + } = pswp; 6196 + 6197 + if (this._croppedZoom && innerRect && this._cropContainer1 && this._cropContainer2) { 6198 + const containerOnePanX = -viewportSize.x + (this._thumbBounds.x - innerRect.x) + innerRect.w; 6199 + const containerOnePanY = -viewportSize.y + (this._thumbBounds.y - innerRect.y) + innerRect.h; 6200 + const containerTwoPanX = viewportSize.x - innerRect.w; 6201 + const containerTwoPanY = viewportSize.y - innerRect.h; 6202 + 6203 + if (animate) { 6204 + this._animateTo(this._cropContainer1, 'transform', toTransformString(containerOnePanX, containerOnePanY)); 6205 + 6206 + this._animateTo(this._cropContainer2, 'transform', toTransformString(containerTwoPanX, containerTwoPanY)); 6207 + } else { 6208 + setTransform(this._cropContainer1, containerOnePanX, containerOnePanY); 6209 + setTransform(this._cropContainer2, containerTwoPanX, containerTwoPanY); 6210 + } 6211 + } 6212 + 6213 + if (currSlide) { 6214 + equalizePoints(currSlide.pan, innerRect || this._thumbBounds); 6215 + currSlide.currZoomLevel = this._thumbBounds.w / currSlide.width; 6216 + 6217 + if (animate) { 6218 + this._animateTo(currSlide.container, 'transform', currSlide.getCurrentTransform()); 6219 + } else { 6220 + currSlide.applyCurrentZoomPan(); 6221 + } 6222 + } 6223 + } 6224 + /** 6225 + * @private 6226 + * @param {HTMLElement} target 6227 + * @param {'transform' | 'opacity'} prop 6228 + * @param {string} propValue 6229 + */ 6230 + 6231 + 6232 + _animateTo(target, prop, propValue) { 6233 + if (!this._duration) { 6234 + target.style[prop] = propValue; 6235 + return; 6236 + } 6237 + 6238 + const { 6239 + animations 6240 + } = this.pswp; 6241 + /** @type {AnimationProps} */ 6242 + 6243 + const animProps = { 6244 + duration: this._duration, 6245 + easing: this.pswp.options.easing, 6246 + onComplete: () => { 6247 + if (!animations.activeAnimations.length) { 6248 + this._onAnimationComplete(); 6249 + } 6250 + }, 6251 + target 6252 + }; 6253 + animProps[prop] = propValue; 6254 + animations.startTransition(animProps); 6255 + } 6256 + 6257 + } 6258 + 6259 + /** 6260 + * @template T 6261 + * @typedef {import('./types.js').Type<T>} Type<T> 6262 + */ 6263 + 6264 + /** @typedef {import('./slide/slide.js').SlideData} SlideData */ 6265 + 6266 + /** @typedef {import('./slide/zoom-level.js').ZoomLevelOption} ZoomLevelOption */ 6267 + 6268 + /** @typedef {import('./ui/ui-element.js').UIElementData} UIElementData */ 6269 + 6270 + /** @typedef {import('./main-scroll.js').ItemHolder} ItemHolder */ 6271 + 6272 + /** @typedef {import('./core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */ 6273 + 6274 + /** @typedef {import('./core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */ 6275 + 6276 + /** @typedef {import('./slide/get-thumb-bounds').Bounds} Bounds */ 6277 + 6278 + /** 6279 + * @template {keyof PhotoSwipeEventsMap} T 6280 + * @typedef {import('./core/eventable.js').EventCallback<T>} EventCallback<T> 6281 + */ 6282 + 6283 + /** 6284 + * @template {keyof PhotoSwipeEventsMap} T 6285 + * @typedef {import('./core/eventable.js').AugmentedEvent<T>} AugmentedEvent<T> 6286 + */ 6287 + 6288 + /** @typedef {{ x: number; y: number; id?: string | number }} Point */ 6289 + 6290 + /** @typedef {{ top: number; bottom: number; left: number; right: number }} Padding */ 6291 + 6292 + /** @typedef {SlideData[]} DataSourceArray */ 6293 + 6294 + /** @typedef {{ gallery: HTMLElement; items?: HTMLElement[] }} DataSourceObject */ 6295 + 6296 + /** @typedef {DataSourceArray | DataSourceObject} DataSource */ 6297 + 6298 + /** @typedef {(point: Point, originalEvent: PointerEvent) => void} ActionFn */ 6299 + 6300 + /** @typedef {'close' | 'next' | 'zoom' | 'zoom-or-close' | 'toggle-controls'} ActionType */ 6301 + 6302 + /** @typedef {Type<PhotoSwipe> | { default: Type<PhotoSwipe> }} PhotoSwipeModule */ 6303 + 6304 + /** @typedef {PhotoSwipeModule | Promise<PhotoSwipeModule> | (() => Promise<PhotoSwipeModule>)} PhotoSwipeModuleOption */ 6305 + 6306 + /** 6307 + * @typedef {string | NodeListOf<HTMLElement> | HTMLElement[] | HTMLElement} ElementProvider 6308 + */ 6309 + 6310 + /** @typedef {Partial<PreparedPhotoSwipeOptions>} PhotoSwipeOptions https://photoswipe.com/options/ */ 6311 + 6312 + /** 6313 + * @typedef {Object} PreparedPhotoSwipeOptions 6314 + * 6315 + * @prop {DataSource} [dataSource] 6316 + * Pass an array of any items via dataSource option. Its length will determine amount of slides 6317 + * (which may be modified further from numItems event). 6318 + * 6319 + * Each item should contain data that you need to generate slide 6320 + * (for image slide it would be src (image URL), width (image width), height, srcset, alt). 6321 + * 6322 + * If these properties are not present in your initial array, you may "pre-parse" each item from itemData filter. 6323 + * 6324 + * @prop {number} bgOpacity 6325 + * Background backdrop opacity, always define it via this option and not via CSS rgba color. 6326 + * 6327 + * @prop {number} spacing 6328 + * Spacing between slides. Defined as ratio relative to the viewport width (0.1 = 10% of viewport). 6329 + * 6330 + * @prop {boolean} allowPanToNext 6331 + * Allow swipe navigation to the next slide when the current slide is zoomed. Does not apply to mouse events. 6332 + * 6333 + * @prop {boolean} loop 6334 + * If set to true you'll be able to swipe from the last to the first image. 6335 + * Option is always false when there are less than 3 slides. 6336 + * 6337 + * @prop {boolean} [wheelToZoom] 6338 + * By default PhotoSwipe zooms image with ctrl-wheel, if you enable this option - image will zoom just via wheel. 6339 + * 6340 + * @prop {boolean} pinchToClose 6341 + * Pinch touch gesture to close the gallery. 6342 + * 6343 + * @prop {boolean} closeOnVerticalDrag 6344 + * Vertical drag gesture to close the PhotoSwipe. 6345 + * 6346 + * @prop {Padding} [padding] 6347 + * Slide area padding (in pixels). 6348 + * 6349 + * @prop {(viewportSize: Point, itemData: SlideData, index: number) => Padding} [paddingFn] 6350 + * The option is checked frequently, so make sure it's performant. Overrides padding option if defined. For example: 6351 + * 6352 + * @prop {number | false} hideAnimationDuration 6353 + * Transition duration in milliseconds, can be 0. 6354 + * 6355 + * @prop {number | false} showAnimationDuration 6356 + * Transition duration in milliseconds, can be 0. 6357 + * 6358 + * @prop {number | false} zoomAnimationDuration 6359 + * Transition duration in milliseconds, can be 0. 6360 + * 6361 + * @prop {string} easing 6362 + * String, 'cubic-bezier(.4,0,.22,1)'. CSS easing function for open/close/zoom transitions. 6363 + * 6364 + * @prop {boolean} escKey 6365 + * Esc key to close. 6366 + * 6367 + * @prop {boolean} arrowKeys 6368 + * Left/right arrow keys for navigation. 6369 + * 6370 + * @prop {boolean} trapFocus 6371 + * Trap focus within PhotoSwipe element while it's open. 6372 + * 6373 + * @prop {boolean} returnFocus 6374 + * Restore focus the last active element after PhotoSwipe is closed. 6375 + * 6376 + * @prop {boolean} clickToCloseNonZoomable 6377 + * If image is not zoomable (for example, smaller than viewport) it can be closed by clicking on it. 6378 + * 6379 + * @prop {ActionType | ActionFn | false} imageClickAction 6380 + * Refer to click and tap actions page. 6381 + * 6382 + * @prop {ActionType | ActionFn | false} bgClickAction 6383 + * Refer to click and tap actions page. 6384 + * 6385 + * @prop {ActionType | ActionFn | false} tapAction 6386 + * Refer to click and tap actions page. 6387 + * 6388 + * @prop {ActionType | ActionFn | false} doubleTapAction 6389 + * Refer to click and tap actions page. 6390 + * 6391 + * @prop {number} preloaderDelay 6392 + * Delay before the loading indicator will be displayed, 6393 + * if image is loaded during it - the indicator will not be displayed at all. Can be zero. 6394 + * 6395 + * @prop {string} indexIndicatorSep 6396 + * Used for slide count indicator ("1 of 10 "). 6397 + * 6398 + * @prop {(options: PhotoSwipeOptions, pswp: PhotoSwipeBase) => Point} [getViewportSizeFn] 6399 + * A function that should return slide viewport width and height, in format {x: 100, y: 100}. 6400 + * 6401 + * @prop {string} errorMsg 6402 + * Message to display when the image wasn't able to load. If you need to display HTML - use contentErrorElement filter. 6403 + * 6404 + * @prop {[number, number]} preload 6405 + * Lazy loading of nearby slides based on direction of movement. Should be an array with two integers, 6406 + * first one - number of items to preload before the current image, second one - after the current image. 6407 + * Two nearby images are always loaded. 6408 + * 6409 + * @prop {string} [mainClass] 6410 + * Class that will be added to the root element of PhotoSwipe, may contain multiple separated by space. 6411 + * Example on Styling page. 6412 + * 6413 + * @prop {HTMLElement} [appendToEl] 6414 + * Element to which PhotoSwipe dialog will be appended when it opens. 6415 + * 6416 + * @prop {number} maxWidthToAnimate 6417 + * Maximum width of image to animate, if initial rendered image width 6418 + * is larger than this value - the opening/closing transition will be automatically disabled. 6419 + * 6420 + * @prop {string} [closeTitle] 6421 + * Translating 6422 + * 6423 + * @prop {string} [zoomTitle] 6424 + * Translating 6425 + * 6426 + * @prop {string} [arrowPrevTitle] 6427 + * Translating 6428 + * 6429 + * @prop {string} [arrowNextTitle] 6430 + * Translating 6431 + * 6432 + * @prop {'zoom' | 'fade' | 'none'} [showHideAnimationType] 6433 + * To adjust opening or closing transition type use lightbox option `showHideAnimationType` (`String`). 6434 + * It supports three values - `zoom` (default), `fade` (default if there is no thumbnail) and `none`. 6435 + * 6436 + * Animations are automatically disabled if user `(prefers-reduced-motion: reduce)`. 6437 + * 6438 + * @prop {number} index 6439 + * Defines start slide index. 6440 + * 6441 + * @prop {(e: MouseEvent) => number} [getClickedIndexFn] 6442 + * 6443 + * @prop {boolean} [arrowPrev] 6444 + * @prop {boolean} [arrowNext] 6445 + * @prop {boolean} [zoom] 6446 + * @prop {boolean} [close] 6447 + * @prop {boolean} [counter] 6448 + * 6449 + * @prop {string} [arrowPrevSVG] 6450 + * @prop {string} [arrowNextSVG] 6451 + * @prop {string} [zoomSVG] 6452 + * @prop {string} [closeSVG] 6453 + * @prop {string} [counterSVG] 6454 + * 6455 + * @prop {string} [arrowPrevTitle] 6456 + * @prop {string} [arrowNextTitle] 6457 + * @prop {string} [zoomTitle] 6458 + * @prop {string} [closeTitle] 6459 + * @prop {string} [counterTitle] 6460 + * 6461 + * @prop {ZoomLevelOption} [initialZoomLevel] 6462 + * @prop {ZoomLevelOption} [secondaryZoomLevel] 6463 + * @prop {ZoomLevelOption} [maxZoomLevel] 6464 + * 6465 + * @prop {boolean} [mouseMovePan] 6466 + * @prop {Point | null} [initialPointerPos] 6467 + * @prop {boolean} [showHideOpacity] 6468 + * 6469 + * @prop {PhotoSwipeModuleOption} [pswpModule] 6470 + * @prop {() => Promise<any>} [openPromise] 6471 + * @prop {boolean} [preloadFirstSlide] 6472 + * @prop {ElementProvider} [gallery] 6473 + * @prop {string} [gallerySelector] 6474 + * @prop {ElementProvider} [children] 6475 + * @prop {string} [childSelector] 6476 + * @prop {string | false} [thumbSelector] 6477 + */ 6478 + 6479 + /** @type {PreparedPhotoSwipeOptions} */ 6480 + 6481 + const defaultOptions = { 6482 + allowPanToNext: true, 6483 + spacing: 0.1, 6484 + loop: true, 6485 + pinchToClose: true, 6486 + closeOnVerticalDrag: true, 6487 + hideAnimationDuration: 333, 6488 + showAnimationDuration: 333, 6489 + zoomAnimationDuration: 333, 6490 + escKey: true, 6491 + arrowKeys: true, 6492 + trapFocus: true, 6493 + returnFocus: true, 6494 + maxWidthToAnimate: 4000, 6495 + clickToCloseNonZoomable: true, 6496 + imageClickAction: 'zoom-or-close', 6497 + bgClickAction: 'close', 6498 + tapAction: 'toggle-controls', 6499 + doubleTapAction: 'zoom', 6500 + indexIndicatorSep: ' / ', 6501 + preloaderDelay: 2000, 6502 + bgOpacity: 0.8, 6503 + index: 0, 6504 + errorMsg: 'The image cannot be loaded', 6505 + preload: [1, 2], 6506 + easing: 'cubic-bezier(.4,0,.22,1)' 6507 + }; 6508 + /** 6509 + * PhotoSwipe Core 6510 + */ 6511 + 6512 + class PhotoSwipe extends PhotoSwipeBase { 6513 + /** 6514 + * @param {PhotoSwipeOptions} [options] 6515 + */ 6516 + constructor(options) { 6517 + super(); 6518 + this.options = this._prepareOptions(options || {}); 6519 + /** 6520 + * offset of viewport relative to document 6521 + * 6522 + * @type {Point} 6523 + */ 6524 + 6525 + this.offset = { 6526 + x: 0, 6527 + y: 0 6528 + }; 6529 + /** 6530 + * @type {Point} 6531 + * @private 6532 + */ 6533 + 6534 + this._prevViewportSize = { 6535 + x: 0, 6536 + y: 0 6537 + }; 6538 + /** 6539 + * Size of scrollable PhotoSwipe viewport 6540 + * 6541 + * @type {Point} 6542 + */ 6543 + 6544 + this.viewportSize = { 6545 + x: 0, 6546 + y: 0 6547 + }; 6548 + /** 6549 + * background (backdrop) opacity 6550 + */ 6551 + 6552 + this.bgOpacity = 1; 6553 + this.currIndex = 0; 6554 + this.potentialIndex = 0; 6555 + this.isOpen = false; 6556 + this.isDestroying = false; 6557 + this.hasMouse = false; 6558 + /** 6559 + * @private 6560 + * @type {SlideData} 6561 + */ 6562 + 6563 + this._initialItemData = {}; 6564 + /** @type {Bounds | undefined} */ 6565 + 6566 + this._initialThumbBounds = undefined; 6567 + /** @type {HTMLDivElement | undefined} */ 6568 + 6569 + this.topBar = undefined; 6570 + /** @type {HTMLDivElement | undefined} */ 6571 + 6572 + this.element = undefined; 6573 + /** @type {HTMLDivElement | undefined} */ 6574 + 6575 + this.template = undefined; 6576 + /** @type {HTMLDivElement | undefined} */ 6577 + 6578 + this.container = undefined; 6579 + /** @type {HTMLElement | undefined} */ 6580 + 6581 + this.scrollWrap = undefined; 6582 + /** @type {Slide | undefined} */ 6583 + 6584 + this.currSlide = undefined; 6585 + this.events = new DOMEvents(); 6586 + this.animations = new Animations(); 6587 + this.mainScroll = new MainScroll(this); 6588 + this.gestures = new Gestures(this); 6589 + this.opener = new Opener(this); 6590 + this.keyboard = new Keyboard(this); 6591 + this.contentLoader = new ContentLoader(this); 6592 + } 6593 + /** @returns {boolean} */ 6594 + 6595 + 6596 + init() { 6597 + if (this.isOpen || this.isDestroying) { 6598 + return false; 6599 + } 6600 + 6601 + this.isOpen = true; 6602 + this.dispatch('init'); // legacy 6603 + 6604 + this.dispatch('beforeOpen'); 6605 + 6606 + this._createMainStructure(); // add classes to the root element of PhotoSwipe 6607 + 6608 + 6609 + let rootClasses = 'pswp--open'; 6610 + 6611 + if (this.gestures.supportsTouch) { 6612 + rootClasses += ' pswp--touch'; 6613 + } 6614 + 6615 + if (this.options.mainClass) { 6616 + rootClasses += ' ' + this.options.mainClass; 6617 + } 6618 + 6619 + if (this.element) { 6620 + this.element.className += ' ' + rootClasses; 6621 + } 6622 + 6623 + this.currIndex = this.options.index || 0; 6624 + this.potentialIndex = this.currIndex; 6625 + this.dispatch('firstUpdate'); // starting index can be modified here 6626 + // initialize scroll wheel handler to block the scroll 6627 + 6628 + this.scrollWheel = new ScrollWheel(this); // sanitize index 6629 + 6630 + if (Number.isNaN(this.currIndex) || this.currIndex < 0 || this.currIndex >= this.getNumItems()) { 6631 + this.currIndex = 0; 6632 + } 6633 + 6634 + if (!this.gestures.supportsTouch) { 6635 + // enable mouse features if no touch support detected 6636 + this.mouseDetected(); 6637 + } // causes forced synchronous layout 6638 + 6639 + 6640 + this.updateSize(); 6641 + this.offset.y = window.pageYOffset; 6642 + this._initialItemData = this.getItemData(this.currIndex); 6643 + this.dispatch('gettingData', { 6644 + index: this.currIndex, 6645 + data: this._initialItemData, 6646 + slide: undefined 6647 + }); // *Layout* - calculate size and position of elements here 6648 + 6649 + this._initialThumbBounds = this.getThumbBounds(); 6650 + this.dispatch('initialLayout'); 6651 + this.on('openingAnimationEnd', () => { 6652 + const { 6653 + itemHolders 6654 + } = this.mainScroll; // Add content to the previous and next slide 6655 + 6656 + if (itemHolders[0]) { 6657 + itemHolders[0].el.style.display = 'block'; 6658 + this.setContent(itemHolders[0], this.currIndex - 1); 6659 + } 6660 + 6661 + if (itemHolders[2]) { 6662 + itemHolders[2].el.style.display = 'block'; 6663 + this.setContent(itemHolders[2], this.currIndex + 1); 6664 + } 6665 + 6666 + this.appendHeavy(); 6667 + this.contentLoader.updateLazy(); 6668 + this.events.add(window, 'resize', this._handlePageResize.bind(this)); 6669 + this.events.add(window, 'scroll', this._updatePageScrollOffset.bind(this)); 6670 + this.dispatch('bindEvents'); 6671 + }); // set content for center slide (first time) 6672 + 6673 + if (this.mainScroll.itemHolders[1]) { 6674 + this.setContent(this.mainScroll.itemHolders[1], this.currIndex); 6675 + } 6676 + 6677 + this.dispatch('change'); 6678 + this.opener.open(); 6679 + this.dispatch('afterInit'); 6680 + return true; 6681 + } 6682 + /** 6683 + * Get looped slide index 6684 + * (for example, -1 will return the last slide) 6685 + * 6686 + * @param {number} index 6687 + * @returns {number} 6688 + */ 6689 + 6690 + 6691 + getLoopedIndex(index) { 6692 + const numSlides = this.getNumItems(); 6693 + 6694 + if (this.options.loop) { 6695 + if (index > numSlides - 1) { 6696 + index -= numSlides; 6697 + } 6698 + 6699 + if (index < 0) { 6700 + index += numSlides; 6701 + } 6702 + } 6703 + 6704 + return clamp(index, 0, numSlides - 1); 6705 + } 6706 + 6707 + appendHeavy() { 6708 + this.mainScroll.itemHolders.forEach(itemHolder => { 6709 + var _itemHolder$slide; 6710 + 6711 + (_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.appendHeavy(); 6712 + }); 6713 + } 6714 + /** 6715 + * Change the slide 6716 + * @param {number} index New index 6717 + */ 6718 + 6719 + 6720 + goTo(index) { 6721 + this.mainScroll.moveIndexBy(this.getLoopedIndex(index) - this.potentialIndex); 6722 + } 6723 + /** 6724 + * Go to the next slide. 6725 + */ 6726 + 6727 + 6728 + next() { 6729 + this.goTo(this.potentialIndex + 1); 6730 + } 6731 + /** 6732 + * Go to the previous slide. 6733 + */ 6734 + 6735 + 6736 + prev() { 6737 + this.goTo(this.potentialIndex - 1); 6738 + } 6739 + /** 6740 + * @see slide/slide.js zoomTo 6741 + * 6742 + * @param {Parameters<Slide['zoomTo']>} args 6743 + */ 6744 + 6745 + 6746 + zoomTo(...args) { 6747 + var _this$currSlide; 6748 + 6749 + (_this$currSlide = this.currSlide) === null || _this$currSlide === void 0 || _this$currSlide.zoomTo(...args); 6750 + } 6751 + /** 6752 + * @see slide/slide.js toggleZoom 6753 + */ 6754 + 6755 + 6756 + toggleZoom() { 6757 + var _this$currSlide2; 6758 + 6759 + (_this$currSlide2 = this.currSlide) === null || _this$currSlide2 === void 0 || _this$currSlide2.toggleZoom(); 6760 + } 6761 + /** 6762 + * Close the gallery. 6763 + * After closing transition ends - destroy it 6764 + */ 6765 + 6766 + 6767 + close() { 6768 + if (!this.opener.isOpen || this.isDestroying) { 6769 + return; 6770 + } 6771 + 6772 + this.isDestroying = true; 6773 + this.dispatch('close'); 6774 + this.events.removeAll(); 6775 + this.opener.close(); 6776 + } 6777 + /** 6778 + * Destroys the gallery: 6779 + * - instantly closes the gallery 6780 + * - unbinds events, 6781 + * - cleans intervals and timeouts 6782 + * - removes elements from DOM 6783 + */ 6784 + 6785 + 6786 + destroy() { 6787 + var _this$element; 6788 + 6789 + if (!this.isDestroying) { 6790 + this.options.showHideAnimationType = 'none'; 6791 + this.close(); 6792 + return; 6793 + } 6794 + 6795 + this.dispatch('destroy'); 6796 + this._listeners = {}; 6797 + 6798 + if (this.scrollWrap) { 6799 + this.scrollWrap.ontouchmove = null; 6800 + this.scrollWrap.ontouchend = null; 6801 + } 6802 + 6803 + (_this$element = this.element) === null || _this$element === void 0 || _this$element.remove(); 6804 + this.mainScroll.itemHolders.forEach(itemHolder => { 6805 + var _itemHolder$slide2; 6806 + 6807 + (_itemHolder$slide2 = itemHolder.slide) === null || _itemHolder$slide2 === void 0 || _itemHolder$slide2.destroy(); 6808 + }); 6809 + this.contentLoader.destroy(); 6810 + this.events.removeAll(); 6811 + } 6812 + /** 6813 + * Refresh/reload content of a slide by its index 6814 + * 6815 + * @param {number} slideIndex 6816 + */ 6817 + 6818 + 6819 + refreshSlideContent(slideIndex) { 6820 + this.contentLoader.removeByIndex(slideIndex); 6821 + this.mainScroll.itemHolders.forEach((itemHolder, i) => { 6822 + var _this$currSlide$index, _this$currSlide3; 6823 + 6824 + let potentialHolderIndex = ((_this$currSlide$index = (_this$currSlide3 = this.currSlide) === null || _this$currSlide3 === void 0 ? void 0 : _this$currSlide3.index) !== null && _this$currSlide$index !== void 0 ? _this$currSlide$index : 0) - 1 + i; 6825 + 6826 + if (this.canLoop()) { 6827 + potentialHolderIndex = this.getLoopedIndex(potentialHolderIndex); 6828 + } 6829 + 6830 + if (potentialHolderIndex === slideIndex) { 6831 + // set the new slide content 6832 + this.setContent(itemHolder, slideIndex, true); // activate the new slide if it's current 6833 + 6834 + if (i === 1) { 6835 + var _itemHolder$slide3; 6836 + 6837 + this.currSlide = itemHolder.slide; 6838 + (_itemHolder$slide3 = itemHolder.slide) === null || _itemHolder$slide3 === void 0 || _itemHolder$slide3.setIsActive(true); 6839 + } 6840 + } 6841 + }); 6842 + this.dispatch('change'); 6843 + } 6844 + /** 6845 + * Set slide content 6846 + * 6847 + * @param {ItemHolder} holder mainScroll.itemHolders array item 6848 + * @param {number} index Slide index 6849 + * @param {boolean} [force] If content should be set even if index wasn't changed 6850 + */ 6851 + 6852 + 6853 + setContent(holder, index, force) { 6854 + if (this.canLoop()) { 6855 + index = this.getLoopedIndex(index); 6856 + } 6857 + 6858 + if (holder.slide) { 6859 + if (holder.slide.index === index && !force) { 6860 + // exit if holder already contains this slide 6861 + // this could be common when just three slides are used 6862 + return; 6863 + } // destroy previous slide 6864 + 6865 + 6866 + holder.slide.destroy(); 6867 + holder.slide = undefined; 6868 + } // exit if no loop and index is out of bounds 6869 + 6870 + 6871 + if (!this.canLoop() && (index < 0 || index >= this.getNumItems())) { 6872 + return; 6873 + } 6874 + 6875 + const itemData = this.getItemData(index); 6876 + holder.slide = new Slide(itemData, index, this); // set current slide 6877 + 6878 + if (index === this.currIndex) { 6879 + this.currSlide = holder.slide; 6880 + } 6881 + 6882 + holder.slide.append(holder.el); 6883 + } 6884 + /** @returns {Point} */ 6885 + 6886 + 6887 + getViewportCenterPoint() { 6888 + return { 6889 + x: this.viewportSize.x / 2, 6890 + y: this.viewportSize.y / 2 6891 + }; 6892 + } 6893 + /** 6894 + * Update size of all elements. 6895 + * Executed on init and on page resize. 6896 + * 6897 + * @param {boolean} [force] Update size even if size of viewport was not changed. 6898 + */ 6899 + 6900 + 6901 + updateSize(force) { 6902 + // let item; 6903 + // let itemIndex; 6904 + if (this.isDestroying) { 6905 + // exit if PhotoSwipe is closed or closing 6906 + // (to avoid errors, as resize event might be delayed) 6907 + return; 6908 + } //const newWidth = this.scrollWrap.clientWidth; 6909 + //const newHeight = this.scrollWrap.clientHeight; 6910 + 6911 + 6912 + const newViewportSize = getViewportSize(this.options, this); 6913 + 6914 + if (!force && pointsEqual(newViewportSize, this._prevViewportSize)) { 6915 + // Exit if dimensions were not changed 6916 + return; 6917 + } //this._prevViewportSize.x = newWidth; 6918 + //this._prevViewportSize.y = newHeight; 6919 + 6920 + 6921 + equalizePoints(this._prevViewportSize, newViewportSize); 6922 + this.dispatch('beforeResize'); 6923 + equalizePoints(this.viewportSize, this._prevViewportSize); 6924 + 6925 + this._updatePageScrollOffset(); 6926 + 6927 + this.dispatch('viewportSize'); // Resize slides only after opener animation is finished 6928 + // and don't re-calculate size on inital size update 6929 + 6930 + this.mainScroll.resize(this.opener.isOpen); 6931 + 6932 + if (!this.hasMouse && window.matchMedia('(any-hover: hover)').matches) { 6933 + this.mouseDetected(); 6934 + } 6935 + 6936 + this.dispatch('resize'); 6937 + } 6938 + /** 6939 + * @param {number} opacity 6940 + */ 6941 + 6942 + 6943 + applyBgOpacity(opacity) { 6944 + this.bgOpacity = Math.max(opacity, 0); 6945 + 6946 + if (this.bg) { 6947 + this.bg.style.opacity = String(this.bgOpacity * this.options.bgOpacity); 6948 + } 6949 + } 6950 + /** 6951 + * Whether mouse is detected 6952 + */ 6953 + 6954 + 6955 + mouseDetected() { 6956 + if (!this.hasMouse) { 6957 + var _this$element2; 6958 + 6959 + this.hasMouse = true; 6960 + (_this$element2 = this.element) === null || _this$element2 === void 0 || _this$element2.classList.add('pswp--has_mouse'); 6961 + } 6962 + } 6963 + /** 6964 + * Page resize event handler 6965 + * 6966 + * @private 6967 + */ 6968 + 6969 + 6970 + _handlePageResize() { 6971 + this.updateSize(); // In iOS webview, if element size depends on document size, 6972 + // it'll be measured incorrectly in resize event 6973 + // 6974 + // https://bugs.webkit.org/show_bug.cgi?id=170595 6975 + // https://hackernoon.com/onresize-event-broken-in-mobile-safari-d8469027bf4d 6976 + 6977 + if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) { 6978 + setTimeout(() => { 6979 + this.updateSize(); 6980 + }, 500); 6981 + } 6982 + } 6983 + /** 6984 + * Page scroll offset is used 6985 + * to get correct coordinates 6986 + * relative to PhotoSwipe viewport. 6987 + * 6988 + * @private 6989 + */ 6990 + 6991 + 6992 + _updatePageScrollOffset() { 6993 + this.setScrollOffset(0, window.pageYOffset); 6994 + } 6995 + /** 6996 + * @param {number} x 6997 + * @param {number} y 6998 + */ 6999 + 7000 + 7001 + setScrollOffset(x, y) { 7002 + this.offset.x = x; 7003 + this.offset.y = y; 7004 + this.dispatch('updateScrollOffset'); 7005 + } 7006 + /** 7007 + * Create main HTML structure of PhotoSwipe, 7008 + * and add it to DOM 7009 + * 7010 + * @private 7011 + */ 7012 + 7013 + 7014 + _createMainStructure() { 7015 + // root DOM element of PhotoSwipe (.pswp) 7016 + this.element = createElement('pswp', 'div'); 7017 + this.element.setAttribute('tabindex', '-1'); 7018 + this.element.setAttribute('role', 'dialog'); // template is legacy prop 7019 + 7020 + this.template = this.element; // Background is added as a separate element, 7021 + // as animating opacity is faster than animating rgba() 7022 + 7023 + this.bg = createElement('pswp__bg', 'div', this.element); 7024 + this.scrollWrap = createElement('pswp__scroll-wrap', 'section', this.element); 7025 + this.container = createElement('pswp__container', 'div', this.scrollWrap); // aria pattern: carousel 7026 + 7027 + this.scrollWrap.setAttribute('aria-roledescription', 'carousel'); 7028 + this.container.setAttribute('aria-live', 'off'); 7029 + this.container.setAttribute('id', 'pswp__items'); 7030 + this.mainScroll.appendHolders(); 7031 + this.ui = new UI(this); 7032 + this.ui.init(); // append to DOM 7033 + 7034 + (this.options.appendToEl || document.body).appendChild(this.element); 7035 + } 7036 + /** 7037 + * Get position and dimensions of small thumbnail 7038 + * {x:,y:,w:} 7039 + * 7040 + * Height is optional (calculated based on the large image) 7041 + * 7042 + * @returns {Bounds | undefined} 7043 + */ 7044 + 7045 + 7046 + getThumbBounds() { 7047 + return getThumbBounds(this.currIndex, this.currSlide ? this.currSlide.data : this._initialItemData, this); 7048 + } 7049 + /** 7050 + * If the PhotoSwipe can have continuous loop 7051 + * @returns Boolean 7052 + */ 7053 + 7054 + 7055 + canLoop() { 7056 + return this.options.loop && this.getNumItems() > 2; 7057 + } 7058 + /** 7059 + * @private 7060 + * @param {PhotoSwipeOptions} options 7061 + * @returns {PreparedPhotoSwipeOptions} 7062 + */ 7063 + 7064 + 7065 + _prepareOptions(options) { 7066 + if (window.matchMedia('(prefers-reduced-motion), (update: slow)').matches) { 7067 + options.showHideAnimationType = 'none'; 7068 + options.zoomAnimationDuration = 0; 7069 + } 7070 + /** @type {PreparedPhotoSwipeOptions} */ 7071 + 7072 + 7073 + return { ...defaultOptions, 7074 + ...options 7075 + }; 7076 + } 7077 + 7078 + } 7079 + 7080 + export { PhotoSwipe as default }; 7081 + //# sourceMappingURL=photoswipe.esm.js.map