vendor: 112,395 bytes, lines 1-4386
1/*! 2 * PhotoSwipe 5.4.3 - https://photoswipe.com 3 * (c) 2023 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; 2999 } 3000 3001 for (let i = 0; i < diffAbs; i++) { 3002 if (positionDifference > 0) { 3003 tempHolder = this.itemHolders.shift(); 3004 3005 if (tempHolder) { 3006 this.itemHolders[2] = tempHolder; // move first to last 3007 3008 this._containerShiftIndex++; 3009 setTransform(tempHolder.el, (this._containerShiftIndex + 2) * this.slideWidth); 3010 pswp.setContent(tempHolder, pswp.currIndex - diffAbs + i + 2); 3011 } 3012 } else { 3013 tempHolder = this.itemHolders.pop(); 3014 3015 if (tempHolder) { 3016 this.itemHolders.unshift(tempHolder); // move last to first 3017 3018 this._containerShiftIndex--; 3019 setTransform(tempHolder.el, this._containerShiftIndex * this.slideWidth); 3020 pswp.setContent(tempHolder, pswp.currIndex + diffAbs - i - 2); 3021 } 3022 } 3023 } // Reset transfrom every 50ish navigations in one direction. 3024 // 3025 // Otherwise transform will keep growing indefinitely, 3026 // which might cause issues as browsers have a maximum transform limit. 3027 // I wasn't able to reach it, but just to be safe. 3028 // This should not cause noticable lag. 3029 3030 3031 if (Math.abs(this._containerShiftIndex) > 50 && !this.isShifted()) { 3032 this.resetPosition(); 3033 this.resize(); 3034 } // Pan transition might be running (and consntantly updating pan position) 3035 3036 3037 pswp.animations.stopAllPan(); 3038 this.itemHolders.forEach((itemHolder, i) => { 3039 if (itemHolder.slide) { 3040 // Slide in the 2nd holder is always active 3041 itemHolder.slide.setIsActive(i === 1); 3042 } 3043 }); 3044 pswp.currSlide = (_this$itemHolders$ = this.itemHolders[1]) === null || _this$itemHolders$ === void 0 ? void 0 : _this$itemHolders$.slide; 3045 pswp.contentLoader.updateLazy(positionDifference); 3046 3047 if (pswp.currSlide) { 3048 pswp.currSlide.applyCurrentZoomPan(); 3049 } 3050 3051 pswp.dispatch('change'); 3052 } 3053 /** 3054 * Move the X position of the main scroll container 3055 * 3056 * @param {number} x 3057 * @param {boolean} [dragging] 3058 */ 3059 3060 3061 moveTo(x, dragging) { 3062 if (!this.pswp.canLoop() && dragging) { 3063 // Apply friction 3064 let newSlideIndexOffset = (this.slideWidth * this._currPositionIndex - x) / this.slideWidth; 3065 newSlideIndexOffset += this.pswp.currIndex; 3066 const delta = Math.round(x - this.x); 3067 3068 if (newSlideIndexOffset < 0 && delta > 0 || newSlideIndexOffset >= this.pswp.getNumItems() - 1 && delta < 0) { 3069 x = this.x + delta * MAIN_SCROLL_END_FRICTION; 3070 } 3071 } 3072 3073 this.x = x; 3074 3075 if (this.pswp.container) { 3076 setTransform(this.pswp.container, x); 3077 } 3078 3079 this.pswp.dispatch('moveMainScroll', { 3080 x, 3081 dragging: dragging !== null && dragging !== void 0 ? dragging : false 3082 }); 3083 } 3084 3085} 3086 3087/** @typedef {import('./photoswipe.js').default} PhotoSwipe */ 3088 3089/** 3090 * @template T 3091 * @typedef {import('./types.js').Methods<T>} Methods<T> 3092 */ 3093 3094const KeyboardKeyCodesMap = { 3095 Escape: 27, 3096 z: 90, 3097 ArrowLeft: 37, 3098 ArrowUp: 38, 3099 ArrowRight: 39, 3100 ArrowDown: 40, 3101 Tab: 9 3102}; 3103/** 3104 * @template {keyof KeyboardKeyCodesMap} T 3105 * @param {T} key 3106 * @param {boolean} isKeySupported 3107 * @returns {T | number | undefined} 3108 */ 3109 3110const getKeyboardEventKey = (key, isKeySupported) => { 3111 return isKeySupported ? key : KeyboardKeyCodesMap[key]; 3112}; 3113/** 3114 * - Manages keyboard shortcuts. 3115 * - Helps trap focus within photoswipe. 3116 */ 3117 3118 3119class Keyboard { 3120 /** 3121 * @param {PhotoSwipe} pswp 3122 */ 3123 constructor(pswp) { 3124 this.pswp = pswp; 3125 /** @private */ 3126 3127 this._wasFocused = false; 3128 pswp.on('bindEvents', () => { 3129 if (pswp.options.trapFocus) { 3130 // Dialog was likely opened by keyboard if initial point is not defined 3131 if (!pswp.options.initialPointerPos) { 3132 // focus causes layout, 3133 // which causes lag during the animation, 3134 // that's why we delay it until the opener transition ends 3135 this._focusRoot(); 3136 } 3137 3138 pswp.events.add(document, 'focusin', 3139 /** @type EventListener */ 3140 this._onFocusIn.bind(this)); 3141 } 3142 3143 pswp.events.add(document, 'keydown', 3144 /** @type EventListener */ 3145 this._onKeyDown.bind(this)); 3146 }); 3147 const lastActiveElement = 3148 /** @type {HTMLElement} */ 3149 document.activeElement; 3150 pswp.on('destroy', () => { 3151 if (pswp.options.returnFocus && lastActiveElement && this._wasFocused) { 3152 lastActiveElement.focus(); 3153 } 3154 }); 3155 } 3156 /** @private */ 3157 3158 3159 _focusRoot() { 3160 if (!this._wasFocused && this.pswp.element) { 3161 this.pswp.element.focus(); 3162 this._wasFocused = true; 3163 } 3164 } 3165 /** 3166 * @private 3167 * @param {KeyboardEvent} e 3168 */ 3169 3170 3171 _onKeyDown(e) { 3172 const { 3173 pswp 3174 } = this; 3175 3176 if (pswp.dispatch('keydown', { 3177 originalEvent: e 3178 }).defaultPrevented) { 3179 return; 3180 } 3181 3182 if (specialKeyUsed(e)) { 3183 // don't do anything if special key pressed 3184 // to prevent from overriding default browser actions 3185 // for example, in Chrome on Mac cmd+arrow-left returns to previous page 3186 return; 3187 } 3188 /** @type {Methods<PhotoSwipe> | undefined} */ 3189 3190 3191 let keydownAction; 3192 /** @type {'x' | 'y' | undefined} */ 3193 3194 let axis; 3195 let isForward = false; 3196 const isKeySupported = ('key' in e); 3197 3198 switch (isKeySupported ? e.key : e.keyCode) { 3199 case getKeyboardEventKey('Escape', isKeySupported): 3200 if (pswp.options.escKey) { 3201 keydownAction = 'close'; 3202 } 3203 3204 break; 3205 3206 case getKeyboardEventKey('z', isKeySupported): 3207 keydownAction = 'toggleZoom'; 3208 break; 3209 3210 case getKeyboardEventKey('ArrowLeft', isKeySupported): 3211 axis = 'x'; 3212 break; 3213 3214 case getKeyboardEventKey('ArrowUp', isKeySupported): 3215 axis = 'y'; 3216 break; 3217 3218 case getKeyboardEventKey('ArrowRight', isKeySupported): 3219 axis = 'x'; 3220 isForward = true; 3221 break; 3222 3223 case getKeyboardEventKey('ArrowDown', isKeySupported): 3224 isForward = true; 3225 axis = 'y'; 3226 break; 3227 3228 case getKeyboardEventKey('Tab', isKeySupported): 3229 this._focusRoot(); 3230 3231 break; 3232 } // if left/right/top/bottom key 3233 3234 3235 if (axis) { 3236 // prevent page scroll 3237 e.preventDefault(); 3238 const { 3239 currSlide 3240 } = pswp; 3241 3242 if (pswp.options.arrowKeys && axis === 'x' && pswp.getNumItems() > 1) { 3243 keydownAction = isForward ? 'next' : 'prev'; 3244 } else if (currSlide && currSlide.currZoomLevel > currSlide.zoomLevels.fit) { 3245 // up/down arrow keys pan the image vertically 3246 // left/right arrow keys pan horizontally. 3247 // Unless there is only one image, 3248 // or arrowKeys option is disabled 3249 currSlide.pan[axis] += isForward ? -80 : 80; 3250 currSlide.panTo(currSlide.pan.x, currSlide.pan.y); 3251 } 3252 } 3253 3254 if (keydownAction) { 3255 e.preventDefault(); // @ts-ignore 3256 3257 pswp[keydownAction](); 3258 } 3259 } 3260 /** 3261 * Trap focus inside photoswipe 3262 * 3263 * @private 3264 * @param {FocusEvent} e 3265 */ 3266 3267 3268 _onFocusIn(e) { 3269 const { 3270 template 3271 } = this.pswp; 3272 3273 if (template && document !== e.target && template !== e.target && !template.contains( 3274 /** @type {Node} */ 3275 e.target)) { 3276 // focus root element 3277 template.focus(); 3278 } 3279 } 3280 3281} 3282 3283const DEFAULT_EASING = 'cubic-bezier(.4,0,.22,1)'; 3284/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */ 3285 3286/** @typedef {Object} DefaultCssAnimationProps 3287 * 3288 * @prop {HTMLElement} target 3289 * @prop {number} [duration] 3290 * @prop {string} [easing] 3291 * @prop {string} [transform] 3292 * @prop {string} [opacity] 3293 * */ 3294 3295/** @typedef {SharedAnimationProps & DefaultCssAnimationProps} CssAnimationProps */ 3296 3297/** 3298 * Runs CSS transition. 3299 */ 3300 3301class CSSAnimation { 3302 /** 3303 * onComplete can be unpredictable, be careful about current state 3304 * 3305 * @param {CssAnimationProps} props 3306 */ 3307 constructor(props) { 3308 var _props$prop; 3309 3310 this.props = props; 3311 const { 3312 target, 3313 onComplete, 3314 transform, 3315 onFinish = () => {}, 3316 duration = 333, 3317 easing = DEFAULT_EASING 3318 } = props; 3319 this.onFinish = onFinish; // support only transform and opacity 3320 3321 const prop = transform ? 'transform' : 'opacity'; 3322 const propValue = (_props$prop = props[prop]) !== null && _props$prop !== void 0 ? _props$prop : ''; 3323 /** @private */ 3324 3325 this._target = target; 3326 /** @private */ 3327 3328 this._onComplete = onComplete; 3329 /** @private */ 3330 3331 this._finished = false; 3332 /** @private */ 3333 3334 this._onTransitionEnd = this._onTransitionEnd.bind(this); // Using timeout hack to make sure that animation 3335 // starts even if the animated property was changed recently, 3336 // otherwise transitionend might not fire or transition won't start. 3337 // https://drafts.csswg.org/css-transitions/#starting 3338 // 3339 // ¯\_(ツ)_/¯ 3340 3341 /** @private */ 3342 3343 this._helperTimeout = setTimeout(() => { 3344 setTransitionStyle(target, prop, duration, easing); 3345 this._helperTimeout = setTimeout(() => { 3346 target.addEventListener('transitionend', this._onTransitionEnd, false); 3347 target.addEventListener('transitioncancel', this._onTransitionEnd, false); // Safari occasionally does not emit transitionend event 3348 // if element property was modified during the transition, 3349 // which may be caused by resize or third party component, 3350 // using timeout as a safety fallback 3351 3352 this._helperTimeout = setTimeout(() => { 3353 this._finalizeAnimation(); 3354 }, duration + 500); 3355 target.style[prop] = propValue; 3356 }, 30); // Do not reduce this number 3357 }, 0); 3358 } 3359 /** 3360 * @private 3361 * @param {TransitionEvent} e 3362 */ 3363 3364 3365 _onTransitionEnd(e) { 3366 if (e.target === this._target) { 3367 this._finalizeAnimation(); 3368 } 3369 } 3370 /** 3371 * @private 3372 */ 3373 3374 3375 _finalizeAnimation() { 3376 if (!this._finished) { 3377 this._finished = true; 3378 this.onFinish(); 3379 3380 if (this._onComplete) { 3381 this._onComplete(); 3382 } 3383 } 3384 } // Destroy is called automatically onFinish 3385 3386 3387 destroy() { 3388 if (this._helperTimeout) { 3389 clearTimeout(this._helperTimeout); 3390 } 3391 3392 removeTransitionStyle(this._target); 3393 3394 this._target.removeEventListener('transitionend', this._onTransitionEnd, false); 3395 3396 this._target.removeEventListener('transitioncancel', this._onTransitionEnd, false); 3397 3398 if (!this._finished) { 3399 this._finalizeAnimation(); 3400 } 3401 } 3402 3403} 3404 3405const DEFAULT_NATURAL_FREQUENCY = 12; 3406const DEFAULT_DAMPING_RATIO = 0.75; 3407/** 3408 * Spring easing helper 3409 */ 3410 3411class SpringEaser { 3412 /** 3413 * @param {number} initialVelocity Initial velocity, px per ms. 3414 * 3415 * @param {number} [dampingRatio] 3416 * Determines how bouncy animation will be. 3417 * From 0 to 1, 0 - always overshoot, 1 - do not overshoot. 3418 * "overshoot" refers to part of animation that 3419 * goes beyond the final value. 3420 * 3421 * @param {number} [naturalFrequency] 3422 * Determines how fast animation will slow down. 3423 * The higher value - the stiffer the transition will be, 3424 * and the faster it will slow down. 3425 * Recommended value from 10 to 50 3426 */ 3427 constructor(initialVelocity, dampingRatio, naturalFrequency) { 3428 this.velocity = initialVelocity * 1000; // convert to "pixels per second" 3429 // https://en.wikipedia.org/wiki/Damping_ratio 3430 3431 this._dampingRatio = dampingRatio || DEFAULT_DAMPING_RATIO; // https://en.wikipedia.org/wiki/Natural_frequency 3432 3433 this._naturalFrequency = naturalFrequency || DEFAULT_NATURAL_FREQUENCY; 3434 this._dampedFrequency = this._naturalFrequency; 3435 3436 if (this._dampingRatio < 1) { 3437 this._dampedFrequency *= Math.sqrt(1 - this._dampingRatio * this._dampingRatio); 3438 } 3439 } 3440 /** 3441 * @param {number} deltaPosition Difference between current and end position of the animation 3442 * @param {number} deltaTime Frame duration in milliseconds 3443 * 3444 * @returns {number} Displacement, relative to the end position. 3445 */ 3446 3447 3448 easeFrame(deltaPosition, deltaTime) { 3449 // Inspired by Apple Webkit and Android spring function implementation 3450 // https://en.wikipedia.org/wiki/Oscillation 3451 // https://en.wikipedia.org/wiki/Damping_ratio 3452 // we ignore mass (assume that it's 1kg) 3453 let displacement = 0; 3454 let coeff; 3455 deltaTime /= 1000; 3456 const naturalDumpingPow = Math.E ** (-this._dampingRatio * this._naturalFrequency * deltaTime); 3457 3458 if (this._dampingRatio === 1) { 3459 coeff = this.velocity + this._naturalFrequency * deltaPosition; 3460 displacement = (deltaPosition + coeff * deltaTime) * naturalDumpingPow; 3461 this.velocity = displacement * -this._naturalFrequency + coeff * naturalDumpingPow; 3462 } else if (this._dampingRatio < 1) { 3463 coeff = 1 / this._dampedFrequency * (this._dampingRatio * this._naturalFrequency * deltaPosition + this.velocity); 3464 const dumpedFCos = Math.cos(this._dampedFrequency * deltaTime); 3465 const dumpedFSin = Math.sin(this._dampedFrequency * deltaTime); 3466 displacement = naturalDumpingPow * (deltaPosition * dumpedFCos + coeff * dumpedFSin); 3467 this.velocity = displacement * -this._naturalFrequency * this._dampingRatio + naturalDumpingPow * (-this._dampedFrequency * deltaPosition * dumpedFSin + this._dampedFrequency * coeff * dumpedFCos); 3468 } // Overdamped (>1) damping ratio is not supported 3469 3470 3471 return displacement; 3472 } 3473 3474} 3475 3476/** @typedef {import('./animations.js').SharedAnimationProps} SharedAnimationProps */ 3477 3478/** 3479 * @typedef {Object} DefaultSpringAnimationProps 3480 * 3481 * @prop {number} start 3482 * @prop {number} end 3483 * @prop {number} velocity 3484 * @prop {number} [dampingRatio] 3485 * @prop {number} [naturalFrequency] 3486 * @prop {(end: number) => void} onUpdate 3487 */ 3488 3489/** @typedef {SharedAnimationProps & DefaultSpringAnimationProps} SpringAnimationProps */ 3490 3491class SpringAnimation { 3492 /** 3493 * @param {SpringAnimationProps} props 3494 */ 3495 constructor(props) { 3496 this.props = props; 3497 this._raf = 0; 3498 const { 3499 start, 3500 end, 3501 velocity, 3502 onUpdate, 3503 onComplete, 3504 onFinish = () => {}, 3505 dampingRatio, 3506 naturalFrequency 3507 } = props; 3508 this.onFinish = onFinish; 3509 const easer = new SpringEaser(velocity, dampingRatio, naturalFrequency); 3510 let prevTime = Date.now(); 3511 let deltaPosition = start - end; 3512 3513 const animationLoop = () => { 3514 if (this._raf) { 3515 deltaPosition = easer.easeFrame(deltaPosition, Date.now() - prevTime); // Stop the animation if velocity is low and position is close to end 3516 3517 if (Math.abs(deltaPosition) < 1 && Math.abs(easer.velocity) < 50) { 3518 // Finalize the animation 3519 onUpdate(end); 3520 3521 if (onComplete) { 3522 onComplete(); 3523 } 3524 3525 this.onFinish(); 3526 } else { 3527 prevTime = Date.now(); 3528 onUpdate(deltaPosition + end); 3529 this._raf = requestAnimationFrame(animationLoop); 3530 } 3531 } 3532 }; 3533 3534 this._raf = requestAnimationFrame(animationLoop); 3535 } // Destroy is called automatically onFinish 3536 3537 3538 destroy() { 3539 if (this._raf >= 0) { 3540 cancelAnimationFrame(this._raf); 3541 } 3542 3543 this._raf = 0; 3544 } 3545 3546} 3547 3548/** @typedef {import('./css-animation.js').CssAnimationProps} CssAnimationProps */ 3549 3550/** @typedef {import('./spring-animation.js').SpringAnimationProps} SpringAnimationProps */ 3551 3552/** @typedef {Object} SharedAnimationProps 3553 * @prop {string} [name] 3554 * @prop {boolean} [isPan] 3555 * @prop {boolean} [isMainScroll] 3556 * @prop {VoidFunction} [onComplete] 3557 * @prop {VoidFunction} [onFinish] 3558 */ 3559 3560/** @typedef {SpringAnimation | CSSAnimation} Animation */ 3561 3562/** @typedef {SpringAnimationProps | CssAnimationProps} AnimationProps */ 3563 3564/** 3565 * Manages animations 3566 */ 3567 3568class Animations { 3569 constructor() { 3570 /** @type {Animation[]} */ 3571 this.activeAnimations = []; 3572 } 3573 /** 3574 * @param {SpringAnimationProps} props 3575 */ 3576 3577 3578 startSpring(props) { 3579 this._start(props, true); 3580 } 3581 /** 3582 * @param {CssAnimationProps} props 3583 */ 3584 3585 3586 startTransition(props) { 3587 this._start(props); 3588 } 3589 /** 3590 * @private 3591 * @param {AnimationProps} props 3592 * @param {boolean} [isSpring] 3593 * @returns {Animation} 3594 */ 3595 3596 3597 _start(props, isSpring) { 3598 const animation = isSpring ? new SpringAnimation( 3599 /** @type SpringAnimationProps */ 3600 props) : new CSSAnimation( 3601 /** @type CssAnimationProps */ 3602 props); 3603 this.activeAnimations.push(animation); 3604 3605 animation.onFinish = () => this.stop(animation); 3606 3607 return animation; 3608 } 3609 /** 3610 * @param {Animation} animation 3611 */ 3612 3613 3614 stop(animation) { 3615 animation.destroy(); 3616 const index = this.activeAnimations.indexOf(animation); 3617 3618 if (index > -1) { 3619 this.activeAnimations.splice(index, 1); 3620 } 3621 } 3622 3623 stopAll() { 3624 // _stopAllAnimations 3625 this.activeAnimations.forEach(animation => { 3626 animation.destroy(); 3627 }); 3628 this.activeAnimations = []; 3629 } 3630 /** 3631 * Stop all pan or zoom transitions 3632 */ 3633 3634 3635 stopAllPan() { 3636 this.activeAnimations = this.activeAnimations.filter(animation => { 3637 if (animation.props.isPan) { 3638 animation.destroy(); 3639 return false; 3640 } 3641 3642 return true; 3643 }); 3644 } 3645 3646 stopMainScroll() { 3647 this.activeAnimations = this.activeAnimations.filter(animation => { 3648 if (animation.props.isMainScroll) { 3649 animation.destroy(); 3650 return false; 3651 } 3652 3653 return true; 3654 }); 3655 } 3656 /** 3657 * Returns true if main scroll transition is running 3658 */ 3659 // isMainScrollRunning() { 3660 // return this.activeAnimations.some((animation) => { 3661 // return animation.props.isMainScroll; 3662 // }); 3663 // } 3664 3665 /** 3666 * Returns true if any pan or zoom transition is running 3667 */ 3668 3669 3670 isPanRunning() { 3671 return this.activeAnimations.some(animation => { 3672 return animation.props.isPan; 3673 }); 3674 } 3675 3676} 3677 3678/** @typedef {import('./photoswipe.js').default} PhotoSwipe */ 3679 3680/** 3681 * Handles scroll wheel. 3682 * Can pan and zoom current slide image. 3683 */ 3684class ScrollWheel { 3685 /** 3686 * @param {PhotoSwipe} pswp 3687 */ 3688 constructor(pswp) { 3689 this.pswp = pswp; 3690 pswp.events.add(pswp.element, 'wheel', 3691 /** @type EventListener */ 3692 this._onWheel.bind(this)); 3693 } 3694 /** 3695 * @private 3696 * @param {WheelEvent} e 3697 */ 3698 3699 3700 _onWheel(e) { 3701 e.preventDefault(); 3702 const { 3703 currSlide 3704 } = this.pswp; 3705 let { 3706 deltaX, 3707 deltaY 3708 } = e; 3709 3710 if (!currSlide) { 3711 return; 3712 } 3713 3714 if (this.pswp.dispatch('wheel', { 3715 originalEvent: e 3716 }).defaultPrevented) { 3717 return; 3718 } 3719 3720 if (e.ctrlKey || this.pswp.options.wheelToZoom) { 3721 // zoom 3722 if (currSlide.isZoomable()) { 3723 let zoomFactor = -deltaY; 3724 3725 if (e.deltaMode === 1 3726 /* DOM_DELTA_LINE */ 3727 ) { 3728 zoomFactor *= 0.05; 3729 } else { 3730 zoomFactor *= e.deltaMode ? 1 : 0.002; 3731 } 3732 3733 zoomFactor = 2 ** zoomFactor; 3734 const destZoomLevel = currSlide.currZoomLevel * zoomFactor; 3735 currSlide.zoomTo(destZoomLevel, { 3736 x: e.clientX, 3737 y: e.clientY 3738 }); 3739 } 3740 } else { 3741 // pan 3742 if (currSlide.isPannable()) { 3743 if (e.deltaMode === 1 3744 /* DOM_DELTA_LINE */ 3745 ) { 3746 // 18 - average line height 3747 deltaX *= 18; 3748 deltaY *= 18; 3749 } 3750 3751 currSlide.panTo(currSlide.pan.x - deltaX, currSlide.pan.y - deltaY); 3752 } 3753 } 3754 } 3755 3756} 3757 3758/** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 3759 3760/** 3761 * @template T 3762 * @typedef {import('../types.js').Methods<T>} Methods<T> 3763 */ 3764 3765/** 3766 * @typedef {Object} UIElementMarkupProps 3767 * @prop {boolean} [isCustomSVG] 3768 * @prop {string} inner 3769 * @prop {string} [outlineID] 3770 * @prop {number | string} [size] 3771 */ 3772 3773/** 3774 * @typedef {Object} UIElementData 3775 * @prop {DefaultUIElements | string} [name] 3776 * @prop {string} [className] 3777 * @prop {UIElementMarkup} [html] 3778 * @prop {boolean} [isButton] 3779 * @prop {keyof HTMLElementTagNameMap} [tagName] 3780 * @prop {string} [title] 3781 * @prop {string} [ariaLabel] 3782 * @prop {(element: HTMLElement, pswp: PhotoSwipe) => void} [onInit] 3783 * @prop {Methods<PhotoSwipe> | ((e: MouseEvent, element: HTMLElement, pswp: PhotoSwipe) => void)} [onClick] 3784 * @prop {'bar' | 'wrapper' | 'root'} [appendTo] 3785 * @prop {number} [order] 3786 */ 3787 3788/** @typedef {'arrowPrev' | 'arrowNext' | 'close' | 'zoom' | 'counter'} DefaultUIElements */ 3789 3790/** @typedef {string | UIElementMarkupProps} UIElementMarkup */ 3791 3792/** 3793 * @param {UIElementMarkup} [htmlData] 3794 * @returns {string} 3795 */ 3796 3797function addElementHTML(htmlData) { 3798 if (typeof htmlData === 'string') { 3799 // Allow developers to provide full svg, 3800 // For example: 3801 // <svg viewBox="0 0 32 32" width="32" height="32" aria-hidden="true" class="pswp__icn"> 3802 // <path d="..." /> 3803 // <circle ... /> 3804 // </svg> 3805 // Can also be any HTML string. 3806 return htmlData; 3807 } 3808 3809 if (!htmlData || !htmlData.isCustomSVG) { 3810 return ''; 3811 } 3812 3813 const svgData = htmlData; 3814 let out = '<svg aria-hidden="true" class="pswp__icn" viewBox="0 0 %d %d" width="%d" height="%d">'; // replace all %d with size 3815 3816 out = out.split('%d').join( 3817 /** @type {string} */ 3818 svgData.size || 32); // Icons may contain outline/shadow, 3819 // to make it we "clone" base icon shape and add border to it. 3820 // Icon itself and border are styled via CSS. 3821 // 3822 // Property shadowID defines ID of element that should be cloned. 3823 3824 if (svgData.outlineID) { 3825 out += '<use class="pswp__icn-shadow" xlink:href="#' + svgData.outlineID + '"/>'; 3826 } 3827 3828 out += svgData.inner; 3829 out += '</svg>'; 3830 return out; 3831} 3832 3833class UIElement { 3834 /** 3835 * @param {PhotoSwipe} pswp 3836 * @param {UIElementData} data 3837 */ 3838 constructor(pswp, data) { 3839 var _container; 3840 3841 const name = data.name || data.className; 3842 let elementHTML = data.html; // @ts-expect-error lookup only by `data.name` maybe? 3843 3844 if (pswp.options[name] === false) { 3845 // exit if element is disabled from options 3846 return; 3847 } // Allow to override SVG icons from options 3848 // @ts-expect-error lookup only by `data.name` maybe? 3849 3850 3851 if (typeof pswp.options[name + 'SVG'] === 'string') { 3852 // arrowPrevSVG 3853 // arrowNextSVG 3854 // closeSVG 3855 // zoomSVG 3856 // @ts-expect-error lookup only by `data.name` maybe? 3857 elementHTML = pswp.options[name + 'SVG']; 3858 } 3859 3860 pswp.dispatch('uiElementCreate', { 3861 data 3862 }); 3863 let className = ''; 3864 3865 if (data.isButton) { 3866 className += 'pswp__button '; 3867 className += data.className || `pswp__button--${data.name}`; 3868 } else { 3869 className += data.className || `pswp__${data.name}`; 3870 } 3871 3872 let tagName = data.isButton ? data.tagName || 'button' : data.tagName || 'div'; 3873 tagName = 3874 /** @type {keyof HTMLElementTagNameMap} */ 3875 tagName.toLowerCase(); 3876 /** @type {HTMLElement} */ 3877 3878 const element = createElement(className, tagName); 3879 3880 if (data.isButton) { 3881 if (tagName === 'button') { 3882 /** @type {HTMLButtonElement} */ 3883 element.type = 'button'; 3884 } 3885 3886 let { 3887 title 3888 } = data; 3889 const { 3890 ariaLabel 3891 } = data; // @ts-expect-error lookup only by `data.name` maybe? 3892 3893 if (typeof pswp.options[name + 'Title'] === 'string') { 3894 // @ts-expect-error lookup only by `data.name` maybe? 3895 title = pswp.options[name + 'Title']; 3896 } 3897 3898 if (title) { 3899 element.title = title; 3900 } 3901 3902 const ariaText = ariaLabel || title; 3903 3904 if (ariaText) { 3905 element.setAttribute('aria-label', ariaText); 3906 } 3907 } 3908 3909 element.innerHTML = addElementHTML(elementHTML); 3910 3911 if (data.onInit) { 3912 data.onInit(element, pswp); 3913 } 3914 3915 if (data.onClick) { 3916 element.onclick = e => { 3917 if (typeof data.onClick === 'string') { 3918 // @ts-ignore 3919 pswp[data.onClick](); 3920 } else if (typeof data.onClick === 'function') { 3921 data.onClick(e, element, pswp); 3922 } 3923 }; 3924 } // Top bar is default position 3925 3926 3927 const appendTo = data.appendTo || 'bar'; 3928 /** @type {HTMLElement | undefined} root element by default */ 3929 3930 let container = pswp.element; 3931 3932 if (appendTo === 'bar') { 3933 if (!pswp.topBar) { 3934 pswp.topBar = createElement('pswp__top-bar pswp__hide-on-close', 'div', pswp.scrollWrap); 3935 } 3936 3937 container = pswp.topBar; 3938 } else { 3939 // element outside of top bar gets a secondary class 3940 // that makes element fade out on close 3941 element.classList.add('pswp__hide-on-close'); 3942 3943 if (appendTo === 'wrapper') { 3944 container = pswp.scrollWrap; 3945 } 3946 } 3947 3948 (_container = container) === null || _container === void 0 || _container.appendChild(pswp.applyFilters('uiElement', element, data)); 3949 } 3950 3951} 3952 3953/* 3954 Backward and forward arrow buttons 3955 */ 3956 3957/** @typedef {import('./ui-element.js').UIElementData} UIElementData */ 3958 3959/** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 3960 3961/** 3962 * 3963 * @param {HTMLElement} element 3964 * @param {PhotoSwipe} pswp 3965 * @param {boolean} [isNextButton] 3966 */ 3967function initArrowButton(element, pswp, isNextButton) { 3968 element.classList.add('pswp__button--arrow'); // TODO: this should point to a unique id for this instance 3969 3970 element.setAttribute('aria-controls', 'pswp__items'); 3971 pswp.on('change', () => { 3972 if (!pswp.options.loop) { 3973 if (isNextButton) { 3974 /** @type {HTMLButtonElement} */ 3975 element.disabled = !(pswp.currIndex < pswp.getNumItems() - 1); 3976 } else { 3977 /** @type {HTMLButtonElement} */ 3978 element.disabled = !(pswp.currIndex > 0); 3979 } 3980 } 3981 }); 3982} 3983/** @type {UIElementData} */ 3984 3985 3986const arrowPrev = { 3987 name: 'arrowPrev', 3988 className: 'pswp__button--arrow--prev', 3989 title: 'Previous', 3990 order: 10, 3991 isButton: true, 3992 appendTo: 'wrapper', 3993 html: { 3994 isCustomSVG: true, 3995 size: 60, 3996 inner: '<path d="M29 43l-3 3-16-16 16-16 3 3-13 13 13 13z" id="pswp__icn-arrow"/>', 3997 outlineID: 'pswp__icn-arrow' 3998 }, 3999 onClick: 'prev', 4000 onInit: initArrowButton 4001}; 4002/** @type {UIElementData} */ 4003 4004const arrowNext = { 4005 name: 'arrowNext', 4006 className: 'pswp__button--arrow--next', 4007 title: 'Next', 4008 order: 11, 4009 isButton: true, 4010 appendTo: 'wrapper', 4011 html: { 4012 isCustomSVG: true, 4013 size: 60, 4014 inner: '<use xlink:href="#pswp__icn-arrow"/>', 4015 outlineID: 'pswp__icn-arrow' 4016 }, 4017 onClick: 'next', 4018 onInit: (el, pswp) => { 4019 initArrowButton(el, pswp, true); 4020 } 4021}; 4022 4023/** @type {import('./ui-element.js').UIElementData} UIElementData */ 4024const closeButton = { 4025 name: 'close', 4026 title: 'Close', 4027 order: 20, 4028 isButton: true, 4029 html: { 4030 isCustomSVG: true, 4031 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"/>', 4032 outlineID: 'pswp__icn-close' 4033 }, 4034 onClick: 'close' 4035}; 4036 4037/** @type {import('./ui-element.js').UIElementData} UIElementData */ 4038const zoomButton = { 4039 name: 'zoom', 4040 title: 'Zoom', 4041 order: 10, 4042 isButton: true, 4043 html: { 4044 isCustomSVG: true, 4045 // eslint-disable-next-line max-len 4046 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"/>', 4047 outlineID: 'pswp__icn-zoom' 4048 }, 4049 onClick: 'toggleZoom' 4050}; 4051 4052/** @type {import('./ui-element.js').UIElementData} UIElementData */ 4053const loadingIndicator = { 4054 name: 'preloader', 4055 appendTo: 'bar', 4056 order: 7, 4057 html: { 4058 isCustomSVG: true, 4059 // eslint-disable-next-line max-len 4060 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"/>', 4061 outlineID: 'pswp__icn-loading' 4062 }, 4063 onInit: (indicatorElement, pswp) => { 4064 /** @type {boolean | undefined} */ 4065 let isVisible; 4066 /** @type {NodeJS.Timeout | null} */ 4067 4068 let delayTimeout = null; 4069 /** 4070 * @param {string} className 4071 * @param {boolean} add 4072 */ 4073 4074 const toggleIndicatorClass = (className, add) => { 4075 indicatorElement.classList.toggle('pswp__preloader--' + className, add); 4076 }; 4077 /** 4078 * @param {boolean} visible 4079 */ 4080 4081 4082 const setIndicatorVisibility = visible => { 4083 if (isVisible !== visible) { 4084 isVisible = visible; 4085 toggleIndicatorClass('active', visible); 4086 } 4087 }; 4088 4089 const updatePreloaderVisibility = () => { 4090 var _pswp$currSlide; 4091 4092 if (!((_pswp$currSlide = pswp.currSlide) !== null && _pswp$currSlide !== void 0 && _pswp$currSlide.content.isLoading())) { 4093 setIndicatorVisibility(false); 4094 4095 if (delayTimeout) { 4096 clearTimeout(delayTimeout); 4097 delayTimeout = null; 4098 } 4099 4100 return; 4101 } 4102 4103 if (!delayTimeout) { 4104 // display loading indicator with delay 4105 delayTimeout = setTimeout(() => { 4106 var _pswp$currSlide2; 4107 4108 setIndicatorVisibility(Boolean((_pswp$currSlide2 = pswp.currSlide) === null || _pswp$currSlide2 === void 0 ? void 0 : _pswp$currSlide2.content.isLoading())); 4109 delayTimeout = null; 4110 }, pswp.options.preloaderDelay); 4111 } 4112 }; 4113 4114 pswp.on('change', updatePreloaderVisibility); 4115 pswp.on('loadComplete', e => { 4116 if (pswp.currSlide === e.slide) { 4117 updatePreloaderVisibility(); 4118 } 4119 }); // expose the method 4120 4121 if (pswp.ui) { 4122 pswp.ui.updatePreloaderVisibility = updatePreloaderVisibility; 4123 } 4124 } 4125}; 4126 4127/** @type {import('./ui-element.js').UIElementData} UIElementData */ 4128const counterIndicator = { 4129 name: 'counter', 4130 order: 5, 4131 onInit: (counterElement, pswp) => { 4132 pswp.on('change', () => { 4133 counterElement.innerText = pswp.currIndex + 1 + pswp.options.indexIndicatorSep + pswp.getNumItems(); 4134 }); 4135 } 4136}; 4137 4138/** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 4139 4140/** @typedef {import('./ui-element.js').UIElementData} UIElementData */ 4141 4142/** 4143 * Set special class on element when image is zoomed. 4144 * 4145 * By default, it is used to adjust 4146 * zoom icon and zoom cursor via CSS. 4147 * 4148 * @param {HTMLElement} el 4149 * @param {boolean} isZoomedIn 4150 */ 4151 4152function setZoomedIn(el, isZoomedIn) { 4153 el.classList.toggle('pswp--zoomed-in', isZoomedIn); 4154} 4155 4156class UI { 4157 /** 4158 * @param {PhotoSwipe} pswp 4159 */ 4160 constructor(pswp) { 4161 this.pswp = pswp; 4162 this.isRegistered = false; 4163 /** @type {UIElementData[]} */ 4164 4165 this.uiElementsData = []; 4166 /** @type {(UIElement | UIElementData)[]} */ 4167 4168 this.items = []; 4169 /** @type {() => void} */ 4170 4171 this.updatePreloaderVisibility = () => {}; 4172 /** 4173 * @private 4174 * @type {number | undefined} 4175 */ 4176 4177 4178 this._lastUpdatedZoomLevel = undefined; 4179 } 4180 4181 init() { 4182 const { 4183 pswp 4184 } = this; 4185 this.isRegistered = false; 4186 this.uiElementsData = [closeButton, arrowPrev, arrowNext, zoomButton, loadingIndicator, counterIndicator]; 4187 pswp.dispatch('uiRegister'); // sort by order 4188 4189 this.uiElementsData.sort((a, b) => { 4190 // default order is 0 4191 return (a.order || 0) - (b.order || 0); 4192 }); 4193 this.items = []; 4194 this.isRegistered = true; 4195 this.uiElementsData.forEach(uiElementData => { 4196 this.registerElement(uiElementData); 4197 }); 4198 pswp.on('change', () => { 4199 var _pswp$element; 4200 4201 (_pswp$element = pswp.element) === null || _pswp$element === void 0 || _pswp$element.classList.toggle('pswp--one-slide', pswp.getNumItems() === 1); 4202 }); 4203 pswp.on('zoomPanUpdate', () => this._onZoomPanUpdate()); 4204 } 4205 /** 4206 * @param {UIElementData} elementData 4207 */ 4208 4209 4210 registerElement(elementData) { 4211 if (this.isRegistered) { 4212 this.items.push(new UIElement(this.pswp, elementData)); 4213 } else { 4214 this.uiElementsData.push(elementData); 4215 } 4216 } 4217 /** 4218 * Fired each time zoom or pan position is changed. 4219 * Update classes that control visibility of zoom button and cursor icon. 4220 * 4221 * @private 4222 */ 4223 4224 4225 _onZoomPanUpdate() { 4226 const { 4227 template, 4228 currSlide, 4229 options 4230 } = this.pswp; 4231 4232 if (this.pswp.opener.isClosing || !template || !currSlide) { 4233 return; 4234 } 4235 4236 let { 4237 currZoomLevel 4238 } = currSlide; // if not open yet - check against initial zoom level 4239 4240 if (!this.pswp.opener.isOpen) { 4241 currZoomLevel = currSlide.zoomLevels.initial; 4242 } 4243 4244 if (currZoomLevel === this._lastUpdatedZoomLevel) { 4245 return; 4246 } 4247 4248 this._lastUpdatedZoomLevel = currZoomLevel; 4249 const currZoomLevelDiff = currSlide.zoomLevels.initial - currSlide.zoomLevels.secondary; // Initial and secondary zoom levels are almost equal 4250 4251 if (Math.abs(currZoomLevelDiff) < 0.01 || !currSlide.isZoomable()) { 4252 // disable zoom 4253 setZoomedIn(template, false); 4254 template.classList.remove('pswp--zoom-allowed'); 4255 return; 4256 } 4257 4258 template.classList.add('pswp--zoom-allowed'); 4259 const potentialZoomLevel = currZoomLevel === currSlide.zoomLevels.initial ? currSlide.zoomLevels.secondary : currSlide.zoomLevels.initial; 4260 setZoomedIn(template, potentialZoomLevel <= currZoomLevel); 4261 4262 if (options.imageClickAction === 'zoom' || options.imageClickAction === 'zoom-or-close') { 4263 template.classList.add('pswp--click-to-zoom'); 4264 } 4265 } 4266 4267} 4268 4269/** @typedef {import('./slide.js').SlideData} SlideData */ 4270 4271/** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 4272 4273/** @typedef {{ x: number; y: number; w: number; innerRect?: { w: number; h: number; x: number; y: number } }} Bounds */ 4274 4275/** 4276 * @param {HTMLElement} el 4277 * @returns Bounds 4278 */ 4279function getBoundsByElement(el) { 4280 const thumbAreaRect = el.getBoundingClientRect(); 4281 return { 4282 x: thumbAreaRect.left, 4283 y: thumbAreaRect.top, 4284 w: thumbAreaRect.width 4285 }; 4286} 4287/** 4288 * @param {HTMLElement} el 4289 * @param {number} imageWidth 4290 * @param {number} imageHeight 4291 * @returns Bounds 4292 */ 4293 4294 4295function getCroppedBoundsByElement(el, imageWidth, imageHeight) { 4296 const thumbAreaRect = el.getBoundingClientRect(); // fill image into the area 4297 // (do they same as object-fit:cover does to retrieve coordinates) 4298 4299 const hRatio = thumbAreaRect.width / imageWidth; 4300 const vRatio = thumbAreaRect.height / imageHeight; 4301 const fillZoomLevel = hRatio > vRatio ? hRatio : vRatio; 4302 const offsetX = (thumbAreaRect.width - imageWidth * fillZoomLevel) / 2; 4303 const offsetY = (thumbAreaRect.height - imageHeight * fillZoomLevel) / 2; 4304 /** 4305 * Coordinates of the image, 4306 * as if it was not cropped, 4307 * height is calculated automatically 4308 * 4309 * @type {Bounds} 4310 */ 4311 4312 const bounds = { 4313 x: thumbAreaRect.left + offsetX, 4314 y: thumbAreaRect.top + offsetY, 4315 w: imageWidth * fillZoomLevel 4316 }; // Coordinates of inner crop area 4317 // relative to the image 4318 4319 bounds.innerRect = { 4320 w: thumbAreaRect.width, 4321 h: thumbAreaRect.height, 4322 x: offsetX, 4323 y: offsetY 4324 }; 4325 return bounds; 4326} 4327/** 4328 * Get dimensions of thumbnail image 4329 * (click on which opens photoswipe or closes photoswipe to) 4330 * 4331 * @param {number} index 4332 * @param {SlideData} itemData 4333 * @param {PhotoSwipe} instance PhotoSwipe instance 4334 * @returns {Bounds | undefined} 4335 */ 4336 4337 4338function getThumbBounds(index, itemData, instance) { 4339 // legacy event, before filters were introduced 4340 const event = instance.dispatch('thumbBounds', { 4341 index, 4342 itemData, 4343 instance 4344 }); // @ts-expect-error 4345 4346 if (event.thumbBounds) { 4347 // @ts-expect-error 4348 return event.thumbBounds; 4349 } 4350 4351 const { 4352 element 4353 } = itemData; 4354 /** @type {Bounds | undefined} */ 4355 4356 let thumbBounds; 4357 /** @type {HTMLElement | null | undefined} */ 4358 4359 let thumbnail; 4360 4361 if (element && instance.options.thumbSelector !== false) { 4362 const thumbSelector = instance.options.thumbSelector || 'img'; 4363 thumbnail = element.matches(thumbSelector) ? element : 4364 /** @type {HTMLElement | null} */ 4365 element.querySelector(thumbSelector); 4366 } 4367 4368 thumbnail = instance.applyFilters('thumbEl', thumbnail, itemData, index); 4369 4370 if (thumbnail) { 4371 if (!itemData.thumbCropped) { 4372 thumbBounds = getBoundsByElement(thumbnail); 4373 } else { 4374 thumbBounds = getCroppedBoundsByElement(thumbnail, itemData.width || itemData.w || 0, itemData.height || itemData.h || 0); 4375 } 4376 } 4377 4378 return instance.applyFilters('thumbBounds', thumbBounds, itemData, index); 4379} 4380 4381/** @typedef {import('../lightbox/lightbox.js').default} PhotoSwipeLightbox */ 4382 4383/** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 4384 4385/** @typedef {import('../photoswipe.js').PhotoSwipeOptions} PhotoSwipeOptions */ 4386
4387/** @typedef {import('../photoswipe.js').DataSource} DataSource */ 4388 4389/** @typedef {import('../ui/ui-element.js').UIElementData} UIElementData */ 4390 4391/** @typedef {import('../slide/content.js').default} ContentDefault */ 4392 4393/** @typedef {import('../slide/slide.js').default} Slide */ 4394 4395/** @typedef {import('../slide/slide.js').SlideData} SlideData */ 4396 4397/** @typedef {import('../slide/zoom-level.js').default} ZoomLevel */ 4398 4399/** @typedef {import('../slide/get-thumb-bounds.js').Bounds} Bounds */ 4400 4401/** 4402 * Allow adding an arbitrary props to the Content 4403 * https://photoswipe.com/custom-content/#using-webp-image-format 4404 * @typedef {ContentDefault & Record<string, any>} Content 4405 */ 4406 4407/** @typedef {{ x?: number; y?: number }} Point */ 4408 4409/** 4410 * @typedef {Object} PhotoSwipeEventsMap https://photoswipe.com/events/ 4411 * 4412 * 4413 * https://photoswipe.com/adding-ui-elements/ 4414 * 4415 * @prop {undefined} uiRegister 4416 * @prop {{ data: UIElementData }} uiElementCreate 4417 * 4418 * 4419 * https://photoswipe.com/events/#initialization-events 4420 * 4421 * @prop {undefined} beforeOpen 4422 * @prop {undefined} firstUpdate 4423 * @prop {undefined} initialLayout 4424 * @prop {undefined} change 4425 * @prop {undefined} afterInit 4426 * @prop {undefined} bindEvents 4427 * 4428 * 4429 * https://photoswipe.com/events/#opening-or-closing-transition-events 4430 * 4431 * @prop {undefined} openingAnimationStart 4432 * @prop {undefined} openingAnimationEnd 4433 * @prop {undefined} closingAnimationStart 4434 * @prop {undefined} closingAnimationEnd 4435 * 4436 * 4437 * https://photoswipe.com/events/#closing-events 4438 * 4439 * @prop {undefined} close 4440 * @prop {undefined} destroy 4441 * 4442 * 4443 * https://photoswipe.com/events/#pointer-and-gesture-events 4444 * 4445 * @prop {{ originalEvent: PointerEvent }} pointerDown 4446 * @prop {{ originalEvent: PointerEvent }} pointerMove 4447 * @prop {{ originalEvent: PointerEvent }} pointerUp 4448 * @prop {{ bgOpacity: number }} pinchClose can be default prevented 4449 * @prop {{ panY: number }} verticalDrag can be default prevented 4450 * 4451 * 4452 * https://photoswipe.com/events/#slide-content-events 4453 * 4454 * @prop {{ content: Content }} contentInit 4455 * @prop {{ content: Content; isLazy: boolean }} contentLoad can be default prevented 4456 * @prop {{ content: Content; isLazy: boolean }} contentLoadImage can be default prevented 4457 * @prop {{ content: Content; slide: Slide; isError?: boolean }} loadComplete 4458 * @prop {{ content: Content; slide: Slide }} loadError 4459 * @prop {{ content: Content; width: number; height: number }} contentResize can be default prevented 4460 * @prop {{ content: Content; width: number; height: number; slide: Slide }} imageSizeChange 4461 * @prop {{ content: Content }} contentLazyLoad can be default prevented 4462 * @prop {{ content: Content }} contentAppend can be default prevented 4463 * @prop {{ content: Content }} contentActivate can be default prevented 4464 * @prop {{ content: Content }} contentDeactivate can be default prevented 4465 * @prop {{ content: Content }} contentRemove can be default prevented 4466 * @prop {{ content: Content }} contentDestroy can be default prevented 4467 * 4468 * 4469 * undocumented 4470 * 4471 * @prop {{ point: Point; originalEvent: PointerEvent }} imageClickAction can be default prevented 4472 * @prop {{ point: Point; originalEvent: PointerEvent }} bgClickAction can be default prevented 4473 * @prop {{ point: Point; originalEvent: PointerEvent }} tapAction can be default prevented 4474 * @prop {{ point: Point; originalEvent: PointerEvent }} doubleTapAction can be default prevented 4475 * 4476 * @prop {{ originalEvent: KeyboardEvent }} keydown can be default prevented 4477 * @prop {{ x: number; dragging: boolean }} moveMainScroll 4478 * @prop {{ slide: Slide }} firstZoomPan 4479 * @prop {{ slide: Slide | undefined, data: SlideData, index: number }} gettingData 4480 * @prop {undefined} beforeResize 4481 * @prop {undefined} resize 4482 * @prop {undefined} viewportSize 4483 * @prop {undefined} updateScrollOffset 4484 * @prop {{ slide: Slide }} slideInit 4485 * @prop {{ slide: Slide }} afterSetContent 4486 * @prop {{ slide: Slide }} slideLoad 4487 * @prop {{ slide: Slide }} appendHeavy can be default prevented 4488 * @prop {{ slide: Slide }} appendHeavyContent 4489 * @prop {{ slide: Slide }} slideActivate 4490 * @prop {{ slide: Slide }} slideDeactivate 4491 * @prop {{ slide: Slide }} slideDestroy
4492 * @prop {{ destZoomLevel: number, centerPoint: Point | undefined, transitionDuration: number | false | undefined }} beforeZoomTo 4493 * @prop {{ slide: Slide }} zoomPanUpdate 4494 * @prop {{ slide: Slide }} initialZoomPan 4495 * @prop {{ slide: Slide }} calcSlideSize 4496 * @prop {undefined} resolutionChanged 4497 * @prop {{ originalEvent: WheelEvent }} wheel can be default prevented 4498 * @prop {{ content: Content }} contentAppendImage can be default prevented 4499 * @prop {{ index: number; itemData: SlideData }} lazyLoadSlide can be default prevented 4500 * @prop {undefined} lazyLoad 4501 * @prop {{ slide: Slide }} calcBounds 4502 * @prop {{ zoomLevels: ZoomLevel, slideData: SlideData }} zoomLevelsUpdate 4503 * 4504 * 4505 * legacy 4506 * 4507 * @prop {undefined} init 4508 * @prop {undefined} initialZoomIn 4509 * @prop {undefined} initialZoomOut 4510 * @prop {undefined} initialZoomInEnd 4511 * @prop {undefined} initialZoomOutEnd 4512 * @prop {{ dataSource: DataSource | undefined, numItems: number }} numItems 4513 * @prop {{ itemData: SlideData; index: number }} itemData 4514 * @prop {{ index: number, itemData: SlideData, instance: PhotoSwipe }} thumbBounds 4515 */ 4516 4517/** 4518 * @typedef {Object} PhotoSwipeFiltersMap https://photoswipe.com/filters/ 4519 * 4520 * @prop {(numItems: number, dataSource: DataSource | undefined) => number} numItems 4521 * Modify the total amount of slides. Example on Data sources page. 4522 * https://photoswipe.com/filters/#numitems 4523 * 4524 * @prop {(itemData: SlideData, index: number) => SlideData} itemData 4525 * Modify slide item data. Example on Data sources page. 4526 * https://photoswipe.com/filters/#itemdata 4527 * 4528 * @prop {(itemData: SlideData, element: HTMLElement, linkEl: HTMLAnchorElement) => SlideData} domItemData 4529 * Modify item data when it's parsed from DOM element. Example on Data sources page. 4530 * https://photoswipe.com/filters/#domitemdata 4531 * 4532 * @prop {(clickedIndex: number, e: MouseEvent, instance: PhotoSwipeLightbox) => number} clickedIndex 4533 * Modify clicked gallery item index. 4534 * https://photoswipe.com/filters/#clickedindex 4535 * 4536 * @prop {(placeholderSrc: string | false, content: Content) => string | false} placeholderSrc 4537 * Modify placeholder image source. 4538 * https://photoswipe.com/filters/#placeholdersrc 4539 * 4540 * @prop {(isContentLoading: boolean, content: Content) => boolean} isContentLoading 4541 * Modify if the content is currently loading. 4542 * https://photoswipe.com/filters/#iscontentloading 4543 * 4544 * @prop {(isContentZoomable: boolean, content: Content) => boolean} isContentZoomable 4545 * Modify if the content can be zoomed. 4546 * https://photoswipe.com/filters/#iscontentzoomable 4547 * 4548 * @prop {(useContentPlaceholder: boolean, content: Content) => boolean} useContentPlaceholder 4549 * Modify if the placeholder should be used for the content. 4550 * https://photoswipe.com/filters/#usecontentplaceholder 4551 * 4552 * @prop {(isKeepingPlaceholder: boolean, content: Content) => boolean} isKeepingPlaceholder 4553 * Modify if the placeholder should be kept after the content is loaded. 4554 * https://photoswipe.com/filters/#iskeepingplaceholder 4555 * 4556 * 4557 * @prop {(contentErrorElement: HTMLElement, content: Content) => HTMLElement} contentErrorElement 4558 * Modify an element when the content has error state (for example, if image cannot be loaded). 4559 * https://photoswipe.com/filters/#contenterrorelement 4560 * 4561 * @prop {(element: HTMLElement, data: UIElementData) => HTMLElement} uiElement 4562 * Modify a UI element that's being created. 4563 * https://photoswipe.com/filters/#uielement 4564 * 4565 * @prop {(thumbnail: HTMLElement | null | undefined, itemData: SlideData, index: number) => HTMLElement} thumbEl 4566 * Modify the thumbnail element from which opening zoom animation starts or ends. 4567 * https://photoswipe.com/filters/#thumbel 4568 * 4569 * @prop {(thumbBounds: Bounds | undefined, itemData: SlideData, index: number) => Bounds} thumbBounds 4570 * Modify the thumbnail bounds from which opening zoom animation starts or ends. 4571 * https://photoswipe.com/filters/#thumbbounds 4572 * 4573 * @prop {(srcsetSizesWidth: number, content: Content) => number} srcsetSizesWidth 4574 * 4575 * @prop {(preventPointerEvent: boolean, event: PointerEvent, pointerType: string) => boolean} preventPointerEvent 4576 * 4577 */ 4578 4579/** 4580 * @template {keyof PhotoSwipeFiltersMap} T 4581 * @typedef {{ fn: PhotoSwipeFiltersMap[T], priority: number }} Filter 4582 */ 4583 4584/** 4585 * @template {keyof PhotoSwipeEventsMap} T 4586 * @typedef {PhotoSwipeEventsMap[T] extends undefined ? PhotoSwipeEvent<T> : PhotoSwipeEvent<T> & PhotoSwipeEventsMap[T]} AugmentedEvent 4587 */ 4588 4589/** 4590 * @template {keyof PhotoSwipeEventsMap} T 4591 * @typedef {(event: AugmentedEvent<T>) => void} EventCallback 4592 */ 4593 4594/** 4595 * Base PhotoSwipe event object 4596 * 4597 * @template {keyof PhotoSwipeEventsMap} T 4598 */ 4599class PhotoSwipeEvent { 4600 /** 4601 * @param {T} type 4602 * @param {PhotoSwipeEventsMap[T]} [details] 4603 */ 4604 constructor(type, details) { 4605 this.type = type; 4606 this.defaultPrevented = false;
vendor: 19,004 bytes, lines 4607-5363
4607 4608 if (details) { 4609 Object.assign(this, details); 4610 } 4611 } 4612 4613 preventDefault() { 4614 this.defaultPrevented = true; 4615 } 4616 4617} 4618/** 4619 * PhotoSwipe base class that can listen and dispatch for events. 4620 * Shared by PhotoSwipe Core and PhotoSwipe Lightbox, extended by base.js 4621 */ 4622 4623 4624class Eventable { 4625 constructor() { 4626 /** 4627 * @type {{ [T in keyof PhotoSwipeEventsMap]?: ((event: AugmentedEvent<T>) => void)[] }} 4628 */ 4629 this._listeners = {}; 4630 /** 4631 * @type {{ [T in keyof PhotoSwipeFiltersMap]?: Filter<T>[] }} 4632 */ 4633 4634 this._filters = {}; 4635 /** @type {PhotoSwipe | undefined} */ 4636 4637 this.pswp = undefined; 4638 /** @type {PhotoSwipeOptions | undefined} */ 4639 4640 this.options = undefined; 4641 } 4642 /** 4643 * @template {keyof PhotoSwipeFiltersMap} T 4644 * @param {T} name 4645 * @param {PhotoSwipeFiltersMap[T]} fn 4646 * @param {number} priority 4647 */ 4648 4649 4650 addFilter(name, fn, priority = 100) { 4651 var _this$_filters$name, _this$_filters$name2, _this$pswp; 4652 4653 if (!this._filters[name]) { 4654 this._filters[name] = []; 4655 } 4656 4657 (_this$_filters$name = this._filters[name]) === null || _this$_filters$name === void 0 || _this$_filters$name.push({ 4658 fn, 4659 priority 4660 }); 4661 (_this$_filters$name2 = this._filters[name]) === null || _this$_filters$name2 === void 0 || _this$_filters$name2.sort((f1, f2) => f1.priority - f2.priority); 4662 (_this$pswp = this.pswp) === null || _this$pswp === void 0 || _this$pswp.addFilter(name, fn, priority); 4663 } 4664 /** 4665 * @template {keyof PhotoSwipeFiltersMap} T 4666 * @param {T} name 4667 * @param {PhotoSwipeFiltersMap[T]} fn 4668 */ 4669 4670 4671 removeFilter(name, fn) { 4672 if (this._filters[name]) { 4673 // @ts-expect-error 4674 this._filters[name] = this._filters[name].filter(filter => filter.fn !== fn); 4675 } 4676 4677 if (this.pswp) { 4678 this.pswp.removeFilter(name, fn); 4679 } 4680 } 4681 /** 4682 * @template {keyof PhotoSwipeFiltersMap} T 4683 * @param {T} name 4684 * @param {Parameters<PhotoSwipeFiltersMap[T]>} args 4685 * @returns {Parameters<PhotoSwipeFiltersMap[T]>[0]} 4686 */ 4687 4688 4689 applyFilters(name, ...args) { 4690 var _this$_filters$name3; 4691 4692 (_this$_filters$name3 = this._filters[name]) === null || _this$_filters$name3 === void 0 || _this$_filters$name3.forEach(filter => { 4693 // @ts-expect-error 4694 args[0] = filter.fn.apply(this, args); 4695 }); 4696 return args[0]; 4697 } 4698 /** 4699 * @template {keyof PhotoSwipeEventsMap} T 4700 * @param {T} name 4701 * @param {EventCallback<T>} fn 4702 */ 4703 4704 4705 on(name, fn) { 4706 var _this$_listeners$name, _this$pswp2; 4707 4708 if (!this._listeners[name]) { 4709 this._listeners[name] = []; 4710 } 4711 4712 (_this$_listeners$name = this._listeners[name]) === null || _this$_listeners$name === void 0 || _this$_listeners$name.push(fn); // When binding events to lightbox, 4713 // also bind events to PhotoSwipe Core, 4714 // if it's open. 4715 4716 (_this$pswp2 = this.pswp) === null || _this$pswp2 === void 0 || _this$pswp2.on(name, fn); 4717 } 4718 /** 4719 * @template {keyof PhotoSwipeEventsMap} T 4720 * @param {T} name 4721 * @param {EventCallback<T>} fn 4722 */ 4723 4724 4725 off(name, fn) { 4726 var _this$pswp3; 4727 4728 if (this._listeners[name]) { 4729 // @ts-expect-error 4730 this._listeners[name] = this._listeners[name].filter(listener => fn !== listener); 4731 } 4732 4733 (_this$pswp3 = this.pswp) === null || _this$pswp3 === void 0 || _this$pswp3.off(name, fn); 4734 } 4735 /** 4736 * @template {keyof PhotoSwipeEventsMap} T 4737 * @param {T} name 4738 * @param {PhotoSwipeEventsMap[T]} [details] 4739 * @returns {AugmentedEvent<T>} 4740 */ 4741 4742 4743 dispatch(name, details) { 4744 var _this$_listeners$name2; 4745 4746 if (this.pswp) { 4747 return this.pswp.dispatch(name, details); 4748 } 4749 4750 const event = 4751 /** @type {AugmentedEvent<T>} */ 4752 new PhotoSwipeEvent(name, details); 4753 (_this$_listeners$name2 = this._listeners[name]) === null || _this$_listeners$name2 === void 0 || _this$_listeners$name2.forEach(listener => { 4754 listener.call(this, event); 4755 }); 4756 return event; 4757 } 4758 4759} 4760 4761class Placeholder { 4762 /** 4763 * @param {string | false} imageSrc 4764 * @param {HTMLElement} container 4765 */ 4766 constructor(imageSrc, container) { 4767 // Create placeholder 4768 // (stretched thumbnail or simple div behind the main image) 4769 4770 /** @type {HTMLImageElement | HTMLDivElement | null} */ 4771 this.element = createElement('pswp__img pswp__img--placeholder', imageSrc ? 'img' : 'div', container); 4772 4773 if (imageSrc) { 4774 const imgEl = 4775 /** @type {HTMLImageElement} */ 4776 this.element; 4777 imgEl.decoding = 'async'; 4778 imgEl.alt = ''; 4779 imgEl.src = imageSrc; 4780 imgEl.setAttribute('role', 'presentation'); 4781 } 4782 4783 this.element.setAttribute('aria-hidden', 'true'); 4784 } 4785 /** 4786 * @param {number} width 4787 * @param {number} height 4788 */ 4789 4790 4791 setDisplayedSize(width, height) { 4792 if (!this.element) { 4793 return; 4794 } 4795 4796 if (this.element.tagName === 'IMG') { 4797 // Use transform scale() to modify img placeholder size 4798 // (instead of changing width/height directly). 4799 // This helps with performance, specifically in iOS15 Safari. 4800 setWidthHeight(this.element, 250, 'auto'); 4801 this.element.style.transformOrigin = '0 0'; 4802 this.element.style.transform = toTransformString(0, 0, width / 250); 4803 } else { 4804 setWidthHeight(this.element, width, height); 4805 } 4806 } 4807 4808 destroy() { 4809 var _this$element; 4810 4811 if ((_this$element = this.element) !== null && _this$element !== void 0 && _this$element.parentNode) { 4812 this.element.remove(); 4813 } 4814 4815 this.element = null; 4816 } 4817 4818} 4819 4820/** @typedef {import('./slide.js').default} Slide */ 4821 4822/** @typedef {import('./slide.js').SlideData} SlideData */ 4823 4824/** @typedef {import('../core/base.js').default} PhotoSwipeBase */ 4825 4826/** @typedef {import('../util/util.js').LoadState} LoadState */ 4827 4828class Content { 4829 /** 4830 * @param {SlideData} itemData Slide data 4831 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance 4832 * @param {number} index 4833 */ 4834 constructor(itemData, instance, index) { 4835 this.instance = instance; 4836 this.data = itemData; 4837 this.index = index; 4838 /** @type {HTMLImageElement | HTMLDivElement | undefined} */ 4839 4840 this.element = undefined; 4841 /** @type {Placeholder | undefined} */ 4842 4843 this.placeholder = undefined; 4844 /** @type {Slide | undefined} */ 4845 4846 this.slide = undefined; 4847 this.displayedImageWidth = 0; 4848 this.displayedImageHeight = 0; 4849 this.width = Number(this.data.w) || Number(this.data.width) || 0; 4850 this.height = Number(this.data.h) || Number(this.data.height) || 0; 4851 this.isAttached = false; 4852 this.hasSlide = false; 4853 this.isDecoding = false; 4854 /** @type {LoadState} */ 4855 4856 this.state = LOAD_STATE.IDLE; 4857 4858 if (this.data.type) { 4859 this.type = this.data.type; 4860 } else if (this.data.src) { 4861 this.type = 'image'; 4862 } else { 4863 this.type = 'html'; 4864 } 4865 4866 this.instance.dispatch('contentInit', { 4867 content: this 4868 }); 4869 } 4870 4871 removePlaceholder() { 4872 if (this.placeholder && !this.keepPlaceholder()) { 4873 // With delay, as image might be loaded, but not rendered 4874 setTimeout(() => { 4875 if (this.placeholder) { 4876 this.placeholder.destroy(); 4877 this.placeholder = undefined; 4878 } 4879 }, 1000); 4880 } 4881 } 4882 /** 4883 * Preload content 4884 * 4885 * @param {boolean} isLazy 4886 * @param {boolean} [reload] 4887 */ 4888 4889 4890 load(isLazy, reload) { 4891 if (this.slide && this.usePlaceholder()) { 4892 if (!this.placeholder) { 4893 const placeholderSrc = this.instance.applyFilters('placeholderSrc', // use image-based placeholder only for the first slide, 4894 // as rendering (even small stretched thumbnail) is an expensive operation 4895 this.data.msrc && this.slide.isFirstSlide ? this.data.msrc : false, this); 4896 this.placeholder = new Placeholder(placeholderSrc, this.slide.container); 4897 } else { 4898 const placeholderEl = this.placeholder.element; // Add placeholder to DOM if it was already created 4899 4900 if (placeholderEl && !placeholderEl.parentElement) { 4901 this.slide.container.prepend(placeholderEl); 4902 } 4903 } 4904 } 4905 4906 if (this.element && !reload) { 4907 return; 4908 } 4909 4910 if (this.instance.dispatch('contentLoad', { 4911 content: this, 4912 isLazy 4913 }).defaultPrevented) { 4914 return; 4915 } 4916 4917 if (this.isImageContent()) { 4918 this.element = createElement('pswp__img', 'img'); // Start loading only after width is defined, as sizes might depend on it. 4919 // Due to Safari feature, we must define sizes before srcset. 4920 4921 if (this.displayedImageWidth) { 4922 this.loadImage(isLazy); 4923 } 4924 } else { 4925 this.element = createElement('pswp__content', 'div'); 4926 this.element.innerHTML = this.data.html || ''; 4927 } 4928 4929 if (reload && this.slide) { 4930 this.slide.updateContentSize(true); 4931 } 4932 } 4933 /** 4934 * Preload image 4935 * 4936 * @param {boolean} isLazy 4937 */ 4938 4939 4940 loadImage(isLazy) { 4941 var _this$data$src, _this$data$alt; 4942 4943 if (!this.isImageContent() || !this.element || this.instance.dispatch('contentLoadImage', { 4944 content: this, 4945 isLazy 4946 }).defaultPrevented) { 4947 return; 4948 } 4949 4950 const imageElement = 4951 /** @type HTMLImageElement */ 4952 this.element; 4953 this.updateSrcsetSizes(); 4954 4955 if (this.data.srcset) { 4956 imageElement.srcset = this.data.srcset; 4957 } 4958 4959 imageElement.src = (_this$data$src = this.data.src) !== null && _this$data$src !== void 0 ? _this$data$src : ''; 4960 imageElement.alt = (_this$data$alt = this.data.alt) !== null && _this$data$alt !== void 0 ? _this$data$alt : ''; 4961 this.state = LOAD_STATE.LOADING; 4962 4963 if (imageElement.complete) { 4964 this.onLoaded(); 4965 } else { 4966 imageElement.onload = () => { 4967 this.onLoaded(); 4968 }; 4969 4970 imageElement.onerror = () => { 4971 this.onError(); 4972 }; 4973 } 4974 } 4975 /** 4976 * Assign slide to content 4977 * 4978 * @param {Slide} slide 4979 */ 4980 4981 4982 setSlide(slide) { 4983 this.slide = slide; 4984 this.hasSlide = true; 4985 this.instance = slide.pswp; // todo: do we need to unset slide? 4986 } 4987 /** 4988 * Content load success handler 4989 */ 4990 4991 4992 onLoaded() { 4993 this.state = LOAD_STATE.LOADED; 4994 4995 if (this.slide && this.element) { 4996 this.instance.dispatch('loadComplete', { 4997 slide: this.slide, 4998 content: this 4999 }); // if content is reloaded 5000 5001 if (this.slide.isActive && this.slide.heavyAppended && !this.element.parentNode) { 5002 this.append(); 5003 this.slide.updateContentSize(true); 5004 } 5005 5006 if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) { 5007 this.removePlaceholder(); 5008 } 5009 } 5010 } 5011 /** 5012 * Content load error handler 5013 */ 5014 5015 5016 onError() { 5017 this.state = LOAD_STATE.ERROR; 5018 5019 if (this.slide) { 5020 this.displayError(); 5021 this.instance.dispatch('loadComplete', { 5022 slide: this.slide, 5023 isError: true, 5024 content: this 5025 }); 5026 this.instance.dispatch('loadError', { 5027 slide: this.slide, 5028 content: this 5029 }); 5030 } 5031 } 5032 /** 5033 * @returns {Boolean} If the content is currently loading 5034 */ 5035 5036 5037 isLoading() { 5038 return this.instance.applyFilters('isContentLoading', this.state === LOAD_STATE.LOADING, this); 5039 } 5040 /** 5041 * @returns {Boolean} If the content is in error state 5042 */ 5043 5044 5045 isError() { 5046 return this.state === LOAD_STATE.ERROR; 5047 } 5048 /** 5049 * @returns {boolean} If the content is image 5050 */ 5051 5052 5053 isImageContent() { 5054 return this.type === 'image'; 5055 } 5056 /** 5057 * Update content size 5058 * 5059 * @param {Number} width 5060 * @param {Number} height 5061 */ 5062 5063 5064 setDisplayedSize(width, height) { 5065 if (!this.element) { 5066 return; 5067 } 5068 5069 if (this.placeholder) { 5070 this.placeholder.setDisplayedSize(width, height); 5071 } 5072 5073 if (this.instance.dispatch('contentResize', { 5074 content: this, 5075 width, 5076 height 5077 }).defaultPrevented) { 5078 return; 5079 } 5080 5081 setWidthHeight(this.element, width, height); 5082 5083 if (this.isImageContent() && !this.isError()) { 5084 const isInitialSizeUpdate = !this.displayedImageWidth && width; 5085 this.displayedImageWidth = width; 5086 this.displayedImageHeight = height; 5087 5088 if (isInitialSizeUpdate) { 5089 this.loadImage(false); 5090 } else { 5091 this.updateSrcsetSizes(); 5092 } 5093 5094 if (this.slide) { 5095 this.instance.dispatch('imageSizeChange', { 5096 slide: this.slide, 5097 width, 5098 height, 5099 content: this 5100 }); 5101 } 5102 } 5103 } 5104 /** 5105 * @returns {boolean} If the content can be zoomed 5106 */ 5107 5108 5109 isZoomable() { 5110 return this.instance.applyFilters('isContentZoomable', this.isImageContent() && this.state !== LOAD_STATE.ERROR, this); 5111 } 5112 /** 5113 * Update image srcset sizes attribute based on width and height 5114 */ 5115 5116 5117 updateSrcsetSizes() { 5118 // Handle srcset sizes attribute. 5119 // 5120 // Never lower quality, if it was increased previously. 5121 // Chrome does this automatically, Firefox and Safari do not, 5122 // so we store largest used size in dataset. 5123 if (!this.isImageContent() || !this.element || !this.data.srcset) { 5124 return; 5125 } 5126 5127 const image = 5128 /** @type HTMLImageElement */ 5129 this.element; 5130 const sizesWidth = this.instance.applyFilters('srcsetSizesWidth', this.displayedImageWidth, this); 5131 5132 if (!image.dataset.largestUsedSize || sizesWidth > parseInt(image.dataset.largestUsedSize, 10)) { 5133 image.sizes = sizesWidth + 'px'; 5134 image.dataset.largestUsedSize = String(sizesWidth); 5135 } 5136 } 5137 /** 5138 * @returns {boolean} If content should use a placeholder (from msrc by default) 5139 */ 5140 5141 5142 usePlaceholder() { 5143 return this.instance.applyFilters('useContentPlaceholder', this.isImageContent(), this); 5144 } 5145 /** 5146 * Preload content with lazy-loading param 5147 */ 5148 5149 5150 lazyLoad() { 5151 if (this.instance.dispatch('contentLazyLoad', { 5152 content: this 5153 }).defaultPrevented) { 5154 return; 5155 } 5156 5157 this.load(true); 5158 } 5159 /** 5160 * @returns {boolean} If placeholder should be kept after content is loaded 5161 */ 5162 5163 5164 keepPlaceholder() { 5165 return this.instance.applyFilters('isKeepingPlaceholder', this.isLoading(), this); 5166 } 5167 /** 5168 * Destroy the content 5169 */ 5170 5171 5172 destroy() { 5173 this.hasSlide = false; 5174 this.slide = undefined; 5175 5176 if (this.instance.dispatch('contentDestroy', { 5177 content: this 5178 }).defaultPrevented) { 5179 return; 5180 } 5181 5182 this.remove(); 5183 5184 if (this.placeholder) { 5185 this.placeholder.destroy(); 5186 this.placeholder = undefined; 5187 } 5188 5189 if (this.isImageContent() && this.element) { 5190 this.element.onload = null; 5191 this.element.onerror = null; 5192 this.element = undefined; 5193 } 5194 } 5195 /** 5196 * Display error message 5197 */ 5198 5199 5200 displayError() { 5201 if (this.slide) { 5202 var _this$instance$option, _this$instance$option2; 5203 5204 let errorMsgEl = createElement('pswp__error-msg', 'div'); 5205 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 : ''; 5206 errorMsgEl = 5207 /** @type {HTMLDivElement} */ 5208 this.instance.applyFilters('contentErrorElement', errorMsgEl, this); 5209 this.element = createElement('pswp__content pswp__error-msg-container', 'div'); 5210 this.element.appendChild(errorMsgEl); 5211 this.slide.container.innerText = ''; 5212 this.slide.container.appendChild(this.element); 5213 this.slide.updateContentSize(true); 5214 this.removePlaceholder(); 5215 } 5216 } 5217 /** 5218 * Append the content 5219 */ 5220 5221 5222 append() { 5223 if (this.isAttached || !this.element) { 5224 return; 5225 } 5226 5227 this.isAttached = true; 5228 5229 if (this.state === LOAD_STATE.ERROR) { 5230 this.displayError(); 5231 return; 5232 } 5233 5234 if (this.instance.dispatch('contentAppend', { 5235 content: this 5236 }).defaultPrevented) { 5237 return; 5238 } 5239 5240 const supportsDecode = ('decode' in this.element); 5241 5242 if (this.isImageContent()) { 5243 // Use decode() on nearby slides 5244 // 5245 // Nearby slide images are in DOM and not hidden via display:none. 5246 // However, they are placed offscreen (to the left and right side). 5247 // 5248 // Some browsers do not composite the image until it's actually visible, 5249 // using decode() helps. 5250 // 5251 // You might ask "why dont you just decode() and then append all images", 5252 // that's because I want to show image before it's fully loaded, 5253 // as browser can render parts of image while it is loading. 5254 // We do not do this in Safari due to partial loading bug. 5255 if (supportsDecode && this.slide && (!this.slide.isActive || isSafari())) { 5256 this.isDecoding = true; // purposefully using finally instead of then, 5257 // as if srcset sizes changes dynamically - it may cause decode error 5258 5259 /** @type {HTMLImageElement} */ 5260 5261 this.element.decode().catch(() => {}).finally(() => { 5262 this.isDecoding = false; 5263 this.appendImage(); 5264 }); 5265 } else { 5266 this.appendImage(); 5267 } 5268 } else if (this.slide && !this.element.parentNode) { 5269 this.slide.container.appendChild(this.element); 5270 } 5271 } 5272 /** 5273 * Activate the slide, 5274 * active slide is generally the current one, 5275 * meaning the user can see it. 5276 */ 5277 5278 5279 activate() { 5280 if (this.instance.dispatch('contentActivate', { 5281 content: this 5282 }).defaultPrevented || !this.slide) { 5283 return; 5284 } 5285 5286 if (this.isImageContent() && this.isDecoding && !isSafari()) { 5287 // add image to slide when it becomes active, 5288 // even if it's not finished decoding 5289 this.appendImage(); 5290 } else if (this.isError()) { 5291 this.load(false, true); // try to reload 5292 } 5293 5294 if (this.slide.holderElement) { 5295 this.slide.holderElement.setAttribute('aria-hidden', 'false'); 5296 } 5297 } 5298 /** 5299 * Deactivate the content 5300 */ 5301 5302 5303 deactivate() { 5304 this.instance.dispatch('contentDeactivate', { 5305 content: this 5306 }); 5307 5308 if (this.slide && this.slide.holderElement) { 5309 this.slide.holderElement.setAttribute('aria-hidden', 'true'); 5310 } 5311 } 5312 /** 5313 * Remove the content from DOM 5314 */ 5315 5316 5317 remove() { 5318 this.isAttached = false; 5319 5320 if (this.instance.dispatch('contentRemove', { 5321 content: this 5322 }).defaultPrevented) { 5323 return; 5324 } 5325 5326 if (this.element && this.element.parentNode) { 5327 this.element.remove(); 5328 } 5329 5330 if (this.placeholder && this.placeholder.element) { 5331 this.placeholder.element.remove(); 5332 } 5333 } 5334 /** 5335 * Append the image content to slide container 5336 */ 5337 5338 5339 appendImage() { 5340 if (!this.isAttached) { 5341 return; 5342 } 5343 5344 if (this.instance.dispatch('contentAppendImage', { 5345 content: this 5346 }).defaultPrevented) { 5347 return; 5348 } // ensure that element exists and is not already appended 5349 5350 5351 if (this.slide && this.element && !this.element.parentNode) { 5352 this.slide.container.appendChild(this.element); 5353 } 5354 5355 if (this.state === LOAD_STATE.LOADED || this.state === LOAD_STATE.ERROR) { 5356 this.removePlaceholder(); 5357 } 5358 } 5359 5360} 5361 5362/** @typedef {import('./content.js').default} Content */ 5363
vendor: 10,948 bytes, lines 5364-5767
5364/** @typedef {import('./slide.js').default} Slide */ 5365 5366/** @typedef {import('./slide.js').SlideData} SlideData */ 5367 5368/** @typedef {import('../core/base.js').default} PhotoSwipeBase */ 5369 5370/** @typedef {import('../photoswipe.js').default} PhotoSwipe */ 5371 5372const MIN_SLIDES_TO_CACHE = 5; 5373/** 5374 * Lazy-load an image 5375 * This function is used both by Lightbox and PhotoSwipe core, 5376 * thus it can be called before dialog is opened. 5377 * 5378 * @param {SlideData} itemData Data about the slide 5379 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox instance 5380 * @param {number} index 5381 * @returns {Content} Image that is being decoded or false. 5382 */ 5383 5384function lazyLoadData(itemData, instance, index) { 5385 const content = instance.createContentFromData(itemData, index); 5386 /** @type {ZoomLevel | undefined} */ 5387 5388 let zoomLevel; 5389 const { 5390 options 5391 } = instance; // We need to know dimensions of the image to preload it, 5392 // as it might use srcset, and we need to define sizes 5393 5394 if (options) { 5395 zoomLevel = new ZoomLevel(options, itemData, -1); 5396 let viewportSize; 5397 5398 if (instance.pswp) { 5399 viewportSize = instance.pswp.viewportSize; 5400 } else { 5401 viewportSize = getViewportSize(options, instance); 5402 } 5403 5404 const panAreaSize = getPanAreaSize(options, viewportSize, itemData, index); 5405 zoomLevel.update(content.width, content.height, panAreaSize); 5406 } 5407 5408 content.lazyLoad(); 5409 5410 if (zoomLevel) { 5411 content.setDisplayedSize(Math.ceil(content.width * zoomLevel.initial), Math.ceil(content.height * zoomLevel.initial)); 5412 } 5413 5414 return content; 5415} 5416/** 5417 * Lazy-loads specific slide. 5418 * This function is used both by Lightbox and PhotoSwipe core, 5419 * thus it can be called before dialog is opened. 5420 * 5421 * By default, it loads image based on viewport size and initial zoom level. 5422 * 5423 * @param {number} index Slide index 5424 * @param {PhotoSwipeBase} instance PhotoSwipe or PhotoSwipeLightbox eventable instance 5425 * @returns {Content | undefined} 5426 */ 5427 5428function lazyLoadSlide(index, instance) { 5429 const itemData = instance.getItemData(index); 5430 5431 if (instance.dispatch('lazyLoadSlide', { 5432 index, 5433 itemData 5434 }).defaultPrevented) { 5435 return; 5436 } 5437 5438 return lazyLoadData(itemData, instance, index); 5439} 5440 5441class ContentLoader { 5442 /** 5443 * @param {PhotoSwipe} pswp 5444 */ 5445 constructor(pswp) { 5446 this.pswp = pswp; // Total amount of cached images 5447 5448 this.limit = Math.max(pswp.options.preload[0] + pswp.options.preload[1] + 1, MIN_SLIDES_TO_CACHE); 5449 /** @type {Content[]} */ 5450 5451 this._cachedItems = []; 5452 } 5453 /** 5454 * Lazy load nearby slides based on `preload` option. 5455 * 5456 * @param {number} [diff] Difference between slide indexes that was changed recently, or 0. 5457 */ 5458 5459 5460 updateLazy(diff) { 5461 const { 5462 pswp 5463 } = this; 5464 5465 if (pswp.dispatch('lazyLoad').defaultPrevented) { 5466 return; 5467 } 5468 5469 const { 5470 preload 5471 } = pswp.options; 5472 const isForward = diff === undefined ? true : diff >= 0; 5473 let i; // preload[1] - num items to preload in forward direction 5474 5475 for (i = 0; i <= preload[1]; i++) { 5476 this.loadSlideByIndex(pswp.currIndex + (isForward ? i : -i)); 5477 } // preload[0] - num items to preload in backward direction 5478 5479 5480 for (i = 1; i <= preload[0]; i++) { 5481 this.loadSlideByIndex(pswp.currIndex + (isForward ? -i : i)); 5482 } 5483 } 5484 /** 5485 * @param {number} initialIndex 5486 */ 5487 5488 5489 loadSlideByIndex(initialIndex) { 5490 const index = this.pswp.getLoopedIndex(initialIndex); // try to get cached content 5491 5492 let content = this.getContentByIndex(index); 5493 5494 if (!content) { 5495 // no cached content, so try to load from scratch: 5496 content = lazyLoadSlide(index, this.pswp); // if content can be loaded, add it to cache: 5497 5498 if (content) { 5499 this.addToCache(content); 5500 } 5501 } 5502 } 5503 /** 5504 * @param {Slide} slide 5505 * @returns {Content} 5506 */ 5507 5508 5509 getContentBySlide(slide) { 5510 let content = this.getContentByIndex(slide.index); 5511 5512 if (!content) { 5513 // create content if not found in cache 5514 content = this.pswp.createContentFromData(slide.data, slide.index); 5515 this.addToCache(content); 5516 } // assign slide to content 5517 5518 5519 content.setSlide(slide); 5520 return content; 5521 } 5522 /** 5523 * @param {Content} content 5524 */ 5525 5526 5527 addToCache(content) { 5528 // move to the end of array 5529 this.removeByIndex(content.index); 5530 5531 this._cachedItems.push(content); 5532 5533 if (this._cachedItems.length > this.limit) { 5534 // Destroy the first content that's not attached 5535 const indexToRemove = this._cachedItems.findIndex(item => { 5536 return !item.isAttached && !item.hasSlide; 5537 }); 5538 5539 if (indexToRemove !== -1) { 5540 const removedItem = this._cachedItems.splice(indexToRemove, 1)[0]; 5541 5542 removedItem.destroy(); 5543 } 5544 } 5545 } 5546 /** 5547 * Removes an image from cache, does not destroy() it, just removes. 5548 * 5549 * @param {number} index 5550 */ 5551 5552 5553 removeByIndex(index) { 5554 const indexToRemove = this._cachedItems.findIndex(item => item.index === index); 5555 5556 if (indexToRemove !== -1) { 5557 this._cachedItems.splice(indexToRemove, 1); 5558 } 5559 } 5560 /** 5561 * @param {number} index 5562 * @returns {Content | undefined} 5563 */ 5564 5565 5566 getContentByIndex(index) { 5567 return this._cachedItems.find(content => content.index === index); 5568 } 5569 5570 destroy() { 5571 this._cachedItems.forEach(content => content.destroy()); 5572 5573 this._cachedItems = []; 5574 } 5575 5576} 5577 5578/** @typedef {import("../photoswipe.js").default} PhotoSwipe */ 5579 5580/** @typedef {import("../slide/slide.js").SlideData} SlideData */ 5581 5582/** 5583 * PhotoSwipe base class that can retrieve data about every slide. 5584 * Shared by PhotoSwipe Core and PhotoSwipe Lightbox 5585 */ 5586 5587class PhotoSwipeBase extends Eventable { 5588 /** 5589 * Get total number of slides 5590 * 5591 * @returns {number} 5592 */ 5593 getNumItems() { 5594 var _this$options; 5595 5596 let numItems = 0; 5597 const dataSource = (_this$options = this.options) === null || _this$options === void 0 ? void 0 : _this$options.dataSource; 5598 5599 if (dataSource && 'length' in dataSource) { 5600 // may be an array or just object with length property 5601 numItems = dataSource.length; 5602 } else if (dataSource && 'gallery' in dataSource) { 5603 // query DOM elements 5604 if (!dataSource.items) { 5605 dataSource.items = this._getGalleryDOMElements(dataSource.gallery); 5606 } 5607 5608 if (dataSource.items) { 5609 numItems = dataSource.items.length; 5610 } 5611 } // legacy event, before filters were introduced 5612 5613 5614 const event = this.dispatch('numItems', { 5615 dataSource, 5616 numItems 5617 }); 5618 return this.applyFilters('numItems', event.numItems, dataSource); 5619 } 5620 /** 5621 * @param {SlideData} slideData 5622 * @param {number} index 5623 * @returns {Content} 5624 */ 5625 5626 5627 createContentFromData(slideData, index) { 5628 return new Content(slideData, this, index); 5629 } 5630 /** 5631 * Get item data by index. 5632 * 5633 * "item data" should contain normalized information that PhotoSwipe needs to generate a slide. 5634 * For example, it may contain properties like 5635 * `src`, `srcset`, `w`, `h`, which will be used to generate a slide with image. 5636 * 5637 * @param {number} index 5638 * @returns {SlideData} 5639 */ 5640 5641 5642 getItemData(index) { 5643 var _this$options2; 5644 5645 const dataSource = (_this$options2 = this.options) === null || _this$options2 === void 0 ? void 0 : _this$options2.dataSource; 5646 /** @type {SlideData | HTMLElement} */ 5647 5648 let dataSourceItem = {}; 5649 5650 if (Array.isArray(dataSource)) { 5651 // Datasource is an array of elements 5652 dataSourceItem = dataSource[index]; 5653 } else if (dataSource && 'gallery' in dataSource) { 5654 // dataSource has gallery property, 5655 // thus it was created by Lightbox, based on 5656 // gallery and children options 5657 // query DOM elements 5658 if (!dataSource.items) { 5659 dataSource.items = this._getGalleryDOMElements(dataSource.gallery); 5660 } 5661 5662 dataSourceItem = dataSource.items[index]; 5663 } 5664 5665 let itemData = dataSourceItem; 5666 5667 if (itemData instanceof Element) { 5668 itemData = this._domElementToItemData(itemData); 5669 } // Dispatching the itemData event, 5670 // it's a legacy verion before filters were introduced 5671 5672 5673 const event = this.dispatch('itemData', { 5674 itemData: itemData || {}, 5675 index 5676 }); 5677 return this.applyFilters('itemData', event.itemData, index); 5678 } 5679 /** 5680 * Get array of gallery DOM elements, 5681 * based on childSelector and gallery element. 5682 * 5683 * @param {HTMLElement} galleryElement 5684 * @returns {HTMLElement[]} 5685 */ 5686 5687 5688 _getGalleryDOMElements(galleryElement) { 5689 var _this$options3, _this$options4; 5690 5691 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) { 5692 return getElementsFromOption(this.options.children, this.options.childSelector, galleryElement) || []; 5693 } 5694 5695 return [galleryElement]; 5696 } 5697 /** 5698 * Converts DOM element to item data object. 5699 * 5700 * @param {HTMLElement} element DOM element 5701 * @returns {SlideData} 5702 */ 5703 5704 5705 _domElementToItemData(element) { 5706 /** @type {SlideData} */ 5707 const itemData = { 5708 element 5709 }; 5710 const linkEl = 5711 /** @type {HTMLAnchorElement} */ 5712 element.tagName === 'A' ? element : element.querySelector('a'); 5713 5714 if (linkEl) { 5715 // src comes from data-pswp-src attribute, 5716 // if it's empty link href is used 5717 itemData.src = linkEl.dataset.pswpSrc || linkEl.href; 5718 5719 if (linkEl.dataset.pswpSrcset) { 5720 itemData.srcset = linkEl.dataset.pswpSrcset; 5721 } 5722 5723 itemData.width = linkEl.dataset.pswpWidth ? parseInt(linkEl.dataset.pswpWidth, 10) : 0; 5724 itemData.height = linkEl.dataset.pswpHeight ? parseInt(linkEl.dataset.pswpHeight, 10) : 0; // support legacy w & h properties 5725 5726 itemData.w = itemData.width; 5727 itemData.h = itemData.height; 5728 5729 if (linkEl.dataset.pswpType) { 5730 itemData.type = linkEl.dataset.pswpType; 5731 } 5732 5733 const thumbnailEl = element.querySelector('img'); 5734 5735 if (thumbnailEl) { 5736 var _thumbnailEl$getAttri; 5737 5738 // msrc is URL to placeholder image that's displayed before large image is loaded 5739 // by default it's displayed only for the first slide 5740 itemData.msrc = thumbnailEl.currentSrc || thumbnailEl.src; 5741 itemData.alt = (_thumbnailEl$getAttri = thumbnailEl.getAttribute('alt')) !== null && _thumbnailEl$getAttri !== void 0 ? _thumbnailEl$getAttri : ''; 5742 } 5743 5744 if (linkEl.dataset.pswpCropped || linkEl.dataset.cropped) { 5745 itemData.thumbCropped = true; 5746 } 5747 } 5748 5749 return this.applyFilters('domItemData', itemData, element, linkEl); 5750 } 5751 /** 5752 * Lazy-load by slide data 5753 * 5754 * @param {SlideData} itemData Data about the slide 5755 * @param {number} index 5756 * @returns {Content} Image that is being decoded or false. 5757 */ 5758 5759 5760 lazyLoadData(itemData, index) { 5761 return lazyLoadData(itemData, this, index); 5762 } 5763 5764} 5765 5766/** @typedef {import('./photoswipe.js').default} PhotoSwipe */ 5767
5768/** @typedef {import('./slide/get-thumb-bounds.js').Bounds} Bounds */ 5769 5770/** @typedef {import('./util/animations.js').AnimationProps} AnimationProps */ 5771// some browsers do not paint 5772// elements which opacity is set to 0, 5773// since we need to pre-render elements for the animation - 5774// we set it to the minimum amount 5775 5776const MIN_OPACITY = 0.003; 5777/** 5778 * Manages opening and closing transitions of the PhotoSwipe. 5779 * 5780 * It can perform zoom, fade or no transition. 5781 */ 5782 5783class Opener { 5784 /** 5785 * @param {PhotoSwipe} pswp 5786 */ 5787 constructor(pswp) { 5788 this.pswp = pswp; 5789 this.isClosed = true; 5790 this.isOpen = false; 5791 this.isClosing = false; 5792 this.isOpening = false; 5793 /** 5794 * @private 5795 * @type {number | false | undefined} 5796 */ 5797 5798 this._duration = undefined; 5799 /** @private */ 5800 5801 this._useAnimation = false; 5802 /** @private */ 5803 5804 this._croppedZoom = false; 5805 /** @private */ 5806 5807 this._animateRootOpacity = false; 5808 /** @private */ 5809 5810 this._animateBgOpacity = false; 5811 /** 5812 * @private 5813 * @type { HTMLDivElement | HTMLImageElement | null | undefined } 5814 */ 5815 5816 this._placeholder = undefined; 5817 /** 5818 * @private 5819 * @type { HTMLDivElement | undefined } 5820 */ 5821 5822 this._opacityElement = undefined; 5823 /** 5824 * @private 5825 * @type { HTMLDivElement | undefined } 5826 */ 5827 5828 this._cropContainer1 = undefined; 5829 /** 5830 * @private 5831 * @type { HTMLElement | null | undefined } 5832 */ 5833 5834 this._cropContainer2 = undefined; 5835 /** 5836 * @private 5837 * @type {Bounds | undefined}
vendor: 9,634 bytes, lines 5837-6171
5837 5838 */ 5839 5840 this._thumbBounds = undefined; 5841 this._prepareOpen = this._prepareOpen.bind(this); // Override initial zoom and pan position 5842 5843 pswp.on('firstZoomPan', this._prepareOpen); 5844 } 5845 5846 open() { 5847 this._prepareOpen(); 5848 5849 this._start(); 5850 } 5851 5852 close() { 5853 if (this.isClosed || this.isClosing || this.isOpening) { 5854 // if we close during opening animation 5855 // for now do nothing, 5856 // browsers aren't good at changing the direction of the CSS transition 5857 return; 5858 } 5859 5860 const slide = this.pswp.currSlide; 5861 this.isOpen = false; 5862 this.isOpening = false; 5863 this.isClosing = true; 5864 this._duration = this.pswp.options.hideAnimationDuration; 5865 5866 if (slide && slide.currZoomLevel * slide.width >= this.pswp.options.maxWidthToAnimate) { 5867 this._duration = 0; 5868 } 5869 5870 this._applyStartProps(); 5871 5872 setTimeout(() => { 5873 this._start(); 5874 }, this._croppedZoom ? 30 : 0); 5875 } 5876 /** @private */ 5877 5878 5879 _prepareOpen() { 5880 this.pswp.off('firstZoomPan', this._prepareOpen); 5881 5882 if (!this.isOpening) { 5883 const slide = this.pswp.currSlide; 5884 this.isOpening = true; 5885 this.isClosing = false; 5886 this._duration = this.pswp.options.showAnimationDuration; 5887 5888 if (slide && slide.zoomLevels.initial * slide.width >= this.pswp.options.maxWidthToAnimate) { 5889 this._duration = 0; 5890 } 5891 5892 this._applyStartProps(); 5893 } 5894 } 5895 /** @private */ 5896 5897 5898 _applyStartProps() { 5899 const { 5900 pswp 5901 } = this; 5902 const slide = this.pswp.currSlide; 5903 const { 5904 options 5905 } = pswp; 5906 5907 if (options.showHideAnimationType === 'fade') { 5908 options.showHideOpacity = true; 5909 this._thumbBounds = undefined; 5910 } else if (options.showHideAnimationType === 'none') { 5911 options.showHideOpacity = false; 5912 this._duration = 0; 5913 this._thumbBounds = undefined; 5914 } else if (this.isOpening && pswp._initialThumbBounds) { 5915 // Use initial bounds if defined 5916 this._thumbBounds = pswp._initialThumbBounds; 5917 } else { 5918 this._thumbBounds = this.pswp.getThumbBounds(); 5919 } 5920 5921 this._placeholder = slide === null || slide === void 0 ? void 0 : slide.getPlaceholderElement(); 5922 pswp.animations.stopAll(); // Discard animations when duration is less than 50ms 5923 5924 this._useAnimation = Boolean(this._duration && this._duration > 50); 5925 this._animateZoom = Boolean(this._thumbBounds) && (slide === null || slide === void 0 ? void 0 : slide.content.usePlaceholder()) && (!this.isClosing || !pswp.mainScroll.isShifted()); 5926 5927 if (!this._animateZoom) { 5928 this._animateRootOpacity = true; 5929 5930 if (this.isOpening && slide) { 5931 slide.zoomAndPanToInitial(); 5932 slide.applyCurrentZoomPan(); 5933 } 5934 } else { 5935 var _options$showHideOpac; 5936 5937 this._animateRootOpacity = (_options$showHideOpac = options.showHideOpacity) !== null && _options$showHideOpac !== void 0 ? _options$showHideOpac : false; 5938 } 5939 5940 this._animateBgOpacity = !this._animateRootOpacity && this.pswp.options.bgOpacity > MIN_OPACITY; 5941 this._opacityElement = this._animateRootOpacity ? pswp.element : pswp.bg; 5942 5943 if (!this._useAnimation) { 5944 this._duration = 0; 5945 this._animateZoom = false; 5946 this._animateBgOpacity = false; 5947 this._animateRootOpacity = true; 5948 5949 if (this.isOpening) { 5950 if (pswp.element) { 5951 pswp.element.style.opacity = String(MIN_OPACITY); 5952 } 5953 5954 pswp.applyBgOpacity(1); 5955 } 5956 5957 return; 5958 } 5959 5960 if (this._animateZoom && this._thumbBounds && this._thumbBounds.innerRect) { 5961 var _this$pswp$currSlide; 5962 5963 // Properties are used when animation from cropped thumbnail 5964 this._croppedZoom = true; 5965 this._cropContainer1 = this.pswp.container; 5966 this._cropContainer2 = (_this$pswp$currSlide = this.pswp.currSlide) === null || _this$pswp$currSlide === void 0 ? void 0 : _this$pswp$currSlide.holderElement; 5967 5968 if (pswp.container) { 5969 pswp.container.style.overflow = 'hidden'; 5970 pswp.container.style.width = pswp.viewportSize.x + 'px'; 5971 } 5972 } else { 5973 this._croppedZoom = false; 5974 } 5975 5976 if (this.isOpening) { 5977 // Apply styles before opening transition 5978 if (this._animateRootOpacity) { 5979 if (pswp.element) { 5980 pswp.element.style.opacity = String(MIN_OPACITY); 5981 } 5982 5983 pswp.applyBgOpacity(1); 5984 } else { 5985 if (this._animateBgOpacity && pswp.bg) { 5986 pswp.bg.style.opacity = String(MIN_OPACITY); 5987 } 5988 5989 if (pswp.element) { 5990 pswp.element.style.opacity = '1'; 5991 } 5992 } 5993 5994 if (this._animateZoom) { 5995 this._setClosedStateZoomPan(); 5996 5997 if (this._placeholder) { 5998 // tell browser that we plan to animate the placeholder 5999 this._placeholder.style.willChange = 'transform'; // hide placeholder to allow hiding of 6000 // elements that overlap it (such as icons over the thumbnail) 6001 6002 this._placeholder.style.opacity = String(MIN_OPACITY); 6003 } 6004 } 6005 } else if (this.isClosing) { 6006 // hide nearby slides to make sure that 6007 // they are not painted during the transition 6008 if (pswp.mainScroll.itemHolders[0]) { 6009 pswp.mainScroll.itemHolders[0].el.style.display = 'none'; 6010 } 6011 6012 if (pswp.mainScroll.itemHolders[2]) { 6013 pswp.mainScroll.itemHolders[2].el.style.display = 'none'; 6014 } 6015 6016 if (this._croppedZoom) { 6017 if (pswp.mainScroll.x !== 0) { 6018 // shift the main scroller to zero position 6019 pswp.mainScroll.resetPosition(); 6020 pswp.mainScroll.resize(); 6021 } 6022 } 6023 } 6024 } 6025 /** @private */ 6026 6027 6028 _start() { 6029 if (this.isOpening && this._useAnimation && this._placeholder && this._placeholder.tagName === 'IMG') { 6030 // To ensure smooth animation 6031 // we wait till the current slide image placeholder is decoded, 6032 // but no longer than 250ms, 6033 // and no shorter than 50ms 6034 // (just using requestanimationframe is not enough in Firefox, 6035 // for some reason) 6036 new Promise(resolve => { 6037 let decoded = false; 6038 let isDelaying = true; 6039 decodeImage( 6040 /** @type {HTMLImageElement} */ 6041 this._placeholder).finally(() => { 6042 decoded = true; 6043 6044 if (!isDelaying) { 6045 resolve(true); 6046 } 6047 }); 6048 setTimeout(() => { 6049 isDelaying = false; 6050 6051 if (decoded) { 6052 resolve(true); 6053 } 6054 }, 50); 6055 setTimeout(resolve, 250); 6056 }).finally(() => this._initiate()); 6057 } else { 6058 this._initiate(); 6059 } 6060 } 6061 /** @private */ 6062 6063 6064 _initiate() { 6065 var _this$pswp$element, _this$pswp$element2; 6066 6067 (_this$pswp$element = this.pswp.element) === null || _this$pswp$element === void 0 || _this$pswp$element.style.setProperty('--pswp-transition-duration', this._duration + 'ms'); 6068 this.pswp.dispatch(this.isOpening ? 'openingAnimationStart' : 'closingAnimationStart'); // legacy event 6069 6070 this.pswp.dispatch( 6071 /** @type {'initialZoomIn' | 'initialZoomOut'} */ 6072 'initialZoom' + (this.isOpening ? 'In' : 'Out')); 6073 (_this$pswp$element2 = this.pswp.element) === null || _this$pswp$element2 === void 0 || _this$pswp$element2.classList.toggle('pswp--ui-visible', this.isOpening); 6074 6075 if (this.isOpening) { 6076 if (this._placeholder) { 6077 // unhide the placeholder 6078 this._placeholder.style.opacity = '1'; 6079 } 6080 6081 this._animateToOpenState(); 6082 } else if (this.isClosing) { 6083 this._animateToClosedState(); 6084 } 6085 6086 if (!this._useAnimation) { 6087 this._onAnimationComplete(); 6088 } 6089 } 6090 /** @private */ 6091 6092 6093 _onAnimationComplete() { 6094 const { 6095 pswp 6096 } = this; 6097 this.isOpen = this.isOpening; 6098 this.isClosed = this.isClosing; 6099 this.isOpening = false; 6100 this.isClosing = false; 6101 pswp.dispatch(this.isOpen ? 'openingAnimationEnd' : 'closingAnimationEnd'); // legacy event 6102 6103 pswp.dispatch( 6104 /** @type {'initialZoomInEnd' | 'initialZoomOutEnd'} */ 6105 'initialZoom' + (this.isOpen ? 'InEnd' : 'OutEnd')); 6106 6107 if (this.isClosed) { 6108 pswp.destroy(); 6109 } else if (this.isOpen) { 6110 var _pswp$currSlide; 6111 6112 if (this._animateZoom && pswp.container) { 6113 pswp.container.style.overflow = 'visible'; 6114 pswp.container.style.width = '100%'; 6115 } 6116 6117 (_pswp$currSlide = pswp.currSlide) === null || _pswp$currSlide === void 0 || _pswp$currSlide.applyCurrentZoomPan(); 6118 } 6119 } 6120 /** @private */ 6121 6122 6123 _animateToOpenState() { 6124 const { 6125 pswp 6126 } = this; 6127 6128 if (this._animateZoom) { 6129 if (this._croppedZoom && this._cropContainer1 && this._cropContainer2) { 6130 this._animateTo(this._cropContainer1, 'transform', 'translate3d(0,0,0)'); 6131 6132 this._animateTo(this._cropContainer2, 'transform', 'none'); 6133 } 6134 6135 if (pswp.currSlide) { 6136 pswp.currSlide.zoomAndPanToInitial(); 6137 6138 this._animateTo(pswp.currSlide.container, 'transform', pswp.currSlide.getCurrentTransform()); 6139 } 6140 } 6141 6142 if (this._animateBgOpacity && pswp.bg) { 6143 this._animateTo(pswp.bg, 'opacity', String(pswp.options.bgOpacity)); 6144 } 6145 6146 if (this._animateRootOpacity && pswp.element) { 6147 this._animateTo(pswp.element, 'opacity', '1'); 6148 } 6149 } 6150 /** @private */ 6151 6152 6153 _animateToClosedState() { 6154 const { 6155 pswp 6156 } = this; 6157 6158 if (this._animateZoom) { 6159 this._setClosedStateZoomPan(true); 6160 } // do not animate opacity if it's already at 0 6161 6162 6163 if (this._animateBgOpacity && pswp.bgOpacity > 0.01 && pswp.bg) { 6164 this._animateTo(pswp.bg, 'opacity', '0'); 6165 } 6166 6167 if (this._animateRootOpacity && pswp.element) { 6168 this._animateTo(pswp.element, 'opacity', '0'); 6169 } 6170 } 6171 /**
vendor: 3,250 bytes, lines 6171-6286
6171 6172 * @private 6173 * @param {boolean} [animate] 6174 */ 6175 6176 6177 _setClosedStateZoomPan(animate) { 6178 if (!this._thumbBounds) return; 6179 const { 6180 pswp 6181 } = this; 6182 const { 6183 innerRect 6184 } = this._thumbBounds; 6185 const { 6186 currSlide, 6187 viewportSize 6188 } = pswp; 6189 6190 if (this._croppedZoom && innerRect && this._cropContainer1 && this._cropContainer2) { 6191 const containerOnePanX = -viewportSize.x + (this._thumbBounds.x - innerRect.x) + innerRect.w; 6192 const containerOnePanY = -viewportSize.y + (this._thumbBounds.y - innerRect.y) + innerRect.h; 6193 const containerTwoPanX = viewportSize.x - innerRect.w; 6194 const containerTwoPanY = viewportSize.y - innerRect.h; 6195 6196 if (animate) { 6197 this._animateTo(this._cropContainer1, 'transform', toTransformString(containerOnePanX, containerOnePanY)); 6198 6199 this._animateTo(this._cropContainer2, 'transform', toTransformString(containerTwoPanX, containerTwoPanY)); 6200 } else { 6201 setTransform(this._cropContainer1, containerOnePanX, containerOnePanY); 6202 setTransform(this._cropContainer2, containerTwoPanX, containerTwoPanY); 6203 } 6204 } 6205 6206 if (currSlide) { 6207 equalizePoints(currSlide.pan, innerRect || this._thumbBounds); 6208 currSlide.currZoomLevel = this._thumbBounds.w / currSlide.width; 6209 6210 if (animate) { 6211 this._animateTo(currSlide.container, 'transform', currSlide.getCurrentTransform()); 6212 } else { 6213 currSlide.applyCurrentZoomPan(); 6214 } 6215 } 6216 } 6217 /** 6218 * @private 6219 * @param {HTMLElement} target 6220 * @param {'transform' | 'opacity'} prop 6221 * @param {string} propValue 6222 */ 6223 6224 6225 _animateTo(target, prop, propValue) { 6226 if (!this._duration) { 6227 target.style[prop] = propValue; 6228 return; 6229 } 6230 6231 const { 6232 animations 6233 } = this.pswp; 6234 /** @type {AnimationProps} */ 6235 6236 const animProps = { 6237 duration: this._duration, 6238 easing: this.pswp.options.easing, 6239 onComplete: () => { 6240 if (!animations.activeAnimations.length) { 6241 this._onAnimationComplete(); 6242 } 6243 }, 6244 target 6245 }; 6246 animProps[prop] = propValue; 6247 animations.startTransition(animProps); 6248 } 6249 6250} 6251 6252/** 6253 * @template T 6254 * @typedef {import('./types.js').Type<T>} Type<T> 6255 */ 6256 6257/** @typedef {import('./slide/slide.js').SlideData} SlideData */ 6258 6259/** @typedef {import('./slide/zoom-level.js').ZoomLevelOption} ZoomLevelOption */ 6260 6261/** @typedef {import('./ui/ui-element.js').UIElementData} UIElementData */ 6262 6263/** @typedef {import('./main-scroll.js').ItemHolder} ItemHolder */ 6264 6265/** @typedef {import('./core/eventable.js').PhotoSwipeEventsMap} PhotoSwipeEventsMap */ 6266 6267/** @typedef {import('./core/eventable.js').PhotoSwipeFiltersMap} PhotoSwipeFiltersMap */ 6268 6269/** @typedef {import('./slide/get-thumb-bounds').Bounds} Bounds */ 6270 6271/** 6272 * @template {keyof PhotoSwipeEventsMap} T 6273 * @typedef {import('./core/eventable.js').EventCallback<T>} EventCallback<T> 6274 */ 6275 6276/** 6277 * @template {keyof PhotoSwipeEventsMap} T 6278 * @typedef {import('./core/eventable.js').AugmentedEvent<T>} AugmentedEvent<T> 6279 */ 6280 6281/** @typedef {{ x: number; y: number; id?: string | number }} Point */ 6282 6283/** @typedef {{ top: number; bottom: number; left: number; right: number }} Padding */ 6284 6285/** @typedef {SlideData[]} DataSourceArray */ 6286
6287/** @typedef {{ gallery: HTMLElement; items?: HTMLElement[] }} DataSourceObject */ 6288 6289/** @typedef {DataSourceArray | DataSourceObject} DataSource */ 6290 6291/** @typedef {(point: Point, originalEvent: PointerEvent) => void} ActionFn */ 6292 6293/** @typedef {'close' | 'next' | 'zoom' | 'zoom-or-close' | 'toggle-controls'} ActionType */ 6294 6295/** @typedef {Type<PhotoSwipe> | { default: Type<PhotoSwipe> }} PhotoSwipeModule */ 6296 6297/** @typedef {PhotoSwipeModule | Promise<PhotoSwipeModule> | (() => Promise<PhotoSwipeModule>)} PhotoSwipeModuleOption */ 6298 6299/** 6300 * @typedef {string | NodeListOf<HTMLElement> | HTMLElement[] | HTMLElement} ElementProvider 6301 */ 6302 6303/** @typedef {Partial<PreparedPhotoSwipeOptions>} PhotoSwipeOptions https://photoswipe.com/options/ */ 6304 6305/** 6306 * @typedef {Object} PreparedPhotoSwipeOptions 6307 * 6308 * @prop {DataSource} [dataSource] 6309 * Pass an array of any items via dataSource option. Its length will determine amount of slides 6310 * (which may be modified further from numItems event). 6311 * 6312 * Each item should contain data that you need to generate slide 6313 * (for image slide it would be src (image URL), width (image width), height, srcset, alt). 6314 * 6315 * If these properties are not present in your initial array, you may "pre-parse" each item from itemData filter. 6316 * 6317 * @prop {number} bgOpacity 6318 * Background backdrop opacity, always define it via this option and not via CSS rgba color. 6319 * 6320 * @prop {number} spacing 6321 * Spacing between slides. Defined as ratio relative to the viewport width (0.1 = 10% of viewport). 6322 * 6323 * @prop {boolean} allowPanToNext 6324 * Allow swipe navigation to the next slide when the current slide is zoomed. Does not apply to mouse events. 6325 * 6326 * @prop {boolean} loop 6327 * If set to true you'll be able to swipe from the last to the first image. 6328 * Option is always false when there are less than 3 slides. 6329 * 6330 * @prop {boolean} [wheelToZoom] 6331 * By default PhotoSwipe zooms image with ctrl-wheel, if you enable this option - image will zoom just via wheel. 6332 * 6333 * @prop {boolean} pinchToClose 6334 * Pinch touch gesture to close the gallery. 6335 * 6336 * @prop {boolean} closeOnVerticalDrag 6337 * Vertical drag gesture to close the PhotoSwipe. 6338 * 6339 * @prop {Padding} [padding] 6340 * Slide area padding (in pixels). 6341 * 6342 * @prop {(viewportSize: Point, itemData: SlideData, index: number) => Padding} [paddingFn] 6343 * The option is checked frequently, so make sure it's performant. Overrides padding option if defined. For example: 6344 * 6345 * @prop {number | false} hideAnimationDuration 6346 * Transition duration in milliseconds, can be 0. 6347 * 6348 * @prop {number | false} showAnimationDuration 6349 * Transition duration in milliseconds, can be 0. 6350 * 6351 * @prop {number | false} zoomAnimationDuration 6352 * Transition duration in milliseconds, can be 0. 6353 * 6354 * @prop {string} easing 6355 * String, 'cubic-bezier(.4,0,.22,1)'. CSS easing function for open/close/zoom transitions. 6356 * 6357 * @prop {boolean} escKey 6358 * Esc key to close. 6359 * 6360 * @prop {boolean} arrowKeys 6361 * Left/right arrow keys for navigation. 6362 * 6363 * @prop {boolean} trapFocus 6364 * Trap focus within PhotoSwipe element while it's open. 6365 * 6366 * @prop {boolean} returnFocus 6367 * Restore focus the last active element after PhotoSwipe is closed. 6368 * 6369 * @prop {boolean} clickToCloseNonZoomable 6370 * If image is not zoomable (for example, smaller than viewport) it can be closed by clicking on it. 6371 * 6372 * @prop {ActionType | ActionFn | false} imageClickAction 6373 * Refer to click and tap actions page. 6374 * 6375 * @prop {ActionType | ActionFn | false} bgClickAction 6376 * Refer to click and tap actions page. 6377 * 6378 * @prop {ActionType | ActionFn | false} tapAction 6379 * Refer to click and tap actions page. 6380 * 6381 * @prop {ActionType | ActionFn | false} doubleTapAction 6382 * Refer to click and tap actions page. 6383 * 6384 * @prop {number} preloaderDelay 6385 * Delay before the loading indicator will be displayed, 6386 * if image is loaded during it - the indicator will not be displayed at all. Can be zero. 6387 * 6388 * @prop {string} indexIndicatorSep 6389 * Used for slide count indicator ("1 of 10 "). 6390 * 6391 * @prop {(options: PhotoSwipeOptions, pswp: PhotoSwipeBase) => Point} [getViewportSizeFn] 6392 * A function that should return slide viewport width and height, in format {x: 100, y: 100}. 6393 * 6394 * @prop {string} errorMsg 6395 * Message to display when the image wasn't able to load. If you need to display HTML - use contentErrorElement filter. 6396 * 6397 * @prop {[number, number]} preload 6398 * Lazy loading of nearby slides based on direction of movement. Should be an array with two integers, 6399 * first one - number of items to preload before the current image, second one - after the current image. 6400 * Two nearby images are always loaded. 6401 * 6402 * @prop {string} [mainClass] 6403 * Class that will be added to the root element of PhotoSwipe, may contain multiple separated by space. 6404 * Example on Styling page. 6405 * 6406 * @prop {HTMLElement} [appendToEl] 6407 * Element to which PhotoSwipe dialog will be appended when it opens. 6408 * 6409 * @prop {number} maxWidthToAnimate 6410 * Maximum width of image to animate, if initial rendered image width 6411 * is larger than this value - the opening/closing transition will be automatically disabled. 6412 * 6413 * @prop {string} [closeTitle] 6414 * Translating 6415 * 6416 * @prop {string} [zoomTitle] 6417 * Translating 6418 * 6419 * @prop {string} [arrowPrevTitle] 6420 * Translating 6421 * 6422 * @prop {string} [arrowNextTitle] 6423 * Translating 6424 * 6425 * @prop {'zoom' | 'fade' | 'none'} [showHideAnimationType] 6426 * To adjust opening or closing transition type use lightbox option `showHideAnimationType` (`String`). 6427 * It supports three values - `zoom` (default), `fade` (default if there is no thumbnail) and `none`. 6428 * 6429 * Animations are automatically disabled if user `(prefers-reduced-motion: reduce)`. 6430 * 6431 * @prop {number} index 6432 * Defines start slide index. 6433 * 6434 * @prop {(e: MouseEvent) => number} [getClickedIndexFn] 6435 * 6436 * @prop {boolean} [arrowPrev] 6437 * @prop {boolean} [arrowNext] 6438 * @prop {boolean} [zoom] 6439 * @prop {boolean} [close] 6440 * @prop {boolean} [counter] 6441 * 6442 * @prop {string} [arrowPrevSVG] 6443 * @prop {string} [arrowNextSVG] 6444 * @prop {string} [zoomSVG] 6445 * @prop {string} [closeSVG] 6446 * @prop {string} [counterSVG] 6447 * 6448 * @prop {string} [arrowPrevTitle] 6449 * @prop {string} [arrowNextTitle] 6450 * @prop {string} [zoomTitle] 6451 * @prop {string} [closeTitle] 6452 * @prop {string} [counterTitle] 6453 * 6454 * @prop {ZoomLevelOption} [initialZoomLevel] 6455 * @prop {ZoomLevelOption} [secondaryZoomLevel] 6456 * @prop {ZoomLevelOption} [maxZoomLevel] 6457 * 6458 * @prop {boolean} [mouseMovePan] 6459 * @prop {Point | null} [initialPointerPos] 6460 * @prop {boolean} [showHideOpacity] 6461 * 6462 * @prop {PhotoSwipeModuleOption} [pswpModule] 6463 * @prop {() => Promise<any>} [openPromise] 6464 * @prop {boolean} [preloadFirstSlide] 6465 * @prop {ElementProvider} [gallery] 6466 * @prop {string} [gallerySelector] 6467 * @prop {ElementProvider} [children] 6468 * @prop {string} [childSelector] 6469 * @prop {string | false} [thumbSelector] 6470 */ 6471 6472/** @type {PreparedPhotoSwipeOptions} */ 6473 6474const defaultOptions = { 6475 allowPanToNext: true, 6476 spacing: 0.1, 6477 loop: true, 6478 pinchToClose: true, 6479 closeOnVerticalDrag: true, 6480 hideAnimationDuration: 333, 6481 showAnimationDuration: 333, 6482 zoomAnimationDuration: 333, 6483 escKey: true, 6484 arrowKeys: true, 6485 trapFocus: true, 6486 returnFocus: true, 6487 maxWidthToAnimate: 4000, 6488 clickToCloseNonZoomable: true, 6489 imageClickAction: 'zoom-or-close', 6490 bgClickAction: 'close',
vendor: 14,421 bytes, lines 6491-7074
6491 tapAction: 'toggle-controls', 6492 doubleTapAction: 'zoom', 6493 indexIndicatorSep: ' / ', 6494 preloaderDelay: 2000, 6495 bgOpacity: 0.8, 6496 index: 0, 6497 errorMsg: 'The image cannot be loaded', 6498 preload: [1, 2], 6499 easing: 'cubic-bezier(.4,0,.22,1)' 6500}; 6501/** 6502 * PhotoSwipe Core 6503 */ 6504 6505class PhotoSwipe extends PhotoSwipeBase { 6506 /** 6507 * @param {PhotoSwipeOptions} [options] 6508 */ 6509 constructor(options) { 6510 super(); 6511 this.options = this._prepareOptions(options || {}); 6512 /** 6513 * offset of viewport relative to document 6514 * 6515 * @type {Point} 6516 */ 6517 6518 this.offset = { 6519 x: 0, 6520 y: 0 6521 }; 6522 /** 6523 * @type {Point} 6524 * @private 6525 */ 6526 6527 this._prevViewportSize = { 6528 x: 0, 6529 y: 0 6530 }; 6531 /** 6532 * Size of scrollable PhotoSwipe viewport 6533 * 6534 * @type {Point} 6535 */ 6536 6537 this.viewportSize = { 6538 x: 0, 6539 y: 0 6540 }; 6541 /** 6542 * background (backdrop) opacity 6543 */ 6544 6545 this.bgOpacity = 1; 6546 this.currIndex = 0; 6547 this.potentialIndex = 0; 6548 this.isOpen = false; 6549 this.isDestroying = false; 6550 this.hasMouse = false; 6551 /** 6552 * @private 6553 * @type {SlideData} 6554 */ 6555 6556 this._initialItemData = {}; 6557 /** @type {Bounds | undefined} */ 6558 6559 this._initialThumbBounds = undefined; 6560 /** @type {HTMLDivElement | undefined} */ 6561 6562 this.topBar = undefined; 6563 /** @type {HTMLDivElement | undefined} */ 6564 6565 this.element = undefined; 6566 /** @type {HTMLDivElement | undefined} */ 6567 6568 this.template = undefined; 6569 /** @type {HTMLDivElement | undefined} */ 6570 6571 this.container = undefined; 6572 /** @type {HTMLElement | undefined} */ 6573 6574 this.scrollWrap = undefined; 6575 /** @type {Slide | undefined} */ 6576 6577 this.currSlide = undefined; 6578 this.events = new DOMEvents(); 6579 this.animations = new Animations(); 6580 this.mainScroll = new MainScroll(this); 6581 this.gestures = new Gestures(this); 6582 this.opener = new Opener(this); 6583 this.keyboard = new Keyboard(this); 6584 this.contentLoader = new ContentLoader(this); 6585 } 6586 /** @returns {boolean} */ 6587 6588 6589 init() { 6590 if (this.isOpen || this.isDestroying) { 6591 return false; 6592 } 6593 6594 this.isOpen = true; 6595 this.dispatch('init'); // legacy 6596 6597 this.dispatch('beforeOpen'); 6598 6599 this._createMainStructure(); // add classes to the root element of PhotoSwipe 6600 6601 6602 let rootClasses = 'pswp--open'; 6603 6604 if (this.gestures.supportsTouch) { 6605 rootClasses += ' pswp--touch'; 6606 } 6607 6608 if (this.options.mainClass) { 6609 rootClasses += ' ' + this.options.mainClass; 6610 } 6611 6612 if (this.element) { 6613 this.element.className += ' ' + rootClasses; 6614 } 6615 6616 this.currIndex = this.options.index || 0; 6617 this.potentialIndex = this.currIndex; 6618 this.dispatch('firstUpdate'); // starting index can be modified here 6619 // initialize scroll wheel handler to block the scroll 6620 6621 this.scrollWheel = new ScrollWheel(this); // sanitize index 6622 6623 if (Number.isNaN(this.currIndex) || this.currIndex < 0 || this.currIndex >= this.getNumItems()) { 6624 this.currIndex = 0; 6625 } 6626 6627 if (!this.gestures.supportsTouch) { 6628 // enable mouse features if no touch support detected 6629 this.mouseDetected(); 6630 } // causes forced synchronous layout 6631 6632 6633 this.updateSize(); 6634 this.offset.y = window.pageYOffset; 6635 this._initialItemData = this.getItemData(this.currIndex); 6636 this.dispatch('gettingData', { 6637 index: this.currIndex, 6638 data: this._initialItemData, 6639 slide: undefined 6640 }); // *Layout* - calculate size and position of elements here 6641 6642 this._initialThumbBounds = this.getThumbBounds(); 6643 this.dispatch('initialLayout'); 6644 this.on('openingAnimationEnd', () => { 6645 const { 6646 itemHolders 6647 } = this.mainScroll; // Add content to the previous and next slide 6648 6649 if (itemHolders[0]) { 6650 itemHolders[0].el.style.display = 'block'; 6651 this.setContent(itemHolders[0], this.currIndex - 1); 6652 } 6653 6654 if (itemHolders[2]) { 6655 itemHolders[2].el.style.display = 'block'; 6656 this.setContent(itemHolders[2], this.currIndex + 1); 6657 } 6658 6659 this.appendHeavy(); 6660 this.contentLoader.updateLazy(); 6661 this.events.add(window, 'resize', this._handlePageResize.bind(this)); 6662 this.events.add(window, 'scroll', this._updatePageScrollOffset.bind(this)); 6663 this.dispatch('bindEvents'); 6664 }); // set content for center slide (first time) 6665 6666 if (this.mainScroll.itemHolders[1]) { 6667 this.setContent(this.mainScroll.itemHolders[1], this.currIndex); 6668 } 6669 6670 this.dispatch('change'); 6671 this.opener.open(); 6672 this.dispatch('afterInit'); 6673 return true; 6674 } 6675 /** 6676 * Get looped slide index 6677 * (for example, -1 will return the last slide) 6678 * 6679 * @param {number} index 6680 * @returns {number} 6681 */ 6682 6683 6684 getLoopedIndex(index) { 6685 const numSlides = this.getNumItems(); 6686 6687 if (this.options.loop) { 6688 if (index > numSlides - 1) { 6689 index -= numSlides; 6690 } 6691 6692 if (index < 0) { 6693 index += numSlides; 6694 } 6695 } 6696 6697 return clamp(index, 0, numSlides - 1); 6698 } 6699 6700 appendHeavy() { 6701 this.mainScroll.itemHolders.forEach(itemHolder => { 6702 var _itemHolder$slide; 6703 6704 (_itemHolder$slide = itemHolder.slide) === null || _itemHolder$slide === void 0 || _itemHolder$slide.appendHeavy(); 6705 }); 6706 } 6707 /** 6708 * Change the slide 6709 * @param {number} index New index 6710 */ 6711 6712 6713 goTo(index) { 6714 this.mainScroll.moveIndexBy(this.getLoopedIndex(index) - this.potentialIndex); 6715 } 6716 /** 6717 * Go to the next slide. 6718 */ 6719 6720 6721 next() { 6722 this.goTo(this.potentialIndex + 1); 6723 } 6724 /** 6725 * Go to the previous slide. 6726 */ 6727 6728 6729 prev() { 6730 this.goTo(this.potentialIndex - 1); 6731 } 6732 /** 6733 * @see slide/slide.js zoomTo 6734 * 6735 * @param {Parameters<Slide['zoomTo']>} args 6736 */ 6737 6738 6739 zoomTo(...args) { 6740 var _this$currSlide; 6741 6742 (_this$currSlide = this.currSlide) === null || _this$currSlide === void 0 || _this$currSlide.zoomTo(...args); 6743 } 6744 /** 6745 * @see slide/slide.js toggleZoom 6746 */ 6747 6748 6749 toggleZoom() { 6750 var _this$currSlide2; 6751 6752 (_this$currSlide2 = this.currSlide) === null || _this$currSlide2 === void 0 || _this$currSlide2.toggleZoom(); 6753 } 6754 /** 6755 * Close the gallery. 6756 * After closing transition ends - destroy it 6757 */ 6758 6759 6760 close() { 6761 if (!this.opener.isOpen || this.isDestroying) { 6762 return; 6763 } 6764 6765 this.isDestroying = true; 6766 this.dispatch('close'); 6767 this.events.removeAll(); 6768 this.opener.close(); 6769 } 6770 /** 6771 * Destroys the gallery: 6772 * - instantly closes the gallery 6773 * - unbinds events, 6774 * - cleans intervals and timeouts 6775 * - removes elements from DOM 6776 */ 6777 6778 6779 destroy() { 6780 var _this$element; 6781 6782 if (!this.isDestroying) { 6783 this.options.showHideAnimationType = 'none'; 6784 this.close(); 6785 return; 6786 } 6787 6788 this.dispatch('destroy'); 6789 this._listeners = {}; 6790 6791 if (this.scrollWrap) { 6792 this.scrollWrap.ontouchmove = null; 6793 this.scrollWrap.ontouchend = null; 6794 } 6795 6796 (_this$element = this.element) === null || _this$element === void 0 || _this$element.remove(); 6797 this.mainScroll.itemHolders.forEach(itemHolder => { 6798 var _itemHolder$slide2; 6799 6800 (_itemHolder$slide2 = itemHolder.slide) === null || _itemHolder$slide2 === void 0 || _itemHolder$slide2.destroy(); 6801 }); 6802 this.contentLoader.destroy(); 6803 this.events.removeAll(); 6804 } 6805 /** 6806 * Refresh/reload content of a slide by its index 6807 * 6808 * @param {number} slideIndex 6809 */ 6810 6811 6812 refreshSlideContent(slideIndex) { 6813 this.contentLoader.removeByIndex(slideIndex); 6814 this.mainScroll.itemHolders.forEach((itemHolder, i) => { 6815 var _this$currSlide$index, _this$currSlide3; 6816 6817 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; 6818 6819 if (this.canLoop()) { 6820 potentialHolderIndex = this.getLoopedIndex(potentialHolderIndex); 6821 } 6822 6823 if (potentialHolderIndex === slideIndex) { 6824 // set the new slide content 6825 this.setContent(itemHolder, slideIndex, true); // activate the new slide if it's current 6826 6827 if (i === 1) { 6828 var _itemHolder$slide3; 6829 6830 this.currSlide = itemHolder.slide; 6831 (_itemHolder$slide3 = itemHolder.slide) === null || _itemHolder$slide3 === void 0 || _itemHolder$slide3.setIsActive(true); 6832 } 6833 } 6834 }); 6835 this.dispatch('change'); 6836 } 6837 /** 6838 * Set slide content 6839 * 6840 * @param {ItemHolder} holder mainScroll.itemHolders array item 6841 * @param {number} index Slide index 6842 * @param {boolean} [force] If content should be set even if index wasn't changed 6843 */ 6844 6845 6846 setContent(holder, index, force) { 6847 if (this.canLoop()) { 6848 index = this.getLoopedIndex(index); 6849 } 6850 6851 if (holder.slide) { 6852 if (holder.slide.index === index && !force) { 6853 // exit if holder already contains this slide 6854 // this could be common when just three slides are used 6855 return; 6856 } // destroy previous slide 6857 6858 6859 holder.slide.destroy(); 6860 holder.slide = undefined; 6861 } // exit if no loop and index is out of bounds 6862 6863 6864 if (!this.canLoop() && (index < 0 || index >= this.getNumItems())) { 6865 return; 6866 } 6867 6868 const itemData = this.getItemData(index); 6869 holder.slide = new Slide(itemData, index, this); // set current slide 6870 6871 if (index === this.currIndex) { 6872 this.currSlide = holder.slide; 6873 } 6874 6875 holder.slide.append(holder.el); 6876 } 6877 /** @returns {Point} */ 6878 6879 6880 getViewportCenterPoint() { 6881 return { 6882 x: this.viewportSize.x / 2, 6883 y: this.viewportSize.y / 2 6884 }; 6885 } 6886 /** 6887 * Update size of all elements. 6888 * Executed on init and on page resize. 6889 * 6890 * @param {boolean} [force] Update size even if size of viewport was not changed. 6891 */ 6892 6893 6894 updateSize(force) { 6895 // let item; 6896 // let itemIndex; 6897 if (this.isDestroying) { 6898 // exit if PhotoSwipe is closed or closing 6899 // (to avoid errors, as resize event might be delayed) 6900 return; 6901 } //const newWidth = this.scrollWrap.clientWidth; 6902 //const newHeight = this.scrollWrap.clientHeight; 6903 6904 6905 const newViewportSize = getViewportSize(this.options, this); 6906 6907 if (!force && pointsEqual(newViewportSize, this._prevViewportSize)) { 6908 // Exit if dimensions were not changed 6909 return; 6910 } //this._prevViewportSize.x = newWidth; 6911 //this._prevViewportSize.y = newHeight; 6912 6913 6914 equalizePoints(this._prevViewportSize, newViewportSize); 6915 this.dispatch('beforeResize'); 6916 equalizePoints(this.viewportSize, this._prevViewportSize); 6917 6918 this._updatePageScrollOffset(); 6919 6920 this.dispatch('viewportSize'); // Resize slides only after opener animation is finished 6921 // and don't re-calculate size on inital size update 6922 6923 this.mainScroll.resize(this.opener.isOpen); 6924 6925 if (!this.hasMouse && window.matchMedia('(any-hover: hover)').matches) { 6926 this.mouseDetected(); 6927 } 6928 6929 this.dispatch('resize'); 6930 } 6931 /** 6932 * @param {number} opacity 6933 */ 6934 6935 6936 applyBgOpacity(opacity) { 6937 this.bgOpacity = Math.max(opacity, 0); 6938 6939 if (this.bg) { 6940 this.bg.style.opacity = String(this.bgOpacity * this.options.bgOpacity); 6941 } 6942 } 6943 /** 6944 * Whether mouse is detected 6945 */ 6946 6947 6948 mouseDetected() { 6949 if (!this.hasMouse) { 6950 var _this$element2; 6951 6952 this.hasMouse = true; 6953 (_this$element2 = this.element) === null || _this$element2 === void 0 || _this$element2.classList.add('pswp--has_mouse'); 6954 } 6955 } 6956 /** 6957 * Page resize event handler 6958 * 6959 * @private 6960 */ 6961 6962 6963 _handlePageResize() { 6964 this.updateSize(); // In iOS webview, if element size depends on document size, 6965 // it'll be measured incorrectly in resize event 6966 // 6967 // https://bugs.webkit.org/show_bug.cgi?id=170595 6968 // https://hackernoon.com/onresize-event-broken-in-mobile-safari-d8469027bf4d 6969 6970 if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) { 6971 setTimeout(() => { 6972 this.updateSize(); 6973 }, 500); 6974 } 6975 } 6976 /** 6977 * Page scroll offset is used 6978 * to get correct coordinates 6979 * relative to PhotoSwipe viewport. 6980 * 6981 * @private 6982 */ 6983 6984 6985 _updatePageScrollOffset() { 6986 this.setScrollOffset(0, window.pageYOffset); 6987 } 6988 /** 6989 * @param {number} x 6990 * @param {number} y 6991 */ 6992 6993 6994 setScrollOffset(x, y) { 6995 this.offset.x = x; 6996 this.offset.y = y; 6997 this.dispatch('updateScrollOffset'); 6998 } 6999 /** 7000 * Create main HTML structure of PhotoSwipe, 7001 * and add it to DOM 7002 * 7003 * @private 7004 */ 7005 7006 7007 _createMainStructure() { 7008 // root DOM element of PhotoSwipe (.pswp) 7009 this.element = createElement('pswp', 'div'); 7010 this.element.setAttribute('tabindex', '-1'); 7011 this.element.setAttribute('role', 'dialog'); // template is legacy prop 7012 7013 this.template = this.element; // Background is added as a separate element, 7014 // as animating opacity is faster than animating rgba() 7015 7016 this.bg = createElement('pswp__bg', 'div', this.element); 7017 this.scrollWrap = createElement('pswp__scroll-wrap', 'section', this.element); 7018 this.container = createElement('pswp__container', 'div', this.scrollWrap); // aria pattern: carousel 7019 7020 this.scrollWrap.setAttribute('aria-roledescription', 'carousel'); 7021 this.container.setAttribute('aria-live', 'off'); 7022 this.container.setAttribute('id', 'pswp__items'); 7023 this.mainScroll.appendHolders(); 7024 this.ui = new UI(this); 7025 this.ui.init(); // append to DOM 7026 7027 (this.options.appendToEl || document.body).appendChild(this.element); 7028 } 7029 /** 7030 * Get position and dimensions of small thumbnail 7031 * {x:,y:,w:} 7032 * 7033 * Height is optional (calculated based on the large image) 7034 * 7035 * @returns {Bounds | undefined} 7036 */ 7037 7038 7039 getThumbBounds() { 7040 return getThumbBounds(this.currIndex, this.currSlide ? this.currSlide.data : this._initialItemData, this); 7041 } 7042 /** 7043 * If the PhotoSwipe can have continuous loop 7044 * @returns Boolean 7045 */ 7046 7047 7048 canLoop() { 7049 return this.options.loop && this.getNumItems() > 2; 7050 } 7051 /** 7052 * @private 7053 * @param {PhotoSwipeOptions} options 7054 * @returns {PreparedPhotoSwipeOptions} 7055 */ 7056 7057 7058 _prepareOptions(options) { 7059 if (window.matchMedia('(prefers-reduced-motion), (update: slow)').matches) { 7060 options.showHideAnimationType = 'none'; 7061 options.zoomAnimationDuration = 0; 7062 } 7063 /** @type {PreparedPhotoSwipeOptions} */ 7064 7065 7066 return { ...defaultOptions, 7067 ...options 7068 }; 7069 } 7070 7071} 7072 7073export { PhotoSwipe as default }; 7074//# 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.