PageSourceSearch

https://artsacademymuseum.org/wp-content/plugins/lightbox-photoswipe/src/lib/photoswipe.js?ver=5.10.1

js artsacademymuseum.org collected 2026-10-02 04:37:17 UTC 124,873 bytes, 3,872 lines download raw bytes

1/*! PhotoSwipe - v4.1.10.aw - 2021-10-22
2* http://photoswipe.com
3* Copyright (c) 2017 Dmitry Semenov;
4* with modifications by Arno Welzel */
5(function (root, factory) { 
6    if (typeof exports === 'object') {
7        module.exports = factory();
8    } else {
9        root.PhotoSwipe = factory();
10    }
11})(this, function () {
12
13    'use strict';
14    var PhotoSwipe = function(template, UiClass, items, options){
15
16/*>>framework-bridge*/
17/**
18 *
19 * Set of generic functions used by gallery.
20 * 
21 * You're free to modify anything here as long as functionality is kept.
22 * 
23 */
24var framework = {
25    features: null,
26    bind: function(target, type, listener, unbind) {
27        var methodName = (unbind ? 'remove' : 'add') + 'EventListener';
28        type = type.split(' ');
29        for(var i = 0; i < type.length; i++) {
30            if(type[i]) {
31                target[methodName]( type[i], listener, false);
32            }
33        }
34    },
35    isArray: function(obj) {
36        return (obj instanceof Array);
37    },
38    createEl: function(classes, tag) {
39        var el = document.createElement(tag || 'div');
40        if(classes) {
41            el.className = classes;
42        }
43        return el;
44    },
45    getScrollY: function() {
46        var yOffset = window.pageYOffset;
47        return yOffset !== undefined ? yOffset : document.documentElement.scrollTop;
48    },
49    unbind: function(target, type, listener) {
50        framework.bind(target,type,listener,true);
51    },
52    removeClass: function(el, className) {
53        var reg = new RegExp('(\\s|^)' + className + '(\\s|$)');
54        el.className = el.className.replace(reg, ' ').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
55    },
56    addClass: function(el, className) {
57        if( !framework.hasClass(el,className) ) {
58            el.className += (el.className ? ' ' : '') + className;
59        }
60    },
61    hasClass: function(el, className) {
62        return el.className && new RegExp('(^|\\s)' + className + '(\\s|$)').test(el.className);
63    },
64    getChildByClass: function(parentEl, childClassName) {
65        var node = parentEl.firstChild;
66        while(node) {
67            if( framework.hasClass(node, childClassName) ) {
68                return node;
69            }
70            node = node.nextSibling;
71        }
72    },
73    arraySearch: function(array, value, key) {
74        var i = array.length;
75        while(i--) {
76            if(array[i][key] === value) {
77                return i;
78            }
79        }
80        return -1;
81    },
82    extend: function(o1, o2, preventOverwrite) {
83        for (var prop in o2) {
84            if (o2.hasOwnProperty(prop)) {
85                if(preventOverwrite && o1.hasOwnProperty(prop)) {
86                    continue;
87                }
88                o1[prop] = o2[prop];
89            }
90        }
91    },
92    easing: {
93        sine: {
94            out: function(k) {
95                return Math.sin(k * (Math.PI / 2));
96            },
97            inOut: function(k) {
98                return - (Math.cos(Math.PI * k) - 1) / 2;
99            }
100        },
101        cubic: {
102            out: function(k) {
103                return --k * k * k + 1;
104            }
105        }
106        /*
107            elastic: {
108                out: function ( k ) {
109
110                    var s, a = 0.1, p = 0.4;
111                    if ( k === 0 ) return 0;
112                    if ( k === 1 ) return 1;
113                    if ( !a || a < 1 ) { a = 1; s = p / 4; }
114                    else s = p * Math.asin( 1 / a ) / ( 2 * Math.PI );
115                    return ( a * Math.pow( 2, - 10 * k) * Math.sin( ( k - s ) * ( 2 * Math.PI ) / p ) + 1 );
116
117                },
118            },
119            back: {
120                out: function ( k ) {
121                    var s = 1.70158;
122                    return --k * k * ( ( s + 1 ) * k + s ) + 1;
123                }
124            }
125        */
126    },
127
128    /**
129     *
130     * @return {object}
131     *
132     * {
133     *  raf : request animation frame function
134     *  caf : cancel animation frame function
135     *  transfrom : transform property key (with vendor), or null if not supported
136     *  oldIE : IE8 or below
137     * }
138     *
139     */
140    detectFeatures: function() {
141        if(framework.features) {
142            return framework.features;
143        }
144        var helperEl = framework.createEl(),
145            helperStyle = helperEl.style,
146            vendor = '',
147            features = {};
148
149        // IE8 and below
150        features.oldIE = document.all && !document.addEventListener;
151
152        features.touch = 'ontouchstart' in window;
153
154        if(window.requestAnimationFrame) {
155            features.raf = window.requestAnimationFrame;
156            features.caf = window.cancelAnimationFrame;
157        }
158
159        features.pointerEvent = navigator.pointerEnabled || navigator.msPointerEnabled;
160
161        // fix false-positive detection of old Android in new IE
162        // (IE11 ua string contains "Android 4.0")
163
164        if(!features.pointerEvent) {
165
166            var ua = navigator.userAgent;
167
168            // Detect if device is iPhone or iPod and if it's older than 
168iOS 8
169            // http://stackoverflow.com/a/14223920
170            //
171            // This detection is made because of buggy top/bottom toolbars
172            // that don't trigger window.resize event.
173            // For more info refer to _isFixedPosition variable in core.js
174
175            if (/iP(hone|od)/.test(navigator.platform)) {
176                var v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);
177                if(v && v.length > 0) {
178                    v = parseInt(v[1], 10);
179                    if(v >= 1 && v < 8 ) {
180                        features.isOldIOSPhone = true;
181                    }
182                }
183            }
184
185            // Detect old Android (before KitKat)
186            // due to bugs related to position:fixed
187            // http://stackoverflow.com/questions/7184573/pick-up-the-android-version-in-the-browser-by-javascript
188
189            var match = ua.match(/Android\s([0-9\.]*)/);
190            var androidversion =  match ? match[1] : 0;
191            androidversion = parseFloat(androidversion);
192            if(androidversion >= 1 ) {
193                if(androidversion < 4.4) {
194                    features.isOldAndroid = true; // for fixed position bug & performance
195                }
196                features.androidVersion = androidversion; // for touchend bug
197            }
198            features.isMobileOpera = /opera mini|opera mobi/i.test(ua);
199
200            // p.s. yes, yes, UA sniffing is bad, propose your solution for above bugs.
201        }
202
203        var styleChecks = ['transform', 'perspective', 'animationName'],
204            vendors = ['', 'webkit','Moz','ms','O'],
205            styleCheckItem,
206            styleName;
207
208        for(var i = 0; i < 4; i++) {
209            vendor = vendors[i];
210
211            for(var a = 0; a < 3; a++) {
212                styleCheckItem = styleChecks[a];
213
214                // uppercase first letter of property name, if vendor is present
215                styleName = vendor + (vendor ?
216                                        styleCheckItem.charAt(0).toUpperCase() + styleCheckItem.slice(1) :
217                                        styleCheckItem);
218
219                if(!features[styleCheckItem] && styleName in helperStyle ) {
220                    features[styleCheckItem] = styleName;
221                }
222            }
223
224            if(vendor && !features.raf) {
225                vendor = vendor.toLowerCase();
226                features.raf = window[vendor+'RequestAnimationFrame'];
227                if(features.raf) {
228                    features.caf = window[vendor+'CancelAnimationFrame'] ||
229                                    window[vendor+'CancelRequestAnimationFrame'];
230                }
231            }
232        }
233
234        if(!features.raf) {
235            var lastTime = 0;
236            features.raf = function(fn) {
237                var currTime = new Date().getTime();
238                var timeToCall = Math.max(0, 16 - (currTime - lastTime));
239                var id = window.setTimeout(function() { fn(currTime + timeToCall); }, timeToCall);
240                lastTime = currTime + timeToCall;
241                return id;
242            };
243            features.caf = function(id) { clearTimeout(id); };
244        }
245
246        // Detect SVG support
247        features.svg = !!document.createElementNS &&
248                        !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect;
249
250        framework.features = features;
251
252        return features;
253    }
254};
255
256framework.detectFeatures();
257
258// Override addEventListener for old versions of IE
259if(framework.features.oldIE) {
260
261    framework.bind = function(target, type, listener, unbind) {
262
263        type = type.split(' ');
264
265        var methodName = (unbind ? 'detach' : 'attach') + 'Event',
266            evName,
267            _handleEv = function() {
268                listener.handleEvent.call(listener);
269            };
270
271        for(var i = 0; i < type.length; i++) {
272            evName = type[i];
273            if(evName) {
274
275                if(typeof listener === 'object' && listener.handleEvent) {
276                    if(!unbind) {
277                        listener['oldIE' + evName] = _handleEv;
278                    } else {
279                        if(!listener['oldIE' + evName]) {
280                            return false;
281                        }
282                    }
283
284                    target[methodName]( 'on' + evName, listener['oldIE' + evName]);
285                } else {
286                    target[methodName]( 'on' + evName, listener);
287                }
288
289            }
290        }
291    };
292
293}
294
295/*>>framework-bridge*/
296
297/*>>core*/
298//function(template, UiClass, items, options)
299
300var self = this;
301
302/**
303 * Static vars, don't change unless you know what you're doing.
304 */
305var DOUBLE_TAP_RADIUS = 25, 
306    NUM_HOLDERS = 3;
307
308/**
309 * Options
310 */
311var _options = {
312    allowPanToNext:true,
313    spacing: 0.12,
314    bgOpacity: 1,
315    mouseUsed: false,
316    loop: true,
317    pinchToClose: true,
318    closeOnScroll: false,
319    zoomOnScroll: true,
320    switchOnScroll : false,
321    closeOnVerticalDrag: true,
322    verticalDragRange: 0.75,
323    hideAnimationDuration: 333,
324    showAnimationDuration: 333,
325    showHideOpacity: false,
326    focus: true,
327    escKey: true,
328    arrowKeys: true,
329    desktopSlider: true,
330    mainScrollEndFriction: 0.35,
331    panEndFriction: 0.35,
332    isClickableElement: function(el) {
333        return el.tagName === 'A';
334    },
335    getDoubleTapZoom: function(isMouseClick, item) {
336        if(isMouseClick) {
337            return 1;
338        } else {
339            return item.initialZoomLevel < 0.7 ? 1 : 1.33;
340        }
341    },
342    maxSpreadZoom: 1.33,
343    modal: true,
344
345    // not fully implemented yet
346    scaleMode: 'fit' // TODO
347};
348framework.extend(_options, options);
349
350
351/**
352 * Private helper variables & functions
353 */
354
355var _getEmptyPoint = function() { 
356        return {x:0,y:0};
357    };
358
359var _isOpen,
360    _isDestroying,
361    _closedByScroll,
362    _currentItemIndex,
363    _containerStyle,
364    _containerShiftIndex,
365    _currPanDist = _getEmptyPoint(),
366    _startPanOffset = _getEmptyPoint(),
367    _panOffset = _getEmptyPoint(),
368    _upMoveEvents, // drag move, drag end & drag cancel events array
369    _downEvents, // drag start events array
370    _globalEventHandlers,
371    _viewportSize = {},
372    _currZoomLevel,
373    _startZoomLevel,
374    _translatePrefix,
375    _translateSufix,
376    _updateSizeInterval,
377    _itemsNeedUpdate,
378    _currPositionIndex = 0,
379    _offset = {},
380    _slideSize = _getEmptyPoint(), // size of slide area, including spacing
381    _itemHolders,
382    _prevItemIndex,
383    _indexDiff = 0, // difference of indexes since last content update
384    _dragStartEvent,
385    _dragMoveEvent,
386    _dragEndEvent,
387    _dragCancelEvent,
388    _transformKey,
389    _pointerEventEnabled,
390    _isFixedPosition = true,
391    _likelyTouchDevice,
392    _modules = [],
393    _requestAF,
394    _cancelAF,
395    _initalClassName,
396    _initalWindowScrollY,
397    _oldIE,
398    _currentWindowScrollY,
399    _features,
400    _windowVisibleSize = {},
401    _renderMaxResolution = false,
402    _orientationChangeTimeout,
403
404
405    // Registers PhotoSWipe module (History, Controller ...)
406    _registerModule = function(name, module) {
407        framework.extend(self, module.publicMethods);
408        _modules.push(name);
409    },
410
411    _getLoopedId = function(index) {
412        var numSlides = _getNumItems();
413        if(index > numSlides - 1) {
414            return index - numSlides;
415        } else  if(index < 0) {
416            return numSlides + index;
417        }
418        return index;
419    },
420
421    // Micro bind/trigger
422    _listeners = {},
423    _listen = function(name, fn) {
424        if(!_listeners[name]) {
425            _listeners[name] = [];
426        }
427        return _listeners[name].push(fn);
428    },
429    _shout = function(name) {
430        var listeners = _listeners[name];
431
432        if(listeners) {
433            var args = Array.prototype.slice.call(arguments);
434            args.shift();
435
436            for(var i = 0; i < listeners.length; i++) {
437                listeners[i].apply(self, args);
438            }
439        }
440    },
441
442    _getCurrentTime = function() {
443        return new Date().getTime();
444    },
445    _applyBgOpacity = function(opacity) {
446        _bgOpacity = opacity;
447        self.bg.style.opacity = opacity * _options.bgOpacity;
448    },
449
450    _applyZoomTransform = function(styleObj,x,y,zoom,item) {
451        if(!_renderMaxResolution || (item && item !== self.currItem) ) {
452            zoom = zoom / (item ? item.fitRatio : self.currItem.fitRatio);
453        }
454
455        styleObj[_transformKey] = _translatePrefix + x + 'px, ' + y + 'px' + _translateSufix + ' scale(' + zoom + ')';
456    },
457    _applyCurrentZoomPan = function( allowRenderResolution ) {
458        if(_currZoomElementStyle) {
459
460            if(allowRenderResolution) {
461                if(_currZoomLevel > self.currItem.fitRatio) {
462                    if(!_renderMaxResolution) {
463                        _setImageSize(self.currItem, false, true);
464                        _renderMaxResolution = true;
465                    }
466                } else {
467                    if(_renderMaxResolution) {
468                        _setImageSize(self.currItem);
469                        _renderMaxResolution = false;
470                    }
471                }
472            }
473
474
475            _applyZoomTransform(_currZoomElementStyle, _panOffset.x, _panOffset.y, _currZoomLevel);
476        }
477    },
478    _applyZoomPanToItem = function(item) {
479        if(item.container) {
480
481            _applyZoomTransform(item.container.style,
482                                item.initialPosition.x,
483                                item.initialPosition.y,
484                                item.initialZoomLevel,
485                                item);
486        }
487    },
488    _setTranslateX = function(x, elStyle) {
489        elStyle[_transformKey] = _translatePrefix + x + 'px, 0px' + _translateSufix;
490    },
491    _moveMainScroll = function(x, dragging) {
492
493        if(!_options.loop && dragging) {
494            var newSlideIndexOffset = _currentItemIndex + (_slideSize.x * _currPositionIndex - x) / _slideSize.x,
495                delta = Math.round(x - _mainScrollPos.x);
496
497            if( (newSlideIndexOffset < 0 && delta > 0) ||
498                (newSlideIndexOffset >= _getNumItems() - 1 && delta < 0) ) {
499                x = _mainScrollPos.x + delta * _options.mainScrollEndFriction;
500            }
501        }
502
503        _mainScrollPos.x = x;
504        _setTranslateX(x, _containerStyle);
505    },
506    _calculatePanOffset = function(axis, zoomLevel) {
507        var m = _midZoomPoint[axis] - _offset[axis];
508        return _startPanOffset[axis] + _currPanDist[axis] + m - m * ( zoomLevel / _startZoomLevel );
509    },
510
511    _equalizePoints = function(p1, p2) {
512        p1.x = p2.x;
513        p1.y = p2.y;
514        if(p2.id) {
515            p1.id = p2.id;
516        }
517    },
518    _roundPoint = function(p) {
519        p.x = Math.round(p.x);
520        p.y = Math.round(p.y);
521    },
522
523    _mouseMoveTimeout = null,
524    _onFirstMouseMove = function() {
525        // Wait until mouse move event is fired at least twice during 100ms
526        // We do this, because some mobile browsers trigger it on touchstart
527        if(_mouseMoveTimeout ) {
528            framework.unbind(document, 'mousemove', _onFirstMouseMove);
529            framework.addClass(template, 'pswp--has_mouse');
530            _options.mouseUsed = true;
531            _shout('mouseUsed');
532        }
533        _mouseMoveTimeout = setTimeout(function() {
534            _mouseMoveTimeout = null;
535        }, 100);
536    },
537
538    _bindEvents = function() {
539        framework.bind(document, 'keydown', self);
540
541        if(_features.transform) {
542            // don't bind click event in browsers that don't support transform (mostly IE8)
543            framework.bind(self.scrollWrap, 'click', self);
544        }
545
546
547        if(!_options.mouseUsed) {
548            framework.bind(document, 'mousemove', _onFirstMouseMove);
549        }
550
551        framework.bind(window, 'resize scroll orientationchange', self);
552
553        _shout('bindEvents');
554    },
555
556    _unbindEvents = function() {
557        framework.unbind(window, 'resize scroll orientationchange', self);
558        framework.unbind(window, 'scroll', _globalEventHandlers.scroll);
559        framework.unbind(document, 'keydown', self);
560        framework.unbind(document, 'mousemove', _onFirstMouseMove);
561
562        if(_features.transform) {
563            framework.unbind(self.scrollWrap, 'click', self);
564        }
565
566        if(_isDragging) {
567            framework.unbind(window, _upMoveEvents, self);
568        }
569
570        clearTimeout(_orientationChangeTimeout);
571
572        _shout('unbindEvents');
573    },
574
575    _calculatePanBounds = function(zoomLevel, update) {
576        var bounds = _calculateItemSize( self.currItem, _viewportSize, zoomLevel );
577        if(update) {
578            _currPanBounds = bounds;
579        }
580        return bounds;
581    },
582
583    _getMinZoomLevel = function(item) {
584        if(!item) {
585            item = self.currItem;
586        }
587        return item.initialZoomLevel;
588    },
589    _getMaxZoomLevel = function(item) {
590        if(!item) {
591            item = self.currItem;
592        }
593        return item.w > 0 ? _options.maxSpreadZoom : 1;
594    },
595
596    // Return true if offset is out of the bounds
597    _modifyDestPanOffset = function(axis, destPanBounds, destPanOffset, destZoomLevel) {
598        if(destZoomLevel === self.currItem.initialZoomLevel) {
599            destPanOffset[axis] = self.currItem.initialPosition[axis];
600            return true;
601        } else {
602            destPanOffset[axis] = _calculatePanOffset(axis, destZoomLevel);
603
604            if(destPanOffset[axis] > destPanBounds.min[axis]) {
605                destPanOffset[axis] = destPanBounds.min[axis];
606                return true;
607            } else if(destPanOffset[axis] < destPanBounds.max[axis] ) {
608                destPanOffset[axis] = destPanBounds.max[axis];
609                return true;
610            }
611        }
612        return false;
613    },
614
615    _setupTransforms = function() {
616
617        if(_transformKey) {
618            // setup 3d transforms
619            var allow3dTransform = _features.perspective && !_likelyTouchDevice;
620            _translatePrefix = 'translate' + (allow3dTransform ? '3d(' : '(');
621            _translateSufix = _features.perspective ? ', 0px)' : ')';
622            return;
623        }
624
625        // Override zoom/pan/move functions in case old browser is used (most likely IE)
626        // (so they use left/top/width/height, instead of CSS transform)
627
628        _transformKey = 'left';
629        framework.addClass(template, 'pswp--ie');
630
631        _setTranslateX = function(x, elStyle) {
632            elStyle.left = x + 'px';
633        };
634        _applyZoomPanToItem = function(item) {
635
636            var zoomRatio = item.fitRatio > 1 ? 1 : item.fitRatio,
637                s = item.container.style,
638                w = zoomRatio * item.w,
639                h = zoomRatio * item.h;
640
641            s.width = w + 'px';
642            s.height = h + 'px';
643            s.left = item.initialPosition.x + 'px';
644            s.top = item.initialPosition.y + 'px';
645
646        };
647        _applyCurrentZoomPan = function() {
648            if(_currZoomElementStyle) {
649
650                var s = _currZoomElementStyle,
651                    item = self.currItem,
652                    zoomRatio = item.fitRatio > 1 ? 1 : item.fitRatio,
653                    w = zoomRatio * item.w,
654                    h = zoomRatio * item.h;
655
656                s.width = w + 'px';
657                s.height = h + 'px';
658
659
660                s.left = _panOffset.x + 'px';
661                s.top = _panOffset.y + 'px';
662            }
663
664        };
665    },
666
667    _onKeyDown = function(e) {
668        var keydownAction = '';
669        if(_options.escKey && e.keyCode === 27) {
670            keydownAction = 'close';
671        } else if(_options.arrowKeys) {
672            if(_options.arrowKeys && e.keyCode === 37) {
673                keydownAction = 'prevAnim';
674            } else if(_options.arrowKeys && e.keyCode === 39) {
675                keydownAction = 'nextAnim';
676            } else if (e.keyCode === 70) {
677                keydownAction = 'fullscreen';
678            }
679        }
680
681        if(keydownAction) {
682            // don't do anything if special key pressed to prevent from overriding default browser actions
683            // e.g. in Chrome on Mac cmd+arrow-left returns to previous page
684            if( !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey ) {
685                if(e.preventDefault) {
686                    e.preventDefault();
687                } else {
688                    e.returnValue = false;
689                }
690                self[keydownAction]();
691            }
692        }
693    },
694
695    _onGlobalClick = function(e) {
696        if(!e) {
697            return;
698        }
699
700        // don't allow click event to pass through when triggering after drag or some other gesture
701        if(_moved || _zoomStarted || _mainScrollAnimating || _verticalDragInitiated) {
702            e.preventDefault();
703            e.stopPropagation();
704        }
705    },
706
707    _updatePageScrollOffset = function() {
708        self.setScrollOffset(0, framework.getScrollY());
709    };
710
711
712
713
714
715
716
717// Micro animation engine
718var _animations = {},
719    _numAnimations = 0,
720    _stopAnimation = function(name) {
721        if(_animations[name]) {
722            if(_animations[name].raf) {
723                _cancelAF( _animations[name].raf );
724            }
725            _numAnimations--;
726            delete _animations[name];
727        }
728    },
729    _registerStartAnimation = function(name) {
730        if(_animations[name]) {
731            _stopAnimation(name);
732        }
733        if(!_animations[name]) {
734            _numAnimations++;
735            _animations[name] = {};
736        }
737    },
738    _stopAllAnimations = function() {
739        for (var prop in _animations) {
740
741            if( _animations.hasOwnProperty( prop ) ) {
742                _stopAnimation(prop);
743            }
744
745        }
746    },
747    _animateProp = function(name, b, endProp, d, easingFn, onUpdate, onComplete) {
748        var startAnimTime = _getCurrentTime(), t;
749        _registerStartAnimation(name);
750
751        var animloop = function(){
752            if ( _animations[name] ) {
753
754                t = _getCurrentTime() - startAnimTime; // time diff
755                //b - beginning (start prop)
756                //d - anim duration
757
758                if ( t >= d ) {
759                    _stopAnimation(name);
760                    onUpdate(endProp);
761                    if(onComplete) {
762                        onComplete();
763                    }
764                    return;
765                }
766                onUpdate( (endProp - b) * easingFn(t/d) + b );
767
768                _animations[name].raf = _requestAF(animloop);
769            }
770        };
771        animloop();
772    };
773
774
775
776var publicMethods = {
777
778    // make a few local variables and functions public
779    shout: _shout,
780    listen: _listen,
781    viewportSize: _viewportSize,
782    options: _options,
783
784    isMainScrollAnimating: function() {
785        return _mainScrollAnimating;
786    },
787    getZoomLevel: function() {
788        return _currZoomLevel;
789    },
790    getCurrentIndex: function() {
791        return _currentItemIndex;
792    },
793    isDragging: function() {
794        return _isDragging;
795    },
796    isZooming: function() {
797        return _isZooming;
798    },
799    setScrollOffset: function(x,y) {
800        _offset.x = x;
801        _currentWindowScrollY = _offset.y = y;
802        _shout('updateScrollOffset', _offset);
803    },
804    applyZoomPan: function(zoomLevel,panX,panY,allowRenderResolution) {
805        _panOffset.x = panX;
806        _panOffset.y = panY;
807        _currZoomLevel = zoomLevel;
808        _applyCurrentZoomPan( allowRenderResolution );
809    },
810
811    init: function() {
812
813        if(_isOpen || _isDestroying) {
814            return;
815        }
816
817        var i;
818
819        self.framework = framework; // basic functionality
820        self.template = template; // root DOM element of PhotoSwipe
821        self.bg = framework.getChildByClass(template, 'pswp__bg');
822
823        _initalClassName = template.className;
824        _isOpen = true;
825
826        _features = framework.detectFeatures();
827        _requestAF = _features.raf;
828        _cancelAF = _features.caf;
829        _transformKey = _features.transform;
830        _oldIE = _features.oldIE;
831
832        self.scrollWrap = framework.getChildByClass(template, 'pswp__scroll-wrap');
833        self.container = framework.getChildByClass(self.scrollWrap, 'pswp__container');
834
835        _containerStyle = self.container.style; // for fast access
836
837        // Objects that hold slides (there are only 3 in DOM)
838        self.itemHolders = _itemHolders = [
839            {el:self.container.children[0] , wrap:0, index: -1},
840            {el:self.container.children[1] , wrap:0, index: -1},
841            {el:self.container.children[2] , wrap:0, index: -1}
842        ];
843
844        // hide nearby item holders until initial zoom animation finishes (to avoid extra Paints)
845        _itemHolders[0].el.style.display = _itemHolders[2].el.style.display = 'none';
846
847        _setupTransforms();
848
849        // Setup global events
850        _globalEventHandlers = {
851            resize: self.updateSize,
852
853            // Fixes: iOS 10.3 resize event
854            // does not update scrollWrap.clientWidth instantly after resize
855            // https://github.com/dimsemenov/PhotoSwipe/issues/1315
856            orientationchange: function() {
857                clearTimeout(_orientationChangeTimeout);
858                _orientationChangeTimeout = setTimeout(function() {
859                    if(_viewportSize.x !== self.scrollWrap.clientWidth) {
860                        self.updateSize();
861                    }
862                }, 500);
863            },
864            scroll: _updatePageScrollOffset,
865            keydown: _onKeyDown,
866            click: _onGlobalClick
867        };
868
869        // disable show/hide effects on old browsers that don't support CSS animations or transforms,
870        // old IOS, Android and Opera mobile. Blackberry seems to work fine, even older models.
871        var oldPhone = _features.isOldIOSPhone || _features.isOldAndroid || _features.isMobileOpera;
872        if(!_features.animationName || !_features.transform || oldPhone) {
873            _options.showAnimationDuration = _options.hideAnimationDuration = 0;
874        }
875
876        // init modules
877        for(i = 0; i < _modules.length; i++) {
878            self['init' + _modules[i]]();
879        }
880
881        // init
882        if(UiClass) {
883            var ui = self.ui = new UiClass(self, framework);
884            ui.init();
885        }
886
887        _shout('firstUpdate');
888        _currentItemIndex = _currentItemIndex || _options.index || 0;
889        // validate index
890        if( isNaN(_currentItemIndex) || _currentItemIndex < 0 || _currentItemIndex >= _getNumItems() ) {
891            _currentItemIndex = 0;
892        }
893        self.currItem = _getItemAt( _currentItemIndex );
894
895
896        if(_features.isOldIOSPhone || _features.isOldAndroid) {
897            _isFixedPosition = false;
898        }
899
900        template.setAttribute('aria-hidden', 'false');
901        if(_options.modal) {
902            if(!_isFixedPosition) {
903                template.style.position = 'absolute';
904                template.style.top = framework.getScrollY() + 'px';
905            } else {
906                template.style.position = 'fixed';
907            }
908        }
909
910        if(_currentWindowScrollY === undefined) {
911            _shout('initialLayout');
912            _currentWindowScrollY = _initalWindowScrollY = framework.getScrollY();
913        }
914
915        // add classes to root element of PhotoSwipe
916        var rootClasses = 'pswp--open ';
917        if(_options.mainClass) {
918            rootClasses += _options.mainClass + ' ';
919        }
920        if(_options.showHideOpacity) {
921            rootClasses += 'pswp--animate_opacity ';
922        }
923        rootClasses += _likelyTouchDevice ? 'pswp--touch' : 'pswp--notouch';
924        rootClasses += _features.animationName ? ' pswp--css_animation' : '';
925        rootClasses += _features.svg ? ' pswp--svg' : '';
926        framework.addClass(template, rootClasses);
927
928        self.updateSize();
929
930        // initial update
931        _containerShiftIndex = -1;
932        _indexDiff = null;
933        for(i = 0; i < NUM_HOLDERS; i++) {
934            _setTranslateX( (i+_containerShiftIndex) * _slideSize.x, _itemHolders[i].el.style);
935        }
936
937        if(!_oldIE) {
938            framework.bind(self.scrollWrap, _downEvents, self); // no dragging for old IE
939        }
940
941        _listen('initialZoomInEnd', function() {
942            self.setContent(_itemHolders[0], _currentItemIndex-1);
943            self.setContent(_itemHolders[2], _currentItemIndex+1);
944
945            _itemHolders[0].el.style.display = _itemHolders[2].el.style.display = 'block';
946
947            if(_options.focus) {
948                // focus causes layout,
949                // which causes lag during the animation,
950                // that's why we delay it untill the initial zoom transition ends
951                template.focus();
952            }
953
954
955            _bindEvents();
956        });
957
958        // set content for center slide (first time)
959        self.setContent(_itemHolders[1], _currentItemIndex);
960
961        self.updateCurrItem();
962
963        _shout('afterInit');
964
965        if(!_isFixedPosition) {
966
967            // On all versions of iOS lower than 8.0, we check size of viewport every second.
968            //
969            // This is done to detect when Safari top & bottom bars appear,
970            // as this action doesn't trigger any events (like resize).
971            //
972            // On iOS8 they fixed this.
973            //
974            // 10 Nov 2014: iOS 7 usage ~40%. iOS 8 usage 56%.
975
976            _updateSizeInterval = setInterval(function() {
977                if(!_numAnimations && !_isDragging && !_isZooming && (_currZoomLevel === self.currItem.initialZoomLevel)  ) {
978                    self.updateSize();
979                }
980            }, 1000);
981        }
982
983        framework.addClass(template, 'pswp--visible');
984    },
985
986    // Close the gallery, then destroy it
987    close: function() {
988        if(!_isOpen) {
989            return;
990        }
991
992        _isOpen = false;
993        _isDestroying = true;
994        _shout('close');
995        _unbindEvents();
996
997        _showOrHide(self.currItem, null, true, self.destroy);
998    },
999
1000    // destroys the gallery (unbinds events, cleans up intervals and timeouts to avoid memory leaks)
1001    destroy: function() {
1002        _shout('destroy');
1003
1004        if(_showOrHideTimeout) {
1005            clearTimeout(_showOrHideTimeout);
1006        }
1007
1008        template.setAttribute('aria-hidden', 'true');
1009        template.className = _initalClassName;
1010
1011        if(_updateSizeInterval) {
1012            clearInterval(_updateSizeInterval);
1013        }
1014
1015        framework.unbind(self.scrollWrap, _downEvents, self);
1016
1017        // we unbind scroll event at the end, as closing animation may depend on it
1018        framework.unbind(window, 'scroll', self);
1019
1020        _stopDragUpdateLoop();
1021
1022        _stopAllAnimations();
1023
1024        _listeners = null;
1025    },
1026
1027    /**
1028     * Pan image to position
1029     * @param {Number} x
1030     * @param {Number} y
1031     * @param {Boolean} force Will ignore bounds if set to true.
1032     */
1033    panTo: function(x,y,force) {
1034        if(!force) {
1035            if(x > _currPanBounds.min.x) {
1036                x = _currPanBounds.min.x;
1037            } else if(x < _currPanBounds.max.x) {
1038                x = _currPanBounds.max.x;
1039            }
1040
1041            if(y > _currPanBounds.min.y) {
1042                y = _currPanBounds.min.y;
1043            } else if(y < _currPanBounds.max.y) {
1044                y = _currPanBounds.max.y;
1045            }
1046        }
1047
1048        _panOffset.x = x;
1049        _panOffset.y = y;
1050        _applyCurrentZoomPan();
1051    },
1052
1053    handleEvent: function (e) {
1054        e = e || window.event;
1055        if(_globalEventHandlers[e.type]) {
1056            _globalEventHandlers[e.type](e);
1057        }
1058    },
1059
1060
1061    goTo: function(index) {
1062
1063        index = _getLoopedId(index);
1064
1065        var diff = index - _currentItemIndex;
1066        _indexDiff = diff;
1067
1068        _currentItemIndex = index;
1069        self.currItem = _getItemAt( _currentItemIndex );
1070        _currPositionIndex -= diff;
1071
1072        _moveMainScroll(_slideSize.x * _currPositionIndex);
1073
1074        _stopAllAnimations();
1075        _mainScrollAnimating = false;
1076
1077        self.updateCurrItem();
1078    },
1079    next: function() {
1080        if(!_options.loop && _currentItemIndex == _getNumItems()-1) return;
1081        self.goTo( _currentItemIndex + 1);
1082    },
1083    prev: function() {
1084        if(!_options.loop && _currentItemIndex == 0) return;
1085        self.goTo( _currentItemIndex - 1);
1086    },
1087    fullscreen: function() {
1088        self.ui.toggleFullscreen();
1089    },
1090
1091    // update current zoom/pan objects
1092    updateCurrZoomItem: function(emulateSetContent) {
1093        if(emulateSetContent) {
1094            _shout('beforeChange', 0);
1095        }
1096
1097        // itemHolder[1] is middle (current) item
1098        if(_itemHolders[1].el.children.length) {
1099            var zoomElement = _itemHolders[1].el.children[0];
1100            if( framework.hasClass(zoomElement, 'pswp__zoom-wrap') ) {
1101                _currZoomElementStyle = zoomElement.style;
1102            } else {
1103                _currZoomElementStyle = null;
1104            }
1105        } else {
1106            _currZoomElementStyle = null;
1107        }
1108
1109        _currPanBounds = self.currItem.bounds;
1110        _startZoomLevel = _currZoomLevel = self.currItem.initialZoomLevel;
1111
1112        _panOffset.x = _currPanBounds.center.x;
1113        _panOffset.y = _currPanBounds.center.y;
1114
1115        if(emulateSetContent) {
1116            _shout('afterChange');
1117        }
1118    },
1119
1120
1121    invalidateCurrItems: function() {
1122        _itemsNeedUpdate = true;
1123        for(var i = 0; i < NUM_HOLDERS; i++) {
1124            if( _itemHolders[i].item ) {
1125                _itemHolders[i].item.needsUpdate = true;
1126            }
1127        }
1128    },
1129
1130    updateCurrItem: function(beforeAnimation) {
1131
1132        if(_indexDiff === 0) {
1133            return;
1134        }
1135
1136        var diffAbs = Math.abs(_indexDiff),
1137            tempHolder;
1138
1139        if(beforeAnimation && diffAbs < 2) {
1140            return;
1141        }
1142
1143
1144        self.currItem = _getItemAt( _currentItemIndex );
1145        _renderMaxResolution = false;
1146
1147        _shout('beforeChange', _indexDiff);
1148
1149        if(diffAbs >= NUM_HOLDERS) {
1150            _containerShiftIndex += _indexDiff + (_indexDiff > 0 ? -NUM_HOLDERS : NUM_HOLDERS);
1151            diffAbs = NUM_HOLDERS;
1152        }
1153        for(var i = 0; i < diffAbs; i++) {
1154            if(_indexDiff > 0) {
1155                tempHolder = _itemHolders.shift();
1156                _itemHolders[NUM_HOLDERS-1] = tempHolder; // move first to last
1157
1158                _containerShiftIndex++;
1159                _setTranslateX( (_containerShiftIndex+2) * _slideSize.x, tempHolder.el.style);
1160                self.setContent(tempHolder, _currentItemIndex - diffAbs + i + 1 + 1);
1161            } else {
1162                tempHolder = _itemHolders.pop();
1163                _itemHolders.unshift( tempHolder ); // move last to first
1164
1165                _containerShiftIndex--;
1166                _setTranslateX( _containerShiftIndex * _slideSize.x, tempHolder.el.style);
1167                self.setContent(tempHolder, _currentItemIndex + diffAbs - i - 1 - 1);
1168            }
1169
1170        }
1171
1172        // reset zoom/pan on previous item
1173        if(_currZoomElementStyle && Math.abs(_indexDiff) === 1) {
1174
1175            var prevItem = _getItemAt(_prevItemIndex);
1176            if(prevItem.initialZoomLevel !== _currZoomLevel) {
1177                _calculateItemSize(prevItem , _viewportSize );
1178                _setImageSize(prevItem);
1179                _applyZoomPanToItem( prevItem );
1180            }
1181
1182        }
1183
1184        // reset diff after update
1185        _indexDiff = 0;
1186
1187        self.updateCurrZoomItem();
1188
1189        _prevItemIndex = _currentItemIndex;
1190
1191        _shout('afterChange');
1192
1193    },
1194
1195
1196
1197    updateSize: function(force) {
1198
1199        if(!_isFixedPosition && _options.modal) {
1200            var windowScrollY = framework.getScrollY();
1201            if(_currentWindowScrollY !== windowScrollY) {
1202                template.style.top = windowScrollY + 'px';
1203                _currentWindowScrollY = windowScrollY;
1204            }
1205            if(!force && _windowVisibleSize.x === window.innerWidth && _windowVisibleSize.y === window.innerHeight) {
1206                return;
1207            }
1208            _windowVisibleSize.x = window.innerWidth;
1209            _windowVisibleSize.y = window.innerHeight;
1210
1211            //template.style.width = _windowVisibleSize.x + 'px';
1212            template.style.height = _windowVisibleSize.y + 'px';
1213        }
1214
1215
1216
1217        _viewportSize.x = self.scrollWrap.clientWidth;
1218        _viewportSize.y = self.scrollWrap.clientHeight;
1219
1220        _updatePageScrollOffset();
1221
1222        _slideSize.x = _viewportSize.x + Math.round(_viewportSize.x * _options.spacing);
1223        _slideSize.y = _viewportSize.y;
1224
1225        _moveMainScroll(_slideSize.x * _currPositionIndex);
1226
1227        _shout('beforeResize'); // even may be used for example to switch image sources
1228
1229
1230        // don't re-calculate size on inital size update
1231        if(_containerShiftIndex !== undefined) {
1232
1233            var holder,
1234                item,
1235                hIndex;
1236
1237            for(var i = 0; i < NUM_HOLDERS; i++) {
1238                holder = _itemHolders[i];
1239                _setTranslateX( (i+_containerShiftIndex) * _slideSize.x, holder.el.style);
1240
1241                hIndex = _currentItemIndex+i-1;
1242
1243                if(_options.loop && _getNumItems() > 2) {
1244                    hIndex = _getLoopedId(hIndex);
1245                }
1246
1247                // update zoom level on items and refresh source (if needsUpdate)
1248                item = _getItemAt( hIndex );
1249
1250                // re-render gallery item if `needsUpdate`,
1251                // or doesn't have `bounds` (entirely new slide object)
1252                if( item && (_itemsNeedUpdate || item.needsUpdate || !item.bounds) ) {
1253
1254                    self.cleanSlide( item );
1255
1256                    self.setContent( holder, hIndex );
1257
1258                    // if "center" slide
1259                    if(i === 1) {
1260                        self.currItem = item;
1261                        self.updateCurrZoomItem(true);
1262                    }
1263
1264                    item.needsUpdate = false;
1265
1266                } else if(holder.index === -1 && hIndex >= 0) {
1267                    // add content first time
1268                    self.setContent( holder, hIndex );
1269                }
1270                if(item && item.container) {
1271                    _calculateItemSize(item, _viewportSize);
1272                    _setImageSize(item);
1273                    _applyZoomPanToItem( item );
1274                }
1275
1276            }
1277            _itemsNeedUpdate = false;
1278        }
1279
1280        _startZoomLevel = _currZoomLevel = self.currItem.initialZoomLevel;
1281        _currPanBounds = self.currItem.bounds;
1282
1283        if(_currPanBounds) {
1284            _panOffset.x = _currPanBounds.center.x;
1285            _panOffset.y = _currPanBounds.center.y;
1286            _applyCurrentZoomPan( true );
1287        }
1288
1289        _shout('resize');
1290    },
1291
1292    // Zoom current item to
1293    zoomTo: function(destZoomLevel, centerPoint, speed, easingFn, updateFn) {
1294        /*
1295            if(destZoomLevel === 'fit') {
1296                destZoomLevel = self.currItem.fitRatio;
1297            } else if(destZoomLevel === 'fill') {
1298                destZoomLevel = self.currItem.fillRatio;
1299            }
1300        */
1301
1302        if(centerPoint) {
1303            _startZoomLevel = _currZoomLevel;
1304            _midZoomPoint.x = Math.abs(centerPoint.x) - _panOffset.x ;
1305            _midZoomPoint.y = Math.abs(centerPoint.y) - _panOffset.y ;
1306            _equalizePoints(_startPanOffset, _panOffset);
1307        }
1308
1309        var destPanBounds = _calculatePanBounds(destZoomLevel, false),
1310            destPanOffset = {};
1311
1312        _modifyDestPanOffset('x', destPanBounds, destPanOffset, destZoomLevel);
1313        _modifyDestPanOffset('y', destPanBounds, destPanOffset, destZoomLevel);
1314
1315        var initialZoomLevel = _currZoomLevel;
1316        var initialPanOffset = {
1317            x: _panOffset.x,
1318            y: _panOffset.y
1319        };
1320
1321        _roundPoint(destPanOffset);
1322
1323        var onUpdate = function(now) {
1324            if(now === 1) {
1325                _currZoomLevel = destZoomLevel;
1326                _panOffset.x = destPanOffset.x;
1327                _panOffset.y = destPanOffset.y;
1328            } else {
1329                _currZoomLevel = (destZoomLevel - initialZoomLevel) * now + initialZoomLevel;
1330                _panOffset.x = (destPanOffset.x - initialPanOffset.x) * now + initialPanOffset.x;
1331                _panOffset.y = (destPanOffset.y - initialPanOffset.
1331y) * now + initialPanOffset.y;
1332            }
1333
1334            if(updateFn) {
1335                updateFn(now);
1336            }
1337
1338            _applyCurrentZoomPan( now === 1 );
1339        };
1340
1341        if(speed) {
1342            _animateProp('customZoomTo', 0, 1, speed, easingFn || framework.easing.sine.inOut, onUpdate);
1343        } else {
1344            onUpdate(1);
1345        }
1346    }
1347
1348
1349};
1350
1351
1352/*>>core*/
1353
1354/*>>gestures*/
1355/**
1356 * Mouse/touch/pointer event handlers.
1357 * 
1358 * separated from @core.js for readability
1359 */
1360
1361var MIN_SWIPE_DISTANCE = 30,
1362    DIRECTION_CHECK_OFFSET = 10; // amount of pixels to drag to determine direction of swipe
1363
1364var _gestureStartTime,
1365    _gestureCheckSpeedTime,
1366
1367    // pool of objects that are used during dragging of zooming
1368    p = {}, // first point
1369    p2 = {}, // second point (for zoom gesture)
1370    delta = {},
1371    _currPoint = {},
1372    _startPoint = {},
1373    _currPointers = [],
1374    _startMainScrollPos = {},
1375    _releaseAnimData,
1376    _posPoints = [], // array of points during dragging, used to determine type of gesture
1377    _tempPoint = {},
1378
1379    _isZoomingIn,
1380    _verticalDragInitiated,
1381    _oldAndroidTouchEndTimeout,
1382    _currZoomedItemIndex = 0,
1383    _centerPoint = _getEmptyPoint(),
1384    _lastReleaseTime = 0,
1385    _isDragging, // at least one pointer is down
1386    _isMultitouch, // at least two _pointers are down
1387    _zoomStarted, // zoom level changed during zoom gesture
1388    _moved,
1389    _dragAnimFrame,
1390    _mainScrollShifted,
1391    _currentPoints, // array of current touch points
1392    _isZooming,
1393    _currPointsDistance,
1394    _startPointsDistance,
1395    _currPanBounds,
1396    _mainScrollPos = _getEmptyPoint(),
1397    _currZoomElementStyle,
1398    _mainScrollAnimating, // true, if animation after swipe gesture is running
1399    _midZoomPoint = _getEmptyPoint(),
1400    _currCenterPoint = _getEmptyPoint(),
1401    _direction,
1402    _isFirstMove,
1403    _opacityChanged,
1404    _bgOpacity,
1405    _wasOverInitialZoom,
1406
1407    _isEqualPoints = function(p1, p2) {
1408        return p1.x === p2.x && p1.y === p2.y;
1409    },
1410    _isNearbyPoints = function(touch0, touch1) {
1411        return Math.abs(touch0.x - touch1.x) < DOUBLE_TAP_RADIUS && Math.abs(touch0.y - touch1.y) < DOUBLE_TAP_RADIUS;
1412    },
1413    _calculatePointsDistance = function(p1, p2) {
1414        _tempPoint.x = Math.abs( p1.x - p2.x );
1415        _tempPoint.y = Math.abs( p1.y - p2.y );
1416        return Math.sqrt(_tempPoint.x * _tempPoint.x + _tempPoint.y * _tempPoint.y);
1417    },
1418    _stopDragUpdateLoop = function() {
1419        if(_dragAnimFrame) {
1420            _cancelAF(_dragAnimFrame);
1421            _dragAnimFrame = null;
1422        }
1423    },
1424    _dragUpdateLoop = function() {
1425        if(_isDragging) {
1426            _dragAnimFrame = _requestAF(_dragUpdateLoop);
1427            _renderMovement();
1428        }
1429    },
1430    _canPan = function() {
1431        return !(_options.scaleMode === 'fit' && _currZoomLevel ===  self.currItem.initialZoomLevel);
1432    },
1433
1434    // find the closest parent DOM element
1435    _closestElement = function(el, fn) {
1436        if(!el || el === document) {
1437            return false;
1438        }
1439
1440        // don't search elements above pswp__scroll-wrap
1441        if(el.getAttribute('class') && el.getAttribute('class').indexOf('pswp__scroll-wrap') > -1 ) {
1442            return false;
1443        }
1444
1445        if( fn(el) ) {
1446            return el;
1447        }
1448
1449        return _closestElement(el.parentNode, fn);
1450    },
1451
1452    _preventObj = {},
1453    _preventDefaultEventBehaviour = function(e, isDown) {
1454        _preventObj.prevent = !_closestElement(e.target, _options.isClickableElement);
1455
1456        _shout('preventDragEvent', e, isDown, _preventObj);
1457        return _preventObj.prevent;
1458
1459    },
1460    _convertTouchToPoint = function(touch, p) {
1461        p.x = touch.pageX;
1462        p.y = touch.pageY;
1463        p.id = touch.identifier;
1464        return p;
1465    },
1466    _findCenterOfPoints = function(p1, p2, pCenter) {
1467        pCenter.x = (p1.x + p2.x) * 0.5;
1468        pCenter.y = (p1.y + p2.y) * 0.5;
1469    },
1470    _pushPosPoint = function(time, x, y) {
1471        if(time - _gestureCheckSpeedTime > 50) {
1472            var o = _posPoints.length > 2 ? _posPoints.shift() : {};
1473            o.x = x;
1474            o.y = y;
1475            _posPoints.push(o);
1476            _gestureCheckSpeedTime = time;
1477        }
1478    },
1479
1480    _calculateVerticalDragOpacityRatio = function() {
1481        var yOffset = _panOffset.y - self.currItem.initialPosition.y; // difference between initial and current position
1482        return 1 -  Math.abs( yOffset / (_viewportSize.y / 2)  );
1483    },
1484
1485
1486    // points pool, reused during touch events
1487    _ePoint1 = {},
1488    _ePoint2 = {},
1489    _tempPointsArr = [],
1490    _tempCounter,
1491    _getTouchPoints = function(e) {
1492        // clean up previous points, without recreating array
1493        while(_tempPointsArr.length > 0) {
1494            _tempPointsArr.pop();
1495        }
1496
1497        if(!_pointerEventEnabled) {
1498            if(e.type.indexOf('touch') > -1) {
1499
1500                if(e.touches && e.touches.length > 0) {
1501                    _tempPointsArr[0] = _convertTouchToPoint(e.touches[0], _ePoint1);
1502                    if(e.touches.length > 1) {
1503                        _tempPointsArr[1] = _convertTouchToPoint(e.touches[1], _ePoint2);
1504                    }
1505                }
1506
1507            } else {
1508                _ePoint1.x = e.pageX;
1509                _ePoint1.y = e.pageY;
1510                _ePoint1.id = '';
1511                _tempPointsArr[0] = _ePoint1;//_ePoint1;
1512            }
1513        } else {
1514            _tempCounter = 0;
1515            // we can use forEach, as pointer events are supported only in modern browsers
1516            _currPointers.forEach(function(p) {
1517                if(_tempCounter === 0) {
1518                    _tempPointsArr[0] = p;
1519                } else if(_tempCounter === 1) {
1520                    _tempPointsArr[1] = p;
1521                }
1522                _tempCounter++;
1523
1524            });
1525        }
1526        return _tempPointsArr;
1527    },
1528
1529    _panOrMoveMainScroll = function(axis, delta) {
1530
1531        var panFriction,
1532            overDiff = 0,
1533            newOffset = _panOffset[axis] + delta[axis],
1534            startOverDiff,
1535            dir = delta[axis] > 0,
1536            newMainScrollPosition = _mainScrollPos.x + delta.x,
1537            mainScrollDiff = _mainScrollPos.x - _startMainScrollPos.x,
1538            newPanPos,
1539            newMainScrollPos;
1540
1541        // calculate fdistance over the bounds and friction
1542        if(newOffset > _currPanBounds.min[axis] || newOffset < _currPanBounds.max[axis]) {
1543            panFriction = _options.panEndFriction;
1544            // Linear increasing of friction, so at 1/4 of viewport it's at max value.
1545            // Looks not as nice as was expected. Left for history.
1546            // panFriction = (1 - (_panOffset[axis] + delta[axis] + panBounds.min[axis]) / (_viewportSize[axis] / 4) );
1547        } else {
1548            panFriction = 1;
1549        }
1550
1551        newOffset = _panOffset[axis] + delta[axis] * panFriction;
1552
1553        // move main scroll or start panning
1554        if(_options.allowPanToNext || _currZoomLevel === self.currItem.initialZoomLevel) {
1555
1556
1557            if(!_currZoomElementStyle) {
1558
1559                newMainScrollPos = newMainScrollPosition;
1560
1561            } else if(_direction === 'h' && axis === 'x' && !_zoomStarted ) {
1562
1563                if(dir) {
1564                    if(newOffset > _currPanBounds.min[axis]) {
1565                        panFriction = _options.panEndFriction;
1566                        overDiff = _currPanBounds.min[axis] - newOffset;
1567                        startOverDiff = _currPanBounds.min[axis] - _startPanOffset[axis];
1568                    }
1569
1570                    // drag right
1571                    if( (startOverDiff <= 0 || mainScrollDiff < 0) && _getNumItems() > 1 ) {
1572                        newMainScrollPos = newMainScrollPosition;
1573                        if(mainScrollDiff < 0 && newMainScrollPosition > _startMainScrollPos.x) {
1574                            newMainScrollPos = _startMainScrollPos.x;
1575                        }
1576                    } else {
1577                        if(_currPanBounds.min.x !== _currPanBounds.max.x) {
1578                            newPanPos = newOffset;
1579                        }
1580
1581                    }
1582
1583                } else {
1584
1585                    if(newOffset < _currPanBounds.max[axis] ) {
1586                        panFriction =_options.panEndFriction;
1587                        overDiff = newOffset - _currPanBounds.max[axis];
1588                        startOverDiff = _startPanOffset[axis] - _currPanBounds.max[axis];
1589                    }
1590
1591                    if( (startOverDiff <= 0 || mainScrollDiff > 0) && _getNumItems() > 1 ) {
1592                        newMainScrollPos = newMainScrollPosition;
1593
1594                        if(mainScrollDiff > 0 && newMainScrollPosition < _startMainScrollPos.x) {
1595                            newMainScrollPos = _startMainScrollPos.x;
1596                        }
1597
1598                    } else {
1599                        if(_currPanBounds.min.x !== _currPanBounds.max.x) {
1600                            newPanPos = newOffset;
1601                        }
1602                    }
1603
1604                }
1605
1606
1607                //
1608            }
1609
1610            if(axis === 'x') {
1611
1612                if(newMainScrollPos !== undefined) {
1613                    _moveMainScroll(newMainScrollPos, true);
1614                    if(newMainScrollPos === _startMainScrollPos.x) {
1615                        _mainScrollShifted = false;
1616                    } else {
1617                        _mainScrollShifted = true;
1618                    }
1619                }
1620
1621                if(_currPanBounds.min.x !== _currPanBounds.max.x) {
1622                    if(newPanPos !== undefined) {
1623                        _panOffset.x = newPanPos;
1624                    } else if(!_mainScrollShifted) {
1625                        _panOffset.x += delta.x * panFriction;
1626                    }
1627                }
1628
1629                return newMainScrollPos !== undefined;
1630            }
1631
1632        }
1633
1634        if(!_mainScrollAnimating) {
1635
1636            if(!_mainScrollShifted) {
1637                if(_currZoomLevel > self.currItem.fitRatio) {
1638                    _panOffset[axis] += delta[axis] * panFriction;
1639
1640                }
1641            }
1642
1643
1644        }
1645
1646    },
1647
1648    // Pointerdown/touchstart/mousedown handler
1649    _onDragStart = function(e) {
1650
1651        // Allow dragging only via left mouse button.
1652        // As this handler is not added in IE8 - we ignore e.which
1653        //
1654        // http://www.quirksmode.org/js/events_properties.html
1655        // https://developer.mozilla.org/en-US/docs/Web/API/event.button
1656        if(e.type === 'mousedown' && e.button > 0  ) {
1657            return;
1658        }
1659
1660        if(_initialZoomRunning) {
1661            e.preventDefault();
1662            return;
1663        }
1664
1665        if(_oldAndroidTouchEndTimeout && e.type === 'mousedown') {
1666            return;
1667        }
1668
1669        if(_preventDefaultEventBehaviour(e, true)) {
1670            e.preventDefault();
1671        }
1672
1673
1674
1675        _shout('pointerDown');
1676
1677        if(_pointerEventEnabled) {
1678            var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
1679            if(pointerIndex < 0) {
1680                pointerIndex = _currPointers.length;
1681            }
1682            _currPointers[pointerIndex] = {x:e.pageX, y:e.pageY, id: e.pointerId};
1683        }
1684
1685
1686
1687        var startPointsList = _getTouchPoints(e),
1688            numPoints = startPointsList.length;
1689
1690        _currentPoints = null;
1691
1692        _stopAllAnimations();
1693
1694        // init drag
1695        if(!_isDragging || numPoints === 1) {
1696
1697
1698
1699            _isDragging = _isFirstMove = true;
1700            framework.bind(window, _upMoveEvents, self);
1701
1702            _isZoomingIn =
1703                _wasOverInitialZoom =
1704                _opacityChanged =
1705                _verticalDragInitiated =
1706                _mainScrollShifted =
1707                _moved =
1708                _isMultitouch =
1709                _zoomStarted = false;
1710
1711            _direction = null;
1712
1713            _shout('firstTouchStart', startPointsList);
1714
1715            _equalizePoints(_startPanOffset, _panOffset);
1716
1717            _currPanDist.x = _currPanDist.y = 0;
1718            _equalizePoints(_currPoint, startPointsList[0]);
1719            _equalizePoints(_startPoint, _currPoint);
1720
1721            //_equalizePoints(_startMainScrollPos, _mainScrollPos);
1722            _startMainScrollPos.x = _slideSize.x * _currPositionIndex;
1723
1724            _posPoints = [{
1725                x: _currPoint.x,
1726                y: _currPoint.y
1727            }];
1728
1729            _gestureCheckSpeedTime = _gestureStartTime = _getCurrentTime();
1730
1731            //_mainScrollAnimationEnd(true);
1732            _calculatePanBounds( _currZoomLevel, true );
1733
1734            // Start rendering
1735            _stopDragUpdateLoop();
1736            _dragUpdateLoop();
1737
1738        }
1739
1740        // init zoom
1741        if(!_isZooming && numPoints > 1 && !_mainScrollAnimating && !_mainScrollShifted) {
1742            _startZoomLevel = _currZoomLevel;
1743            _zoomStarted = false; // true if zoom changed at least once
1744
1745            _isZooming = _isMultitouch = true;
1746            _currPanDist.y = _currPanDist.x = 0;
1747
1748            _equalizePoints(_startPanOffset, _panOffset);
1749
1750            _equalizePoints(p, startPointsList[0]);
1751            _equalizePoints(p2, startPointsList[1]);
1752
1753            _findCenterOfPoints(p, p2, _currCenterPoint);
1754
1755            _midZoomPoint.x = Math.abs(_currCenterPoint.x) - _panOffset.x;
1756            _midZoomPoint.y = Math.abs(_currCenterPoint.y) - _panOffset.
1756y;
1757            _currPointsDistance = _startPointsDistance = _calculatePointsDistance(p, p2);
1758        }
1759
1760
1761    },
1762
1763    // Pointermove/touchmove/mousemove handler
1764    _onDragMove = function(e) {
1765
1766        e.preventDefault();
1767
1768        if(_pointerEventEnabled) {
1769            var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
1770            if(pointerIndex > -1) {
1771                var p = _currPointers[pointerIndex];
1772                p.x = e.pageX;
1773                p.y = e.pageY;
1774            }
1775        }
1776
1777        if(_isDragging) {
1778            var touchesList = _getTouchPoints(e);
1779            if(!_direction && !_moved && !_isZooming) {
1780
1781                if(_mainScrollPos.x !== _slideSize.x * _currPositionIndex) {
1782                    // if main scroll position is shifted – direction is always horizontal
1783                    _direction = 'h';
1784                } else {
1785                    var diff = Math.abs(touchesList[0].x - _currPoint.x) - Math.abs(touchesList[0].y - _currPoint.y);
1786                    // check the direction of movement
1787                    if(Math.abs(diff) >= DIRECTION_CHECK_OFFSET) {
1788                        _direction = diff > 0 ? 'h' : 'v';
1789                        _currentPoints = touchesList;
1790                    }
1791                }
1792
1793            } else {
1794                _currentPoints = touchesList;
1795            }
1796        }
1797    },
1798    //
1799    _renderMovement =  function() {
1800
1801        if(!_currentPoints) {
1802            return;
1803        }
1804
1805        var numPoints = _currentPoints.length;
1806
1807        if(numPoints === 0) {
1808            return;
1809        }
1810
1811        _equalizePoints(p, _currentPoints[0]);
1812
1813        delta.x = p.x - _currPoint.x;
1814        delta.y = p.y - _currPoint.y;
1815
1816        if(_isZooming && numPoints > 1) {
1817            // Handle behaviour for more than 1 point
1818
1819            _currPoint.x = p.x;
1820            _currPoint.y = p.y;
1821
1822            // check if one of two points changed
1823            if( !delta.x && !delta.y && _isEqualPoints(_currentPoints[1], p2) ) {
1824                return;
1825            }
1826
1827            _equalizePoints(p2, _currentPoints[1]);
1828
1829
1830            if(!_zoomStarted) {
1831                _zoomStarted = true;
1832                _shout('zoomGestureStarted');
1833            }
1834
1835            // Distance between two points
1836            var pointsDistance = _calculatePointsDistance(p,p2);
1837
1838            var zoomLevel = _calculateZoomLevel(pointsDistance);
1839
1840            // slightly over the of initial zoom level
1841            if(zoomLevel > self.currItem.initialZoomLevel + self.currItem.initialZoomLevel / 15) {
1842                _wasOverInitialZoom = true;
1843            }
1844
1845            // Apply the friction if zoom level is out of the bounds
1846            var zoomFriction = 1,
1847                minZoomLevel = _getMinZoomLevel(),
1848                maxZoomLevel = _getMaxZoomLevel();
1849
1850            if ( zoomLevel < minZoomLevel ) {
1851
1852                if(_options.pinchToClose && !_wasOverInitialZoom && _startZoomLevel <= self.currItem.initialZoomLevel) {
1853                    // fade out background if zooming out
1854                    var minusDiff = minZoomLevel - zoomLevel;
1855                    var percent = 1 - minusDiff / (minZoomLevel / 1.2);
1856
1857                    _applyBgOpacity(percent);
1858                    _shout('onPinchClose', percent);
1859                    _opacityChanged = true;
1860                } else {
1861                    zoomFriction = (minZoomLevel - zoomLevel) / minZoomLevel;
1862                    if(zoomFriction > 1) {
1863                        zoomFriction = 1;
1864                    }
1865                    zoomLevel = minZoomLevel - zoomFriction * (minZoomLevel / 3);
1866                }
1867
1868            } else if ( zoomLevel > maxZoomLevel ) {
1869                // 1.5 - extra zoom level above the max. E.g. if max is x6, real max 6 + 1.5 = 7.5
1870                zoomFriction = (zoomLevel - maxZoomLevel) / ( minZoomLevel * 6 );
1871                if(zoomFriction > 1) {
1872                    zoomFriction = 1;
1873                }
1874                zoomLevel = maxZoomLevel + zoomFriction * minZoomLevel;
1875            }
1876
1877            if(zoomFriction < 0) {
1878                zoomFriction = 0;
1879            }
1880
1881            // distance between touch points after friction is applied
1882            _currPointsDistance = pointsDistance;
1883
1884            // _centerPoint - The point in the middle of two pointers
1885            _findCenterOfPoints(p, p2, _centerPoint);
1886
1887            // paning with two pointers pressed
1888            _currPanDist.x += _centerPoint.x - _currCenterPoint.x;
1889            _currPanDist.y += _centerPoint.y - _currCenterPoint.y;
1890            _equalizePoints(_currCenterPoint, _centerPoint);
1891
1892            _panOffset.x = _calculatePanOffset('x', zoomLevel);
1893            _panOffset.y = _calculatePanOffset('y', zoomLevel);
1894
1895            _isZoomingIn = zoomLevel > _currZoomLevel;
1896            _currZoomLevel = zoomLevel;
1897            _applyCurrentZoomPan();
1898
1899        } else {
1900
1901            // handle behaviour for one point (dragging or panning)
1902
1903            if(!_direction) {
1904                return;
1905            }
1906
1907            if(_isFirstMove) {
1908                _isFirstMove = false;
1909
1910                // subtract drag distance that was used during the detection direction
1911
1912                if( Math.abs(delta.x) >= DIRECTION_CHECK_OFFSET) {
1913                    delta.x -= _currentPoints[0].x - _startPoint.x;
1914                }
1915
1916                if( Math.abs(delta.y) >= DIRECTION_CHECK_OFFSET) {
1917                    delta.y -= _currentPoints[0].y - _startPoint.y;
1918                }
1919            }
1920
1921            _currPoint.x = p.x;
1922            _currPoint.y = p.y;
1923
1924            // do nothing if pointers position hasn't changed
1925            if(delta.x === 0 && delta.y === 0) {
1926                return;
1927            }
1928
1929            if(_direction === 'v' && _options.closeOnVerticalDrag) {
1930                if(!_canPan()) {
1931                    _currPanDist.y += delta.y;
1932                    _panOffset.y += delta.y;
1933
1934                    var opacityRatio = _calculateVerticalDragOpacityRatio();
1935
1936                    _verticalDragInitiated = true;
1937                    _shout('onVerticalDrag', opacityRatio);
1938
1939                    _applyBgOpacity(opacityRatio);
1940                    _applyCurrentZoomPan();
1941                    return ;
1942                }
1943            }
1944
1945            _pushPosPoint(_getCurrentTime(), p.x, p.y);
1946
1947            _moved = true;
1948            _currPanBounds = self.currItem.bounds;
1949
1950            var mainScrollChanged = _panOrMoveMainScroll('x', delta);
1951            if(!mainScrollChanged) {
1952                _panOrMoveMainScroll('y', delta);
1953
1954                _roundPoint(_panOffset);
1955                _applyCurrentZoomPan();
1956            }
1957
1958        }
1959
1960    },
1961
1962    // Pointerup/pointercancel/touchend/touchcancel/mouseup event handler
1963    _onDragRelease = function(e) {
1964
1965        if(_features.isOldAndroid ) {
1966
1967            if(_oldAndroidTouchEndTimeout && e.type === 'mouseup') {
1968                return;
1969            }
1970
1971            // on Android (v4.1, 4.2, 4.3 & possibly older)
1972            // ghost mousedown/up event isn't preventable via e.preventDefault,
1973            // which causes fake mousedown event
1974            // so we block mousedown/up for 600ms
1975            if( e.type.indexOf('touch') > -1 ) {
1976                clearTimeout(_oldAndroidTouchEndTimeout);
1977                _oldAndroidTouchEndTimeout = setTimeout(function() {
1978                    _oldAndroidTouchEndTimeout = 0;
1979                }, 600);
1980            }
1981
1982        }
1983
1984        _shout('pointerUp');
1985
1986        if(_preventDefaultEventBehaviour(e, false)) {
1987            e.preventDefault();
1988        }
1989
1990        var releasePoint;
1991
1992        if(_pointerEventEnabled) {
1993            var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
1994
1995            if(pointerIndex > -1) {
1996                releasePoint = _currPointers.splice(pointerIndex, 1)[0];
1997
1998                if(navigator.pointerEnabled) {
1999                    releasePoint.type = e.pointerType || 'mouse';
2000                } else {
2001                    var MSPOINTER_TYPES = {
2002                        4: 'mouse', // event.MSPOINTER_TYPE_MOUSE
2003                        2: 'touch', // event.MSPOINTER_TYPE_TOUCH
2004                        3: 'pen' // event.MSPOINTER_TYPE_PEN
2005                    };
2006                    releasePoint.type = MSPOINTER_TYPES[e.pointerType];
2007
2008                    if(!releasePoint.type) {
2009                        releasePoint.type = e.pointerType || 'mouse';
2010                    }
2011                }
2012
2013            }
2014        }
2015
2016        var touchList = _getTouchPoints(e),
2017            gestureType,
2018            numPoints = touchList.length;
2019
2020        if(e.type === 'mouseup') {
2021            numPoints = 0;
2022        }
2023
2024        // Do nothing if there were 3 touch points or more
2025        if(numPoints === 2) {
2026            _currentPoints = null;
2027            return true;
2028        }
2029
2030        // if second pointer released
2031        if(numPoints === 1) {
2032            _equalizePoints(_startPoint, touchList[0]);
2033        }
2034
2035
2036        // pointer hasn't moved, send "tap release" point
2037        if(numPoints === 0 && !_direction && !_mainScrollAnimating) {
2038            if(!releasePoint) {
2039                if(e.type === 'mouseup') {
2040                    releasePoint = {x: e.pageX, y: e.pageY, type:'mou
2040se'};
2041                } else if(e.changedTouches && e.changedTouches[0]) {
2042                    releasePoint = {x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY, type:'touch'};
2043                }
2044            }
2045
2046            _shout('touchRelease', e, releasePoint);
2047        }
2048
2049        // Difference in time between releasing of two last touch points (zoom gesture)
2050        var releaseTimeDiff = -1;
2051
2052        // Gesture completed, no pointers left
2053        if(numPoints === 0) {
2054            _isDragging = false;
2055            framework.unbind(window, _upMoveEvents, self);
2056
2057            _stopDragUpdateLoop();
2058
2059            if(_isZooming) {
2060                // Two points released at the same time
2061                releaseTimeDiff = 0;
2062            } else if(_lastReleaseTime !== -1) {
2063                releaseTimeDiff = _getCurrentTime() - _lastReleaseTime;
2064            }
2065        }
2066        _lastReleaseTime = numPoints === 1 ? _getCurrentTime() : -1;
2067
2068        if(releaseTimeDiff !== -1 && releaseTimeDiff < 150) {
2069            gestureType = 'zoom';
2070        } else {
2071            gestureType = 'swipe';
2072        }
2073
2074        if(_isZooming && numPoints < 2) {
2075            _isZooming = false;
2076
2077            // Only second point released
2078            if(numPoints === 1) {
2079                gestureType = 'zoomPointerUp';
2080            }
2081            _shout('zoomGestureEnded');
2082        }
2083
2084        _currentPoints = null;
2085        if(!_moved && !_zoomStarted && !_mainScrollAnimating && !_verticalDragInitiated) {
2086            // nothing to animate
2087            return;
2088        }
2089
2090        _stopAllAnimations();
2091
2092
2093        if(!_releaseAnimData) {
2094            _releaseAnimData = _initDragReleaseAnimationData();
2095        }
2096
2097        _releaseAnimData.calculateSwipeSpeed('x');
2098
2099
2100        if(_verticalDragInitiated) {
2101
2102            var opacityRatio = _calculateVerticalDragOpacityRatio();
2103
2104            if(opacityRatio < _options.verticalDragRange) {
2105                self.close();
2106            } else {
2107                var initalPanY = _panOffset.y,
2108                    initialBgOpacity = _bgOpacity;
2109
2110                _animateProp('verticalDrag', 0, 1, 300, framework.easing.cubic.out, function(now) {
2111
2112                    _panOffset.y = (self.currItem.initialPosition.y - initalPanY) * now + initalPanY;
2113
2114                    _applyBgOpacity(  (1 - initialBgOpacity) * now + initialBgOpacity );
2115                    _applyCurrentZoomPan();
2116                });
2117
2118                _shout('onVerticalDrag', 1);
2119            }
2120
2121            return;
2122        }
2123
2124
2125        // main scroll
2126        if(  (_mainScrollShifted || _mainScrollAnimating) && numPoints === 0) {
2127            var itemChanged = _finishSwipeMainScrollGesture(gestureType, _releaseAnimData);
2128            if(itemChanged) {
2129                return;
2130            }
2131            gestureType = 'zoomPointerUp';
2132        }
2133
2134        // prevent zoom/pan animation when main scroll animation runs
2135        if(_mainScrollAnimating) {
2136            return;
2137        }
2138
2139        // Complete simple zoom gesture (reset zoom level if it's out of the bounds)
2140        if(gestureType !== 'swipe') {
2141            _completeZoomGesture();
2142            return;
2143        }
2144
2145        // Complete pan gesture if main scroll is not shifted, and it's possible to pan current image
2146        if(!_mainScrollShifted && _currZoomLevel > self.currItem.fitRatio) {
2147            _completePanGesture(_releaseAnimData);
2148        }
2149    },
2150
2151
2152    // Returns object with data about gesture
2153    // It's created only once and then reused
2154    _initDragReleaseAnimationData  = function() {
2155        // temp local vars
2156        var lastFlickDuration,
2157            tempReleasePos;
2158
2159        // s = this
2160        var s = {
2161            lastFlickOffset: {},
2162            lastFlickDist: {},
2163            lastFlickSpeed: {},
2164            slowDownRatio:  {},
2165            slowDownRatioReverse:  {},
2166            speedDecelerationRatio:  {},
2167            speedDecelerationRatioAbs:  {},
2168            distanceOffset:  {},
2169            backAnimDestination: {},
2170            backAnimStarted: {},
2171            calculateSwipeSpeed: function(axis) {
2172
2173
2174                if( _posPoints.length > 1) {
2175                    lastFlickDuration = _getCurrentTime() - _gestureCheckSpeedTime + 50;
2176                    tempReleasePos = _posPoints[_posPoints.length-2][axis];
2177                } else {
2178                    lastFlickDuration = _getCurrentTime() - _gestureStartTime; // total gesture duration
2179                    tempReleasePos = _startPoint[axis];
2180                }
2181                s.lastFlickOffset[axis] = _currPoint[axis] - tempReleasePos;
2182                s.lastFlickDist[axis] = Math.abs(s.lastFlickOffset[axis]);
2183                if(s.lastFlickDist[axis] > 20) {
2184                    s.lastFlickSpeed[axis] = s.lastFlickOffset[axis] / lastFlickDuration;
2185                } else {
2186                    s.lastFlickSpeed[axis] = 0;
2187                }
2188                if( Math.abs(s.lastFlickSpeed[axis]) < 0.1 ) {
2189                    s.lastFlickSpeed[axis] = 0;
2190                }
2191
2192                s.slowDownRatio[axis] = 0.95;
2193                s.slowDownRatioReverse[axis] = 1 - s.slowDownRatio[axis];
2194                s.speedDecelerationRatio[axis] = 1;
2195            },
2196
2197            calculateOverBoundsAnimOffset: function(axis, speed) {
2198                if(!s.backAnimStarted[axis]) {
2199
2200                    if(_panOffset[axis] > _currPanBounds.min[axis]) {
2201                        s.backAnimDestination[axis] = _currPanBounds.min[axis];
2202
2203                    } else if(_panOffset[axis] < _currPanBounds.max[axis]) {
2204                        s.backAnimDestination[axis] = _currPanBounds.max[axis];
2205                    }
2206
2207                    if(s.backAnimDestination[axis] !== undefined) {
2208                        s.slowDownRatio[axis] = 0.7;
2209                        s.slowDownRatioReverse[axis] = 1 - s.slowDownRatio[axis];
2210                        if(s.speedDecelerationRatioAbs[axis] < 0.05) {
2211
2212                            s.lastFlickSpeed[axis] = 0;
2213                            s.backAnimStarted[axis] = true;
2214
2215                            _animateProp('bounceZoomPan'+axis,_panOffset[axis],
2216                                s.backAnimDestination[axis],
2217                                speed || 300,
2218                                framework.easing.sine.out,
2219                                function(pos) {
2220                                    _panOffset[axis] = pos;
2221                                    _applyCurrentZoomPan();
2222                                }
2223                            );
2224
2225                        }
2226                    }
2227                }
2228            },
2229
2230            // Reduces the speed by slowDownRatio (per 10ms)
2231            calculateAnimOffset: function(axis) {
2232                if(!s.backAnimStarted[axis]) {
2233                    s.speedDecelerationRatio[axis] = s.speedDecelerationRatio[axis] * (s.slowDownRatio[axis] +
2234                                                s.slowDownRatioReverse[axis] -
2235                                                s.slowDownRatioReverse[axis] * s.timeDiff / 10);
2236
2237                    s.speedDecelerationRatioAbs[axis] = Math.abs(s.lastFlickSpeed[axis] * s.speedDecelerationRatio[axis]);
2238                    s.distanceOffset[axis] = s.lastFlickSpeed[axis] * s.speedDecelerationRatio[axis] * s.timeDiff;
2239                    _panOffset[axis] += s.distanceOffset[axis];
2240
2241                }
2242            },
2243
2244            panAnimLoop: function() {
2245                if ( _animations.zoomPan ) {
2246                    _animations.zoomPan.raf = _requestAF(s.panAnimLoop);
2247
2248                    s.now = _getCurrentTime();
2249                    s.timeDiff = s.now - s.lastNow;
2250                    s.lastNow = s.now;
2251
2252                    s.calculateAnimOffset('x');
2253                    s.calculateAnimOffset('y');
2254
2255                    _applyCurrentZoomPan();
2256
2257                    s.calculateOverBoundsAnimOffset('x');
2258                    s.calculateOverBoundsAnimOffset('y');
2259
2260
2261                    if (s.speedDecelerationRatioAbs.x < 0.05 && s.speedDecelerationRatioAbs.y < 0.05) {
2262
2263                        // round pan position
2264                        _panOffset.x = Math.round(_panOffset.x);
2265                        _panOffset.y = Math.round(_panOffset.y);
2266                        _applyCurrentZoomPan();
2267
2268                        _stopAnimation('zoomPan');
2269                        return;
2270                    }
2271                }
2272
2273            }
2274        };
2275        return s;
2276    },
2277
2278    _completePanGesture = function(animData) {
2279        // calculate swipe speed for Y axis (paanning)
2280        animData.calculateSwipeSpeed('y');
2281
2282        _currPanBounds = self.currItem.bounds;
2283
2284        animData.backAnimDestination = {};
2285        animData.backAnimStarted = {};
2286
2287        // Avoid acceleration animation if speed is too low
2288        if(Math.abs(animData.lastFlickSpeed.x) <= 0.05 && Math.abs(animData.lastFlickSpeed.y) <= 0.05 ) {
2289            animData.speedDecelerationRatioAbs.x = animData.speedDecelerationRatioAbs.y = 0;
2290
2291            // Run pan drag release animation. E.g. if you drag image and release finger without momentum.
2292            animData.calculateOverBoundsAnimOffset('x');
2293            animData.calculateOverBoundsAnimOffset('y');
2294            return true;
2295        }
2296
2297        // Animation loop that controls the acceleration after pan gesture ends
2298        _registerStartAnimation('zoomPan');
2299        animData.lastNow = _getCurrentTime();
2300        animData.panAnimLoop();
2301    },
2302
2303
2304    _finishSwipeMainScrollGesture = function(gestureType, _releaseAnimData) {
2305        var itemChanged;
2306        if(!_mainScrollAnimating) {
2307            _currZoomedItemIndex = _currentItemIndex;
2308        }
2309
2310
2311
2312        var itemsDiff;
2313
2314        if(gestureType === 'swipe') {
2315            var totalShiftDist = _currPoint.x - _startPoint.x,
2316                isFastLastFlick = _releaseAnimData.lastFlickDist.x < 10;
2317
2318            // if container is shifted for more than MIN_SWIPE_DISTANCE,
2319            // and last flick gesture was in right direction
2320            if(totalShiftDist > MIN_SWIPE_DISTANCE &&
2321                (isFastLastFlick || _releaseAnimData.lastFlickOffset.x > 20) ) {
2322                // go to prev item
2323                itemsDiff = -1;
2324            } else if(totalShiftDist < -MIN_SWIPE_DISTANCE &&
2325                (isFastLastFlick || _releaseAnimData.lastFlickOffset.x < -20) ) {
2326                // go to next item
2327                itemsDiff = 1;
2328            }
2329        }
2330
2331        var nextCircle;
2332
2333        if(itemsDiff) {
2334
2335            _currentItemIndex += itemsDiff;
2336
2337            if(_currentItemIndex < 0) {
2338                _currentItemIndex = _options.loop ? _getNumItems()-1 : 0;
2339                nextCircle = true;
2340            } else if(_currentItemIndex >= _getNumItems()) {
2341                _currentItemIndex = _options.loop ? 0 : _getNumItems()-1;
2342                nextCircle = true;
2343            }
2344
2345            if(!nextCircle || _options.loop) {
2346                _indexDiff += itemsDiff;
2347                _currPositionIndex -= itemsDiff;
2348                itemChanged = true;
2349            }
2350
2351
2352
2353        }
2354
2355        var animateToX = _slideSize.x * _currPositionIndex;
2356        var animateToDist = Math.abs( animateToX - _mainScrollPos.x );
2357        var finishAnimDuration;
2358
2359
2360        if(!itemChanged && animateToX > _mainScrollPos.x !== _releaseAnimData.lastFlickSpeed.x > 0) {
2361            // "return to current" duration, e.g. when dragging from slide 0 to -1
2362            finishAnimDuration = 333;
2363        } else {
2364            finishAnimDuration = Math.abs(_releaseAnimData.lastFlickSpeed.x) > 0 ?
2365                                    animateToDist / Math.abs(_releaseAnimData.lastFlickSpeed.x) :
2366                                    333;
2367
2368            finishAnimDuration = Math.min(finishAnimDuration, 400);
2369            finishAnimDuration = Math.max(finishAnimDuration, 250);
2370        }
2371
2372        if(_currZoomedItemIndex === _currentItemIndex) {
2373            itemChanged = false;
2374        }
2375
2376        _mainScrollAnimating = true;
2377
2378        _shout('mainScrollAnimStart');
2379
2380        _animateProp('mainScroll', _mainScrollPos.x, animateToX, finishAnimDuration, framework.easing.cubic.out,
2381            _moveMainScroll,
2382            function() {
2383                _stopAllAnimations();
2384                _mainScrollAnimating = false;
2385                _currZoomedItemIndex = -1;
2386
2387                if(itemChanged || _currZoomedItemIndex !== _currentItemIndex) {
2388                    self.updateCurrItem();
2389                }
2390
2391                _shout('mainScrollAnimComplete');
2392            }
2393        );
2394
2395        if(itemChanged) {
2396            self.updateCurrItem(true);
2397        }
2398
2399        return itemChanged;
2400    },
2401
2402    _calculateZoomLevel = function(touchesDistance) {
2403        return  1 / _startPointsDistance * touchesDistance * _startZoomLevel;
2404    },
2405
2406    // Resets zoom if it's out of bounds
2407    _completeZoomGesture = function() {
2408        var destZoomLevel = _currZoomLevel,
2409            minZoomLevel = _getMinZoomLevel(),
2410            maxZoomLevel = _getMaxZoomLevel();
2411
2412        if ( _currZoomLevel < minZoomLevel ) {
2413            destZoomLevel = minZoomLevel;
2414        } else if ( _currZoomLevel > maxZoomLevel ) {
2415            destZoomLevel = maxZoomLevel;
2416        }
2417
2418        var destOpacity = 1,
2419            onUpdate,
2420            initialOpacity = _bgOpacity;
2421
2422        if(_opacityChanged && !_isZoomingIn && !_wasOverInitialZoom && _currZoomLevel < minZoomLevel) {
2423            //_closedByScroll = true;
2424            self.close();
2425            return true;
2426        }
2427
2428        if(_opacityChanged) {
2429            onUpdate = function(now) {
2430                _applyBgOpacity(  (destOpacity - initialOpacity) * now + initialOpacity );
2431            };
2432        }
2433
2434        self.zoomTo(destZoomLevel, 0, 200,  framework.easing.cubic.out, onUpdate);
2435        return true;
2436    };
2437
2438
2439_registerModule('Gestures', {
2440    publicMethods: {
2441
2442        initGestures: function() {
2443
2444            // helper function that builds touch/pointer/mouse events
2445            var addEventNames = function(pref, down, move, up, cancel) {
2446                _dragStartEvent = pref + down;
2447                _dragMoveEvent = pref + move;
2448                _dragEndEvent = pref + up;
2449                if(cancel) {
2450                    _dragCancelEvent = pref + cancel;
2451                } else {
2452                    _dragCancelEvent = '';
2453                }
2454            };
2455
2456            _pointerEventEnabled = _features.pointerEvent;
2457            if(_pointerEventEnabled && _features.touch) {
2458                // we don't need touch events, if browser supports pointer events
2459                _features.touch = false;
2460            }
2461
2462            if(_pointerEventEnabled) {
2463                if(navigator.pointerEnabled) {
2464                    addEventNames('pointer', 'down', 'move', 'up', 'cancel');
2465                } else {
2466                    // IE10 pointer events are case-sensitive
2467                    addEventNames('MSPointer', 'Down', 'Move', 'Up', 'Cancel');
2468                }
2469            } else if(_features.touch) {
2470                addEventNames('touch', 'start', 'move', 'end', 'cancel');
2471                _likelyTouchDevice = true;
2472            } else {
2473                addEventNames('mouse', 'down', 'move', 'up');
2474            }
2475
2476            _upMoveEvents = _dragMoveEvent + ' ' + _dragEndEvent  + ' ' +  _dragCancelEvent;
2477            _downEvents = _dragStartEvent;
2478
2479            if(_pointerEventEnabled && !_likelyTouchDevice) {
2480                _likelyTouchDevice = (navigator.maxTouchPoints > 1) || (navigator.msMaxTouchPoints > 1);
2481            }
2482            // make variable public
2483            self.likelyTouchDevice = _likelyTouchDevice;
2484
2485            _globalEventHandlers[_dragStartEvent] = _onDragStart;
2486            _globalEventHandlers[_dragMoveEvent] = _onDragMove;
2487            _globalEventHandlers[_dragEndEvent] = _onDragRelease; // the Kraken
2488
2489            if(_dragCancelEvent) {
2490                _globalEventHandlers[_dragCancelEvent] = _globalEventHandlers[_dragEndEvent];
2491            }
2492
2493            // Bind mouse events on device with detected hardware touch support, in case it supports multiple types of input.
2494            if(_features.touch) {
2495                _downEvents += ' mousedown';
2496                _upMoveEvents += ' mousemove mouseup';
2497                _globalEventHandlers.mousedown = _globalEventHandlers[_dragStartEvent];
2498                _globalEventHandlers.mousemove = _globalEventHandlers[_dragMoveEvent];
2499                _globalEventHandlers.mouseup = _globalEventHandlers[_dragEndEvent];
2500            }
2501
2502            if(!_likelyTouchDevice) {
2503                // don't allow pan to next slide from zoomed state on Desktop
2504                _options.allowPanToNext = false;
2505            }
2506        }
2507
2508    }
2509});
2510
2511
2512/*>>gestures*/
2513
2514/*>>show-hide-transition*/
2515/**
2516 * show-hide-transition.js:
2517 *
2518 * Manages initial opening or closing transition.
2519 *
2520 * If you're not planning to use transition for gallery at all,
2521 * you may set options hideAnimationDuration and showAnimationDuration to 0,
2522 * and just delete startAnimation function.
2523 * 
2524 */
2525
2526
2527var _showOrHideTimeout,
2528    _showOrHide = function(item, img, out, completeFn) {
2529
2530        if(_showOrHideTimeout) {
2531            clearTimeout(_showOrHideTimeout);
2532        }
2533
2534        _initialZoomRunning = true;
2535        _initialContentSet = true;
2536
2537        // dimensions of small thumbnail {x:,y:,w:}.
2538        // Height is optional, as calculated based on large image.
2539        var thumbBounds;
2540        if(item.initialLayout) {
2541            thumbBounds = item.initialLayout;
2542            item.initialLayout = null;
2543        } else {
2544            thumbBounds = _options.getThumbBoundsFn && _options.getThumbBoundsFn(_currentItemIndex);
2545        }
2546
2547        var duration = out ? _options.hideAnimationDuration : _options.showAnimationDuration;
2548
2549        var onComplete = function() {
2550            _stopAnimation('initialZoom');
2551            if(!out) {
2552                _applyBgOpacity(1);
2553                if(img) {
2554                    img.style.display = 'block';
2555                }
2556                framework.addClass(template, 'pswp--animated-in');
2557                _shout('initialZoom' + (out ? 'OutEnd' : 'InEnd'));
2558            } else {
2559                self.template.removeAttribute('style');
2560                self.bg.removeAttribute('style');
2561            }
2562
2563            if(completeFn) {
2564                completeFn();
2565            }
2566            _initialZoomRunning = false;
2567        };
2568
2569        // if bounds aren't provided, just open gallery without animation
2570        if(!duration || !thumbBounds || thumbBounds.x === undefined) {
2571
2572            _shout('initialZoom' + (out ? 'Out' : 'In') );
2573
2574            if(!out) {
2575                _currZoomLevel = item.initialZoomLevel;
2576                _equalizePoints(_panOffset,  item.initialPosition );
2577                _applyCurrentZoomPan();
2578
2579                template.style.opacity = 1;
2580                _applyBgOpacity(1);
2581            } else {
2582                template.style.opacity = 0;
2583            }
2584
2585            if(duration) {
2586                setTimeout(function() {
2587                    onComplete();
2588                }, duration);
2589            } else {
2590                onComplete();
2591            }
2592
2593            return;
2594        }
2595
2596        var startAnimation = function() {
2597            var closeWithRaf = _closedByScroll,
2598                fadeEverything = !self.currItem.src || self.currItem.loadError || _options.showHideOpacity;
2599
2600            // apply hw-acceleration to image
2601            if(item.miniImg) {
2602                item.miniImg.style.webkitBackfaceVisibility = 'hidden';
2603            }
2604
2605            if(!out) {
2606                _currZoomLevel = thumbBounds.w / item.w;
2607                _panOffset.x = thumbBounds.x;
2608                _panOffset.y = thumbBounds.y - _initalWindowScrollY;
2609
2610                self[fadeEverything ? 'template' : 'bg'].style.opacity = 0.001;
2611                _applyCurrentZoomPan();
2612            }
2613
2614            _registerStartAnimation('initialZoom');
2615
2616            if(out && !closeWithRaf) {
2617                framework.removeClass(template, 'pswp--animated-in');
2618            }
2619
2620            if(fadeEverything) {
2621                if(out) {
2622                    framework[ (closeWithRaf ? 'remove' : 'add') + 'Class' ](template, 'pswp--animate_opacity');
2623                } else {
2624                    setTimeout(function() {
2625                        framework.addClass(template, 'pswp--animate_opacity');
2626                    }, 30);
2627                }
2628            }
2629
2630            _showOrHideTimeout = setTimeout(function() {
2631
2632                _shout('initialZoom' + (out ? 'Out' : 'In') );
2633
2634
2635                if(!out) {
2636
2637                    // "in" animation always uses CSS transitions (instead of rAF).
2638                    // CSS transition work faster here,
2639                    // as developer may also want to animate other things,
2640                    // like ui on top of sliding area, which can be animated just via CSS
2641
2642                    _currZoomLevel = item.initialZoomLevel;
2643                    _equalizePoints(_panOffset,  item.initialPosition );
2644                    _applyCurrentZoomPan();
2645                    _applyBgOpacity(1);
2646
2647                    if(fadeEverything) {
2648                        template.style.opacity = 1;
2649                    } else {
2650                        _applyBgOpacity(1);
2651                    }
2652
2653                    _showOrHideTimeout = setTimeout(onComplete, duration + 20);
2654                } else {
2655
2656                    // "out" animation uses rAF only when PhotoSwipe is closed by browser scroll, to recalculate position
2657                    var destZoomLevel = thumbBounds.w / item.w,
2658                        initialPanOffset = {
2659                            x: _panOffset.x,
2660                            y: _panOffset.y
2661                        },
2662                        initialZoomLevel = _currZoomLevel,
2663                        initalBgOpacity = _bgOpacity,
2664                        onUpdate = function(now) {
2665
2666                            if(now === 1) {
2667                                _currZoomLevel = destZoomLevel;
2668                                _panOffset.x = thumbBounds.x;
2669                                _panOffset.y = thumbBounds.y  - _currentWindowScrollY;
2670                            } else {
2671                                _currZoomLevel = (destZoomLevel - initialZoomLevel) * now + initialZoomLevel;
2672                                _panOffset.x = (thumbBounds.x - initialPanOffset.x) * now + initialPanOffset.x;
2673                                _panOffset.y = (thumbBounds.y - _currentWindowScrollY - initialPanOffset.y) * now + initialPanOffset.y;
2674                            }
2675
2676                            _applyCurrentZoomPan();
2677                            if(fadeEverything) {
2678                                template.style.opacity = 1 - now;
2679                            } else {
2680                                _applyBgOpacity( initalBgOpacity - now * initalBgOpacity );
2681                            }
2682                        };
2683
2684                    if(closeWithRaf) {
2685                        _animateProp('initialZoom', 0, 1, duration, framework.easing.cubic.out, onUpdate, onComplete);
2686                    } else {
2687                        onUpdate(1);
2688                        _showOrHideTimeout = setTimeout(onComplete, duration + 20);
2689                    }
2690                }
2691
2692            }, out ? 25 : 90); // Main purpose of this delay is to give browser time to paint and
2693                    // create composite layers of PhotoSwipe UI parts (background, controls, caption, arrows).
2694                    // Which avoids lag at the beginning of scale transition.
2695        };
2696        startAnimation();
2697
2698
2699    };
2700
2701/*>>show-hide-transition*/
2702
2703/*>>items-controller*/
2704/**
2705*
2706* Controller manages gallery items, their dimensions, and their content.
2707* 
2708*/
2709
2710var _items,
2711    _tempPanAreaSize = {},
2712    _imagesToAppendPool = [],
2713    _initialContentSet,
2714    _initialZoomRunning,
2715    _controllerDefaultOptions = {
2716        index: 0,
2717        errorMsg: '<div class="pswp__error-msg"><a href="%url%" target="_blank">The image</a> could not be loaded.</div>',
2718        forceProgressiveLoading: false, // TODO
2719        preload: [1,1],
2720        getNumItemsFn: function() {
2721            return _items.length;
2722        }
2723    };
2724
2725
2726var _getItemAt,
2727    _getNumItems,
2728    _initialIsLoop,
2729    _getZeroBounds = function() {
2730        return {
2731            center:{x:0,y:0},
2732            max:{x:0,y:0},
2733            min:{x:0,y:0}
2734        };
2735    },
2736    _calculateSingleItemPanBounds = function(item, realPanElementW, realPanElementH ) {
2737        var bounds = item.bounds;
2738
2739        // position of element when it's centered
2740        bounds.center.x = Math.round((_tempPanAreaSize.x - realPanElementW) / 2);
2741        bounds.center.y = Math.round((_tempPanAreaSize.y - realPanElementH) / 2) + item.vGap.top;
2742
2743        // maximum pan position
2744        bounds.max.x = (realPanElementW > _tempPanAreaSize.x) ?
2745                            Math.round(_tempPanAreaSize.x - realPanElementW) :
2746                            bounds.center.x;
2747
2748        bounds.max.y = (realPanElementH > _tempPanAreaSize.y) ?
2749                            Math.round(_tempPanAreaSize.y - realPanElementH) + item.vGap.top :
2750                            bounds.center.y;
2751
2752        // minimum pan position
2753        bounds.min.x = (realPanElementW > _tempPanAreaSize.x) ? 0 : bounds.center.x;
2754        bounds.min.y = (realPanElementH > _tempPanAreaSize.y) ? item.vGap.top : bounds.center.y;
2755    },
2756    _calculateItemSize = function(item, viewportSize, zoomLevel) {
2757
2758        if (item.src && !item.loadError) {
2759            var isInitial = !zoomLevel;
2760
2761            if(isInitial) {
2762                if(!item.vGap) {
2763                    item.vGap = {top:0,bottom:0};
2764                }
2765                // allows overriding vertical margin for individual items
2766                _shout('parseVerticalMargin', item);
2767            }
2768
2769
2770            _tempPanAreaSize.x = viewportSize.x;
2771            _tempPanAreaSize.y = viewportSize.y - item.vGap.top - item.vGap.bottom;
2772
2773            if (isInitial) {
2774                var hRatio = _tempPanAreaSize.x / item.w;
2775                var vRatio = _tempPanAreaSize.y / item.h;
2776
2777                item.fitRatio = hRatio < vRatio ? hRatio : vRatio;
2778                //item.fillRatio = hRatio > vRatio ? hRatio : vRatio;
2779
2780                var scaleMode = _options.scaleMode;
2781
2782                if (scaleMode === 'orig') {
2783                    zoomLevel = 1;
2784                } else if (scaleMode === 'fit') {
2785                    zoomLevel = item.fitRatio;
2786                }
2787
2788                if (zoomLevel > 1) {
2789                    zoomLevel = 1;
2790                }
2791
2792                item.initialZoomLevel = zoomLevel;
2793
2794                if(!item.bounds) {
2795                    // reuse bounds object
2796                    item.bounds = _getZeroBounds();
2797                }
2798            }
2799
2800            if(!zoomLevel) {
2801                return;
2802            }
2803
2804            _calculateSingleItemPanBounds(item, item.w * zoomLevel, item.h * zoomLevel);
2805
2806            if (isInitial && zoomLevel === item.initialZoomLevel) {
2807                item.initialPosition = item.bounds.center;
2808            }
2809
2810            return item.bounds;
2811        } else {
2812            item.w = item.h = 0;
2813            item.initialZoomLevel = item.fitRatio = 1;
2814            item.bounds = _getZeroBounds();
2815            item.initialPosition = item.bounds.center;
2816
2817            // if it's not image, we return zero bounds (content is not zoomable)
2818            return item.bounds;
2819        }
2820
2821    },
2822
2823
2824
2825
2826    _appendImage = function(index, item, baseDiv, img, preventAnimation, keepPlaceholder) {
2827
2828
2829        if(item.loadError) {
2830            return;
2831        }
2832
2833        if(img) {
2834
2835            item.imageAppended = true;
2836            _setImageSize(item, img, (item === self.currItem && _renderMaxResolution) );
2837
2838            baseDiv.appendChild(img);
2839
2840            if(keepPlaceholder) {
2841                setTimeout(function() {
2842                    if(item && item.loaded && item.placeholder) {
2843                        item.placeholder.style.display = 'none';
2844                        item.placeholder = null;
2845                    }
2846                }, 500);
2847            }
2848        }
2849    },
2850
2851
2852
2853    _preloadImage = function(item) {
2854        item.loading = true;
2855        item.loaded = false;
2856        var img = item.img = framework.createEl('pswp__img', 'img');
2857
2858        var onComplete = function() {
2859            item.loading = false;
2860            item.loaded = true;
2861
2862            if(item.loadComplete) {
2863                item.loadComplete(item);
2864            } else {
2865                item.img = null; // no need to store image object
2866            }
2867            img.onload = img.onerror = null;
2868            img = null;
2869        };
2870        img.onload = onComplete;
2871        img.onerror = function() {
2872            item.loadError = true;
2873            onComplete();
2874        };
2875
2876        img.src = item.src;// + '?a=' + Math.random();
2877
2878        return img;
2879    },
2880    _checkForError = function(item, cleanUp) {
2881        if(item.src && item.loadError && item.container) {
2882
2883            if(cleanUp) {
2884                item.container.innerHTML = '';
2885            }
2886
2887            item.container.innerHTML = _options.errorMsg.replace('%url%',  item.src );
2888            return true;
2889
2890        }
2891    },
2892    _setImageSize = function(item, img, maxRes) {
2893        if(!item.src) {
2894            return;
2895        }
2896
2897        if(!img) {
2898            img = item.container.lastChild;
2899        }
2900
2901        var w = maxRes ? item.w : Math.round(item.w * item.fitRatio),
2902            h = maxRes ? item.h : Math.round(item.h * item.fitRatio);
2903
2904        if(item.placeholder && !item.loaded) {
2905            item.placeholder.style.width = w + 'px';
2906            item.placeholder.style.height = h + 'px';
2907        }
2908
2909        img.style.width = w + 'px';
2910        img.style.height = h + 'px';
2911    },
2912    _appendImagesPool = function() {
2913
2914        if(_imagesToAppendPool.length) {
2915            var poolItem;
2916
2917            for(var i = 0; i < _imagesToAppendPool.length; i++) {
2918                poolItem = _imagesToAppendPool[i];
2919                if( poolItem.holder.index === poolItem.index ) {
2920                    _appendImage(poolItem.index, poolItem.item, poolItem.baseDiv, poolItem.img, false, poolItem.clearPlaceholder);
2921                }
2922            }
2923            _imagesToAppendPool = [];
2924        }
2925    };
2926
2927
2928
2929_registerModule('Controller', {
2930
2931    publicMethods: {
2932
2933        lazyLoadItem: function(index) {
2934            index = _getLoopedId(index);
2935            var item = _getItemAt(index);
2936
2937            if(!item || ((item.loaded || item.loading) && !_itemsNeedUpdate)) {
2938                return;
2939            }
2940
2941            _shout('gettingData', index, item);
2942
2943            if (!item.src) {
2944                return;
2945            }
2946
2947            _preloadImage(item);
2948        },
2949        initController: function() {
2950            framework.extend(_options, _controllerDefaultOptions, true);
2951            self.items = _items = items;
2952            _getItemAt = self.getItemAt;
2953            _getNumItems = _options.getNumItemsFn; //self.getNumItems;
2954
2955
2956
2957            _initialIsLoop = _options.loop;
2958            if(_getNumItems() < 2) {
2959                _options.loop = false; // disable loop if less then 2 items
2960            } else if(_getNumItems() == 2) {
2961                // this is a workaround to avoid a very rare bug:
2962                // when there are only two images and you open the second image first,
2963                // it can happen that the first image won't show at all
2964                self.lazyLoadItem(0);
2965                self.lazyLoadItem(1);
2966            }
2967
2968            _listen('beforeChange', function(diff) {
2969
2970                var p = _options.preload,
2971                    isNext = diff === null ? true : (diff >= 0),
2972                    preloadBefore = Math.min(p[0], _getNumItems() ),
2973                    preloadAfter = Math.min(p[1], _getNumItems() ),
2974                    i;
2975
2976
2977                for(i = 1; i <= (isNext ? preloadAfter : preloadBefore); i++) {
2978                    self.lazyLoadItem(_currentItemIndex+i);
2979                }
2980                for(i = 1; i <= (isNext ? preloadBefore : preloadAfter); i++) {
2981                    self.lazyLoadItem(_currentItemIndex-i);
2982                }
2983            });
2984
2985            _listen('initialLayout', function() {
2986                self.currItem.initialLayout = _options.getThumbBoundsFn && _options.getThumbBoundsFn(_currentItemIndex);
2987            });
2988
2989            _listen('mainScrollAnimComplete', _appendImagesPool);
2990            _listen('initialZoomInEnd', _appendImagesPool);
2991
2992
2993
2994            _listen('destroy', function() {
2995                var item;
2996                for(var i = 0; i < _items.length; i++) {
2997                    item = _items[i];
2998                    // remove reference to DOM elements, for GC
2999                    if(item.container) {
3000                        item.container = null;
3001                    }
3002                    if(item.placeholder) {
3003                        item.placeholder = null;
3004                    }
3005                    if(item.img) {
3006                        item.img = null;
3007                    }
3008                    if(item.preloader) {
3009                        item.preloader = null;
3010                    }
3011                    if(item.loadError) {
3012                        item.loaded = item.loadError = false;
3013                    }
3014                }
3015                _imagesToAppendPool = null;
3016            });
3017        },
3018
3019
3020        getItemAt: function(index) {
3021            if (index >= 0) {
3022                return _items[index] !== undefined ? _items[index] : false;
3023            }
3024            return false;
3025        },
3026
3027        allowProgressiveImg: function() {
3028            // 1. Progressive image loading isn't working on webkit/blink
3029            //    when hw-acceleration (e.g. translateZ) is applied to IMG element.
3030            //    That's why in PhotoSwipe parent element gets zoom transform, not image itself.
3031            //
3032            // 2. Progressive image loading sometimes blinks in webkit/blink when applying animation to parent element.
3033            //    That's why it's disabled on touch devices (mainly because of swipe transition)
3034            //
3035            // 3. Progressive image loading sometimes doesn't work in IE (up to 11).
3036
3037            // Don't allow progressive loading on non-large touch devices
3038            return _options.forceProgressiveLoading || !_likelyTouchDevice || _options.mouseUsed || screen.width > 1200;
3039            // 1200 - to eliminate touch devices with large screen (like Chromebook Pixel)
3040        },
3041
3042        setContent: function(holder, index) {
3043
3044            if(_options.loop) {
3045                index = _getLoopedId(index);
3046            }
3047
3048            var prevItem = self.getItemAt(holder.index);
3049            if(prevItem) {
3050                prevItem.container = null;
3051            }
3052
3053            var item = self.getItemAt(index),
3054                img;
3055
3056            if(!item) {
3057                holder.el.innerHTML = '';
3058                return;
3059            }
3060
3061            // allow to override data
3062            _shout('gettingData', index, item);
3063
3064            holder.index = index;
3065            holder.item = item;
3066
3067            // base container DIV is created only once for each of 3 holders
3068            var baseDiv = item.container = framework.createEl('pswp__zoom-wrap');
3069
3070
3071
3072            if(!item.src && item.html) {
3073                if(item.html.tagName) {
3074                    baseDiv.appendChild(item.html);
3075                } else {
3076                    baseDiv.innerHTML = item.html;
3077                }
3078            }
3079
3080            _checkForError(item);
3081
3082            _calculateItemSize(item, _viewportSize);
3083
3084            if(item.src && !item.loadError && !item.loaded) {
3085
3086                item.loadComplete = function(item) {
3087
3088                    // gallery closed before image finished loading
3089                    if(!_isOpen) {
3090                        return;
3091                    }
3092
3093                    // check if holder hasn't changed while image was loading
3094                    if(holder && holder.index === index ) {
3095                        if( _checkForError(item, true) ) {
3096                            item.loadComplete = item.img = null;
3097                            _calculateItemSize(item, _viewportSize);
3098                            _applyZoomPanToItem(item);
3099
3100                            if(holder.index === _currentItemIndex) {
3101                                // recalculate dimensions
3102                                self.updateCurrZoomItem();
3103                            }
3104                            return;
3105                        }
3106                        if( !item.imageAppended ) {
3107                            if(_features.transform && (_mainScrollAnimating || _initialZoomRunning) ) {
3108                                _imagesToAppendPool.push({
3109                                    item:item,
3110                                    baseDiv:baseDiv,
3111                                    img:item.img,
3112                                    index:index,
3113                                    holder:holder,
3114                                    clearPlaceholder:true
3115                                });
3116                            } else {
3117                                _appendImage(index, item, baseDiv, item.img, _mainScrollAnimating || _initialZoomRunning, true);
3118                            }
3119                        } else {
3120                            // remove preloader & mini-img
3121                            if(!_initialZoomRunning && item.placeholder) {
3122                                item.placeholder.style.display = 'none';
3123                                item.placeholder = null;
3124                            }
3125                        }
3126                    }
3127
3128                    item.loadComplete = null;
3129                    item.img = null; // no need to store image element after it's added
3130
3131                    _shout('imageLoadComplete', index, item);
3132                };
3133
3134                if(framework.features.transform) {
3135
3136                    var placeholderClassName = 'pswp__img pswp__img--placeholder';
3137                    placeholderClassName += (item.msrc ? '' : ' pswp__img--placeholder--blank');
3138
3139                    var placeholder = framework.createEl(placeholderClassName, item.msrc ? 'img' : '');
3140                    if(item.msrc) {
3141                        placeholder.src = item.msrc;
3142                    }
3143
3144                    _setImageSize(item, placeholder);
3145
3146                    baseDiv.appendChild(placeholder);
3147                    item.placeholder = placeholder;
3148
3149                }
3150
3151
3152
3153
3154                if(!item.loading) {
3155                    _preloadImage(item);
3156                }
3157
3158
3159                if( self.allowProgressiveImg() ) {
3160                    // just append image
3161                    if(!_initialContentSet && _features.transform) {
3162                        _imagesToAppendPool.push({
3163                            item:item,
3164                            baseDiv:baseDiv,
3165                            img:item.img,
3166                            index:index,
3167                            holder:holder
3168                        });
3169                    } else {
3170                        _appendImage(index, item, baseDiv, item.img, true, true);
3171                    }
3172                }
3173
3174            } else if(item.src && !item.loadError) {
3175                // image object is created every time, due to bugs of image loading & delay when switching images
3176                img = framework.createEl('pswp__img', 'img');
3177                img.style.opacity = 1;
3178                img.src = item.src;
3179                _setImageSize(item, img);
3180                _appendImage(index, item, baseDiv, img, true);
3181            }
3182
3183
3184            if(!_initialContentSet && index === _currentItemIndex) {
3185                _currZoomElementStyle = baseDiv.style;
3186                _showOrHide(item, (img ||item.img) );
3187            } else {
3188                _applyZoomPanToItem(item);
3189            }
3190
3191            holder.el.innerHTML = '';
3192            holder.el.appendChild(baseDiv);
3193        },
3194
3195        cleanSlide: function( item ) {
3196            if(item.img ) {
3197                item.img.onload = item.img.onerror = null;
3198            }
3199            item.loaded = item.loading = item.img = item.imageAppended = false;
3200        }
3201
3202    }
3203});
3204
3205/*>>items-controller*/
3206
3207/*>>tap*/
3208/**
3209 * tap.js:
3210 *
3211 * Displatches tap and double-tap events.
3212 * 
3213 */
3214
3215var tapTimer,
3216    tapReleasePoint = {},
3217    _dispatchTapEvent = function(origEvent, releasePoint, pointerType) {
3218        var e = document.createEvent( 'CustomEvent' ),
3219            eDetail = {
3220                origEvent:origEvent,
3221                target:origEvent.target,
3222                releasePoint: releasePoint,
3223                pointerType:pointerType || 'touch'
3224            };
3225
3226        e.initCustomEvent( 'pswpTap', true, true, eDetail );
3227        origEvent.target.dispatchEvent(e);
3228    };
3229
3230_registerModule('Tap', {
3231    publicMethods: {
3232        initTap: function() {
3233            _listen('firstTouchStart', self.onTapStart);
3234            _listen('touchRelease', self.onTapRelease);
3235            _listen('destroy', function() {
3236                tapReleasePoint = {};
3237                tapTimer = null;
3238            });
3239        },
3240        onTapStart: function(touchList) {
3241            if(touchList.length > 1) {
3242                clearTimeout(tapTimer);
3243                tapTimer = null;
3244            }
3245        },
3246        onTapRelease: function(e, releasePoint) {
3247            if(!releasePoint) {
3248                return;
3249            }
3250
3251            if(!_moved && !_isMultitouch && !_numAnimations) {
3252                var p0 = releasePoint;
3253                if(tapTimer) {
3254                    clearTimeout(tapTimer);
3255                    tapTimer = null;
3256
3257                    // Check if taped on the same place
3258                    if ( _isNearbyPoints(p0, tapReleasePoint) ) {
3259                        _shout('doubleTap', p0);
3260                        return;
3261                    }
3262                }
3263
3264                if(releasePoint.type === 'mouse') {
3265                    _dispatchTapEvent(e, releasePoint, 'mouse');
3266                    return;
3267                }
3268
3269                var clickedTagName = e.target.tagName.toUpperCase();
3270                // avoid double tap delay on buttons and elements that have class pswp__single-tap
3271                if(clickedTagName === 'BUTTON' || framework.hasClass(e.target, 'pswp__single-tap') ) {
3272                    _dispatchTapEvent(e, releasePoint);
3273                    return;
3274                }
3275
3276                _equalizePoints(tapReleasePoint, p0);
3277
3278                tapTimer = setTimeout(function() {
3279                    _dispatchTapEvent(e, releasePoint);
3280                    tapTimer = null;
3281                }, 300);
3282            }
3283        }
3284    }
3285});
3286
3287/*>>tap*/
3288
3289/*>>desktop-zoom*/
3290/**
3291 *
3292 * desktop-zoom.js:
3293 *
3294 * - Binds mousewheel event for paning zoomed image.
3295 * - Manages "dragging", "zoomed-in", "zoom-out" classes.
3296 *   (which are used for cursors and zoom icon)
3297 * - Adds toggleDesktopZoom function.
3298 * 
3299 */
3300
3301var _wheelDelta;
3302
3303_registerModule('DesktopZoom', {
3304
3305    publicMethods: {
3306
3307        initDesktopZoom: function() {
3308
3309            if(_oldIE) {
3310                // no zoom for old IE (<=8)
3311                return;
3312            }
3313
3314            if(_likelyTouchDevice) {
3315                // if detected hardware touch support, we wait until mouse is used,
3316                // and only then apply desktop-zoom features
3317                _listen('mouseUsed', function() {
3318                    self.setupDesktopZoom();
3319                });
3320            } else {
3321                self.setupDesktopZoom(true);
3322            }
3323
3324        },
3325
3326        setupDesktopZoom: function(onInit) {
3327
3328            _wheelDelta = {};
3329
3330            var events = 'wheel mousewheel DOMMouseScroll';
3331
3332            _listen('bindEvents', function() {
3333                framework.bind(template, events,  self.handleMouseWheel);
3334            });
3335
3336            _listen('unbindEvents', function() {
3337                if(_wheelDelta) {
3338                    framework.unbind(template, events, self.handleMouseWheel);
3339                }
3340            });
3341
3342            self.mouseZoomedIn = false;
3343
3344            var hasDraggingClass,
3345                updateZoomable = function() {
3346                    if(self.mouseZoomedIn) {
3347                        framework.removeClass(template, 'pswp--zoomed-in');
3348                        self.mouseZoomedIn = false;
3349                    }
3350                    if(_currZoomLevel < 1) {
3351                        framework.addClass(template, 'pswp--zoom-allowed');
3352                    } else {
3353                        framework.removeClass(template, 'pswp--zoom-allowed');
3354                    }
3355                    removeDraggingClass();
3356                },
3357                removeDraggingClass = function() {
3358                    if(hasDraggingClass) {
3359                        framework.removeClass(template, 'pswp--dragging');
3360                        hasDraggingClass = false;
3361                    }
3362                };
3363
3364            _listen('resize' , updateZoomable);
3365            _listen('afterChange' , updateZoomable);
3366            _listen('pointerDown', function() {
3367                if(self.mouseZoomedIn) {
3368                    hasDraggingClass = true;
3369                    framework.addClass(template, 'pswp--dragging');
3370                }
3371            });
3372            _listen('pointerUp', removeDraggingClass);
3373
3374            if(!onInit) {
3375                updateZoomable();
3376            }
3377
3378        },
3379
3380        handleMouseWheel: function(e) {
3381            // https://developer.mozilla.org/en-US/docs/Web/Events/wheel
3382            _wheelDelta.x = 0;
3383            _wheelDelta.y = 0;
3384
3385            if('deltaX' in e) {
3386                if(e.deltaMode === 1 /* DOM_DELTA_LINE */) {
3387                    // 18 - average line height
3388                    _wheelDelta.x = e.deltaX * 18;
3389                    _wheelDelta.y = e.deltaY * 18;
3390                } else {
3391                    _wheelDelta.x = e.deltaX;
3392                    _wheelDelta.y = e.deltaY;
3393                }
3394            } else if('wheelDelta' in e) {
3395                if(e.wheelDeltaX) {
3396                    _wheelDelta.x = -0.16 * e.wheelDeltaX;
3397                }
3398                if(e.wheelDeltaY) {
3399                    _wheelDelta.y = -0.16 * e.wheelDeltaY;
3400                } else {
3401                    _wheelDelta.y = -0.16 * e.wheelDelta;
3402                }
3403            } else if('detail' in e) {
3404                _wheelDelta.y = e.detail;
3405            }
3406
3407            if(_currZoomLevel <= self.currItem.fitRatio || _options.zoomOnScroll || _options.switchOnScroll) {
3408                if( _options.modal ) {
3409                    if (_options.zoomOnScroll) {
3410                        // Zoom in or out using scroll wheel
3411                        var centerPoint = {x: _viewportSize.x / 2 + _offset.x, y: _viewportSize.y / 2 + _offset.y};
3412
3413                        if ((_wheelDelta.x === 0 && _wheelDelta.y > 0) || (_wheelDelta.x > 0 && _wheelDelta.y === 0)) {
3414                            self.zoomTo(self.currItem.initialZoomLevel, centerPoint, 333);
3415                            self.mouseZoomedIn = false;
3416                            framework['remove' + 'Class'](template, 'pswp--zoomed-in');
3417                        } else if ((_wheelDelta.x === 0 && _wheelDelta.y < 0) || (_wheelDelta.x < 0 && _wheelDelta.y === 0)) {
3418                            var doubleTapZoomLevel = _options.getDoubleTapZoom(true, self.currItem);
3419                            self.mouseZoomedIn = true;
3420                            self.zoomTo(doubleTapZoomLevel, centerPoint, 333);
3421                            framework['add' + 'Class'](template, 'pswp--zoomed-in');
3422                        }
3423                        e.preventDefault();
3424                    } else if (_options.switchOnScroll) {
3425                        if ((_wheelDelta.x === 0 && _wheelDelta.y > 0) || (_wheelDelta.x > 0 && _wheelDelta.y === 0)) {
3426                            self.nextAnim();
3427                        } else if ((_wheelDelta.x === 0 && _wheelDelta.y < 0) || (_wheelDelta.x < 0 && _wheelDelta.y === 0)) {
3428                            self.prevAnim();
3429                        }
3430                        e.preventDefault();
3431                    } else if (!_options.closeOnScroll || _numAnimations || _isDragging) {
3432                        e.preventDefault();
3433                    } else if(_transformKey && Math.abs(e.deltaY) > 2) {
3434                        // close PhotoSwipe
3435                        // if browser supports transforms & scroll changed enough
3436                        _closedByScroll = true;
3437                        self.close();
3438                    }
3439
3440                }
3441                return true;
3442            }
3443
3444            // allow just one event to fire
3445            e.stopPropagation();
3446
3447            // scroll according to wheel movement
3448            if(_wheelDelta.x != 0 || _wheelDelta.y != 0) {
3449                _calculatePanBounds(_currZoomLevel, true);
3450
3451                var newPanX = _panOffset.x - _wheelDelta.x,
3452                    newPanY = _panOffset.y - _wheelDelta.y;
3453
3454                // only prevent scrolling in nonmodal mode when not at edges
3455                if (_options.modal ||
3456                    (
3457                        newPanX <= _currPanBounds.min.x && newPanX >= _currPanBounds.max.x &&
3458                        newPanY <= _currPanBounds.min.y && newPanY >= _currPanBounds.max.y
3459                    )) {
3460                    e.preventDefault();
3461                }
3462
3463                // TODO: use rAF instead of mousewheel?
3464                self.panTo(newPanX, newPanY);
3465            }
3466        },
3467
3468        toggleDesktopZoom: function(centerPoint) {
3469            centerPoint = centerPoint || {x:_viewportSize.x/2 + _offset.x, y:_viewportSize.y/2 + _offset.y };
3470
3471            var doubleTapZoomLevel = _options.getDoubleTapZoom(true, self.currItem);
3472            var zoomOut = _currZoomLevel === doubleTapZoomLevel;
3473
3474            self.mouseZoomedIn = !zoomOut;
3475
3476            self.zoomTo(zoomOut ? self.currItem.initialZoomLevel : doubleTapZoomLevel, centerPoint, 333);
3477            framework[ (!zoomOut ? 'add' : 'remove') + 'Class'](template, 'pswp--zoomed-in');
3478        }
3479
3480    }
3481});
3482
3483
3484/*>>desktop-zoom*/
3485
3486/*>>history*/
3487/**
3488 *
3489 * history.js:
3490 *
3491 * - Back button to close gallery.
3492 * 
3493 * - Unique URL for each slide: example.com/&pid=1&gid=3
3494 *   (where PID is picture index, and GID and gallery index)
3495 *   
3496 * - Switch URL when slides change.
3497 * 
3498 */
3499
3500
3501var _historyDefaultOptions = {
3502    history: true,
3503    galleryUID: 1
3504};
3505
3506var _historyUpdateTimeout,
3507    _hashChangeTimeout,
3508    _hashAnimCheckTimeout,
3509    _hashChangedByScript,
3510    _hashChangedByHistory,
3511    _hashReseted,
3512    _initialHash,
3513    _historyChanged,
3514    _closedFromURL,
3515    _urlChangedOnce,
3516    _windowLoc,
3517
3518    _supportsPushState,
3519
3520    _getHash = function() {
3521        return _windowLoc.hash.substring(1);
3522    },
3523    _cleanHistoryTimeouts = function() {
3524
3525        if(_historyUpdateTimeout) {
3526            clearTimeout(_historyUpdateTimeout);
3527        }
3528
3529        if(_hashAnimCheckTimeout) {
3530            clearTimeout(_hashAnimCheckTimeout);
3531        }
3532    },
3533
3534    // pid - Picture index
3535    // gid - Gallery index
3536    _parseItemIndexFromURL = function() {
3537        var hash = _getHash(),
3538            params = {};
3539
3540        if(hash.length < 5) { // pid=1
3541            return params;
3542        }
3543
3544        var i, vars = hash.split('&');
3545        for (i = 0; i < vars.length; i++) {
3546            if(!vars[i]) {
3547                continue;
3548            }
3549            var pair = vars[i].split('=');
3550            if(pair.length < 2) {
3551                continue;
3552            }
3553            params[pair[0]] = pair[1];
3554        }
3555        if(_options.galleryPIDs) {
3556            // detect custom pid in hash and search for it among the items collection
3557            var searchfor = params.pid;
3558            params.pid = 0; // if custom pid cannot be found, fallback to the first item
3559            for(i = 0; i < _items.length; i++) {
3560                if(_items[i].pid === searchfor) {
3561                    params.pid = i;
3562                    break;
3563                }
3564            }
3565        } else {
3566            params.pid = parseInt(params.pid,10)-1;
3567        }
3568        if( params.pid < 0 ) {
3569            params.pid = 0;
3570        }
3571        return params;
3572    },
3573    _updateHash = function() {
3574
3575        if(_hashAnimCheckTimeout) {
3576            clearTimeout(_hashAnimCheckTimeout);
3577        }
3578
3579
3580        if(_numAnimations || _isDragging) {
3581            // changing browser URL forces layout/paint in some browsers, which causes noticable lag during animation
3582            // that's why we update hash only when no animations running
3583            _hashAnimCheckTimeout = setTimeout(_updateHash, 500);
3584            return;
3585        }
3586
3587        if(_hashChangedByScript) {
3588            clearTimeout(_hashChangeTimeout);
3589        } else {
3590            _hashChangedByScript = true;
3591        }
3592
3593
3594        var pid = (_currentItemIndex + 1);
3595        var item = _getItemAt( _currentItemIndex );
3596        if(item.hasOwnProperty('pid')) {
3597            // carry forward any custom pid assigned to the item
3598            pid = item.pid;
3599        }
3600        var newHash = _initialHash + '&'  +  'gid=' + _options.galleryUID + '&' + 'pid=' + pid;
3601
3602        if(!_historyChanged) {
3603            if(_windowLoc.hash.indexOf(newHash) === -1) {
3604                _urlChangedOnce = true;
3605            }
3606            // first time - add new hisory record, then just replace
3607        }
3608
3609        var newURL = _windowLoc.href.split('#')[0] + '#' +  newHash;
3610
3611        if( _supportsPushState ) {
3612
3613            if('#' + newHash !== window.location.hash) {
3614                history[_historyChanged ? 'replaceState' : 'pushState']('', document.title, newURL);
3615            }
3616
3617        } else {
3618            if(_historyChanged) {
3619                _windowLoc.replace( newURL );
3620            } else {
3621                _windowLoc.hash = newHash;
3622            }
3623        }
3624
3625
3626
3627        _historyChanged = true;
3628        _hashChangeTimeout = setTimeout(function() {
3629            _hashChangedByScript = false;
3630        }, 60);
3631    };
3632
3633
3634
3635
3636
3637_registerModule('History', {
3638
3639
3640
3641    publicMethods: {
3642        initHistory: function() {
3643
3644            framework.extend(_options, _historyDefaultOptions, true);
3645
3646            if( !_options.history ) {
3647                return;
3648            }
3649
3650
3651            _windowLoc = window.location;
3652            _urlChangedOnce = false;
3653            _closedFromURL = false;
3654            _historyChanged = false;
3655            _initialHash = _getHash();
3656            _supportsPushState = ('pushState' in history);
3657
3658
3659            if(_initialHash.indexOf('gid=') > -1) {
3660                _initialHash = _initialHash.split('&gid=')[0];
3661                _initialHash = _initialHash.split('?gid=')[0];
3662            }
3663
3664
3665            _listen('afterChange', self.updateURL);
3666            _listen('unbindEvents', function() {
3667                framework.unbind(window, 'hashchange', self.onHashChange);
3668            });
3669
3670
3671            var returnToOriginal = function() {
3672                _hashReseted = true;
3673                if(!_closedFromURL) {
3674
3675                    if(_urlChangedOnce) {
3676                        history.back();
3677                    } else {
3678
3679                        if(_initialHash) {
3680                            _windowLoc.hash = _initialHash;
3681                        } else {
3682                            if (_supportsPushState) {
3683
3684                                // remove hash from url without refreshing it or scrolling to top
3685                                history.pushState('', document.title,  _windowLoc.pathname + _windowLoc.search );
3686                            } else {
3687                                _windowLoc.hash = '';
3688                            }
3689                        }
3690                    }
3691
3692                }
3693
3694                _cleanHistoryTimeouts();
3695            };
3696
3697
3698            _listen('unbindEvents', function() {
3699                if(_closedByScroll) {
3700                    // if PhotoSwipe is closed by scroll, we go "back" before the closing animation starts
3701                    // this is done to keep the scroll position
3702                    returnToOriginal();
3703                }
3704            });
3705            _listen('destroy', function() {
3706                if(!_hashReseted) {
3707                    returnToOriginal();
3708                }
3709            });
3710            _listen('firstUpdate', function() {
3711                _currentItemIndex = _parseItemIndexFromURL().pid;
3712            });
3713
3714
3715
3716
3717            var index = _initialHash.indexOf('pid=');
3718            if(index > -1) {
3719                _initialHash = _initialHash.substring(0, index);
3720                if(_initialHash.slice(-1) === '&') {
3721                    _initialHash = _initialHash.slice(0, -1);
3722                }
3723            }
3724
3725
3726            setTimeout(function() {
3727                if(_isOpen) { // hasn't destroyed yet
3728                    framework.bind(window, 'hashchange', self.onHashChange);
3729                }
3730            }, 40);
3731
3732        },
3733        onHashChange: function() {
3734
3735            if(_getHash() === _initialHash) {
3736
3737                _closedFromURL = true;
3738                self.close();
3739                return;
3740            }
3741            if(!_hashChangedByScript) {
3742
3743                _hashChangedByHistory = true;
3744                self.goTo( _parseItemIndexFromURL().pid );
3745                _hashChangedByHistory = false;
3746            }
3747
3748        },
3749        updateURL: function() {
3750
3751            // Delay the update of URL, to avoid lag during transition,
3752            // and to not to trigger actions like "refresh page sound" or "blinking favicon" to often
3753
3754            _cleanHistoryTimeouts();
3755
3756
3757            if(_hashChangedByHistory) {
3758                return;
3759            }
3760
3761            if(!_historyChanged) {
3762                _updateHash(); // first time
3763            } else {
3764                _historyUpdateTimeout = setTimeout(_updateHash, 800);
3765            }
3766        }
3767
3768    }
3769});
3770
3771
3772/*>>history*/
3773
3774
3775    /**
3776     * Adds public methods prevAnim() and nextAnim() which animates
3777     * the slide transition when switching to the previous or next
3778     * slide respectively.
3779     *
3780     * Based on https://github.com/dimsemenov/PhotoSwipe/pull/1179
3781     * with some modifications to support disabling the animation
3782     * using an additional boolean option "desktopSlider".
3783     */
3784
3785    var slideAnim = function(dir) {
3786
3787        var itemsDiff = dir,
3788            itemChanged,
3789            nextCircle;
3790
3791        if(!_mainScrollAnimating) {
3792            _currZoomedItemIndex = _currentItemIndex;
3793        }
3794
3795        if(itemsDiff) {
3796
3797            _currentItemIndex += itemsDiff;
3798
3799            if(_currentItemIndex < 0) {
3800                _currentItemIndex = _options.loop ? _getNumItems()-1 : 0;
3801                nextCircle = true;
3802            } else if(_currentItemIndex >= _getNumItems()) {
3803                _currentItemIndex = _options.loop ? 0 : _getNumItems()-1;
3804                nextCircle = true;
3805            }
3806
3807            if(!nextCircle || _options.loop) {
3808                _indexDiff += itemsDiff;
3809                _currPositionIndex -= itemsDiff;
3810                itemChanged = true;
3811            }
3812        }
3813
3814        var animateToX = _slideSize.x * _currPositionIndex;
3815        var animateToDist = Math.abs( animateToX - _mainScrollPos.x );
3816        var finishAnimDuration = (animateToDist > 4000) ? 400 : 300;
3817
3818        if(_currZoomedItemIndex === _currentItemIndex) {
3819            itemChanged = false;
3820        }
3821
3822        _mainScrollAnimating = true;
3823
3824        _shout('mainScrollAnimStart');
3825
3826        _animateProp('mainScroll', _mainScrollPos.x, animateToX, finishAnimDuration, framework.easing.cubic.out,
3827            _moveMainScroll,
3828            function() {
3829                _stopAllAnimations();
3830                _mainScrollAnimating = false;
3831                _currZoomedItemIndex = -1;
3832
3833                if(itemChanged || _currZoomedItemIndex !== _currentItemIndex) {
3834                    self.updateCurrItem();
3835                }
3836
3837                _shout('mainScrollAnimComplete');
3838            }
3839        );
3840
3841        if(itemChanged) {
3842            self.updateCurrItem(true);
3843        }
3844
3845        return itemChanged;
3846    };
3847
3848    _registerModule('AnimateSlide', {
3849        publicMethods: {
3850            initAnimateSlide: function() {},
3851
3852            nextAnim: function(){
3853                if (_options.desktopSlider) {
3854                    slideAnim(1);
3855                } else {
3856                    self.next();
3857                }
3858            },
3859
3860            prevAnim: function(){
3861                if (_options.desktopSlider) {
3862                    slideAnim(-1);
3863                } else {
3864                    self.prev();
3865                }
3866            }
3867        }
3868    });
3869
3870    framework.extend(self, publicMethods); };
3871    return PhotoSwipe;
3872});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.