vendor: 112,716 bytes, lines 1-4393
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 */ 14function 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 33function 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 47function 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 59function 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 72function 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 84function 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 96function 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 114function setTransform(el, x, y, scale) { 115 el.style.transform = toTransformString(x, y, scale); 116} 117const 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 127function 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 141function 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 149function removeTransitionStyle(el) { 150 setTransitionStyle(el); 151} 152/** 153 * @param {HTMLImageElement} img 154 * @returns {Promise<HTMLImageElement | void>} 155 */ 156 157function 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 176const 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 190function 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 202function 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 226function isSafari() { 227 return !!(navigator.vendor && navigator.vendor.match(/apple/i)); 228} 229 230// Detect passive event listener support 231let supportsPassive = false; 232/* eslint-disable */ 233 234try { 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 253class 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 */ 365function 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 419function 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 445function 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 462class 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 559const 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 575class 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 733class 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 1231const PAN_END_FRICTION = 0.35; 1232const VERTICAL_DRAG_FRICTION = 0.6; // 1 corresponds to the third of viewport height 1233 1234const MIN_RATIO_TO_CLOSE = 0.4; // Minimum speed required to navigate 1235// to next or previous slide 1236 1237const MIN_NEXT_SLIDE_SPEED = 0.5; 1238/** 1239 * @param {number} initialVelocity 1240 * @param {number} decelerationRate 1241 * @returns {number} 1242 */ 1243 1244function project(initialVelocity, decelerationRate) { 1245 return initialVelocity * decelerationRate / (1 - decelerationRate); 1246} 1247/** 1248 * Handles single pointer dragging 1249 */ 1250 1251 1252class 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 1613const UPPER_ZOOM_FRICTION = 0.05; 1614const 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 1624function 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 1630class 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 */ 1919function didTapOnMainContent(event) { 1920 return !! 1921 /** @type {HTMLElement} */ 1922 event.target.closest('.pswp__container'); 1923} 1924/** 1925 * Tap, double-tap handler. 1926 */ 1927 1928 1929class 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 2051const AXIS_SWIPE_HYSTERISIS = 10; //const PAN_END_FRICTION = 0.35; 2052 2053const DOUBLE_TAP_DELAY = 300; // ms 2054 2055const 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 2065class 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 2747const 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 2758class 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 3101const 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 3117const getKeyboardEventKey = (key, isKeySupported) => { 3118 return isKeySupported ? key : KeyboardKeyCodesMap[key]; 3119}; 3120/** 3121 * - Manages keyboard shortcuts. 3122 * - Helps trap focus within photoswipe. 3123 */ 3124 3125 3126class 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 3290const 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 3308class 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 3412const DEFAULT_NATURAL_FREQUENCY = 12; 3413const DEFAULT_DAMPING_RATIO = 0.75; 3414/** 3415 * Spring easing helper 3416 */ 3417 3418class 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 3498class 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 3575class 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 */ 3691class 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 3804function 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 3840class 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 */ 3974function 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 3993const 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 4011const 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 */ 4031const 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 */ 4045const 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 */ 4060const 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 */ 4135const 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 4159function setZoomedIn(el, isZoomedIn) { 4160 el.classList.toggle('pswp--zoomed-in', isZoomedIn); 4161} 4162 4163class 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 */ 4286function 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 4302function 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 4345function 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 */ 4606class PhotoSwipeEvent { 4607 /** 4608 * @param {T} type 4609 * @param {PhotoSwipeEventsMap[T]} [details] 4610 */ 4611 constructor(type, details) { 4612 this.type = type; 4613 this.defaultPrevented = false;
vendor: 19,004 bytes, lines 4614-5370
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 4631class 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 4768class 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 4835class 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
vendor: 10,948 bytes, lines 5371-5774
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 5379const 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 5391function 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 5435function 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 5448class 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 5594class 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 5783const 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 5790class 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}
vendor: 9,634 bytes, lines 5844-6178
5844 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 /**
vendor: 3,250 bytes, lines 6178-6293
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 6481const 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',
vendor: 14,421 bytes, lines 6498-7081
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 6512class 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 7080export { PhotoSwipe as default }; 7081//# sourceMappingURL=photoswipe.esm.js.map
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.