UNPKG

photoswipe

Version:
517 lines (417 loc) 13.1 kB
import { createElement, equalizePoints, pointsEqual, clamp, } from './util/util.js'; import DOMEvents from './util/dom-events.js'; import Slide from './slide/slide.js'; import Gestures from './gestures/gestures.js'; import MainScroll from './main-scroll.js'; import Keyboard from './keyboard.js'; import Animations from './util/animations.js'; import ScrollWheel from './scroll-wheel.js'; import UI from './ui/ui.js'; import { getViewportSize } from './util/viewport-size.js'; import { getThumbBounds } from './slide/get-thumb-bounds.js'; import PhotoSwipeBase from './core/base.js'; import Opener from './opener.js'; import ContentLoader from './slide/loader.js'; const defaultOptions = { allowPanToNext: true, spacing: 0.1, loop: true, pinchToClose: true, closeOnVerticalDrag: true, hideAnimationDuration: 333, showAnimationDuration: 333, zoomAnimationDuration: 333, escKey: true, arrowKeys: true, returnFocus: true, maxWidthToAnimate: 4000, clickToCloseNonZoomable: true, imageClickAction: 'zoom-or-close', bgClickAction: 'close', tapAction: 'toggle-controls', doubleTapAction: 'zoom', indexIndicatorSep: ' / ', preloaderDelay: 2000, bgOpacity: 0.8, index: 0, errorMsg: 'The image cannot be loaded', preload: [1, 2], easing: 'cubic-bezier(.4,0,.22,1)' }; class PhotoSwipe extends PhotoSwipeBase { constructor(options) { super(); this._prepareOptions(options); // offset of viewport relative to document this.offset = {}; this._prevViewportSize = {}; // Size of scrollable PhotoSwipe viewport this.viewportSize = {}; // background (backdrop) opacity this.bgOpacity = 1; this.events = new DOMEvents(); /** @type {Animations} */ this.animations = new Animations(); this.mainScroll = new MainScroll(this); this.gestures = new Gestures(this); this.opener = new Opener(this); this.keyboard = new Keyboard(this); this.contentLoader = new ContentLoader(this); } init() { if (this.isOpen || this.isDestroying) { return; } this.isOpen = true; this.dispatch('init'); // legacy this.dispatch('beforeOpen'); this._createMainStructure(); // add classes to the root element of PhotoSwipe let rootClasses = 'pswp--open'; if (this.gestures.supportsTouch) { rootClasses += ' pswp--touch'; } if (this.options.mainClass) { rootClasses += ' ' + this.options.mainClass; } this.element.className += ' ' + rootClasses; this.currIndex = this.options.index || 0; this.potentialIndex = this.currIndex; this.dispatch('firstUpdate'); // starting index can be modified here // initialize scroll wheel handler to block the scroll this.scrollWheel = new ScrollWheel(this); // sanitize index if (Number.isNaN(this.currIndex) || this.currIndex < 0 || this.currIndex >= this.getNumItems()) { this.currIndex = 0; } if (!this.gestures.supportsTouch) { // enable mouse features if no touch support detected this.mouseDetected(); } // causes forced synchronous layout this.updateSize(); this.offset.y = window.pageYOffset; this._initialItemData = this.getItemData(this.currIndex); this.dispatch('gettingData', this.currIndex, this._initialItemData, true); // *Layout* - calculate size and position of elements here this._initialThumbBounds = this.getThumbBounds(); this.dispatch('initialLayout'); this.on('openingAnimationEnd', () => { // Add content to the previous and next slide this.setContent(this.mainScroll.itemHolders[0], this.currIndex - 1); this.setContent(this.mainScroll.itemHolders[2], this.currIndex + 1); this.mainScroll.itemHolders[0].el.style.display = 'block'; this.mainScroll.itemHolders[2].el.style.display = 'block'; this.appendHeavy(); this.contentLoader.updateLazy(); this.events.add(window, 'resize', this._handlePageResize.bind(this)); this.events.add(window, 'scroll', this._updatePageScrollOffset.bind(this)); this.dispatch('bindEvents'); }); // set content for center slide (first time) this.setContent(this.mainScroll.itemHolders[1], this.currIndex); this.dispatch('change'); this.opener.open(); this.dispatch('afterInit'); return true; } /** * Get looped slide index * (for example, -1 will return the last slide) * * @param {Integer} index */ getLoopedIndex(index) { const numSlides = this.getNumItems(); if (this.options.loop) { if (index > numSlides - 1) { index -= numSlides; } if (index < 0) { index += numSlides; } } index = clamp(index, 0, numSlides - 1); return index; } appendHeavy() { this.mainScroll.itemHolders.forEach((itemHolder) => { if (itemHolder.slide) { itemHolder.slide.appendHeavy(); } }); } /** * Change the slide * @param {Integer} New index */ goTo(index) { this.mainScroll.moveIndexBy( this.getLoopedIndex(index) - this.potentialIndex ); } /** * Go to the next slide. */ next() { this.goTo(this.potentialIndex + 1); } /** * Go to the previous slide. */ prev() { this.goTo(this.potentialIndex - 1); } /** * @see slide/slide.js zoomTo */ zoomTo(...args) { this.currSlide.zoomTo(...args); } /** * @see slide/slide.js toggleZoom */ toggleZoom() { this.currSlide.toggleZoom(); } /** * Close the gallery. * After closing transition ends - destroy it */ close() { if (!this.opener.isOpen || this.isDestroying) { return; } this.isDestroying = true; this.dispatch('close'); this.events.removeAll(); this.opener.close(); } /** * Destroys the gallery: * - instantly closes the gallery * - unbinds events, * - cleans intervals and timeouts * - removes elements from DOM */ destroy() { if (!this.isDestroying) { this.options.showHideAnimationType = 'none'; this.close(); return; } this.dispatch('destroy'); this.listeners = null; this.scrollWrap.ontouchmove = null; this.scrollWrap.ontouchend = null; this.element.remove(); this.mainScroll.itemHolders.forEach((itemHolder) => { if (itemHolder.slide) { itemHolder.slide.destroy(); } }); this.contentLoader.destroy(); this.events.removeAll(); } /** * Refresh/reload content of a slide by its index * * @param {Integer} slideIndex */ refreshSlideContent(slideIndex) { this.contentLoader.removeByIndex(slideIndex); this.mainScroll.itemHolders.forEach((itemHolder, i) => { let potentialHolderIndex = this.currSlide.index - 1 + i; if (this.canLoop()) { potentialHolderIndex = this.getLoopedIndex(potentialHolderIndex); } if (potentialHolderIndex === slideIndex) { // set the new slide content this.setContent(itemHolder, slideIndex, true); // activate the new slide if it's current if (i === 1) { this.currSlide = itemHolder.slide; itemHolder.slide.setIsActive(true); } } }); this.dispatch('change'); } /** * Set slide content * * @param {Object} holder mainScroll.itemHolders array item * @param {Integer} index Slide index * @param {Boolean} force If content should be set even if index wasn't changed */ setContent(holder, index, force) { if (this.canLoop()) { index = this.getLoopedIndex(index); } if (holder.slide) { if (holder.slide.index === index && !force) { // exit if holder already contains this slide // this could be common when just three slides are used return; } // destroy previous slide holder.slide.destroy(); holder.slide = null; } // exit if no loop and index is out of bounds if (!this.canLoop() && (index < 0 || index >= this.getNumItems())) { return; } const itemData = this.getItemData(index); holder.slide = new Slide(itemData, index, this); // set current slide if (index === this.currIndex) { this.currSlide = holder.slide; } holder.slide.append(holder.el); } getViewportCenterPoint() { return { x: this.viewportSize.x / 2, y: this.viewportSize.y / 2 }; } /** * Update size of all elements. * Executed on init and on page resize. * * @param {Boolean} force Update size even if size of viewport was not changed. */ updateSize(force) { // let item; // let itemIndex; if (this.isDestroying) { // exit if PhotoSwipe is closed or closing // (to avoid errors, as resize event might be delayed) return; } //const newWidth = this.scrollWrap.clientWidth; //const newHeight = this.scrollWrap.clientHeight; const newViewportSize = getViewportSize(this.options, this); if (!force && pointsEqual(newViewportSize, this._prevViewportSize)) { // Exit if dimensions were not changed return; } //this._prevViewportSize.x = newWidth; //this._prevViewportSize.y = newHeight; equalizePoints(this._prevViewportSize, newViewportSize); this.dispatch('beforeResize'); equalizePoints(this.viewportSize, this._prevViewportSize); this._updatePageScrollOffset(); this.dispatch('viewportSize'); // Resize slides only after opener animation is finished // and don't re-calculate size on inital size update this.mainScroll.resize(this.opener.isOpen); if (!this.hasMouse && window.matchMedia('(any-hover: hover)').matches) { this.mouseDetected(); } this.dispatch('resize'); } applyBgOpacity(opacity) { this.bgOpacity = Math.max(opacity, 0); this.bg.style.opacity = this.bgOpacity * this.options.bgOpacity; } /** * Whether mouse is detected */ mouseDetected() { if (!this.hasMouse) { this.hasMouse = true; this.element.classList.add('pswp--has_mouse'); } } /** * Page resize event handler */ _handlePageResize() { this.updateSize(); // In iOS webview, if element size depends on document size, // it'll be measured incorrectly in resize event // // https://bugs.webkit.org/show_bug.cgi?id=170595 // https://hackernoon.com/onresize-event-broken-in-mobile-safari-d8469027bf4d if (/iPhone|iPad|iPod/i.test(window.navigator.userAgent)) { setTimeout(() => { this.updateSize(); }, 500); } } /** * Page scroll offset is used * to get correct coordinates * relative to PhotoSwipe viewport. */ _updatePageScrollOffset() { this.setScrollOffset(0, window.pageYOffset); } setScrollOffset(x, y) { this.offset.x = x; this.offset.y = y; this.dispatch('updateScrollOffset'); } /** * Create main HTML structure of PhotoSwipe, * and add it to DOM */ _createMainStructure() { // root DOM element of PhotoSwipe (.pswp) this.element = createElement('pswp'); this.element.setAttribute('tabindex', -1); this.element.setAttribute('role', 'dialog'); // template is legacy prop this.template = this.element; // Background is added as a separate element, // as animating opacity is faster than animating rgba() this.bg = createElement('pswp__bg', false, this.element); this.scrollWrap = createElement('pswp__scroll-wrap', false, this.element); this.container = createElement('pswp__container', false, this.scrollWrap); this.mainScroll.appendHolders(); this.ui = new UI(this); this.ui.init(); // append to DOM (this.options.appendToEl || document.body).appendChild(this.element); } /** * Get position and dimensions of small thumbnail * {x:,y:,w:} * * Height is optional (calculated based on the large image) */ getThumbBounds() { return getThumbBounds( this.currIndex, this.currSlide ? this.currSlide.data : this._initialItemData, this ); } /** * If the PhotoSwipe can have continious loop * @returns Boolean */ canLoop() { return (this.options.loop && this.getNumItems() > 2); } _prepareOptions(options) { if (window.matchMedia('(prefers-reduced-motion), (update: slow)').matches) { options.showHideAnimationType = 'none'; options.zoomAnimationDuration = 0; } this.options = { ...defaultOptions, ...options }; } } export default PhotoSwipe;