PageSourceSearch

https://marklandwood.com/CMSWebParts/Viewers/Effects/PhotoSwipe/dist/photoswipe.js?639245304075617766

js marklandwood.com collected 2026-09-26 07:10:21 UTC 140,856 bytes, 3,877 lines download raw bytes

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

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.