PageSourceSearch

https://primerv.com/cdn/shop/t/34/assets/photoswipe.min.js?v=96706414436924913981790810796

js primerv.com collected 2026-10-09 23:06:02 UTC 95,062 bytes, 3,418 lines download raw bytes

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

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.