woocommerce/woocommerce

View on GitHub
assets/js/photoswipe/photoswipe.js

Summary

Maintainability
F
1 mo
Test Coverage
/*! PhotoSwipe - v4.1.3 - 2019-01-08
* http://photoswipe.com
* Copyright (c) 2019 Dmitry Semenov; */
(function (root, factory) { 
    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else if (typeof exports === 'object') {
        module.exports = factory();
    } else {
        root.PhotoSwipe = factory();
    }
})(this, function () {

    'use strict';
    var PhotoSwipe = function(template, UiClass, items, options){

/*>>framework-bridge*/
/**
 *
 * Set of generic functions used by gallery.
 * 
 * You're free to modify anything here as long as functionality is kept.
 * 
 */
var framework = {
    features: null,
    bind: function(target, type, listener, unbind) {
        var methodName = (unbind ? 'remove' : 'add') + 'EventListener';
        type = type.split(' ');
        for(var i = 0; i < type.length; i++) {
            if(type[i]) {
                target[methodName]( type[i], listener, false);
            }
        }
    },
    isArray: function(obj) {
        return (obj instanceof Array);
    },
    createEl: function(classes, tag) {
        var el = document.createElement(tag || 'div');
        if(classes) {
            el.className = classes;
        }
        return el;
    },
    getScrollY: function() {
        var yOffset = window.pageYOffset;
        return yOffset !== undefined ? yOffset : document.documentElement.scrollTop;
    },
    unbind: function(target, type, listener) {
        framework.bind(target,type,listener,true);
    },
    removeClass: function(el, className) {
        var reg = new RegExp('(\\s|^)' + className + '(\\s|$)');
        el.className = el.className.replace(reg, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, ''); 
    },
    addClass: function(el, className) {
        if( !framework.hasClass(el,className) ) {
            el.className += (el.className ? ' ' : '') + className;
        }
    },
    hasClass: function(el, className) {
        return el.className && new RegExp('(^|\\s)' + className + '(\\s|$)').test(el.className);
    },
    getChildByClass: function(parentEl, childClassName) {
        var node = parentEl.firstChild;
        while(node) {
            if( framework.hasClass(node, childClassName) ) {
                return node;
            }
            node = node.nextSibling;
        }
    },
    arraySearch: function(array, value, key) {
        var i = array.length;
        while(i--) {
            if(array[i][key] === value) {
                return i;
            } 
        }
        return -1;
    },
    extend: function(o1, o2, preventOverwrite) {
        for (var prop in o2) {
            if (o2.hasOwnProperty(prop)) {
                if(preventOverwrite && o1.hasOwnProperty(prop)) {
                    continue;
                }
                o1[prop] = o2[prop];
            }
        }
    },
    easing: {
        sine: {
            out: function(k) {
                return Math.sin(k * (Math.PI / 2));
            },
            inOut: function(k) {
                return - (Math.cos(Math.PI * k) - 1) / 2;
            }
        },
        cubic: {
            out: function(k) {
                return --k * k * k + 1;
            }
        }
        /*
            elastic: {
                out: function ( k ) {

                    var s, a = 0.1, p = 0.4;
                    if ( k === 0 ) return 0;
                    if ( k === 1 ) return 1;
                    if ( !a || a < 1 ) { a = 1; s = p / 4; }
                    else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI );
                    return ( a * Math.pow( 2, - 10 * k) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) + 1 );

                },
            },
            back: {
                out: function ( k ) {
                    var s = 1.70158;
                    return --k * k * ( ( s + 1 ) * k + s ) + 1;
                }
            }
        */
    },

    /**
     * 
     * @return {object}
     * 
     * {
     *  raf : request animation frame function
     *  caf : cancel animation frame function
     *  transfrom : transform property key (with vendor), or null if not supported
     *  oldIE : IE8 or below
     * }
     * 
     */
    detectFeatures: function() {
        if(framework.features) {
            return framework.features;
        }
        var helperEl = framework.createEl(),
            helperStyle = helperEl.style,
            vendor = '',
            features = {};

        // IE8 and below
        features.oldIE = document.all && !document.addEventListener;

        features.touch = 'ontouchstart' in window;

        if(window.requestAnimationFrame) {
            features.raf = window.requestAnimationFrame;
            features.caf = window.cancelAnimationFrame;
        }

        features.pointerEvent = !!(window.PointerEvent) || navigator.msPointerEnabled;

        // fix false-positive detection of old Android in new IE
        // (IE11 ua string contains "Android 4.0")
        
        if(!features.pointerEvent) { 

            var ua = navigator.userAgent;

            // Detect if device is iPhone or iPod and if it's older than iOS 8
            // http://stackoverflow.com/a/14223920
            // 
            // This detection is made because of buggy top/bottom toolbars
            // that don't trigger window.resize event.
            // For more info refer to _isFixedPosition variable in core.js

            if (/iP(hone|od)/.test(navigator.platform)) {
                var v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);
                if(v && v.length > 0) {
                    v = parseInt(v[1], 10);
                    if(v >= 1 && v < 8 ) {
                        features.isOldIOSPhone = true;
                    }
                }
            }

            // Detect old Android (before KitKat)
            // due to bugs related to position:fixed
            // http://stackoverflow.com/questions/7184573/pick-up-the-android-version-in-the-browser-by-javascript
            
            var match = ua.match(/Android\s([0-9\.]*)/);
            var androidversion =  match ? match[1] : 0;
            androidversion = parseFloat(androidversion);
            if(androidversion >= 1 ) {
                if(androidversion < 4.4) {
                    features.isOldAndroid = true; // for fixed position bug & performance
                }
                features.androidVersion = androidversion; // for touchend bug
            }    
            features.isMobileOpera = /opera mini|opera mobi/i.test(ua);

            // p.s. yes, yes, UA sniffing is bad, propose your solution for above bugs.
        }
        
        var styleChecks = ['transform', 'perspective', 'animationName'],
            vendors = ['', 'webkit','Moz','ms','O'],
            styleCheckItem,
            styleName;

        for(var i = 0; i < 4; i++) {
            vendor = vendors[i];

            for(var a = 0; a < 3; a++) {
                styleCheckItem = styleChecks[a];

                // uppercase first letter of property name, if vendor is present
                styleName = vendor + (vendor ? 
                                        styleCheckItem.charAt(0).toUpperCase() + styleCheckItem.slice(1) : 
                                        styleCheckItem);
            
                if(!features[styleCheckItem] && styleName in helperStyle ) {
                    features[styleCheckItem] = styleName;
                }
            }

            if(vendor && !features.raf) {
                vendor = vendor.toLowerCase();
                features.raf = window[vendor+'RequestAnimationFrame'];
                if(features.raf) {
                    features.caf = window[vendor+'CancelAnimationFrame'] || 
                                    window[vendor+'CancelRequestAnimationFrame'];
                }
            }
        }
            
        if(!features.raf) {
            var lastTime = 0;
            features.raf = function(fn) {
                var currTime = new Date().getTime();
                var timeToCall = Math.max(0, 16 - (currTime - lastTime));
                var id = window.setTimeout(function() { fn(currTime + timeToCall); }, timeToCall);
                lastTime = currTime + timeToCall;
                return id;
            };
            features.caf = function(id) { clearTimeout(id); };
        }

        // Detect SVG support
        features.svg = !!document.createElementNS && 
                        !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect;

        framework.features = features;

        return features;
    }
};

framework.detectFeatures();

// Override addEventListener for old versions of IE
if(framework.features.oldIE) {

    framework.bind = function(target, type, listener, unbind) {
        
        type = type.split(' ');

        var methodName = (unbind ? 'detach' : 'attach') + 'Event',
            evName,
            _handleEv = function() {
                listener.handleEvent.call(listener);
            };

        for(var i = 0; i < type.length; i++) {
            evName = type[i];
            if(evName) {

                if(typeof listener === 'object' && listener.handleEvent) {
                    if(!unbind) {
                        listener['oldIE' + evName] = _handleEv;
                    } else {
                        if(!listener['oldIE' + evName]) {
                            return false;
                        }
                    }

                    target[methodName]( 'on' + evName, listener['oldIE' + evName]);
                } else {
                    target[methodName]( 'on' + evName, listener);
                }

            }
        }
    };
    
}

/*>>framework-bridge*/

/*>>core*/
//function(template, UiClass, items, options)

var self = this;

/**
 * Static vars, don't change unless you know what you're doing.
 */
var DOUBLE_TAP_RADIUS = 25, 
    NUM_HOLDERS = 3;

/**
 * Options
 */
var _options = {
    allowPanToNext:true,
    spacing: 0.12,
    bgOpacity: 1,
    mouseUsed: false,
    loop: true,
    pinchToClose: true,
    closeOnScroll: true,
    closeOnVerticalDrag: true,
    verticalDragRange: 0.75,
    hideAnimationDuration: 333,
    showAnimationDuration: 333,
    showHideOpacity: false,
    focus: true,
    escKey: true,
    arrowKeys: true,
    mainScrollEndFriction: 0.35,
    panEndFriction: 0.35,
    isClickableElement: function(el) {
        return el.tagName === 'A';
    },
    getDoubleTapZoom: function(isMouseClick, item) {
        if(isMouseClick) {
            return 1;
        } else {
            return item.initialZoomLevel < 0.7 ? 1 : 1.33;
        }
    },
    maxSpreadZoom: 1.33,
    modal: true,

    // not fully implemented yet
    scaleMode: 'fit' // TODO
};
framework.extend(_options, options);


/**
 * Private helper variables & functions
 */

var _getEmptyPoint = function() { 
        return {x:0,y:0}; 
    };

var _isOpen,
    _isDestroying,
    _closedByScroll,
    _currentItemIndex,
    _containerStyle,
    _containerShiftIndex,
    _currPanDist = _getEmptyPoint(),
    _startPanOffset = _getEmptyPoint(),
    _panOffset = _getEmptyPoint(),
    _upMoveEvents, // drag move, drag end & drag cancel events array
    _downEvents, // drag start events array
    _globalEventHandlers,
    _viewportSize = {},
    _currZoomLevel,
    _startZoomLevel,
    _translatePrefix,
    _translateSufix,
    _updateSizeInterval,
    _itemsNeedUpdate,
    _currPositionIndex = 0,
    _offset = {},
    _slideSize = _getEmptyPoint(), // size of slide area, including spacing
    _itemHolders,
    _prevItemIndex,
    _indexDiff = 0, // difference of indexes since last content update
    _dragStartEvent,
    _dragMoveEvent,
    _dragEndEvent,
    _dragCancelEvent,
    _transformKey,
    _pointerEventEnabled,
    _isFixedPosition = true,
    _likelyTouchDevice,
    _modules = [],
    _requestAF,
    _cancelAF,
    _initalClassName,
    _initalWindowScrollY,
    _oldIE,
    _currentWindowScrollY,
    _features,
    _windowVisibleSize = {},
    _renderMaxResolution = false,
    _orientationChangeTimeout,


    // Registers PhotoSWipe module (History, Controller ...)
    _registerModule = function(name, module) {
        framework.extend(self, module.publicMethods);
        _modules.push(name);
    },

    _getLoopedId = function(index) {
        var numSlides = _getNumItems();
        if(index > numSlides - 1) {
            return index - numSlides;
        } else  if(index < 0) {
            return numSlides + index;
        }
        return index;
    },
    
    // Micro bind/trigger
    _listeners = {},
    _listen = function(name, fn) {
        if(!_listeners[name]) {
            _listeners[name] = [];
        }
        return _listeners[name].push(fn);
    },
    _shout = function(name) {
        var listeners = _listeners[name];

        if(listeners) {
            var args = Array.prototype.slice.call(arguments);
            args.shift();

            for(var i = 0; i < listeners.length; i++) {
                listeners[i].apply(self, args);
            }
        }
    },

    _getCurrentTime = function() {
        return new Date().getTime();
    },
    _applyBgOpacity = function(opacity) {
        _bgOpacity = opacity;
        self.bg.style.opacity = opacity * _options.bgOpacity;
    },

    _applyZoomTransform = function(styleObj,x,y,zoom,item) {
        if(!_renderMaxResolution || (item && item !== self.currItem) ) {
            zoom = zoom / (item ? item.fitRatio : self.currItem.fitRatio);    
        }
            
        styleObj[_transformKey] = _translatePrefix + x + 'px, ' + y + 'px' + _translateSufix + ' scale(' + zoom + ')';
    },
    _applyCurrentZoomPan = function( allowRenderResolution ) {
        if(_currZoomElementStyle) {

            if(allowRenderResolution) {
                if(_currZoomLevel > self.currItem.fitRatio) {
                    if(!_renderMaxResolution) {
                        _setImageSize(self.currItem, false, true);
                        _renderMaxResolution = true;
                    }
                } else {
                    if(_renderMaxResolution) {
                        _setImageSize(self.currItem);
                        _renderMaxResolution = false;
                    }
                }
            }
            

            _applyZoomTransform(_currZoomElementStyle, _panOffset.x, _panOffset.y, _currZoomLevel);
        }
    },
    _applyZoomPanToItem = function(item) {
        if(item.container) {

            _applyZoomTransform(item.container.style, 
                                item.initialPosition.x, 
                                item.initialPosition.y, 
                                item.initialZoomLevel,
                                item);
        }
    },
    _setTranslateX = function(x, elStyle) {
        elStyle[_transformKey] = _translatePrefix + x + 'px, 0px' + _translateSufix;
    },
    _moveMainScroll = function(x, dragging) {

        if(!_options.loop && dragging) {
            var newSlideIndexOffset = _currentItemIndex + (_slideSize.x * _currPositionIndex - x) / _slideSize.x,
                delta = Math.round(x - _mainScrollPos.x);

            if( (newSlideIndexOffset < 0 && delta > 0) || 
                (newSlideIndexOffset >= _getNumItems() - 1 && delta < 0) ) {
                x = _mainScrollPos.x + delta * _options.mainScrollEndFriction;
            } 
        }
        
        _mainScrollPos.x = x;
        _setTranslateX(x, _containerStyle);
    },
    _calculatePanOffset = function(axis, zoomLevel) {
        var m = _midZoomPoint[axis] - _offset[axis];
        return _startPanOffset[axis] + _currPanDist[axis] + m - m * ( zoomLevel / _startZoomLevel );
    },
    
    _equalizePoints = function(p1, p2) {
        p1.x = p2.x;
        p1.y = p2.y;
        if(p2.id) {
            p1.id = p2.id;
        }
    },
    _roundPoint = function(p) {
        p.x = Math.round(p.x);
        p.y = Math.round(p.y);
    },

    _mouseMoveTimeout = null,
    _onFirstMouseMove = function() {
        // Wait until mouse move event is fired at least twice during 100ms
        // We do this, because some mobile browsers trigger it on touchstart
        if(_mouseMoveTimeout ) { 
            framework.unbind(document, 'mousemove', _onFirstMouseMove);
            framework.addClass(template, 'pswp--has_mouse');
            _options.mouseUsed = true;
            _shout('mouseUsed');
        }
        _mouseMoveTimeout = setTimeout(function() {
            _mouseMoveTimeout = null;
        }, 100);
    },

    _bindEvents = function() {
        framework.bind(document, 'keydown', self);

        if(_features.transform) {
            // don't bind click event in browsers that don't support transform (mostly IE8)
            framework.bind(self.scrollWrap, 'click', self);
        }
        

        if(!_options.mouseUsed) {
            framework.bind(document, 'mousemove', _onFirstMouseMove);
        }

        framework.bind(window, 'resize scroll orientationchange', self);

        _shout('bindEvents');
    },

    _unbindEvents = function() {
        framework.unbind(window, 'resize scroll orientationchange', self);
        framework.unbind(window, 'scroll', _globalEventHandlers.scroll);
        framework.unbind(document, 'keydown', self);
        framework.unbind(document, 'mousemove', _onFirstMouseMove);

        if(_features.transform) {
            framework.unbind(self.scrollWrap, 'click', self);
        }

        if(_isDragging) {
            framework.unbind(window, _upMoveEvents, self);
        }

        clearTimeout(_orientationChangeTimeout);

        _shout('unbindEvents');
    },
    
    _calculatePanBounds = function(zoomLevel, update) {
        var bounds = _calculateItemSize( self.currItem, _viewportSize, zoomLevel );
        if(update) {
            _currPanBounds = bounds;
        }
        return bounds;
    },
    
    _getMinZoomLevel = function(item) {
        if(!item) {
            item = self.currItem;
        }
        return item.initialZoomLevel;
    },
    _getMaxZoomLevel = function(item) {
        if(!item) {
            item = self.currItem;
        }
        return item.w > 0 ? _options.maxSpreadZoom : 1;
    },

    // Return true if offset is out of the bounds
    _modifyDestPanOffset = function(axis, destPanBounds, destPanOffset, destZoomLevel) {
        if(destZoomLevel === self.currItem.initialZoomLevel) {
            destPanOffset[axis] = self.currItem.initialPosition[axis];
            return true;
        } else {
            destPanOffset[axis] = _calculatePanOffset(axis, destZoomLevel); 

            if(destPanOffset[axis] > destPanBounds.min[axis]) {
                destPanOffset[axis] = destPanBounds.min[axis];
                return true;
            } else if(destPanOffset[axis] < destPanBounds.max[axis] ) {
                destPanOffset[axis] = destPanBounds.max[axis];
                return true;
            }
        }
        return false;
    },

    _setupTransforms = function() {

        if(_transformKey) {
            // setup 3d transforms
            var allow3dTransform = _features.perspective && !_likelyTouchDevice;
            _translatePrefix = 'translate' + (allow3dTransform ? '3d(' : '(');
            _translateSufix = _features.perspective ? ', 0px)' : ')';    
            return;
        }

        // Override zoom/pan/move functions in case old browser is used (most likely IE)
        // (so they use left/top/width/height, instead of CSS transform)
    
        _transformKey = 'left';
        framework.addClass(template, 'pswp--ie');

        _setTranslateX = function(x, elStyle) {
            elStyle.left = x + 'px';
        };
        _applyZoomPanToItem = function(item) {

            var zoomRatio = item.fitRatio > 1 ? 1 : item.fitRatio,
                s = item.container.style,
                w = zoomRatio * item.w,
                h = zoomRatio * item.h;

            s.width = w + 'px';
            s.height = h + 'px';
            s.left = item.initialPosition.x + 'px';
            s.top = item.initialPosition.y + 'px';

        };
        _applyCurrentZoomPan = function() {
            if(_currZoomElementStyle) {

                var s = _currZoomElementStyle,
                    item = self.currItem,
                    zoomRatio = item.fitRatio > 1 ? 1 : item.fitRatio,
                    w = zoomRatio * item.w,
                    h = zoomRatio * item.h;

                s.width = w + 'px';
                s.height = h + 'px';


                s.left = _panOffset.x + 'px';
                s.top = _panOffset.y + 'px';
            }
            
        };
    },

    _onKeyDown = function(e) {
        var keydownAction = '';
        if(_options.escKey && e.keyCode === 27) { 
            keydownAction = 'close';
        } else if(_options.arrowKeys) {
            if(e.keyCode === 37) {
                keydownAction = 'prev';
            } else if(e.keyCode === 39) { 
                keydownAction = 'next';
            }
        }

        if(keydownAction) {
            // don't do anything if special key pressed to prevent from overriding default browser actions
            // e.g. in Chrome on Mac cmd+arrow-left returns to previous page
            if( !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey ) {
                if(e.preventDefault) {
                    e.preventDefault();
                } else {
                    e.returnValue = false;
                } 
                self[keydownAction]();
            }
        }
    },

    _onGlobalClick = function(e) {
        if(!e) {
            return;
        }

        // don't allow click event to pass through when triggering after drag or some other gesture
        if(_moved || _zoomStarted || _mainScrollAnimating || _verticalDragInitiated) {
            e.preventDefault();
            e.stopPropagation();
        }
    },

    _updatePageScrollOffset = function() {
        self.setScrollOffset(0, framework.getScrollY());        
    };
    


    



// Micro animation engine
var _animations = {},
    _numAnimations = 0,
    _stopAnimation = function(name) {
        if(_animations[name]) {
            if(_animations[name].raf) {
                _cancelAF( _animations[name].raf );
            }
            _numAnimations--;
            delete _animations[name];
        }
    },
    _registerStartAnimation = function(name) {
        if(_animations[name]) {
            _stopAnimation(name);
        }
        if(!_animations[name]) {
            _numAnimations++;
            _animations[name] = {};
        }
    },
    _stopAllAnimations = function() {
        for (var prop in _animations) {

            if( _animations.hasOwnProperty( prop ) ) {
                _stopAnimation(prop);
            } 
            
        }
    },
    _animateProp = function(name, b, endProp, d, easingFn, onUpdate, onComplete) {
        var startAnimTime = _getCurrentTime(), t;
        _registerStartAnimation(name);

        var animloop = function(){
            if ( _animations[name] ) {
                
                t = _getCurrentTime() - startAnimTime; // time diff
                //b - beginning (start prop)
                //d - anim duration

                if ( t >= d ) {
                    _stopAnimation(name);
                    onUpdate(endProp);
                    if(onComplete) {
                        onComplete();
                    }
                    return;
                }
                onUpdate( (endProp - b) * easingFn(t/d) + b );

                _animations[name].raf = _requestAF(animloop);
            }
        };
        animloop();
    };
    


var publicMethods = {

    // make a few local variables and functions public
    shout: _shout,
    listen: _listen,
    viewportSize: _viewportSize,
    options: _options,

    isMainScrollAnimating: function() {
        return _mainScrollAnimating;
    },
    getZoomLevel: function() {
        return _currZoomLevel;
    },
    getCurrentIndex: function() {
        return _currentItemIndex;
    },
    isDragging: function() {
        return _isDragging;
    },    
    isZooming: function() {
        return _isZooming;
    },
    setScrollOffset: function(x,y) {
        _offset.x = x;
        _currentWindowScrollY = _offset.y = y;
        _shout('updateScrollOffset', _offset);
    },
    applyZoomPan: function(zoomLevel,panX,panY,allowRenderResolution) {
        _panOffset.x = panX;
        _panOffset.y = panY;
        _currZoomLevel = zoomLevel;
        _applyCurrentZoomPan( allowRenderResolution );
    },

    init: function() {

        if(_isOpen || _isDestroying) {
            return;
        }

        var i;

        self.framework = framework; // basic functionality
        self.template = template; // root DOM element of PhotoSwipe
        self.bg = framework.getChildByClass(template, 'pswp__bg');

        _initalClassName = template.className;
        _isOpen = true;
                
        _features = framework.detectFeatures();
        _requestAF = _features.raf;
        _cancelAF = _features.caf;
        _transformKey = _features.transform;
        _oldIE = _features.oldIE;
        
        self.scrollWrap = framework.getChildByClass(template, 'pswp__scroll-wrap');
        self.container = framework.getChildByClass(self.scrollWrap, 'pswp__container');

        _containerStyle = self.container.style; // for fast access

        // Objects that hold slides (there are only 3 in DOM)
        self.itemHolders = _itemHolders = [
            {el:self.container.children[0] , wrap:0, index: -1},
            {el:self.container.children[1] , wrap:0, index: -1},
            {el:self.container.children[2] , wrap:0, index: -1}
        ];

        // hide nearby item holders until initial zoom animation finishes (to avoid extra Paints)
        _itemHolders[0].el.style.display = _itemHolders[2].el.style.display = 'none';

        _setupTransforms();

        // Setup global events
        _globalEventHandlers = {
            resize: self.updateSize,

            // Fixes: iOS 10.3 resize event
            // does not update scrollWrap.clientWidth instantly after resize
            // https://github.com/dimsemenov/PhotoSwipe/issues/1315
            orientationchange: function() {
                clearTimeout(_orientationChangeTimeout);
                _orientationChangeTimeout = setTimeout(function() {
                    if(_viewportSize.x !== self.scrollWrap.clientWidth) {
                        self.updateSize();
                    }
                }, 500);
            },
            scroll: _updatePageScrollOffset,
            keydown: _onKeyDown,
            click: _onGlobalClick
        };

        // disable show/hide effects on old browsers that don't support CSS animations or transforms, 
        // old IOS, Android and Opera mobile. Blackberry seems to work fine, even older models.
        var oldPhone = _features.isOldIOSPhone || _features.isOldAndroid || _features.isMobileOpera;
        if(!_features.animationName || !_features.transform || oldPhone) {
            _options.showAnimationDuration = _options.hideAnimationDuration = 0;
        }

        // init modules
        for(i = 0; i < _modules.length; i++) {
            self['init' + _modules[i]]();
        }
        
        // init
        if(UiClass) {
            var ui = self.ui = new UiClass(self, framework);
            ui.init();
        }

        _shout('firstUpdate');
        _currentItemIndex = _currentItemIndex || _options.index || 0;
        // validate index
        if( isNaN(_currentItemIndex) || _currentItemIndex < 0 || _currentItemIndex >= _getNumItems() ) {
            _currentItemIndex = 0;
        }
        self.currItem = _getItemAt( _currentItemIndex );

        
        if(_features.isOldIOSPhone || _features.isOldAndroid) {
            _isFixedPosition = false;
        }
        
        template.setAttribute('aria-hidden', 'false');
        if(_options.modal) {
            if(!_isFixedPosition) {
                template.style.position = 'absolute';
                template.style.top = framework.getScrollY() + 'px';
            } else {
                template.style.position = 'fixed';
            }
        }

        if(_currentWindowScrollY === undefined) {
            _shout('initialLayout');
            _currentWindowScrollY = _initalWindowScrollY = framework.getScrollY();
        }
        
        // add classes to root element of PhotoSwipe
        var rootClasses = 'pswp--open ';
        if(_options.mainClass) {
            rootClasses += _options.mainClass + ' ';
        }
        if(_options.showHideOpacity) {
            rootClasses += 'pswp--animate_opacity ';
        }
        rootClasses += _likelyTouchDevice ? 'pswp--touch' : 'pswp--notouch';
        rootClasses += _features.animationName ? ' pswp--css_animation' : '';
        rootClasses += _features.svg ? ' pswp--svg' : '';
        framework.addClass(template, rootClasses);

        self.updateSize();

        // initial update
        _containerShiftIndex = -1;
        _indexDiff = null;
        for(i = 0; i < NUM_HOLDERS; i++) {
            _setTranslateX( (i+_containerShiftIndex) * _slideSize.x, _itemHolders[i].el.style);
        }

        if(!_oldIE) {
            framework.bind(self.scrollWrap, _downEvents, self); // no dragging for old IE
        }    

        _listen('initialZoomInEnd', function() {
            self.setContent(_itemHolders[0], _currentItemIndex-1);
            self.setContent(_itemHolders[2], _currentItemIndex+1);

            _itemHolders[0].el.style.display = _itemHolders[2].el.style.display = 'block';

            if(_options.focus) {
                // focus causes layout, 
                // which causes lag during the animation, 
                // that's why we delay it untill the initial zoom transition ends
                template.focus();
            }
             

            _bindEvents();
        });

        // set content for center slide (first time)
        self.setContent(_itemHolders[1], _currentItemIndex);
        
        self.updateCurrItem();

        _shout('afterInit');

        if(!_isFixedPosition) {

            // On all versions of iOS lower than 8.0, we check size of viewport every second.
            // 
            // This is done to detect when Safari top & bottom bars appear, 
            // as this action doesn't trigger any events (like resize). 
            // 
            // On iOS8 they fixed this.
            // 
            // 10 Nov 2014: iOS 7 usage ~40%. iOS 8 usage 56%.
            
            _updateSizeInterval = setInterval(function() {
                if(!_numAnimations && !_isDragging && !_isZooming && (_currZoomLevel === self.currItem.initialZoomLevel)  ) {
                    self.updateSize();
                }
            }, 1000);
        }

        framework.addClass(template, 'pswp--visible');
    },

    // Close the gallery, then destroy it
    close: function() {
        if(!_isOpen) {
            return;
        }

        _isOpen = false;
        _isDestroying = true;
        _shout('close');
        _unbindEvents();

        _showOrHide(self.currItem, null, true, self.destroy);
    },

    // destroys the gallery (unbinds events, cleans up intervals and timeouts to avoid memory leaks)
    destroy: function() {
        _shout('destroy');

        if(_showOrHideTimeout) {
            clearTimeout(_showOrHideTimeout);
        }
        
        template.setAttribute('aria-hidden', 'true');
        template.className = _initalClassName;

        if(_updateSizeInterval) {
            clearInterval(_updateSizeInterval);
        }

        framework.unbind(self.scrollWrap, _downEvents, self);

        // we unbind scroll event at the end, as closing animation may depend on it
        framework.unbind(window, 'scroll', self);

        _stopDragUpdateLoop();

        _stopAllAnimations();

        _listeners = null;
    },

    /**
     * Pan image to position
     * @param {Number} x     
     * @param {Number} y     
     * @param {Boolean} force Will ignore bounds if set to true.
     */
    panTo: function(x,y,force) {
        if(!force) {
            if(x > _currPanBounds.min.x) {
                x = _currPanBounds.min.x;
            } else if(x < _currPanBounds.max.x) {
                x = _currPanBounds.max.x;
            }

            if(y > _currPanBounds.min.y) {
                y = _currPanBounds.min.y;
            } else if(y < _currPanBounds.max.y) {
                y = _currPanBounds.max.y;
            }
        }
        
        _panOffset.x = x;
        _panOffset.y = y;
        _applyCurrentZoomPan();
    },
    
    handleEvent: function (e) {
        e = e || window.event;
        if(_globalEventHandlers[e.type]) {
            _globalEventHandlers[e.type](e);
        }
    },


    goTo: function(index) {

        index = _getLoopedId(index);

        var diff = index - _currentItemIndex;
        _indexDiff = diff;

        _currentItemIndex = index;
        self.currItem = _getItemAt( _currentItemIndex );
        _currPositionIndex -= diff;
        
        _moveMainScroll(_slideSize.x * _currPositionIndex);
        

        _stopAllAnimations();
        _mainScrollAnimating = false;

        self.updateCurrItem();
    },
    next: function() {
        self.goTo( _currentItemIndex + 1);
    },
    prev: function() {
        self.goTo( _currentItemIndex - 1);
    },

    // update current zoom/pan objects
    updateCurrZoomItem: function(emulateSetContent) {
        if(emulateSetContent) {
            _shout('beforeChange', 0);
        }

        // itemHolder[1] is middle (current) item
        if(_itemHolders[1].el.children.length) {
            var zoomElement = _itemHolders[1].el.children[0];
            if( framework.hasClass(zoomElement, 'pswp__zoom-wrap') ) {
                _currZoomElementStyle = zoomElement.style;
            } else {
                _currZoomElementStyle = null;
            }
        } else {
            _currZoomElementStyle = null;
        }
        
        _currPanBounds = self.currItem.bounds;    
        _startZoomLevel = _currZoomLevel = self.currItem.initialZoomLevel;

        _panOffset.x = _currPanBounds.center.x;
        _panOffset.y = _currPanBounds.center.y;

        if(emulateSetContent) {
            _shout('afterChange');
        }
    },


    invalidateCurrItems: function() {
        _itemsNeedUpdate = true;
        for(var i = 0; i < NUM_HOLDERS; i++) {
            if( _itemHolders[i].item ) {
                _itemHolders[i].item.needsUpdate = true;
            }
        }
    },

    updateCurrItem: function(beforeAnimation) {

        if(_indexDiff === 0) {
            return;
        }

        var diffAbs = Math.abs(_indexDiff),
            tempHolder;

        if(beforeAnimation && diffAbs < 2) {
            return;
        }


        self.currItem = _getItemAt( _currentItemIndex );
        _renderMaxResolution = false;
        
        _shout('beforeChange', _indexDiff);

        if(diffAbs >= NUM_HOLDERS) {
            _containerShiftIndex += _indexDiff + (_indexDiff > 0 ? -NUM_HOLDERS : NUM_HOLDERS);
            diffAbs = NUM_HOLDERS;
        }
        for(var i = 0; i < diffAbs; i++) {
            if(_indexDiff > 0) {
                tempHolder = _itemHolders.shift();
                _itemHolders[NUM_HOLDERS-1] = tempHolder; // move first to last

                _containerShiftIndex++;
                _setTranslateX( (_containerShiftIndex+2) * _slideSize.x, tempHolder.el.style);
                self.setContent(tempHolder, _currentItemIndex - diffAbs + i + 1 + 1);
            } else {
                tempHolder = _itemHolders.pop();
                _itemHolders.unshift( tempHolder ); // move last to first

                _containerShiftIndex--;
                _setTranslateX( _containerShiftIndex * _slideSize.x, tempHolder.el.style);
                self.setContent(tempHolder, _currentItemIndex + diffAbs - i - 1 - 1);
            }
            
        }

        // reset zoom/pan on previous item
        if(_currZoomElementStyle && Math.abs(_indexDiff) === 1) {

            var prevItem = _getItemAt(_prevItemIndex);
            if(prevItem.initialZoomLevel !== _currZoomLevel) {
                _calculateItemSize(prevItem , _viewportSize );
                _setImageSize(prevItem);
                _applyZoomPanToItem( prevItem );                 
            }

        }

        // reset diff after update
        _indexDiff = 0;

        self.updateCurrZoomItem();

        _prevItemIndex = _currentItemIndex;

        _shout('afterChange');
        
    },



    updateSize: function(force) {
        
        if(!_isFixedPosition && _options.modal) {
            var windowScrollY = framework.getScrollY();
            if(_currentWindowScrollY !== windowScrollY) {
                template.style.top = windowScrollY + 'px';
                _currentWindowScrollY = windowScrollY;
            }
            if(!force && _windowVisibleSize.x === window.innerWidth && _windowVisibleSize.y === window.innerHeight) {
                return;
            }
            _windowVisibleSize.x = window.innerWidth;
            _windowVisibleSize.y = window.innerHeight;

            //template.style.width = _windowVisibleSize.x + 'px';
            template.style.height = _windowVisibleSize.y + 'px';
        }



        _viewportSize.x = self.scrollWrap.clientWidth;
        _viewportSize.y = self.scrollWrap.clientHeight;

        _updatePageScrollOffset();

        _slideSize.x = _viewportSize.x + Math.round(_viewportSize.x * _options.spacing);
        _slideSize.y = _viewportSize.y;

        _moveMainScroll(_slideSize.x * _currPositionIndex);

        _shout('beforeResize'); // even may be used for example to switch image sources


        // don't re-calculate size on inital size update
        if(_containerShiftIndex !== undefined) {

            var holder,
                item,
                hIndex;

            for(var i = 0; i < NUM_HOLDERS; i++) {
                holder = _itemHolders[i];
                _setTranslateX( (i+_containerShiftIndex) * _slideSize.x, holder.el.style);

                hIndex = _currentItemIndex+i-1;

                if(_options.loop && _getNumItems() > 2) {
                    hIndex = _getLoopedId(hIndex);
                }

                // update zoom level on items and refresh source (if needsUpdate)
                item = _getItemAt( hIndex );

                // re-render gallery item if `needsUpdate`,
                // or doesn't have `bounds` (entirely new slide object)
                if( item && (_itemsNeedUpdate || item.needsUpdate || !item.bounds) ) {

                    self.cleanSlide( item );
                    
                    self.setContent( holder, hIndex );

                    // if "center" slide
                    if(i === 1) {
                        self.currItem = item;
                        self.updateCurrZoomItem(true);
                    }

                    item.needsUpdate = false;

                } else if(holder.index === -1 && hIndex >= 0) {
                    // add content first time
                    self.setContent( holder, hIndex );
                }
                if(item && item.container) {
                    _calculateItemSize(item, _viewportSize);
                    _setImageSize(item);
                    _applyZoomPanToItem( item );
                }
                
            }
            _itemsNeedUpdate = false;
        }    

        _startZoomLevel = _currZoomLevel = self.currItem.initialZoomLevel;
        _currPanBounds = self.currItem.bounds;

        if(_currPanBounds) {
            _panOffset.x = _currPanBounds.center.x;
            _panOffset.y = _currPanBounds.center.y;
            _applyCurrentZoomPan( true );
        }
        
        _shout('resize');
    },
    
    // Zoom current item to
    zoomTo: function(destZoomLevel, centerPoint, speed, easingFn, updateFn) {
        /*
            if(destZoomLevel === 'fit') {
                destZoomLevel = self.currItem.fitRatio;
            } else if(destZoomLevel === 'fill') {
                destZoomLevel = self.currItem.fillRatio;
            }
        */

        if(centerPoint) {
            _startZoomLevel = _currZoomLevel;
            _midZoomPoint.x = Math.abs(centerPoint.x) - _panOffset.x ;
            _midZoomPoint.y = Math.abs(centerPoint.y) - _panOffset.y ;
            _equalizePoints(_startPanOffset, _panOffset);
        }

        var destPanBounds = _calculatePanBounds(destZoomLevel, false),
            destPanOffset = {};

        _modifyDestPanOffset('x', destPanBounds, destPanOffset, destZoomLevel);
        _modifyDestPanOffset('y', destPanBounds, destPanOffset, destZoomLevel);

        var initialZoomLevel = _currZoomLevel;
        var initialPanOffset = {
            x: _panOffset.x,
            y: _panOffset.y
        };

        _roundPoint(destPanOffset);

        var onUpdate = function(now) {
            if(now === 1) {
                _currZoomLevel = destZoomLevel;
                _panOffset.x = destPanOffset.x;
                _panOffset.y = destPanOffset.y;
            } else {
                _currZoomLevel = (destZoomLevel - initialZoomLevel) * now + initialZoomLevel;
                _panOffset.x = (destPanOffset.x - initialPanOffset.x) * now + initialPanOffset.x;
                _panOffset.y = (destPanOffset.y - initialPanOffset.y) * now + initialPanOffset.y;
            }

            if(updateFn) {
                updateFn(now);
            }

            _applyCurrentZoomPan( now === 1 );
        };

        if(speed) {
            _animateProp('customZoomTo', 0, 1, speed, easingFn || framework.easing.sine.inOut, onUpdate);
        } else {
            onUpdate(1);
        }
    }


};


/*>>core*/

/*>>gestures*/
/**
 * Mouse/touch/pointer event handlers.
 * 
 * separated from @core.js for readability
 */

var MIN_SWIPE_DISTANCE = 30,
    DIRECTION_CHECK_OFFSET = 10; // amount of pixels to drag to determine direction of swipe

var _gestureStartTime,
    _gestureCheckSpeedTime,

    // pool of objects that are used during dragging of zooming
    p = {}, // first point
    p2 = {}, // second point (for zoom gesture)
    delta = {},
    _currPoint = {},
    _startPoint = {},
    _currPointers = [],
    _startMainScrollPos = {},
    _releaseAnimData,
    _posPoints = [], // array of points during dragging, used to determine type of gesture
    _tempPoint = {},

    _isZoomingIn,
    _verticalDragInitiated,
    _oldAndroidTouchEndTimeout,
    _currZoomedItemIndex = 0,
    _centerPoint = _getEmptyPoint(),
    _lastReleaseTime = 0,
    _isDragging, // at least one pointer is down
    _isMultitouch, // at least two _pointers are down
    _zoomStarted, // zoom level changed during zoom gesture
    _moved,
    _dragAnimFrame,
    _mainScrollShifted,
    _currentPoints, // array of current touch points
    _isZooming,
    _currPointsDistance,
    _startPointsDistance,
    _currPanBounds,
    _mainScrollPos = _getEmptyPoint(),
    _currZoomElementStyle,
    _mainScrollAnimating, // true, if animation after swipe gesture is running
    _midZoomPoint = _getEmptyPoint(),
    _currCenterPoint = _getEmptyPoint(),
    _direction,
    _isFirstMove,
    _opacityChanged,
    _bgOpacity,
    _wasOverInitialZoom,

    _isEqualPoints = function(p1, p2) {
        return p1.x === p2.x && p1.y === p2.y;
    },
    _isNearbyPoints = function(touch0, touch1) {
        return Math.abs(touch0.x - touch1.x) < DOUBLE_TAP_RADIUS && Math.abs(touch0.y - touch1.y) < DOUBLE_TAP_RADIUS;
    },
    _calculatePointsDistance = function(p1, p2) {
        _tempPoint.x = Math.abs( p1.x - p2.x );
        _tempPoint.y = Math.abs( p1.y - p2.y );
        return Math.sqrt(_tempPoint.x * _tempPoint.x + _tempPoint.y * _tempPoint.y);
    },
    _stopDragUpdateLoop = function() {
        if(_dragAnimFrame) {
            _cancelAF(_dragAnimFrame);
            _dragAnimFrame = null;
        }
    },
    _dragUpdateLoop = function() {
        if(_isDragging) {
            _dragAnimFrame = _requestAF(_dragUpdateLoop);
            _renderMovement();
        }
    },
    _canPan = function() {
        return !(_options.scaleMode === 'fit' && _currZoomLevel ===  self.currItem.initialZoomLevel);
    },
    
    // find the closest parent DOM element
    _closestElement = function(el, fn) {
          if(!el || el === document) {
              return false;
          }

          // don't search elements above pswp__scroll-wrap
          if(el.getAttribute('class') && el.getAttribute('class').indexOf('pswp__scroll-wrap') > -1 ) {
              return false;
          }

          if( fn(el) ) {
              return el;
          }

          return _closestElement(el.parentNode, fn);
    },

    _preventObj = {},
    _preventDefaultEventBehaviour = function(e, isDown) {
        _preventObj.prevent = !_closestElement(e.target, _options.isClickableElement);

        _shout('preventDragEvent', e, isDown, _preventObj);
        return _preventObj.prevent;

    },
    _convertTouchToPoint = function(touch, p) {
        p.x = touch.pageX;
        p.y = touch.pageY;
        p.id = touch.identifier;
        return p;
    },
    _findCenterOfPoints = function(p1, p2, pCenter) {
        pCenter.x = (p1.x + p2.x) * 0.5;
        pCenter.y = (p1.y + p2.y) * 0.5;
    },
    _pushPosPoint = function(time, x, y) {
        if(time - _gestureCheckSpeedTime > 50) {
            var o = _posPoints.length > 2 ? _posPoints.shift() : {};
            o.x = x;
            o.y = y; 
            _posPoints.push(o);
            _gestureCheckSpeedTime = time;
        }
    },

    _calculateVerticalDragOpacityRatio = function() {
        var yOffset = _panOffset.y - self.currItem.initialPosition.y; // difference between initial and current position
        return 1 -  Math.abs( yOffset / (_viewportSize.y / 2)  );
    },

    
    // points pool, reused during touch events
    _ePoint1 = {},
    _ePoint2 = {},
    _tempPointsArr = [],
    _tempCounter,
    _getTouchPoints = function(e) {
        // clean up previous points, without recreating array
        while(_tempPointsArr.length > 0) {
            _tempPointsArr.pop();
        }

        if(!_pointerEventEnabled) {
            if(e.type.indexOf('touch') > -1) {

                if(e.touches && e.touches.length > 0) {
                    _tempPointsArr[0] = _convertTouchToPoint(e.touches[0], _ePoint1);
                    if(e.touches.length > 1) {
                        _tempPointsArr[1] = _convertTouchToPoint(e.touches[1], _ePoint2);
                    }
                }
                
            } else {
                _ePoint1.x = e.pageX;
                _ePoint1.y = e.pageY;
                _ePoint1.id = '';
                _tempPointsArr[0] = _ePoint1;//_ePoint1;
            }
        } else {
            _tempCounter = 0;
            // we can use forEach, as pointer events are supported only in modern browsers
            _currPointers.forEach(function(p) {
                if(_tempCounter === 0) {
                    _tempPointsArr[0] = p;
                } else if(_tempCounter === 1) {
                    _tempPointsArr[1] = p;
                }
                _tempCounter++;

            });
        }
        return _tempPointsArr;
    },

    _panOrMoveMainScroll = function(axis, delta) {

        var panFriction,
            overDiff = 0,
            newOffset = _panOffset[axis] + delta[axis],
            startOverDiff,
            dir = delta[axis] > 0,
            newMainScrollPosition = _mainScrollPos.x + delta.x,
            mainScrollDiff = _mainScrollPos.x - _startMainScrollPos.x,
            newPanPos,
            newMainScrollPos;

        // calculate fdistance over the bounds and friction
        if(newOffset > _currPanBounds.min[axis] || newOffset < _currPanBounds.max[axis]) {
            panFriction = _options.panEndFriction;
            // Linear increasing of friction, so at 1/4 of viewport it's at max value. 
            // Looks not as nice as was expected. Left for history.
            // panFriction = (1 - (_panOffset[axis] + delta[axis] + panBounds.min[axis]) / (_viewportSize[axis] / 4) );
        } else {
            panFriction = 1;
        }
        
        newOffset = _panOffset[axis] + delta[axis] * panFriction;

        // move main scroll or start panning
        if(_options.allowPanToNext || _currZoomLevel === self.currItem.initialZoomLevel) {


            if(!_currZoomElementStyle) {
                
                newMainScrollPos = newMainScrollPosition;

            } else if(_direction === 'h' && axis === 'x' && !_zoomStarted ) {
                
                if(dir) {
                    if(newOffset > _currPanBounds.min[axis]) {
                        panFriction = _options.panEndFriction;
                        overDiff = _currPanBounds.min[axis] - newOffset;
                        startOverDiff = _currPanBounds.min[axis] - _startPanOffset[axis];
                    }
                    
                    // drag right
                    if( (startOverDiff <= 0 || mainScrollDiff < 0) && _getNumItems() > 1 ) {
                        newMainScrollPos = newMainScrollPosition;
                        if(mainScrollDiff < 0 && newMainScrollPosition > _startMainScrollPos.x) {
                            newMainScrollPos = _startMainScrollPos.x;
                        }
                    } else {
                        if(_currPanBounds.min.x !== _currPanBounds.max.x) {
                            newPanPos = newOffset;
                        }
                        
                    }

                } else {

                    if(newOffset < _currPanBounds.max[axis] ) {
                        panFriction =_options.panEndFriction;
                        overDiff = newOffset - _currPanBounds.max[axis];
                        startOverDiff = _startPanOffset[axis] - _currPanBounds.max[axis];
                    }

                    if( (startOverDiff <= 0 || mainScrollDiff > 0) && _getNumItems() > 1 ) {
                        newMainScrollPos = newMainScrollPosition;

                        if(mainScrollDiff > 0 && newMainScrollPosition < _startMainScrollPos.x) {
                            newMainScrollPos = _startMainScrollPos.x;
                        }

                    } else {
                        if(_currPanBounds.min.x !== _currPanBounds.max.x) {
                            newPanPos = newOffset;
                        }
                    }

                }


                //
            }

            if(axis === 'x') {

                if(newMainScrollPos !== undefined) {
                    _moveMainScroll(newMainScrollPos, true);
                    if(newMainScrollPos === _startMainScrollPos.x) {
                        _mainScrollShifted = false;
                    } else {
                        _mainScrollShifted = true;
                    }
                }

                if(_currPanBounds.min.x !== _currPanBounds.max.x) {
                    if(newPanPos !== undefined) {
                        _panOffset.x = newPanPos;
                    } else if(!_mainScrollShifted) {
                        _panOffset.x += delta.x * panFriction;
                    }
                }

                return newMainScrollPos !== undefined;
            }

        }

        if(!_mainScrollAnimating) {
            
            if(!_mainScrollShifted) {
                if(_currZoomLevel > self.currItem.fitRatio) {
                    _panOffset[axis] += delta[axis] * panFriction;
                
                }
            }

            
        }
        
    },

    // Pointerdown/touchstart/mousedown handler
    _onDragStart = function(e) {

        // Allow dragging only via left mouse button.
        // As this handler is not added in IE8 - we ignore e.which
        // 
        // http://www.quirksmode.org/js/events_properties.html
        // https://developer.mozilla.org/en-US/docs/Web/API/event.button
        if(e.type === 'mousedown' && e.button > 0  ) {
            return;
        }

        if(_initialZoomRunning) {
            e.preventDefault();
            return;
        }

        if(_oldAndroidTouchEndTimeout && e.type === 'mousedown') {
            return;
        }

        if(_preventDefaultEventBehaviour(e, true)) {
            e.preventDefault();
        }



        _shout('pointerDown');

        if(_pointerEventEnabled) {
            var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
            if(pointerIndex < 0) {
                pointerIndex = _currPointers.length;
            }
            _currPointers[pointerIndex] = {x:e.pageX, y:e.pageY, id: e.pointerId};
        }
        


        var startPointsList = _getTouchPoints(e),
            numPoints = startPointsList.length;

        _currentPoints = null;

        _stopAllAnimations();

        // init drag
        if(!_isDragging || numPoints === 1) {

            

            _isDragging = _isFirstMove = true;
            framework.bind(window, _upMoveEvents, self);

            _isZoomingIn = 
                _wasOverInitialZoom = 
                _opacityChanged = 
                _verticalDragInitiated = 
                _mainScrollShifted = 
                _moved = 
                _isMultitouch = 
                _zoomStarted = false;

            _direction = null;

            _shout('firstTouchStart', startPointsList);

            _equalizePoints(_startPanOffset, _panOffset);

            _currPanDist.x = _currPanDist.y = 0;
            _equalizePoints(_currPoint, startPointsList[0]);
            _equalizePoints(_startPoint, _currPoint);

            //_equalizePoints(_startMainScrollPos, _mainScrollPos);
            _startMainScrollPos.x = _slideSize.x * _currPositionIndex;

            _posPoints = [{
                x: _currPoint.x,
                y: _currPoint.y
            }];

            _gestureCheckSpeedTime = _gestureStartTime = _getCurrentTime();

            //_mainScrollAnimationEnd(true);
            _calculatePanBounds( _currZoomLevel, true );
            
            // Start rendering
            _stopDragUpdateLoop();
            _dragUpdateLoop();
            
        }

        // init zoom
        if(!_isZooming && numPoints > 1 && !_mainScrollAnimating && !_mainScrollShifted) {
            _startZoomLevel = _currZoomLevel;
            _zoomStarted = false; // true if zoom changed at least once

            _isZooming = _isMultitouch = true;
            _currPanDist.y = _currPanDist.x = 0;

            _equalizePoints(_startPanOffset, _panOffset);

            _equalizePoints(p, startPointsList[0]);
            _equalizePoints(p2, startPointsList[1]);

            _findCenterOfPoints(p, p2, _currCenterPoint);

            _midZoomPoint.x = Math.abs(_currCenterPoint.x) - _panOffset.x;
            _midZoomPoint.y = Math.abs(_currCenterPoint.y) - _panOffset.y;
            _currPointsDistance = _startPointsDistance = _calculatePointsDistance(p, p2);
        }


    },

    // Pointermove/touchmove/mousemove handler
    _onDragMove = function(e) {

        e.preventDefault();

        if(_pointerEventEnabled) {
            var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
            if(pointerIndex > -1) {
                var p = _currPointers[pointerIndex];
                p.x = e.pageX;
                p.y = e.pageY; 
            }
        }

        if(_isDragging) {
            var touchesList = _getTouchPoints(e);
            if(!_direction && !_moved && !_isZooming) {

                if(_mainScrollPos.x !== _slideSize.x * _currPositionIndex) {
                    // if main scroll position is shifted – direction is always horizontal
                    _direction = 'h';
                } else {
                    var diff = Math.abs(touchesList[0].x - _currPoint.x) - Math.abs(touchesList[0].y - _currPoint.y);
                    // check the direction of movement
                    if(Math.abs(diff) >= DIRECTION_CHECK_OFFSET) {
                        _direction = diff > 0 ? 'h' : 'v';
                        _currentPoints = touchesList;
                    }
                }
                
            } else {
                _currentPoints = touchesList;
            }
        }    
    },
    // 
    _renderMovement =  function() {

        if(!_currentPoints) {
            return;
        }

        var numPoints = _currentPoints.length;

        if(numPoints === 0) {
            return;
        }

        _equalizePoints(p, _currentPoints[0]);

        delta.x = p.x - _currPoint.x;
        delta.y = p.y - _currPoint.y;

        if(_isZooming && numPoints > 1) {
            // Handle behaviour for more than 1 point

            _currPoint.x = p.x;
            _currPoint.y = p.y;
        
            // check if one of two points changed
            if( !delta.x && !delta.y && _isEqualPoints(_currentPoints[1], p2) ) {
                return;
            }

            _equalizePoints(p2, _currentPoints[1]);


            if(!_zoomStarted) {
                _zoomStarted = true;
                _shout('zoomGestureStarted');
            }
            
            // Distance between two points
            var pointsDistance = _calculatePointsDistance(p,p2);

            var zoomLevel = _calculateZoomLevel(pointsDistance);

            // slightly over the of initial zoom level
            if(zoomLevel > self.currItem.initialZoomLevel + self.currItem.initialZoomLevel / 15) {
                _wasOverInitialZoom = true;
            }

            // Apply the friction if zoom level is out of the bounds
            var zoomFriction = 1,
                minZoomLevel = _getMinZoomLevel(),
                maxZoomLevel = _getMaxZoomLevel();

            if ( zoomLevel < minZoomLevel ) {
                
                if(_options.pinchToClose && !_wasOverInitialZoom && _startZoomLevel <= self.currItem.initialZoomLevel) {
                    // fade out background if zooming out
                    var minusDiff = minZoomLevel - zoomLevel;
                    var percent = 1 - minusDiff / (minZoomLevel / 1.2);

                    _applyBgOpacity(percent);
                    _shout('onPinchClose', percent);
                    _opacityChanged = true;
                } else {
                    zoomFriction = (minZoomLevel - zoomLevel) / minZoomLevel;
                    if(zoomFriction > 1) {
                        zoomFriction = 1;
                    }
                    zoomLevel = minZoomLevel - zoomFriction * (minZoomLevel / 3);
                }
                
            } else if ( zoomLevel > maxZoomLevel ) {
                // 1.5 - extra zoom level above the max. E.g. if max is x6, real max 6 + 1.5 = 7.5
                zoomFriction = (zoomLevel - maxZoomLevel) / ( minZoomLevel * 6 );
                if(zoomFriction > 1) {
                    zoomFriction = 1;
                }
                zoomLevel = maxZoomLevel + zoomFriction * minZoomLevel;
            }

            if(zoomFriction < 0) {
                zoomFriction = 0;
            }

            // distance between touch points after friction is applied
            _currPointsDistance = pointsDistance;

            // _centerPoint - The point in the middle of two pointers
            _findCenterOfPoints(p, p2, _centerPoint);
        
            // paning with two pointers pressed
            _currPanDist.x += _centerPoint.x - _currCenterPoint.x;
            _currPanDist.y += _centerPoint.y - _currCenterPoint.y;
            _equalizePoints(_currCenterPoint, _centerPoint);

            _panOffset.x = _calculatePanOffset('x', zoomLevel);
            _panOffset.y = _calculatePanOffset('y', zoomLevel);

            _isZoomingIn = zoomLevel > _currZoomLevel;
            _currZoomLevel = zoomLevel;
            _applyCurrentZoomPan();

        } else {

            // handle behaviour for one point (dragging or panning)

            if(!_direction) {
                return;
            }

            if(_isFirstMove) {
                _isFirstMove = false;

                // subtract drag distance that was used during the detection direction  

                if( Math.abs(delta.x) >= DIRECTION_CHECK_OFFSET) {
                    delta.x -= _currentPoints[0].x - _startPoint.x;
                }
                
                if( Math.abs(delta.y) >= DIRECTION_CHECK_OFFSET) {
                    delta.y -= _currentPoints[0].y - _startPoint.y;
                }
            }

            _currPoint.x = p.x;
            _currPoint.y = p.y;

            // do nothing if pointers position hasn't changed
            if(delta.x === 0 && delta.y === 0) {
                return;
            }

            if(_direction === 'v' && _options.closeOnVerticalDrag) {
                if(!_canPan()) {
                    _currPanDist.y += delta.y;
                    _panOffset.y += delta.y;

                    var opacityRatio = _calculateVerticalDragOpacityRatio();

                    _verticalDragInitiated = true;
                    _shout('onVerticalDrag', opacityRatio);

                    _applyBgOpacity(opacityRatio);
                    _applyCurrentZoomPan();
                    return ;
                }
            }

            _pushPosPoint(_getCurrentTime(), p.x, p.y);

            _moved = true;
            _currPanBounds = self.currItem.bounds;
            
            var mainScrollChanged = _panOrMoveMainScroll('x', delta);
            if(!mainScrollChanged) {
                _panOrMoveMainScroll('y', delta);

                _roundPoint(_panOffset);
                _applyCurrentZoomPan();
            }

        }

    },
    
    // Pointerup/pointercancel/touchend/touchcancel/mouseup event handler
    _onDragRelease = function(e) {

        if(_features.isOldAndroid ) {

            if(_oldAndroidTouchEndTimeout && e.type === 'mouseup') {
                return;
            }

            // on Android (v4.1, 4.2, 4.3 & possibly older) 
            // ghost mousedown/up event isn't preventable via e.preventDefault,
            // which causes fake mousedown event
            // so we block mousedown/up for 600ms
            if( e.type.indexOf('touch') > -1 ) {
                clearTimeout(_oldAndroidTouchEndTimeout);
                _oldAndroidTouchEndTimeout = setTimeout(function() {
                    _oldAndroidTouchEndTimeout = 0;
                }, 600);
            }
            
        }

        _shout('pointerUp');

        if(_preventDefaultEventBehaviour(e, false)) {
            e.preventDefault();
        }

        var releasePoint;

        if(_pointerEventEnabled) {
            var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
            
            if(pointerIndex > -1) {
                releasePoint = _currPointers.splice(pointerIndex, 1)[0];

                if(navigator.msPointerEnabled) {
                    var MSPOINTER_TYPES = {
                        4: 'mouse', // event.MSPOINTER_TYPE_MOUSE
                        2: 'touch', // event.MSPOINTER_TYPE_TOUCH 
                        3: 'pen' // event.MSPOINTER_TYPE_PEN
                    };
                    releasePoint.type = MSPOINTER_TYPES[e.pointerType];

                    if(!releasePoint.type) {
                        releasePoint.type = e.pointerType || 'mouse';
                    }
                } else {
                    releasePoint.type = e.pointerType || 'mouse';
                }

            }
        }

        var touchList = _getTouchPoints(e),
            gestureType,
            numPoints = touchList.length;

        if(e.type === 'mouseup') {
            numPoints = 0;
        }

        // Do nothing if there were 3 touch points or more
        if(numPoints === 2) {
            _currentPoints = null;
            return true;
        }

        // if second pointer released
        if(numPoints === 1) {
            _equalizePoints(_startPoint, touchList[0]);
        }                


        // pointer hasn't moved, send "tap release" point
        if(numPoints === 0 && !_direction && !_mainScrollAnimating) {
            if(!releasePoint) {
                if(e.type === 'mouseup') {
                    releasePoint = {x: e.pageX, y: e.pageY, type:'mouse'};
                } else if(e.changedTouches && e.changedTouches[0]) {
                    releasePoint = {x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY, type:'touch'};
                }        
            }

            _shout('touchRelease', e, releasePoint);
        }

        // Difference in time between releasing of two last touch points (zoom gesture)
        var releaseTimeDiff = -1;

        // Gesture completed, no pointers left
        if(numPoints === 0) {
            _isDragging = false;
            framework.unbind(window, _upMoveEvents, self);

            _stopDragUpdateLoop();

            if(_isZooming) {
                // Two points released at the same time
                releaseTimeDiff = 0;
            } else if(_lastReleaseTime !== -1) {
                releaseTimeDiff = _getCurrentTime() - _lastReleaseTime;
            }
        }
        _lastReleaseTime = numPoints === 1 ? _getCurrentTime() : -1;
        
        if(releaseTimeDiff !== -1 && releaseTimeDiff < 150) {
            gestureType = 'zoom';
        } else {
            gestureType = 'swipe';
        }

        if(_isZooming && numPoints < 2) {
            _isZooming = false;

            // Only second point released
            if(numPoints === 1) {
                gestureType = 'zoomPointerUp';
            }
            _shout('zoomGestureEnded');
        }

        _currentPoints = null;
        if(!_moved && !_zoomStarted && !_mainScrollAnimating && !_verticalDragInitiated) {
            // nothing to animate
            return;
        }
    
        _stopAllAnimations();

        
        if(!_releaseAnimData) {
            _releaseAnimData = _initDragReleaseAnimationData();
        }
        
        _releaseAnimData.calculateSwipeSpeed('x');


        if(_verticalDragInitiated) {

            var opacityRatio = _calculateVerticalDragOpacityRatio();

            if(opacityRatio < _options.verticalDragRange) {
                self.close();
            } else {
                var initalPanY = _panOffset.y,
                    initialBgOpacity = _bgOpacity;

                _animateProp('verticalDrag', 0, 1, 300, framework.easing.cubic.out, function(now) {
                    
                    _panOffset.y = (self.currItem.initialPosition.y - initalPanY) * now + initalPanY;

                    _applyBgOpacity(  (1 - initialBgOpacity) * now + initialBgOpacity );
                    _applyCurrentZoomPan();
                });

                _shout('onVerticalDrag', 1);
            }

            return;
        }


        // main scroll 
        if(  (_mainScrollShifted || _mainScrollAnimating) && numPoints === 0) {
            var itemChanged = _finishSwipeMainScrollGesture(gestureType, _releaseAnimData);
            if(itemChanged) {
                return;
            }
            gestureType = 'zoomPointerUp';
        }

        // prevent zoom/pan animation when main scroll animation runs
        if(_mainScrollAnimating) {
            return;
        }
        
        // Complete simple zoom gesture (reset zoom level if it's out of the bounds)  
        if(gestureType !== 'swipe') {
            _completeZoomGesture();
            return;
        }
    
        // Complete pan gesture if main scroll is not shifted, and it's possible to pan current image
        if(!_mainScrollShifted && _currZoomLevel > self.currItem.fitRatio) {
            _completePanGesture(_releaseAnimData);
        }
    },


    // Returns object with data about gesture
    // It's created only once and then reused
    _initDragReleaseAnimationData  = function() {
        // temp local vars
        var lastFlickDuration,
            tempReleasePos;

        // s = this
        var s = {
            lastFlickOffset: {},
            lastFlickDist: {},
            lastFlickSpeed: {},
            slowDownRatio:  {},
            slowDownRatioReverse:  {},
            speedDecelerationRatio:  {},
            speedDecelerationRatioAbs:  {},
            distanceOffset:  {},
            backAnimDestination: {},
            backAnimStarted: {},
            calculateSwipeSpeed: function(axis) {
                

                if( _posPoints.length > 1) {
                    lastFlickDuration = _getCurrentTime() - _gestureCheckSpeedTime + 50;
                    tempReleasePos = _posPoints[_posPoints.length-2][axis];
                } else {
                    lastFlickDuration = _getCurrentTime() - _gestureStartTime; // total gesture duration
                    tempReleasePos = _startPoint[axis];
                }
                s.lastFlickOffset[axis] = _currPoint[axis] - tempReleasePos;
                s.lastFlickDist[axis] = Math.abs(s.lastFlickOffset[axis]);
                if(s.lastFlickDist[axis] > 20) {
                    s.lastFlickSpeed[axis] = s.lastFlickOffset[axis] / lastFlickDuration;
                } else {
                    s.lastFlickSpeed[axis] = 0;
                }
                if( Math.abs(s.lastFlickSpeed[axis]) < 0.1 ) {
                    s.lastFlickSpeed[axis] = 0;
                }
                
                s.slowDownRatio[axis] = 0.95;
                s.slowDownRatioReverse[axis] = 1 - s.slowDownRatio[axis];
                s.speedDecelerationRatio[axis] = 1;
            },

            calculateOverBoundsAnimOffset: function(axis, speed) {
                if(!s.backAnimStarted[axis]) {

                    if(_panOffset[axis] > _currPanBounds.min[axis]) {
                        s.backAnimDestination[axis] = _currPanBounds.min[axis];
                        
                    } else if(_panOffset[axis] < _currPanBounds.max[axis]) {
                        s.backAnimDestination[axis] = _currPanBounds.max[axis];
                    }

                    if(s.backAnimDestination[axis] !== undefined) {
                        s.slowDownRatio[axis] = 0.7;
                        s.slowDownRatioReverse[axis] = 1 - s.slowDownRatio[axis];
                        if(s.speedDecelerationRatioAbs[axis] < 0.05) {

                            s.lastFlickSpeed[axis] = 0;
                            s.backAnimStarted[axis] = true;

                            _animateProp('bounceZoomPan'+axis,_panOffset[axis], 
                                s.backAnimDestination[axis], 
                                speed || 300, 
                                framework.easing.sine.out, 
                                function(pos) {
                                    _panOffset[axis] = pos;
                                    _applyCurrentZoomPan();
                                }
                            );

                        }
                    }
                }
            },

            // Reduces the speed by slowDownRatio (per 10ms)
            calculateAnimOffset: function(axis) {
                if(!s.backAnimStarted[axis]) {
                    s.speedDecelerationRatio[axis] = s.speedDecelerationRatio[axis] * (s.slowDownRatio[axis] + 
                                                s.slowDownRatioReverse[axis] - 
                                                s.slowDownRatioReverse[axis] * s.timeDiff / 10);

                    s.speedDecelerationRatioAbs[axis] = Math.abs(s.lastFlickSpeed[axis] * s.speedDecelerationRatio[axis]);
                    s.distanceOffset[axis] = s.lastFlickSpeed[axis] * s.speedDecelerationRatio[axis] * s.timeDiff;
                    _panOffset[axis] += s.distanceOffset[axis];

                }
            },

            panAnimLoop: function() {
                if ( _animations.zoomPan ) {
                    _animations.zoomPan.raf = _requestAF(s.panAnimLoop);

                    s.now = _getCurrentTime();
                    s.timeDiff = s.now - s.lastNow;
                    s.lastNow = s.now;
                    
                    s.calculateAnimOffset('x');
                    s.calculateAnimOffset('y');

                    _applyCurrentZoomPan();
                    
                    s.calculateOverBoundsAnimOffset('x');
                    s.calculateOverBoundsAnimOffset('y');


                    if (s.speedDecelerationRatioAbs.x < 0.05 && s.speedDecelerationRatioAbs.y < 0.05) {

                        // round pan position
                        _panOffset.x = Math.round(_panOffset.x);
                        _panOffset.y = Math.round(_panOffset.y);
                        _applyCurrentZoomPan();
                        
                        _stopAnimation('zoomPan');
                        return;
                    }
                }

            }
        };
        return s;
    },

    _completePanGesture = function(animData) {
        // calculate swipe speed for Y axis (paanning)
        animData.calculateSwipeSpeed('y');

        _currPanBounds = self.currItem.bounds;
        
        animData.backAnimDestination = {};
        animData.backAnimStarted = {};

        // Avoid acceleration animation if speed is too low
        if(Math.abs(animData.lastFlickSpeed.x) <= 0.05 && Math.abs(animData.lastFlickSpeed.y) <= 0.05 ) {
            animData.speedDecelerationRatioAbs.x = animData.speedDecelerationRatioAbs.y = 0;

            // Run pan drag release animation. E.g. if you drag image and release finger without momentum.
            animData.calculateOverBoundsAnimOffset('x');
            animData.calculateOverBoundsAnimOffset('y');
            return true;
        }

        // Animation loop that controls the acceleration after pan gesture ends
        _registerStartAnimation('zoomPan');
        animData.lastNow = _getCurrentTime();
        animData.panAnimLoop();
    },


    _finishSwipeMainScrollGesture = function(gestureType, _releaseAnimData) {
        var itemChanged;
        if(!_mainScrollAnimating) {
            _currZoomedItemIndex = _currentItemIndex;
        }


        
        var itemsDiff;

        if(gestureType === 'swipe') {
            var totalShiftDist = _currPoint.x - _startPoint.x,
                isFastLastFlick = _releaseAnimData.lastFlickDist.x < 10;

            // if container is shifted for more than MIN_SWIPE_DISTANCE, 
            // and last flick gesture was in right direction
            if(totalShiftDist > MIN_SWIPE_DISTANCE && 
                (isFastLastFlick || _releaseAnimData.lastFlickOffset.x > 20) ) {
                // go to prev item
                itemsDiff = -1;
            } else if(totalShiftDist < -MIN_SWIPE_DISTANCE && 
                (isFastLastFlick || _releaseAnimData.lastFlickOffset.x < -20) ) {
                // go to next item
                itemsDiff = 1;
            }
        }

        var nextCircle;

        if(itemsDiff) {
            
            _currentItemIndex += itemsDiff;

            if(_currentItemIndex < 0) {
                _currentItemIndex = _options.loop ? _getNumItems()-1 : 0;
                nextCircle = true;
            } else if(_currentItemIndex >= _getNumItems()) {
                _currentItemIndex = _options.loop ? 0 : _getNumItems()-1;
                nextCircle = true;
            }

            if(!nextCircle || _options.loop) {
                _indexDiff += itemsDiff;
                _currPositionIndex -= itemsDiff;
                itemChanged = true;
            }
            

            
        }

        var animateToX = _slideSize.x * _currPositionIndex;
        var animateToDist = Math.abs( animateToX - _mainScrollPos.x );
        var finishAnimDuration;


        if(!itemChanged && animateToX > _mainScrollPos.x !== _releaseAnimData.lastFlickSpeed.x > 0) {
            // "return to current" duration, e.g. when dragging from slide 0 to -1
            finishAnimDuration = 333; 
        } else {
            finishAnimDuration = Math.abs(_releaseAnimData.lastFlickSpeed.x) > 0 ? 
                                    animateToDist / Math.abs(_releaseAnimData.lastFlickSpeed.x) : 
                                    333;

            finishAnimDuration = Math.min(finishAnimDuration, 400);
            finishAnimDuration = Math.max(finishAnimDuration, 250);
        }

        if(_currZoomedItemIndex === _currentItemIndex) {
            itemChanged = false;
        }
        
        _mainScrollAnimating = true;
        
        _shout('mainScrollAnimStart');

        _animateProp('mainScroll', _mainScrollPos.x, animateToX, finishAnimDuration, framework.easing.cubic.out, 
            _moveMainScroll,
            function() {
                _stopAllAnimations();
                _mainScrollAnimating = false;
                _currZoomedItemIndex = -1;
                
                if(itemChanged || _currZoomedItemIndex !== _currentItemIndex) {
                    self.updateCurrItem();
                }
                
                _shout('mainScrollAnimComplete');
            }
        );

        if(itemChanged) {
            self.updateCurrItem(true);
        }

        return itemChanged;
    },

    _calculateZoomLevel = function(touchesDistance) {
        return  1 / _startPointsDistance * touchesDistance * _startZoomLevel;
    },

    // Resets zoom if it's out of bounds
    _completeZoomGesture = function() {
        var destZoomLevel = _currZoomLevel,
            minZoomLevel = _getMinZoomLevel(),
            maxZoomLevel = _getMaxZoomLevel();

        if ( _currZoomLevel < minZoomLevel ) {
            destZoomLevel = minZoomLevel;
        } else if ( _currZoomLevel > maxZoomLevel ) {
            destZoomLevel = maxZoomLevel;
        }

        var destOpacity = 1,
            onUpdate,
            initialOpacity = _bgOpacity;

        if(_opacityChanged && !_isZoomingIn && !_wasOverInitialZoom && _currZoomLevel < minZoomLevel) {
            //_closedByScroll = true;
            self.close();
            return true;
        }

        if(_opacityChanged) {
            onUpdate = function(now) {
                _applyBgOpacity(  (destOpacity - initialOpacity) * now + initialOpacity );
            };
        }

        self.zoomTo(destZoomLevel, 0, 200,  framework.easing.cubic.out, onUpdate);
        return true;
    };


_registerModule('Gestures', {
    publicMethods: {

        initGestures: function() {

            // helper function that builds touch/pointer/mouse events
            var addEventNames = function(pref, down, move, up, cancel) {
                _dragStartEvent = pref + down;
                _dragMoveEvent = pref + move;
                _dragEndEvent = pref + up;
                if(cancel) {
                    _dragCancelEvent = pref + cancel;
                } else {
                    _dragCancelEvent = '';
                }
            };

            _pointerEventEnabled = _features.pointerEvent;
            if(_pointerEventEnabled && _features.touch) {
                // we don't need touch events, if browser supports pointer events
                _features.touch = false;
            }

            if(_pointerEventEnabled) {
                if(navigator.msPointerEnabled) {
                    // IE10 pointer events are case-sensitive
                    addEventNames('MSPointer', 'Down', 'Move', 'Up', 'Cancel');
                } else {
                    addEventNames('pointer', 'down', 'move', 'up', 'cancel');
                }
            } else if(_features.touch) {
                addEventNames('touch', 'start', 'move', 'end', 'cancel');
                _likelyTouchDevice = true;
            } else {
                addEventNames('mouse', 'down', 'move', 'up');    
            }

            _upMoveEvents = _dragMoveEvent + ' ' + _dragEndEvent  + ' ' +  _dragCancelEvent;
            _downEvents = _dragStartEvent;

            if(_pointerEventEnabled && !_likelyTouchDevice) {
                _likelyTouchDevice = (navigator.maxTouchPoints > 1) || (navigator.msMaxTouchPoints > 1);
            }
            // make variable public
            self.likelyTouchDevice = _likelyTouchDevice; 
            
            _globalEventHandlers[_dragStartEvent] = _onDragStart;
            _globalEventHandlers[_dragMoveEvent] = _onDragMove;
            _globalEventHandlers[_dragEndEvent] = _onDragRelease; // the Kraken

            if(_dragCancelEvent) {
                _globalEventHandlers[_dragCancelEvent] = _globalEventHandlers[_dragEndEvent];
            }

            // Bind mouse events on device with detected hardware touch support, in case it supports multiple types of input.
            if(_features.touch) {
                _downEvents += ' mousedown';
                _upMoveEvents += ' mousemove mouseup';
                _globalEventHandlers.mousedown = _globalEventHandlers[_dragStartEvent];
                _globalEventHandlers.mousemove = _globalEventHandlers[_dragMoveEvent];
                _globalEventHandlers.mouseup = _globalEventHandlers[_dragEndEvent];
            }

            if(!_likelyTouchDevice) {
                // don't allow pan to next slide from zoomed state on Desktop
                _options.allowPanToNext = false;
            }
        }

    }
});


/*>>gestures*/

/*>>show-hide-transition*/
/**
 * show-hide-transition.js:
 *
 * Manages initial opening or closing transition.
 *
 * If you're not planning to use transition for gallery at all,
 * you may set options hideAnimationDuration and showAnimationDuration to 0,
 * and just delete startAnimation function.
 * 
 */


var _showOrHideTimeout,
    _showOrHide = function(item, img, out, completeFn) {

        if(_showOrHideTimeout) {
            clearTimeout(_showOrHideTimeout);
        }

        _initialZoomRunning = true;
        _initialContentSet = true;
        
        // dimensions of small thumbnail {x:,y:,w:}.
        // Height is optional, as calculated based on large image.
        var thumbBounds; 
        if(item.initialLayout) {
            thumbBounds = item.initialLayout;
            item.initialLayout = null;
        } else {
            thumbBounds = _options.getThumbBoundsFn && _options.getThumbBoundsFn(_currentItemIndex);
        }

        var duration = out ? _options.hideAnimationDuration : _options.showAnimationDuration;

        var onComplete = function() {
            _stopAnimation('initialZoom');
            if(!out) {
                _applyBgOpacity(1);
                if(img) {
                    img.style.display = 'block';
                }
                framework.addClass(template, 'pswp--animated-in');
                _shout('initialZoom' + (out ? 'OutEnd' : 'InEnd'));
            } else {
                self.template.removeAttribute('style');
                self.bg.removeAttribute('style');
            }

            if(completeFn) {
                completeFn();
            }
            _initialZoomRunning = false;
        };

        // if bounds aren't provided, just open gallery without animation
        if(!duration || !thumbBounds || thumbBounds.x === undefined) {

            _shout('initialZoom' + (out ? 'Out' : 'In') );

            _currZoomLevel = item.initialZoomLevel;
            _equalizePoints(_panOffset,  item.initialPosition );
            _applyCurrentZoomPan();

            template.style.opacity = out ? 0 : 1;
            _applyBgOpacity(1);

            if(duration) {
                setTimeout(function() {
                    onComplete();
                }, duration);
            } else {
                onComplete();
            }

            return;
        }

        var startAnimation = function() {
            var closeWithRaf = _closedByScroll,
                fadeEverything = !self.currItem.src || self.currItem.loadError || _options.showHideOpacity;
            
            // apply hw-acceleration to image
            if(item.miniImg) {
                item.miniImg.style.webkitBackfaceVisibility = 'hidden';
            }

            if(!out) {
                _currZoomLevel = thumbBounds.w / item.w;
                _panOffset.x = thumbBounds.x;
                _panOffset.y = thumbBounds.y - _initalWindowScrollY;

                self[fadeEverything ? 'template' : 'bg'].style.opacity = 0.001;
                _applyCurrentZoomPan();
            }

            _registerStartAnimation('initialZoom');
            
            if(out && !closeWithRaf) {
                framework.removeClass(template, 'pswp--animated-in');
            }

            if(fadeEverything) {
                if(out) {
                    framework[ (closeWithRaf ? 'remove' : 'add') + 'Class' ](template, 'pswp--animate_opacity');
                } else {
                    setTimeout(function() {
                        framework.addClass(template, 'pswp--animate_opacity');
                    }, 30);
                }
            }

            _showOrHideTimeout = setTimeout(function() {

                _shout('initialZoom' + (out ? 'Out' : 'In') );
                

                if(!out) {

                    // "in" animation always uses CSS transitions (instead of rAF).
                    // CSS transition work faster here, 
                    // as developer may also want to animate other things, 
                    // like ui on top of sliding area, which can be animated just via CSS
                    
                    _currZoomLevel = item.initialZoomLevel;
                    _equalizePoints(_panOffset,  item.initialPosition );
                    _applyCurrentZoomPan();
                    _applyBgOpacity(1);

                    if(fadeEverything) {
                        template.style.opacity = 1;
                    } else {
                        _applyBgOpacity(1);
                    }

                    _showOrHideTimeout = setTimeout(onComplete, duration + 20);
                } else {

                    // "out" animation uses rAF only when PhotoSwipe is closed by browser scroll, to recalculate position
                    var destZoomLevel = thumbBounds.w / item.w,
                        initialPanOffset = {
                            x: _panOffset.x,
                            y: _panOffset.y
                        },
                        initialZoomLevel = _currZoomLevel,
                        initalBgOpacity = _bgOpacity,
                        onUpdate = function(now) {
                            
                            if(now === 1) {
                                _currZoomLevel = destZoomLevel;
                                _panOffset.x = thumbBounds.x;
                                _panOffset.y = thumbBounds.y  - _currentWindowScrollY;
                            } else {
                                _currZoomLevel = (destZoomLevel - initialZoomLevel) * now + initialZoomLevel;
                                _panOffset.x = (thumbBounds.x - initialPanOffset.x) * now + initialPanOffset.x;
                                _panOffset.y = (thumbBounds.y - _currentWindowScrollY - initialPanOffset.y) * now + initialPanOffset.y;
                            }
                            
                            _applyCurrentZoomPan();
                            if(fadeEverything) {
                                template.style.opacity = 1 - now;
                            } else {
                                _applyBgOpacity( initalBgOpacity - now * initalBgOpacity );
                            }
                        };

                    if(closeWithRaf) {
                        _animateProp('initialZoom', 0, 1, duration, framework.easing.cubic.out, onUpdate, onComplete);
                    } else {
                        onUpdate(1);
                        _showOrHideTimeout = setTimeout(onComplete, duration + 20);
                    }
                }
            
            }, out ? 25 : 90); // Main purpose of this delay is to give browser time to paint and
                    // create composite layers of PhotoSwipe UI parts (background, controls, caption, arrows).
                    // Which avoids lag at the beginning of scale transition.
        };
        startAnimation();

        
    };

/*>>show-hide-transition*/

/*>>items-controller*/
/**
*
* Controller manages gallery items, their dimensions, and their content.
* 
*/

var _items,
    _tempPanAreaSize = {},
    _imagesToAppendPool = [],
    _initialContentSet,
    _initialZoomRunning,
    _controllerDefaultOptions = {
        index: 0,
        errorMsg: '<div class="pswp__error-msg"><a href="%url%" target="_blank">The image</a> could not be loaded.</div>',
        forceProgressiveLoading: false, // TODO
        preload: [1,1],
        getNumItemsFn: function() {
            return _items.length;
        }
    };


var _getItemAt,
    _getNumItems,
    _initialIsLoop,
    _getZeroBounds = function() {
        return {
            center:{x:0,y:0}, 
            max:{x:0,y:0}, 
            min:{x:0,y:0}
        };
    },
    _calculateSingleItemPanBounds = function(item, realPanElementW, realPanElementH ) {
        var bounds = item.bounds;

        // position of element when it's centered
        bounds.center.x = Math.round((_tempPanAreaSize.x - realPanElementW) / 2);
        bounds.center.y = Math.round((_tempPanAreaSize.y - realPanElementH) / 2) + item.vGap.top;

        // maximum pan position
        bounds.max.x = (realPanElementW > _tempPanAreaSize.x) ? 
                            Math.round(_tempPanAreaSize.x - realPanElementW) : 
                            bounds.center.x;
        
        bounds.max.y = (realPanElementH > _tempPanAreaSize.y) ? 
                            Math.round(_tempPanAreaSize.y - realPanElementH) + item.vGap.top : 
                            bounds.center.y;
        
        // minimum pan position
        bounds.min.x = (realPanElementW > _tempPanAreaSize.x) ? 0 : bounds.center.x;
        bounds.min.y = (realPanElementH > _tempPanAreaSize.y) ? item.vGap.top : bounds.center.y;
    },
    _calculateItemSize = function(item, viewportSize, zoomLevel) {

        if (item.src && !item.loadError) {
            var isInitial = !zoomLevel;
            
            if(isInitial) {
                if(!item.vGap) {
                    item.vGap = {top:0,bottom:0};
                }
                // allows overriding vertical margin for individual items
                _shout('parseVerticalMargin', item);
            }


            _tempPanAreaSize.x = viewportSize.x;
            _tempPanAreaSize.y = viewportSize.y - item.vGap.top - item.vGap.bottom;

            if (isInitial) {
                var hRatio = _tempPanAreaSize.x / item.w;
                var vRatio = _tempPanAreaSize.y / item.h;

                item.fitRatio = hRatio < vRatio ? hRatio : vRatio;
                //item.fillRatio = hRatio > vRatio ? hRatio : vRatio;

                var scaleMode = _options.scaleMode;

                if (scaleMode === 'orig') {
                    zoomLevel = 1;
                } else if (scaleMode === 'fit') {
                    zoomLevel = item.fitRatio;
                }

                if (zoomLevel > 1) {
                    zoomLevel = 1;
                }

                item.initialZoomLevel = zoomLevel;
                
                if(!item.bounds) {
                    // reuse bounds object
                    item.bounds = _getZeroBounds(); 
                }
            }

            if(!zoomLevel) {
                return;
            }

            _calculateSingleItemPanBounds(item, item.w * zoomLevel, item.h * zoomLevel);

            if (isInitial && zoomLevel === item.initialZoomLevel) {
                item.initialPosition = item.bounds.center;
            }

            return item.bounds;
        } else {
            item.w = item.h = 0;
            item.initialZoomLevel = item.fitRatio = 1;
            item.bounds = _getZeroBounds();
            item.initialPosition = item.bounds.center;

            // if it's not image, we return zero bounds (content is not zoomable)
            return item.bounds;
        }
        
    },

    


    _appendImage = function(index, item, baseDiv, img, preventAnimation, keepPlaceholder) {
        

        if(item.loadError) {
            return;
        }

        if(img) {

            item.imageAppended = true;
            _setImageSize(item, img, (item === self.currItem && _renderMaxResolution) );
            
            baseDiv.appendChild(img);

            if(keepPlaceholder) {
                setTimeout(function() {
                    if(item && item.loaded && item.placeholder) {
                        item.placeholder.style.display = 'none';
                        item.placeholder = null;
                    }
                }, 500);
            }
        }
    },
    


    _preloadImage = function(item) {
        item.loading = true;
        item.loaded = false;
        var img = item.img = framework.createEl('pswp__img', 'img');
        var onComplete = function() {
            item.loading = false;
            item.loaded = true;

            if(item.loadComplete) {
                item.loadComplete(item);
            } else {
                item.img = null; // no need to store image object
            }
            img.onload = img.onerror = null;
            img = null;
        };
        img.onload = onComplete;
        img.onerror = function() {
            item.loadError = true;
            onComplete();
        };        

        img.src = item.src;// + '?a=' + Math.random();
        img.alt = item.alt || '';

        return img;
    },
    _checkForError = function(item, cleanUp) {
        if(item.src && item.loadError && item.container) {

            if(cleanUp) {
                item.container.innerHTML = '';
            }

            item.container.innerHTML = _options.errorMsg.replace('%url%',  item.src );
            return true;
            
        }
    },
    _setImageSize = function(item, img, maxRes) {
        if(!item.src) {
            return;
        }

        if(!img) {
            img = item.container.lastChild;
        }

        var w = maxRes ? item.w : Math.round(item.w * item.fitRatio),
            h = maxRes ? item.h : Math.round(item.h * item.fitRatio);
        
        if(item.placeholder && !item.loaded) {
            item.placeholder.style.width = w + 'px';
            item.placeholder.style.height = h + 'px';
        }

        img.style.width = w + 'px';
        img.style.height = h + 'px';
    },
    _appendImagesPool = function() {

        if(_imagesToAppendPool.length) {
            var poolItem;

            for(var i = 0; i < _imagesToAppendPool.length; i++) {
                poolItem = _imagesToAppendPool[i];
                if( poolItem.holder.index === poolItem.index ) {
                    _appendImage(poolItem.index, poolItem.item, poolItem.baseDiv, poolItem.img, false, poolItem.clearPlaceholder);
                }
            }
            _imagesToAppendPool = [];
        }
    };
    


_registerModule('Controller', {

    publicMethods: {

        lazyLoadItem: function(index) {
            index = _getLoopedId(index);
            var item = _getItemAt(index);

            if(!item || ((item.loaded || item.loading) && !_itemsNeedUpdate)) {
                return;
            }

            _shout('gettingData', index, item);

            if (!item.src) {
                return;
            }

            _preloadImage(item);
        },
        initController: function() {
            framework.extend(_options, _controllerDefaultOptions, true);
            self.items = _items = items;
            _getItemAt = self.getItemAt;
            _getNumItems = _options.getNumItemsFn; //self.getNumItems;



            _initialIsLoop = _options.loop;
            if(_getNumItems() < 3) {
                _options.loop = false; // disable loop if less then 3 items
            }

            _listen('beforeChange', function(diff) {

                var p = _options.preload,
                    isNext = diff === null ? true : (diff >= 0),
                    preloadBefore = Math.min(p[0], _getNumItems() ),
                    preloadAfter = Math.min(p[1], _getNumItems() ),
                    i;


                for(i = 1; i <= (isNext ? preloadAfter : preloadBefore); i++) {
                    self.lazyLoadItem(_currentItemIndex+i);
                }
                for(i = 1; i <= (isNext ? preloadBefore : preloadAfter); i++) {
                    self.lazyLoadItem(_currentItemIndex-i);
                }
            });

            _listen('initialLayout', function() {
                self.currItem.initialLayout = _options.getThumbBoundsFn && _options.getThumbBoundsFn(_currentItemIndex);
            });

            _listen('mainScrollAnimComplete', _appendImagesPool);
            _listen('initialZoomInEnd', _appendImagesPool);



            _listen('destroy', function() {
                var item;
                for(var i = 0; i < _items.length; i++) {
                    item = _items[i];
                    // remove reference to DOM elements, for GC
                    if(item.container) {
                        item.container = null; 
                    }
                    if(item.placeholder) {
                        item.placeholder = null;
                    }
                    if(item.img) {
                        item.img = null;
                    }
                    if(item.preloader) {
                        item.preloader = null;
                    }
                    if(item.loadError) {
                        item.loaded = item.loadError = false;
                    }
                }
                _imagesToAppendPool = null;
            });
        },


        getItemAt: function(index) {
            if (index >= 0) {
                return _items[index] !== undefined ? _items[index] : false;
            }
            return false;
        },

        allowProgressiveImg: function() {
            // 1. Progressive image loading isn't working on webkit/blink 
            //    when hw-acceleration (e.g. translateZ) is applied to IMG element.
            //    That's why in PhotoSwipe parent element gets zoom transform, not image itself.
            //    
            // 2. Progressive image loading sometimes blinks in webkit/blink when applying animation to parent element.
            //    That's why it's disabled on touch devices (mainly because of swipe transition)
            //    
            // 3. Progressive image loading sometimes doesn't work in IE (up to 11).

            // Don't allow progressive loading on non-large touch devices
            return _options.forceProgressiveLoading || !_likelyTouchDevice || _options.mouseUsed || screen.width > 1200; 
            // 1200 - to eliminate touch devices with large screen (like Chromebook Pixel)
        },

        setContent: function(holder, index) {

            if(_options.loop) {
                index = _getLoopedId(index);
            }

            var prevItem = self.getItemAt(holder.index);
            if(prevItem) {
                prevItem.container = null;
            }
    
            var item = self.getItemAt(index),
                img;
            
            if(!item) {
                holder.el.innerHTML = '';
                return;
            }

            // allow to override data
            _shout('gettingData', index, item);

            holder.index = index;
            holder.item = item;

            // base container DIV is created only once for each of 3 holders
            var baseDiv = item.container = framework.createEl('pswp__zoom-wrap'); 

            

            if(!item.src && item.html) {
                if(item.html.tagName) {
                    baseDiv.appendChild(item.html);
                } else {
                    baseDiv.innerHTML = item.html;
                }
            }

            _checkForError(item);

            _calculateItemSize(item, _viewportSize);
            
            if(item.src && !item.loadError && !item.loaded) {

                item.loadComplete = function(item) {

                    // gallery closed before image finished loading
                    if(!_isOpen) {
                        return;
                    }

                    // check if holder hasn't changed while image was loading
                    if(holder && holder.index === index ) {
                        if( _checkForError(item, true) ) {
                            item.loadComplete = item.img = null;
                            _calculateItemSize(item, _viewportSize);
                            _applyZoomPanToItem(item);

                            if(holder.index === _currentItemIndex) {
                                // recalculate dimensions
                                self.updateCurrZoomItem();
                            }
                            return;
                        }
                        if( !item.imageAppended ) {
                            if(_features.transform && (_mainScrollAnimating || _initialZoomRunning) ) {
                                _imagesToAppendPool.push({
                                    item:item,
                                    baseDiv:baseDiv,
                                    img:item.img,
                                    index:index,
                                    holder:holder,
                                    clearPlaceholder:true
                                });
                            } else {
                                _appendImage(index, item, baseDiv, item.img, _mainScrollAnimating || _initialZoomRunning, true);
                            }
                        } else {
                            // remove preloader & mini-img
                            if(!_initialZoomRunning && item.placeholder) {
                                item.placeholder.style.display = 'none';
                                item.placeholder = null;
                            }
                        }
                    }

                    item.loadComplete = null;
                    item.img = null; // no need to store image element after it's added

                    _shout('imageLoadComplete', index, item);
                };

                if(framework.features.transform) {
                    
                    var placeholderClassName = 'pswp__img pswp__img--placeholder'; 
                    placeholderClassName += (item.msrc ? '' : ' pswp__img--placeholder--blank');

                    var placeholder = framework.createEl(placeholderClassName, item.msrc ? 'img' : '');
                    if(item.msrc) {
                        placeholder.src = item.msrc;
                    }
                    
                    _setImageSize(item, placeholder);

                    baseDiv.appendChild(placeholder);
                    item.placeholder = placeholder;

                }
                

                

                if(!item.loading) {
                    _preloadImage(item);
                }


                if( self.allowProgressiveImg() ) {
                    // just append image
                    if(!_initialContentSet && _features.transform) {
                        _imagesToAppendPool.push({
                            item:item, 
                            baseDiv:baseDiv, 
                            img:item.img, 
                            index:index, 
                            holder:holder
                        });
                    } else {
                        _appendImage(index, item, baseDiv, item.img, true, true);
                    }
                }
                
            } else if(item.src && !item.loadError) {
                // image object is created every time, due to bugs of image loading & delay when switching images
                img = framework.createEl('pswp__img', 'img');
                img.style.opacity = 1;
                img.src = item.src;
                _setImageSize(item, img);
                _appendImage(index, item, baseDiv, img, true);
            }
            

            if(!_initialContentSet && index === _currentItemIndex) {
                _currZoomElementStyle = baseDiv.style;
                _showOrHide(item, (img ||item.img) );
            } else {
                _applyZoomPanToItem(item);
            }

            holder.el.innerHTML = '';
            holder.el.appendChild(baseDiv);
        },

        cleanSlide: function( item ) {
            if(item.img ) {
                item.img.onload = item.img.onerror = null;
            }
            item.loaded = item.loading = item.img = item.imageAppended = false;
        }

    }
});

/*>>items-controller*/

/*>>tap*/
/**
 * tap.js:
 *
 * Displatches tap and double-tap events.
 * 
 */

var tapTimer,
    tapReleasePoint = {},
    _dispatchTapEvent = function(origEvent, releasePoint, pointerType) {        
        var e = document.createEvent( 'CustomEvent' ),
            eDetail = {
                origEvent:origEvent, 
                target:origEvent.target, 
                releasePoint: releasePoint, 
                pointerType:pointerType || 'touch'
            };

        e.initCustomEvent( 'pswpTap', true, true, eDetail );
        origEvent.target.dispatchEvent(e);
    };

_registerModule('Tap', {
    publicMethods: {
        initTap: function() {
            _listen('firstTouchStart', self.onTapStart);
            _listen('touchRelease', self.onTapRelease);
            _listen('destroy', function() {
                tapReleasePoint = {};
                tapTimer = null;
            });
        },
        onTapStart: function(touchList) {
            if(touchList.length > 1) {
                clearTimeout(tapTimer);
                tapTimer = null;
            }
        },
        onTapRelease: function(e, releasePoint) {
            if(!releasePoint) {
                return;
            }

            if(!_moved && !_isMultitouch && !_numAnimations) {
                var p0 = releasePoint;
                if(tapTimer) {
                    clearTimeout(tapTimer);
                    tapTimer = null;

                    // Check if taped on the same place
                    if ( _isNearbyPoints(p0, tapReleasePoint) ) {
                        _shout('doubleTap', p0);
                        return;
                    }
                }

                if(releasePoint.type === 'mouse') {
                    _dispatchTapEvent(e, releasePoint, 'mouse');
                    return;
                }

                var clickedTagName = e.target.tagName.toUpperCase();
                // avoid double tap delay on buttons and elements that have class pswp__single-tap
                if(clickedTagName === 'BUTTON' || framework.hasClass(e.target, 'pswp__single-tap') ) {
                    _dispatchTapEvent(e, releasePoint);
                    return;
                }

                _equalizePoints(tapReleasePoint, p0);

                tapTimer = setTimeout(function() {
                    _dispatchTapEvent(e, releasePoint);
                    tapTimer = null;
                }, 300);
            }
        }
    }
});

/*>>tap*/

/*>>desktop-zoom*/
/**
 *
 * desktop-zoom.js:
 *
 * - Binds mousewheel event for paning zoomed image.
 * - Manages "dragging", "zoomed-in", "zoom-out" classes.
 *   (which are used for cursors and zoom icon)
 * - Adds toggleDesktopZoom function.
 * 
 */

var _wheelDelta;
    
_registerModule('DesktopZoom', {

    publicMethods: {

        initDesktopZoom: function() {

            if(_oldIE) {
                // no zoom for old IE (<=8)
                return;
            }

            if(_likelyTouchDevice) {
                // if detected hardware touch support, we wait until mouse is used,
                // and only then apply desktop-zoom features
                _listen('mouseUsed', function() {
                    self.setupDesktopZoom();
                });
            } else {
                self.setupDesktopZoom(true);
            }

        },

        setupDesktopZoom: function(onInit) {

            _wheelDelta = {};

            var events = 'wheel mousewheel DOMMouseScroll';
            
            _listen('bindEvents', function() {
                framework.bind(template, events,  self.handleMouseWheel);
            });

            _listen('unbindEvents', function() {
                if(_wheelDelta) {
                    framework.unbind(template, events, self.handleMouseWheel);
                }
            });

            self.mouseZoomedIn = false;

            var hasDraggingClass,
                updateZoomable = function() {
                    if(self.mouseZoomedIn) {
                        framework.removeClass(template, 'pswp--zoomed-in');
                        self.mouseZoomedIn = false;
                    }
                    if(_currZoomLevel < 1) {
                        framework.addClass(template, 'pswp--zoom-allowed');
                    } else {
                        framework.removeClass(template, 'pswp--zoom-allowed');
                    }
                    removeDraggingClass();
                },
                removeDraggingClass = function() {
                    if(hasDraggingClass) {
                        framework.removeClass(template, 'pswp--dragging');
                        hasDraggingClass = false;
                    }
                };

            _listen('resize' , updateZoomable);
            _listen('afterChange' , updateZoomable);
            _listen('pointerDown', function() {
                if(self.mouseZoomedIn) {
                    hasDraggingClass = true;
                    framework.addClass(template, 'pswp--dragging');
                }
            });
            _listen('pointerUp', removeDraggingClass);

            if(!onInit) {
                updateZoomable();
            }
            
        },

        handleMouseWheel: function(e) {

            if(_currZoomLevel <= self.currItem.fitRatio) {
                if( _options.modal ) {

                    if (!_options.closeOnScroll || _numAnimations || _isDragging) {
                        e.preventDefault();
                    } else if(_transformKey && Math.abs(e.deltaY) > 2) {
                        // close PhotoSwipe
                        // if browser supports transforms & scroll changed enough
                        _closedByScroll = true;
                        self.close();
                    }

                }
                return true;
            }

            // allow just one event to fire
            e.stopPropagation();

            // https://developer.mozilla.org/en-US/docs/Web/Events/wheel
            _wheelDelta.x = 0;

            if('deltaX' in e) {
                if(e.deltaMode === 1 /* DOM_DELTA_LINE */) {
                    // 18 - average line height
                    _wheelDelta.x = e.deltaX * 18;
                    _wheelDelta.y = e.deltaY * 18;
                } else {
                    _wheelDelta.x = e.deltaX;
                    _wheelDelta.y = e.deltaY;
                }
            } else if('wheelDelta' in e) {
                if(e.wheelDeltaX) {
                    _wheelDelta.x = -0.16 * e.wheelDeltaX;
                }
                if(e.wheelDeltaY) {
                    _wheelDelta.y = -0.16 * e.wheelDeltaY;
                } else {
                    _wheelDelta.y = -0.16 * e.wheelDelta;
                }
            } else if('detail' in e) {
                _wheelDelta.y = e.detail;
            } else {
                return;
            }

            _calculatePanBounds(_currZoomLevel, true);

            var newPanX = _panOffset.x - _wheelDelta.x,
                newPanY = _panOffset.y - _wheelDelta.y;

            // only prevent scrolling in nonmodal mode when not at edges
            if (_options.modal ||
                (
                newPanX <= _currPanBounds.min.x && newPanX >= _currPanBounds.max.x &&
                newPanY <= _currPanBounds.min.y && newPanY >= _currPanBounds.max.y
                ) ) {
                e.preventDefault();
            }

            // TODO: use rAF instead of mousewheel?
            self.panTo(newPanX, newPanY);
        },

        toggleDesktopZoom: function(centerPoint) {
            centerPoint = centerPoint || {x:_viewportSize.x/2 + _offset.x, y:_viewportSize.y/2 + _offset.y };

            var doubleTapZoomLevel = _options.getDoubleTapZoom(true, self.currItem);
            var zoomOut = _currZoomLevel === doubleTapZoomLevel;
            
            self.mouseZoomedIn = !zoomOut;

            self.zoomTo(zoomOut ? self.currItem.initialZoomLevel : doubleTapZoomLevel, centerPoint, 333);
            framework[ (!zoomOut ? 'add' : 'remove') + 'Class'](template, 'pswp--zoomed-in');
        }

    }
});


/*>>desktop-zoom*/

/*>>history*/
/**
 *
 * history.js:
 *
 * - Back button to close gallery.
 * 
 * - Unique URL for each slide: example.com/&pid=1&gid=3
 *   (where PID is picture index, and GID and gallery index)
 *   
 * - Switch URL when slides change.
 * 
 */


var _historyDefaultOptions = {
    history: true,
    galleryUID: 1
};

var _historyUpdateTimeout,
    _hashChangeTimeout,
    _hashAnimCheckTimeout,
    _hashChangedByScript,
    _hashChangedByHistory,
    _hashReseted,
    _initialHash,
    _historyChanged,
    _closedFromURL,
    _urlChangedOnce,
    _windowLoc,

    _supportsPushState,

    _getHash = function() {
        return _windowLoc.hash.substring(1);
    },
    _cleanHistoryTimeouts = function() {

        if(_historyUpdateTimeout) {
            clearTimeout(_historyUpdateTimeout);
        }

        if(_hashAnimCheckTimeout) {
            clearTimeout(_hashAnimCheckTimeout);
        }
    },

    // pid - Picture index
    // gid - Gallery index
    _parseItemIndexFromURL = function() {
        var hash = _getHash(),
            params = {};

        if(hash.length < 5) { // pid=1
            return params;
        }

        var i, vars = hash.split('&');
        for (i = 0; i < vars.length; i++) {
            if(!vars[i]) {
                continue;
            }
            var pair = vars[i].split('=');    
            if(pair.length < 2) {
                continue;
            }
            params[pair[0]] = pair[1];
        }
        if(_options.galleryPIDs) {
            // detect custom pid in hash and search for it among the items collection
            var searchfor = params.pid;
            params.pid = 0; // if custom pid cannot be found, fallback to the first item
            for(i = 0; i < _items.length; i++) {
                if(_items[i].pid === searchfor) {
                    params.pid = i;
                    break;
                }
            }
        } else {
            params.pid = parseInt(params.pid,10)-1;
        }
        if( params.pid < 0 ) {
            params.pid = 0;
        }
        return params;
    },
    _updateHash = function() {

        if(_hashAnimCheckTimeout) {
            clearTimeout(_hashAnimCheckTimeout);
        }


        if(_numAnimations || _isDragging) {
            // changing browser URL forces layout/paint in some browsers, which causes noticable lag during animation
            // that's why we update hash only when no animations running
            _hashAnimCheckTimeout = setTimeout(_updateHash, 500);
            return;
        }
        
        if(_hashChangedByScript) {
            clearTimeout(_hashChangeTimeout);
        } else {
            _hashChangedByScript = true;
        }


        var pid = (_currentItemIndex + 1);
        var item = _getItemAt( _currentItemIndex );
        if(item.hasOwnProperty('pid')) {
            // carry forward any custom pid assigned to the item
            pid = item.pid;
        }
        var newHash = _initialHash + '&'  +  'gid=' + _options.galleryUID + '&' + 'pid=' + pid;

        if(!_historyChanged) {
            if(_windowLoc.hash.indexOf(newHash) === -1) {
                _urlChangedOnce = true;
            }
            // first time - add new hisory record, then just replace
        }

        var newURL = _windowLoc.href.split('#')[0] + '#' +  newHash;

        if( _supportsPushState ) {

            if('#' + newHash !== window.location.hash) {
                history[_historyChanged ? 'replaceState' : 'pushState']('', document.title, newURL);
            }

        } else {
            if(_historyChanged) {
                _windowLoc.replace( newURL );
            } else {
                _windowLoc.hash = newHash;
            }
        }
        
        

        _historyChanged = true;
        _hashChangeTimeout = setTimeout(function() {
            _hashChangedByScript = false;
        }, 60);
    };



    

_registerModule('History', {

    

    publicMethods: {
        initHistory: function() {

            framework.extend(_options, _historyDefaultOptions, true);

            if( !_options.history ) {
                return;
            }


            _windowLoc = window.location;
            _urlChangedOnce = false;
            _closedFromURL = false;
            _historyChanged = false;
            _initialHash = _getHash();
            _supportsPushState = ('pushState' in history);


            if(_initialHash.indexOf('gid=') > -1) {
                _initialHash = _initialHash.split('&gid=')[0];
                _initialHash = _initialHash.split('?gid=')[0];
            }
            

            _listen('afterChange', self.updateURL);
            _listen('unbindEvents', function() {
                framework.unbind(window, 'hashchange', self.onHashChange);
            });


            var returnToOriginal = function() {
                _hashReseted = true;
                if(!_closedFromURL) {

                    if(_urlChangedOnce) {
                        history.back();
                    } else {

                        if(_initialHash) {
                            _windowLoc.hash = _initialHash;
                        } else {
                            if (_supportsPushState) {

                                // remove hash from url without refreshing it or scrolling to top
                                history.pushState('', document.title,  _windowLoc.pathname + _windowLoc.search );
                            } else {
                                _windowLoc.hash = '';
                            }
                        }
                    }
                    
                }

                _cleanHistoryTimeouts();
            };


            _listen('unbindEvents', function() {
                if(_closedByScroll) {
                    // if PhotoSwipe is closed by scroll, we go "back" before the closing animation starts
                    // this is done to keep the scroll position
                    returnToOriginal();
                }
            });
            _listen('destroy', function() {
                if(!_hashReseted) {
                    returnToOriginal();
                }
            });
            _listen('firstUpdate', function() {
                _currentItemIndex = _parseItemIndexFromURL().pid;
            });

            

            
            var index = _initialHash.indexOf('pid=');
            if(index > -1) {
                _initialHash = _initialHash.substring(0, index);
                if(_initialHash.slice(-1) === '&') {
                    _initialHash = _initialHash.slice(0, -1);
                }
            }
            

            setTimeout(function() {
                if(_isOpen) { // hasn't destroyed yet
                    framework.bind(window, 'hashchange', self.onHashChange);
                }
            }, 40);
            
        },
        onHashChange: function() {

            if(_getHash() === _initialHash) {

                _closedFromURL = true;
                self.close();
                return;
            }
            if(!_hashChangedByScript) {

                _hashChangedByHistory = true;
                self.goTo( _parseItemIndexFromURL().pid );
                _hashChangedByHistory = false;
            }
            
        },
        updateURL: function() {

            // Delay the update of URL, to avoid lag during transition, 
            // and to not to trigger actions like "refresh page sound" or "blinking favicon" to often
            
            _cleanHistoryTimeouts();
            

            if(_hashChangedByHistory) {
                return;
            }

            if(!_historyChanged) {
                _updateHash(); // first time
            } else {
                _historyUpdateTimeout = setTimeout(_updateHash, 800);
            }
        }
    
    }
});


/*>>history*/
    framework.extend(self, publicMethods); };
    return PhotoSwipe;
});