PageSourceSearch

https://spbrzozow.pl/photos/pics/js/photoswipe.js

js spbrzozow.pl collected 2026-09-28 16:27:05 UTC 99,314 bytes, 3,882 lines download raw bytes

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

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.