PageSourceSearch

https://gogasprojectdesign.gr/templates/cargo/vendor/photoswipe/photoswipe.js

js gogasprojectdesign.gr collected 2026-09-26 16:34:39 UTC 94,042 bytes, 3,734 lines download raw bytes

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

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.