PageSourceSearch

https://www.fish180.com.tw/style/js/product/photoswipe.js

js fish180.com.tw collected 2026-10-08 22:58:22 UTC 103,143 bytes, 3,735 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
1433      // don't search elements above pswp__scroll-wrap
1434      if(el.getAttribute('class') && el.getAttribute('class').indexOf('pswp__scroll-wrap') > -1 ) {
1435        return false;
1436      }
1437    
1438      if( fn(el) ) {
1439        return el;
1440      }
1441
1442      return _closestElement(el.parentNode, fn);
1443  },
1444
1445  _preventObj = {},
1446  _preventDefaultEventBehaviour = function(e, isDown) {
1447      _preventObj.prevent = !_closestElement(e.target, _options.isClickableElement);
1448
1449    _shout('preventDragEvent', e, isDown, _preventObj);
1450    return _preventObj.prevent;
1451
1452  },
1453  _convertTouchToPoint = function(touch, p) {
1454    p.x = touch.pageX;
1455    p.y = touch.pageY;
1456    p.id = touch.identifier;
1457    return p;
1458  },
1459  _findCenterOfPoints = function(p1, p2, pCenter) {
1460    pCenter.x = (p1.x + p2.x) * 0.5;
1461    pCenter.y = (p1.y + p2.y) * 0.5;
1462  },
1463  _pushPosPoint = function(time, x, y) {
1464    if(time - _gestureCheckSpeedTime > 50) {
1465      var o = _posPoints.length > 2 ? _posPoints.shift() : {};
1466      o.x = x;
1467      o.y = y; 
1468      _posPoints.push(o);
1469      _gestureCheckSpeedTime = time;
1470    }
1471  },
1472
1473  _calculateVerticalDragOpacityRatio = function() {
1474    var yOffset = _panOffset.y - self.currItem.initialPosition.y; // difference between initial and current position
1475    return 1 -  Math.abs( yOffset / (_viewportSize.y / 2)  );
1476  },
1477
1478  
1479  // points pool, reused during touch events
1480  _ePoint1 = {},
1481  _ePoint2 = {},
1482  _tempPointsArr = [],
1483  _tempCounter,
1484  _getTouchPoints = function(e) {
1485    // clean up previous points, without recreating array
1486    while(_tempPointsArr.length > 0) {
1487      _tempPointsArr.pop();
1488    }
1489
1490    if(!_pointerEventEnabled) {
1491      if(e.type.indexOf('touch') > -1) {
1492
1493        if(e.touches && e.touches.length > 0) {
1494          _tempPointsArr[0] = _convertTouchToPoint(e.touches[0], _ePoint1);
1495          if(e.touches.length > 1) {
1496            _tempPointsArr[1] = _convertTouchToPoint(e.touches[1], _ePoint2);
1497          }
1498        }
1499        
1500      } else {
1501        _ePoint1.x = e.pageX;
1502        _ePoint1.y = e.pageY;
1503        _ePoint1.id = '';
1504        _tempPointsArr[0] = _ePoint1;//_ePoint1;
1505      }
1506    } else {
1507      _tempCounter = 0;
1508      // we can use forEach, as pointer events are supported only in modern browsers
1509      _currPointers.forEach(function(p) {
1510        if(_tempCounter === 0) {
1511          _tempPointsArr[0] = p;
1512        } else if(_tempCounter === 1) {
1513          _tempPointsArr[1] = p;
1514        }
1515        _tempCounter++;
1516
1517      });
1518    }
1519    return _tempPointsArr;
1520  },
1521
1522  _panOrMoveMainScroll = function(axis, delta) {
1523
1524    var panFriction,
1525      overDiff = 0,
1526      newOffset = _panOffset[axis] + delta[axis],
1527      startOverDiff,
1528      dir = delta[axis] > 0,
1529      newMainScrollPosition = _mainScrollPos.x + delta.x,
1530      mainScrollDiff = _mainScrollPos.x - _startMainScrollPos.x,
1531      newPanPos,
1532      newMainScrollPos;
1533
1534    // calculate fdistance over the bounds and friction
1535    if(newOffset > _currPanBounds.min[axis] || newOffset < _currPanBounds.max[axis]) {
1536      panFriction = _options.panEndFriction;
1537      // Linear increasing of friction, so at 1/4 of viewport it's at max value. 
1538      // Looks not as nice as was expected. Left for history.
1539      // panFriction = (1 - (_panOffset[axis] + delta[axis] + panBounds.min[axis]) / (_viewportSize[axis] / 4) );
1540    } else {
1541      panFriction = 1;
1542    }
1543    
1544    newOffset = _panOffset[axis] + delta[axis] * panFriction;
1545
1546    // move main scroll or start panning
1547    if(_options.allowPanToNext || _currZoomLevel === self.currItem.initialZoomLevel) {
1548
1549
1550      if(!_currZoomElementStyle) {
1551        
1552        newMainScrollPos = newMainScrollPosition;
1553
1554      } else if(_direction === 'h' && axis === 'x' && !_zoomStarted ) {
1555        
1556        if(dir) {
1557          if(newOffset > _currPanBounds.min[axis]) {
1558            panFriction = _options.panEndFriction;
1559            overDiff = _currPanBounds.min[axis] - newOffset;
1560            startOverDiff = _currPanBounds.min[axis] - _startPanOffset[axis];
1561          }
1562          
1563          // drag right
1564          if( (startOverDiff <= 0 || mainScrollDiff < 0) && _getNumItems() > 1 ) {
1565            newMainScrollPos = newMainScrollPosition;
1566            if(mainScrollDiff < 0 && newMainScrollPosition > _startMainScrollPos.x) {
1567              newMainScrollPos = _startMainScrollPos.x;
1568            }
1569          } else {
1570            if(_currPanBounds.min.x !== _currPanBounds.max.x) {
1571              newPanPos = newOffset;
1572            }
1573            
1574          }
1575
1576        } else {
1577
1578          if(newOffset < _currPanBounds.max[axis] ) {
1579            panFriction =_options.panEndFriction;
1580            overDiff = newOffset - _currPanBounds.max[axis];
1581            startOverDiff = _startPanOffset[axis] - _currPanBounds.max[axis];
1582          }
1583
1584          if( (startOverDiff <= 0 || mainScrollDiff > 0) && _getNumItems() > 1 ) {
1585            newMainScrollPos = newMainScrollPosition;
1586
1587            if(mainScrollDiff > 0 && newMainScrollPosition < _startMainScrollPos.x) {
1588              newMainScrollPos = _startMainScrollPos.x;
1589            }
1590
1591          } else {
1592            if(_currPanBounds.min.x !== _currPanBounds.max.x) {
1593              newPanPos = newOffset;
1594            }
1595          }
1596
1597        }
1598
1599
1600        //
1601      }
1602
1603      if(axis === 'x') {
1604
1605        if(newMainScrollPos !== undefined) {
1606          _moveMainScroll(newMainScrollPos, true);
1607          if(newMainScrollPos === _startMainScrollPos.x) {
1608            _mainScrollShifted = false;
1609          } else {
1610            _mainScrollShifted = true;
1611          }
1612        }
1613
1614        if(_currPanBounds.min.x !== _currPanBounds.max.x) {
1615          if(newPanPos !== undefined) {
1616            _panOffset.x = newPanPos;
1617          } else if(!_mainScrollShifted) {
1618            _panOffset.x += delta.x * panFriction;
1619          }
1620        }
1621
1622        return newMainScrollPos !== undefined;
1623      }
1624
1625    }
1626
1627    if(!_mainScrollAnimating) {
1628      
1629      if(!_mainScrollShifted) {
1630        if(_currZoomLevel > self.currItem.fitRatio) {
1631          _panOffset[axis] += delta[axis] * panFriction;
1632        
1633        }
1634      }
1635
1636      
1637    }
1638    
1639  },
1640
1641  // Pointerdown/touchstart/mousedown handler
1642  _onDragStart = function(e) {
1643
1644    // Allow dragging only via left mouse button.
1645    // As this handler is not added in IE8 - we ignore e.which
1646    // 
1647    // http://www.quirksmode.org/js/events_properties.html
1648    // https://developer.mozilla.org/en-US/docs/Web/API/event.button
1649    if(e.type === 'mousedown' && e.button > 0  ) {
1650      return;
1651    }
1652
1653    if(_initialZoomRunning) {
1654      e.preventDefault();
1655      return;
1656    }
1657
1658    if(_oldAndroidTouchEndTimeout && e.type === 'mousedown') {
1659      return;
1660    }
1661
1662    if(_preventDefaultEventBehaviour(e, true)) {
1663      e.preventDefault();
1664    }
1665
1666
1667
1668    _shout('pointerDown');
1669
1670    if(_pointerEventEnabled) {
1671      var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
1672      if(pointerIndex < 0) {
1673        pointerIndex = _currPointers.length;
1674      }
1675      _currPointers[pointerIndex] = {x:e.pageX, y:e.pageY, id: e.pointerId};
1676    }
1677    
1678
1679
1680    var startPointsList = _getTouchPoints(e),
1681      numPoints = startPointsList.length;
1682
1683    _currentPoints = null;
1684
1685    _stopAllAnimations();
1686
1687    // init drag
1688    if(!_isDragging || numPoints === 1) {
1689
1690      
1691
1692      _isDragging = _isFirstMove = true;
1693      framework.bind(window, _upMoveEvents, self);
1694
1695      _isZoomingIn = 
1696        _wasOverInitialZoom = 
1697        _opacityChanged = 
1698        _verticalDragInitiated = 
1699        _mainScrollShifted = 
1700        _moved = 
1701        _isMultitouch = 
1702        _zoomStarted = false;
1703
1704      _direction = null;
1705
1706      _shout('firstTouchStart', startPointsList);
1707
1708      _equalizePoints(_startPanOffset, _panOffset);
1709
1710      _currPanDist.x = _currPanDist.y = 0;
1711      _equalizePoints(_currPoint, startPointsList[0]);
1712      _equalizePoints(_startPoint, _currPoint);
1713
1714      //_equalizePoints(_startMainScrollPos, _mainScrollPos);
1715      _startMainScrollPos.x = _slideSize.x * _currPositionIndex;
1716
1717      _posPoints = [{
1718        x: _currPoint.x,
1719        y: _currPoint.y
1720      }];
1721
1722      _gestureCheckSpeedTime = _gestureStartTime = _getCurrentTime();
1723
1724      //_mainScrollAnimationEnd(true);
1725      _calculatePanBounds( _currZoomLevel, true );
1726      
1727      // Start rendering
1728      _stopDragUpdateLoop();
1729      _dragUpdateLoop();
1730      
1731    }
1732
1733    // init zoom
1734    if(!_isZooming && numPoints > 1 && !_mainScrollAnimating && !_mainScrollShifted) {
1735      _startZoomLevel = _currZoomLevel;
1736      _zoomStarted = false; // true if zoom changed at least once
1737
1738      _isZooming = _isMultitouch = true;
1739      _currPanDist.y = _currPanDist.x = 0;
1740
1741      _equalizePoints(_startPanOffset, _panOffset);
1742
1743      _equalizePoints(p, startPointsList[0]);
1744      _equalizePoints(p2, startPointsList[1]);
1745
1746      _findCenterOfPoints(p, p2, _currCenterPoint);
1747
1748      _midZoomPoint.x = Math.abs(_currCenterPoint.x) - _panOffset.x;
1749      _midZoomPoint.y = Math.abs(_currCenterPoint.y) - _panOffset.y;
1750      _currPointsDistance = _startPointsDistance = _calculatePointsDistance(p, p2);
1751    }
1752
1753
1754  },
1755
1756  // Pointermove/touchmove/mousemove handler
1757  _onDragMove = function(e) {
1758
1759    e.preventDefault();
1760
1761    if(_pointerEventEnabled) {
1762      var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
1763      if(pointerIndex > -1) {
1764        var p = _currPointers[pointerIndex];
1765        p.x = e.pageX;
1766        p.y = e.pageY; 
1767      }
1768    }
1769
1770    if(_isDragging) {
1771      var touchesList = _getTouchPoints(e);
1772      if(!_direction && !_moved && !_isZooming) {
1773
1774        if(_mainScrollPos.x !== _slideSize.x * _currPositionIndex) {
1775          // if main scroll position is shifted – direction is always horizontal
1776          _direction = 'h';
1777        } else {
1778          var diff = Math.abs(touchesList[0].x - _currPoint.x) - Math.abs(touchesList[0].y - _currPoint.y);
1779          // check the direction of movement
1780          if(Math.abs(diff) >= DIRECTION_CHECK_OFFSET) {
1781            _direction = diff > 0 ? 'h' : 'v';
1782            _currentPoints = touchesList;
1783          }
1784        }
1785        
1786      } else {
1787        _currentPoints = touchesList;
1788      }
1789    }  
1790  },
1791  // 
1792  _renderMovement =  function() {
1793
1794    if(!_currentPoints) {
1795      return;
1796    }
1797
1798    var numPoints = _currentPoints.length;
1799
1800    if(numPoints === 0) {
1801      return;
1802    }
1803
1804    _equalizePoints(p, _currentPoints[0]);
1805
1806    delta.x = p.x - _currPoint.x;
1807    delta.y = p.y - _currPoint.y;
1808
1809    if(_isZooming && numPoints > 1) {
1810      // Handle behaviour for more than 1 point
1811
1812      _currPoint.x = p.x;
1813      _currPoint.y = p.y;
1814    
1815      // check if one of two points changed
1816      if( !delta.x && !delta.y && _isEqualPoints(_currentPoints[1], p2) ) {
1817        return;
1818      }
1819
1820      _equalizePoints(p2, _currentPoints[1]);
1821
1822
1823      if(!_zoomStarted) {
1824        _zoomStarted = true;
1825        _shout('zoomGestureStarted');
1826      }
1827      
1828      // Distance between two points
1829      var pointsDistance = _calculatePointsDistance(p,p2);
1830
1831      var zoomLevel = _calculateZoomLevel(pointsDistance);
1832
1833      // slightly over the of initial zoom level
1834      if(zoomLevel > self.currItem.initialZoomLevel + self.currItem.initialZoomLevel / 15) {
1835        _wasOverInitialZoom = true;
1836      }
1837
1838      // Apply the friction if zoom level is out of the bounds
1839      var zoomFriction = 1,
1840        minZoomLevel = _getMinZoomLevel(),
1841        maxZoomLevel = _getMaxZoomLevel();
1842
1843      if ( zoomLevel < minZoomLevel ) {
1844        
1845        if(_options.pinchToClose && !_wasOverInitialZoom && _startZoomLevel <= self.currItem.initialZoomLevel) {
1846          // fade out background if zooming out
1847          var minusDiff = minZoomLevel - zoomLevel;
1848          var percent = 1 - minusDiff / (minZoomLevel / 1.2);
1849
1850          _applyBgOpacity(percent);
1851          _shout('onPinchClose', percent);
1852          _opacityChanged = true;
1853        } else {
1854          zoomFriction = (minZoomLevel - zoomLevel) / minZoomLevel;
1855          if(zoomFriction > 1) {
1856            zoomFriction = 1;
1857          }
1858          zoomLevel = minZoomLevel - zoomFriction * (minZoomLevel / 3);
1859        }
1860        
1861      } else if ( zoomLevel > maxZoomLevel ) {
1862        // 1.5 - extra zoom level above the max. E.g. if max is x6, real max 6 + 1.5 = 7.5
1863        zoomFriction = (zoomLevel - maxZoomLevel) / ( minZoomLevel * 6 );
1864        if(zoomFriction > 1) {
1865          zoomFriction = 1;
1866        }
1867        zoomLevel = maxZoomLevel + zoomFriction * minZoomLevel;
1868      }
1869
1870      if(zoomFriction < 0) {
1871        zoomFriction = 0;
1872      }
1873
1874      // distance between touch points after friction is applied
1875      _currPointsDistance = pointsDistance;
1876
1877      // _centerPoint - The point in the middle of two pointers
1878      _findCenterOfPoints(p, p2, _centerPoint);
1879    
1880      // paning with two pointers pressed
1881      _currPanDist.x += _centerPoint.x - _currCenterPoint.x;
1882      _currPanDist.y += _centerPoint.y - _currCenterPoint.y;
1883      _equalizePoints(_currCenterPoint, _centerPoint);
1884
1885      _panOffset.x = _calculatePanOffset('x', zoomLevel);
1886      _panOffset.y = _calculatePanOffset('y', zoomLevel);
1887
1888      _isZoomingIn = zoomLevel > _currZoomLevel;
1889      _currZoomLevel = zoomLevel;
1890      _applyCurrentZoomPan();
1891
1892    } else {
1893
1894      // handle behaviour for one point (dragging or panning)
1895
1896      if(!_direction) {
1897        return;
1898      }
1899
1900      if(_isFirstMove) {
1901        _isFirstMove = false;
1902
1903        // subtract drag distance that was used during the detection direction  
1904
1905        if( Math.abs(delta.x) >= DIRECTION_CHECK_OFFSET) {
1906          delta.x -= _currentPoints[0].x - _startPoint.x;
1907        }
1908        
1909        if( Math.abs(delta.y) >= DIRECTION_CHECK_OFFSET) {
1910          delta.y -= _currentPoints[0].y - _startPoint.y;
1911        }
1912      }
1913
1914      _currPoint.x = p.x;
1915      _currPoint.y = p.y;
1916
1917      // do nothing if pointers position hasn't changed
1918      if(delta.x === 0 && delta.y === 0) {
1919        return;
1920      }
1921
1922      if(_direction === 'v' && _options.closeOnVerticalDrag) {
1923        if(!_canPan()) {
1924          _currPanDist.y += delta.y;
1925          _panOffset.y += delta.y;
1926
1927          var opacityRatio = _calculateVerticalDragOpacityRatio();
1928
1929          _verticalDragInitiated = true;
1930          _shout('onVerticalDrag', opacityRatio);
1931
1932          _applyBgOpacity(opacityRatio);
1933          _applyCurrentZoomPan();
1934          return ;
1935        }
1936      }
1937
1938      _pushPosPoint(_getCurrentTime(), p.x, p.y);
1939
1940      _moved = true;
1941      _currPanBounds = self.currItem.bounds;
1942      
1943      var mainScrollChanged = _panOrMoveMainScroll('x', delta);
1944      if(!mainScrollChanged) {
1945        _panOrMoveMainScroll('y', delta);
1946
1947        _roundPoint(_panOffset);
1948        _applyCurrentZoomPan();
1949      }
1950
1951    }
1952
1953  },
1954  
1955  // Pointerup/pointercancel/touchend/touchcancel/mouseup event handler
1956  _onDragRelease = function(e) {
1957
1958    if(_features.isOldAndroid ) {
1959
1960      if(_oldAndroidTouchEndTimeout && e.type === 'mouseup') {
1961        return;
1962      }
1963
1964      // on Android (v4.1, 4.2, 4.3 & possibly older) 
1965      // ghost mousedown/up event isn't preventable via e.preventDefault,
1966      // which causes fake mousedown event
1967      // so we block mousedown/up for 600ms
1968      if( e.type.indexOf('touch') > -1 ) {
1969        clearTimeout(_oldAndroidTouchEndTimeout);
1970        _oldAndroidTouchEndTimeout = setTimeout(function() {
1971          _oldAndroidTouchEndTimeout = 0;
1972        }, 600);
1973      }
1974      
1975    }
1976
1977    _shout('pointerUp');
1978
1979    if(_preventDefaultEventBehaviour(e, false)) {
1980      e.preventDefault();
1981    }
1982
1983    var releasePoint;
1984
1985    if(_pointerEventEnabled) {
1986      var pointerIndex = framework.arraySearch(_currPointers, e.pointerId, 'id');
1987      
1988      if(pointerIndex > -1) {
1989        releasePoint = _currPointers.splice(pointerIndex, 1)[0];
1990
1991        if(navigator.pointerEnabled) {
1992          releasePoint.type = e.pointerType || 'mouse';
1993        } else {
1994          var MSPOINTER_TYPES = {
1995            4: 'mouse', // event.MSPOINTER_TYPE_MOUSE
1996            2: 'touch', // event.MSPOINTER_TYPE_TOUCH 
1997            3: 'pen' // event.MSPOINTER_TYPE_PEN
1998          };
1999          releasePoint.type = MSPOINTER_TYPES[e.pointerType];
2000
2001          if(!releasePoint.type) {
2002            releasePoint.type = e.pointerType || 'mouse';
2003          }
2004        }
2005
2006      }
2007    }
2008
2009    var touchList = _getTouchPoints(e),
2010      gestureType,
2011      numPoints = touchList.length;
2012
2013    if(e.type === 'mouseup') {
2014      numPoints = 0;
2015    }
2016
2017    // Do nothing if there were 3 touch points or more
2018    if(numPoints === 2) {
2019      _currentPoints = null;
2020      return true;
2021    }
2022
2023    // if second pointer released
2024    if(numPoints === 1) {
2025      _equalizePoints(_startPoint, touchList[0]);
2026    }        
2027
2028
2029    // pointer hasn't moved, send "tap release" point
2030    if(numPoints === 0 && !_direction && !_mainScrollAnimating) {
2031      if(!releasePoint) {
2032        if(e.type === 'mouseup') {
2033          releasePoint = {x: e.pageX, y: e.pageY, type:'mouse'};
2034        } else if(e.changedTouches && e.changedTouches[0]) {
2035          releasePoint = {x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY, type:'touch'};
2036        }    
2037      }
2038
2039      _shout('touchRelease', e, releasePoint);
2040    }
2041
2042    // Difference in time between releasing of two last touch points (zoom gesture)
2043    var releaseTimeDiff = -1;
2044
2045    // Gesture completed, no pointers left
2046    if(numPoints === 0) {
2047      _isDragging = false;
2048      framework.unbind(window, _upMoveEvents, self);
2049
2050      _stopDragUpdateLoop();
2051
2052      if(_isZooming) {
2053        // Two points released at the same time
2054        releaseTimeDiff = 0;
2055      } else if(_lastReleaseTime !== -1) {
2056        releaseTimeDiff = _getCurrentTime() - _lastReleaseTime;
2057      }
2058    }
2059    _lastReleaseTime = numPoints === 1 ? _getCurrentTime() : -1;
2060    
2061    if(releaseTimeDiff !== -1 && releaseTimeDiff < 150) {
2062      gestureType = 'zoom';
2063    } else {
2064      gestureType = 'swipe';
2065    }
2066
2067    if(_isZooming && numPoints < 2) {
2068      _isZooming = false;
2069
2070      // Only second point released
2071      if(numPoints === 1) {
2072        gestureType = 'zoomPointerUp';
2073      }
2074      _shout('zoomGestureEnded');
2075    }
2076
2077    _currentPoints = null;
2078    if(!_moved && !_zoomStarted && !_mainScrollAnimating && !_verticalDragInitiated) {
2079      // nothing to animate
2080      return;
2081    }
2082  
2083    _stopAllAnimations();
2084
2085    
2086    if(!_releaseAnimData) {
2087      _releaseAnimData = _initDragReleaseAnimationData();
2088    }
2089    
2090    _releaseAnimData.calculateSwipeSpeed('x');
2091
2092
2093    if(_verticalDragInitiated) {
2094
2095      var opacityRatio = _calculateVerticalDragOpacityRatio();
2096
2097      if(opacityRatio < _options.verticalDragRange) {
2098        self.close();
2099      } else {
2100        var initalPanY = _panOffset.y,
2101          initialBgOpacity = _bgOpacity;
2102
2103        _animateProp('verticalDrag', 0, 1, 300, framework.easing.cubic.out, function(now) {
2104          
2105          _panOffset.y = (self.currItem.initialPosition.y - initalPanY) * now + initalPanY;
2106
2107          _applyBgOpacity(  (1 - initialBgOpacity) * now + initialBgOpacity );
2108          _applyCurrentZoomPan();
2109        });
2110
2111        _shout('onVerticalDrag', 1);
2112      }
2113
2114      return;
2115    }
2116
2117
2118    // main scroll 
2119    if(  (_mainScrollShifted || _mainScrollAnimating) && numPoints === 0) {
2120      var itemChanged = _finishSwipeMainScrollGesture(gestureType, _releaseAnimData);
2121      if(itemChanged) {
2122        return;
2123      }
2124      gestureType = 'zoomPointerUp';
2125    }
2126
2127    // prevent zoom/pan animation when main scroll animation runs
2128    if(_mainScrollAnimating) {
2129      return;
2130    }
2131    
2132    // Complete simple zoom gesture (reset zoom level if it's out of the bounds)  
2133    if(gestureType !== 'swipe') {
2134      _completeZoomGesture();
2135      return;
2136    }
2137  
2138    // Complete pan gesture if main scroll is not shifted, and it's possible to pan current image
2139    if(!_mainScrollShifted && _currZoomLevel > self.currItem.fitRatio) {
2140      _completePanGesture(_releaseAnimData);
2141    }
2142  },
2143
2144
2145  // Returns object with data about gesture
2146  // It's created only once and then reused
2147  _initDragReleaseAnimationData  = function() {
2148    // temp local vars
2149    var lastFlickDuration,
2150      tempReleasePos;
2151
2152    // s = this
2153    var s = {
2154      lastFlickOffset: {},
2155      lastFlickDist: {},
2156      lastFlickSpeed: {},
2157      slowDownRatio:  {},
2158      slowDownRatioReverse:  {},
2159      speedDecelerationRatio:  {},
2160      speedDecelerationRatioAbs:  {},
2161      distanceOffset:  {},
2162      backAnimDestination: {},
2163      backAnimStarted: {},
2164      calculateSwipeSpeed: function(axis) {
2165        
2166
2167        if( _posPoints.length > 1) {
2168          lastFlickDuration = _getCurrentTime() - _gestureCheckSpeedTime + 50;
2169          tempReleasePos = _posPoints[_posPoints.length-2][axis];
2170        } else {
2171          lastFlickDuration = _getCurrentTime() - _gestureStartTime; // total gesture duration
2172          tempReleasePos = _startPoint[axis];
2173        }
2174        s.lastFlickOffset[axis] = _currPoint[axis] - tempReleasePos;
2175        s.lastFlickDist[axis] = Math.abs(s.lastFlickOffset[axis]);
2176        if(s.lastFlickDist[axis] > 20) {
2177          s.lastFlickSpeed[axis] = s.lastFlickOffset[axis] / lastFlickDuration;
2178        } else {
2179          s.lastFlickSpeed[axis] = 0;
2180        }
2181        if( Math.abs(s.lastFlickSpeed[axis]) < 0.1 ) {
2182          s.lastFlickSpeed[axis] = 0;
2183        }
2184        
2185        s.slowDownRatio[axis] = 0.95;
2186        s.slowDownRatioReverse[axis] = 1 - s.slowDownRatio[axis];
2187        s.speedDecelerationRatio[axis] = 1;
2188      },
2189
2190      calculateOverBoundsAnimOffset: function(axis, speed) {
2191        if(!s.backAnimStarted[axis]) {
2192
2193          if(_panOffset[axis] > _currPanBounds.min[axis]) {
2194            s.backAnimDestination[axis] = _currPanBounds.min[axis];
2195            
2196          } else if(_panOffset[axis] < _currPanBounds.max[axis]) {
2197            s.backAnimDestination[axis] = _currPanBounds.max[axis];
2198          }
2199
2200          if(s.backAnimDestination[axis] !== undefined) {
2201            s.slowDownRatio[axis] = 0.7;
2202            s.slowDownRatioReverse[axis] = 1 - s.slowDownRatio[axis];
2203            if(s.speedDecelerationRatioAbs[axis] < 0.05) {
2204
2205              s.lastFlickSpeed[axis] = 0;
2206              s.backAnimStarted[axis] = true;
2207
2208              _animateProp('bounceZoomPan'+axis,_panOffset[axis], 
2209                s.backAnimDestination[axis], 
2210                speed || 300, 
2211                framework.easing.sine.out, 
2212                function(pos) {
2213                  _panOffset[axis] = pos;
2214                  _applyCurrentZoomPan();
2215                }
2216              );
2217
2218            }
2219          }
2220        }
2221      },
2222
2223      // Reduces the speed by slowDownRatio (per 10ms)
2224      calculateAnimOffset: function(axis) {
2225        if(!s.backAnimStarted[axis]) {
2226          s.speedDecelerationRatio[axis] = s.speedDecelerationRatio[axis] * (s.slowDownRatio[axis] + 
2227                        s.slowDownRatioReverse[axis] - 
2228                        s.slowDownRatioReverse[axis] * s.timeDiff / 10);
2229
2230          s.speedDecelerationRatioAbs[axis] = Math.abs(s.lastFlickSpeed[axis] * s.speedDecelerationRatio[axis]);
2231          s.distanceOffset[axis] = s.lastFlickSpeed[axis] * s.speedDecelerationRatio[axis] * s.timeDiff;
2232          _panOffset[axis] += s.distanceOffset[axis];
2233
2234        }
2235      },
2236
2237      panAnimLoop: function() {
2238        if ( _animations.zoomPan ) {
2239          _animations.zoomPan.raf = _requestAF(s.panAnimLoop);
2240
2241          s.now = _getCurrentTime();
2242          s.timeDiff = s.now - s.lastNow;
2243          s.lastNow = s.now;
2244          
2245          s.calculateAnimOffset('x');
2246          s.calculateAnimOffset('y');
2247
2248          _applyCurrentZoomPan();
2249          
2250          s.calculateOverBoundsAnimOffset('x');
2251          s.calculateOverBoundsAnimOffset('y');
2252
2253
2254          if (s.speedDecelerationRatioAbs.x < 0.05 && s.speedDecelerationRatioAbs.y < 0.05) {
2255
2256            // round pan position
2257            _panOffset.x = Math.round(_panOffset.x);
2258            _panOffset.y = Math.round(_panOffset.y);
2259            _applyCurrentZoomPan();
2260            
2261            _stopAnimation('zoomPan');
2262            return;
2263          }
2264        }
2265
2266      }
2267    };
2268    return s;
2269  },
2270
2271  _completePanGesture = function(animData) {
2272    // calculate swipe speed for Y axis (paanning)
2273    animData.calculateSwipeSpeed('y');
2274
2275    _currPanBounds = self.currItem.bounds;
2276    
2277    animData.backAnimDestination = {};
2278    animData.backAnimStarted = {};
2279
2280    // Avoid acceleration animation if speed is too low
2281    if(Math.abs(animData.lastFlickSpeed.x) <= 0.05 && Math.abs(animData.lastFlickSpeed.y) <= 0.05 ) {
2282      animData.speedDecelerationRatioAbs.x = animData.speedDecelerationRatioAbs.y = 0;
2283
2284      // Run pan drag release animation. E.g. if you drag image and release finger without momentum.
2285      animData.calculateOverBoundsAnimOffset('x');
2286      animData.calculateOverBoundsAnimOffset('y');
2287      return true;
2288    }
2289
2290    // Animation loop that controls the acceleration after pan gesture ends
2291    _registerStartAnimation('zoomPan');
2292    animData.lastNow = _getCurrentTime();
2293    animData.panAnimLoop();
2294  },
2295
2296
2297  _finishSwipeMainScrollGesture = function(gestureType, _releaseAnimData) {
2298    var itemChanged;
2299    if(!_mainScrollAnimating) {
2300      _currZoomedItemIndex = _currentItemIndex;
2301    }
2302
2303
2304    
2305    var itemsDiff;
2306
2307    if(gestureType === 'swipe') {
2308      var totalShiftDist = _currPoint.x - _startPoint.x,
2309        isFastLastFlick = _releaseAnimData.lastFlickDist.x < 10;
2310
2311      // if container is shifted for more than MIN_SWIPE_DISTANCE, 
2312      // and last flick gesture was in right direction
2313      if(totalShiftDist > MIN_SWIPE_DISTANCE && 
2314        (isFastLastFlick || _releaseAnimData.lastFlickOffset.x > 20) ) {
2315        // go to prev item
2316        itemsDiff = -1;
2317      } else if(totalShiftDist < -MIN_SWIPE_DISTANCE && 
2318        (isFastLastFlick || _releaseAnimData.lastFlickOffset.x < -20) ) {
2319        // go to next item
2320        itemsDiff = 1;
2321      }
2322    }
2323
2324    var nextCircle;
2325
2326    if(itemsDiff) {
2327      
2328      _currentItemIndex += itemsDiff;
2329
2330      if(_currentItemIndex < 0) {
2331        _currentItemIndex = _options.loop ? _getNumItems()-1 : 0;
2332        nextCircle = true;
2333      } else if(_currentItemIndex >= _getNumItems()) {
2334        _currentItemIndex = _options.loop ? 0 : _getNumItems()-1;
2335        nextCircle = true;
2336      }
2337
2338      if(!nextCircle || _options.loop) {
2339        _indexDiff += itemsDiff;
2340        _currPositionIndex -= itemsDiff;
2341        itemChanged = true;
2342      }
2343      
2344
2345      
2346    }
2347
2348    var animateToX = _slideSize.x * _currPositionIndex;
2349    var animateToDist = Math.abs( animateToX - _mainScrollPos.x );
2350    var finishAnimDuration;
2351
2352
2353    if(!itemChanged && animateToX > _mainScrollPos.x !== _releaseAnimData.lastFlickSpeed.x > 0) {
2354      // "return to current" duration, e.g. when dragging from slide 0 to -1
2355      finishAnimDuration = 333; 
2356    } else {
2357      finishAnimDuration = Math.abs(_releaseAnimData.lastFlickSpeed.x) > 0 ? 
2358                  animateToDist / Math.abs(_releaseAnimData.lastFlickSpeed.x) : 
2359                  333;
2360
2361      finishAnimDuration = Math.min(finishAnimDuration, 400);
2362      finishAnimDuration = Math.max(finishAnimDuration, 250);
2363    }
2364
2365    if(_currZoomedItemIndex === _currentItemIndex) {
2366      itemChanged = false;
2367    }
2368    
2369    _mainScrollAnimating = true;
2370    
2371    _shout('mainScrollAnimStart');
2372
2373    _animateProp('mainScroll', _mainScrollPos.x, animateToX, finishAnimDuration, framework.easing.cubic.out, 
2374      _moveMainScroll,
2375      function() {
2376        _stopAllAnimations();
2377        _mainScrollAnimating = false;
2378        _currZoomedItemIndex = -1;
2379        
2380        if(itemChanged || _currZoomedItemIndex !== _currentItemIndex) {
2381          self.updateCurrItem();
2382        }
2383        
2384        _shout('mainScrollAnimComplete');
2385      }
2386    );
2387
2388    if(itemChanged) {
2389      self.updateCurrItem(true);
2390    }
2391
2392    return itemChanged;
2393  },
2394
2395  _calculateZoomLevel = function(touchesDistance) {
2396    return  1 / _startPointsDistance * touchesDistance * _startZoomLevel;
2397  },
2398
2399  // Resets zoom if it's out of bounds
2400  _completeZoomGesture = function() {
2401    var destZoomLevel = _currZoomLevel,
2402      minZoomLevel = _getMinZoomLevel(),
2403      maxZoomLevel = _getMaxZoomLevel();
2404
2405    if ( _currZoomLevel < minZoomLevel ) {
2406      destZoomLevel = minZoomLevel;
2407    } else if ( _currZoomLevel > maxZoomLevel ) {
2408      destZoomLevel = maxZoomLevel;
2409    }
2410
2411    var destOpacity = 1,
2412      onUpdate,
2413      initialOpacity = _bgOpacity;
2414
2415    if(_opacityChanged && !_isZoomingIn && !_wasOverInitialZoom && _currZoomLevel < minZoomLevel) {
2416      //_closedByScroll = true;
2417      self.close();
2418      return true;
2419    }
2420
2421    if(_opacityChanged) {
2422      onUpdate = function(now) {
2423        _applyBgOpacity(  (destOpacity - initialOpacity) * now + initialOpacity );
2424      };
2425    }
2426
2427    self.zoomTo(destZoomLevel, 0, 200,  framework.easing.cubic.out, onUpdate);
2428    return true;
2429  };
2430
2431
2432_registerModule('Gestures', {
2433  publicMethods: {
2434
2435    initGestures: function() {
2436
2437      // helper function that builds touch/pointer/mouse events
2438      var addEventNames = function(pref, down, move, up, cancel) {
2439        _dragStartEvent = pref + down;
2440        _dragMoveEvent = pref + move;
2441        _dragEndEvent = pref + up;
2442        if(cancel) {
2443          _dragCancelEvent = pref + cancel;
2444        } else {
2445          _dragCancelEvent = '';
2446        }
2447      };
2448
2449      _pointerEventEnabled = _features.pointerEvent;
2450      if(_pointerEventEnabled && _features.touch) {
2451        // we don't need touch events, if browser supports pointer events
2452        _features.touch = false;
2453      }
2454
2455      if(_pointerEventEnabled) {
2456        if(navigator.pointerEnabled) {
2457          addEventNames('pointer', 'down', 'move', 'up', 'cancel');
2458        } else {
2459          // IE10 pointer events are case-sensitive
2460          addEventNames('MSPointer', 'Down', 'Move', 'Up', 'Cancel');
2461        }
2462      } else if(_features.touch) {
2463        addEventNames('touch', 'start', 'move', 'end', 'cancel');
2464        _likelyTouchDevice = true;
2465      } else {
2466        addEventNames('mouse', 'down', 'move', 'up');  
2467      }
2468
2469      _upMoveEvents = _dragMoveEvent + ' ' + _dragEndEvent  + ' ' +  _dragCancelEvent;
2470      _downEvents = _dragStartEvent;
2471
2472      if(_pointerEventEnabled && !_likelyTouchDevice) {
2473        _likelyTouchDevice = (navigator.maxTouchPoints > 1) || (navigator.msMaxTouchPoints > 1);
2474      }
2475      // make variable public
2476      self.likelyTouchDevice = _likelyTouchDevice; 
2477      
2478      _globalEventHandlers[_dragStartEvent] = _onDragStart;
2479      _globalEventHandlers[_dragMoveEvent] = _onDragMove;
2480      _globalEventHandlers[_dragEndEvent] = _onDragRelease; // the Kraken
2481
2482      if(_dragCancelEvent) {
2483        _globalEventHandlers[_dragCancelEvent] = _globalEventHandlers[_dragEndEvent];
2484      }
2485
2486      // Bind mouse events on device with detected hardware touch support, in case it supports multiple types of input.
2487      if(_features.touch) {
2488        _downEvents += ' mousedown';
2489        _upMoveEvents += ' mousemove mouseup';
2490        _globalEventHandlers.mousedown = _globalEventHandlers[_dragStartEvent];
2491        _globalEventHandlers.mousemove = _globalEventHandlers[_dragMoveEvent];
2492        _globalEventHandlers.mouseup = _globalEventHandlers[_dragEndEvent];
2493      }
2494
2495      if(!_likelyTouchDevice) {
2496        // don't allow pan to next slide from zoomed state on Desktop
2497        _options.allowPanToNext = false;
2498      }
2499    }
2500
2501  }
2502});
2503
2504
2505/*>>gestures*/
2506
2507/*>>show-hide-transition*/
2508/**
2509 * show-hide-transition.js:
2510 *
2511 * Manages initial opening or closing transition.
2512 *
2513 * If you're not planning to use transition for gallery at all,
2514 * you may set options hideAnimationDuration and showAnimationDuration to 0,
2515 * and just delete startAnimation function.
2516 * 
2517 */
2518
2519
2520var _showOrHideTimeout,
2521  _showOrHide = function(item, img, out, completeFn) {
2522
2523    if(_showOrHideTimeout) {
2524      clearTimeout(_showOrHideTimeout);
2525    }
2526
2527    _initialZoomRunning = true;
2528    _initialContentSet = true;
2529    
2530    // dimensions of small thumbnail {x:,y:,w:}.
2531    // Height is optional, as calculated based on large image.
2532    var thumbBounds; 
2533    if(item.initialLayout) {
2534      thumbBounds = item.initialLayout;
2535      item.initialLayout = null;
2536    } else {
2537      thumbBounds = _options.getThumbBoundsFn && _options.getThumbBoundsFn(_currentItemIndex);
2538    }
2539
2540    var duration = out ? _options.hideAnimationDuration : _options.showAnimationDuration;
2541
2542    var onComplete = function() {
2543      _stopAnimation('initialZoom');
2544      if(!out) {
2545        _applyBgOpacity(1);
2546        if(img) {
2547          img.style.display = 'block';
2548        }
2549        framework.addClass(template, 'pswp--animated-in');
2550        _shout('initialZoom' + (out ? 'OutEnd' : 'InEnd'));
2551      } else {
2552        self.template.removeAttribute('style');
2553        self.bg.removeAttribute('style');
2554      }
2555
2556      if(completeFn) {
2557        completeFn();
2558      }
2559      _initialZoomRunning = false;
2560    };
2561
2562    // if bounds aren't provided, just open gallery without animation
2563    if(!duration || !thumbBounds || thumbBounds.x === undefined) {
2564
2565      _shout('initialZoom' + (out ? 'Out' : 'In') );
2566
2567      _currZoomLevel = item.initialZoomLevel;
2568      _equalizePoints(_panOffset,  item.initialPosition );
2569      _applyCurrentZoomPan();
2570
2571      template.style.opacity = out ? 0 : 1;
2572      _applyBgOpacity(1);
2573
2574      if(duration) {
2575        setTimeout(function() {
2576          onComplete();
2577        }, duration);
2578      } else {
2579        onComplete();
2580      }
2581
2582      return;
2583    }
2584
2585    var startAnimation = function() {
2586      var closeWithRaf = _closedByScroll,
2587        fadeEverything = !self.currItem.src || self.currItem.loadError || _options.showHideOpacity;
2588      
2589      // apply hw-acceleration to image
2590      if(item.miniImg) {
2591        item.miniImg.style.webkitBackfaceVisibility = 'hidden';
2592      }
2593
2594      if(!out) {
2595        _currZoomLevel = thumbBounds.w / item.w;
2596        _panOffset.x = thumbBounds.x;
2597        _panOffset.y = thumbBounds.y - _initalWindowScrollY;
2598
2599        self[fadeEverything ? 'template' : 'bg'].style.opacity = 0.001;
2600        _applyCurrentZoomPan();
2601      }
2602
2603      _registerStartAnimation('initialZoom');
2604      
2605      if(out && !closeWithRaf) {
2606        framework.removeClass(template, 'pswp--animated-in');
2607      }
2608
2609      if(fadeEverything) {
2610        if(out) {
2611          framework[ (closeWithRaf ? 'remove' : 'add') + 'Class' ](template, 'pswp--animate_opacity');
2612        } else {
2613          setTimeout(function() {
2614            framework.addClass(template, 'pswp--animate_opacity');
2615          }, 30);
2616        }
2617      }
2618
2619      _showOrHideTimeout = setTimeout(function() {
2620
2621        _shout('initialZoom' + (out ? 'Out' : 'In') );
2622        
2623
2624        if(!out) {
2625
2626          // "in" animation always uses CSS transitions (instead of rAF).
2627          // CSS transition work faster here, 
2628          // as developer may also want to animate other things, 
2629          // like ui on top of sliding area, which can be animated just via CSS
2630          
2631          _currZoomLevel = item.initialZoomLevel;
2632          _equalizePoints(_panOffset,  item.initialPosition );
2633          _applyCurrentZoomPan();
2634          _applyBgOpacity(1);
2635
2636          if(fadeEverything) {
2637            template.style.opacity = 1;
2638          } else {
2639            _applyBgOpacity(1);
2640          }
2641
2642          _showOrHideTimeout = setTimeout(onComplete, duration + 20);
2643        } else {
2644
2645          // "out" animation uses rAF only when PhotoSwipe is closed by browser scroll, to recalculate position
2646          var destZoomLevel = thumbBounds.w / item.w,
2647            initialPanOffset = {
2648              x: _panOffset.x,
2649              y: _panOffset.y
2650            },
2651            initialZoomLevel = _currZoomLevel,
2652            initalBgOpacity = _bgOpacity,
2653            onUpdate = function(now) {
2654              
2655              if(now === 1) {
2656                _currZoomLevel = destZoomLevel;
2657                _panOffset.x = thumbBounds.x;
2658                _panOffset.y = thumbBounds.y  - _currentWindowScrollY;
2659              } else {
2660                _currZoomLevel = (destZoomLevel - initialZoomLevel) * now + initialZoomLevel;
2661                _panOffset.x = (thumbBounds.x - initialPanOffset.x) * now + initialPanOffset.x;
2662                _panOffset.y = (thumbBounds.y - _currentWindowScrollY - initialPanOffset.y) * now + initialPanOffset.y;
2663              }
2664              
2665              _applyCurrentZoomPan();
2666              if(fadeEverything) {
2667                template.style.opacity = 1 - now;
2668              } else {
2669                _applyBgOpacity( initalBgOpacity - now * initalBgOpacity );
2670              }
2671            };
2672
2673          if(closeWithRaf) {
2674            _animateProp('initialZoom', 0, 1, duration, framework.easing.cubic.out, onUpdate, onComplete);
2675          } else {
2676            onUpdate(1);
2677            _showOrHideTimeout = setTimeout(onComplete, duration + 20);
2678          }
2679        }
2680      
2681      }, out ? 25 : 90); // Main purpose of this delay is to give browser time to paint and
2682          // create composite layers of PhotoSwipe UI parts (background, controls, caption, arrows).
2683          // Which avoids lag at the beginning of scale transition.
2684    };
2685    startAnimation();
2686
2687    
2688  };
2689
2690/*>>show-hide-transition*/
2691
2692/*>>items-controller*/
2693/**
2694*
2695* Controller manages gallery items, their dimensions, and their content.
2696* 
2697*/
2698
2699var _items,
2700  _tempPanAreaSize = {},
2701  _imagesToAppendPool = [],
2702  _initialContentSet,
2703  _initialZoomRunning,
2704  _controllerDefaultOptions = {
2705    index: 0,
2706    errorMsg: '<div class="pswp__error-msg"><a href="%url%" target="_blank">The image</a> could not be loaded.</div>',
2707    forceProgressiveLoading: false, // TODO
2708    preload: [1,1],
2709    getNumItemsFn: function() {
2710      return _items.length;
2711    }
2712  };
2713
2714
2715var _getItemAt,
2716  _getNumItems,
2717  _initialIsLoop,
2718  _getZeroBounds = function() {
2719    return {
2720      center:{x:0,y:0}, 
2721      max:{x:0,y:0}, 
2722      min:{x:0,y:0}
2723    };
2724  },
2725  _calculateSingleItemPanBounds = function(item, realPanElementW, realPanElementH ) {
2726    var bounds = item.bounds;
2727
2728    // position of element when it's centered
2729    bounds.center.x = Math.round((_tempPanAreaSize.x - realPanElementW) / 2);
2730    bounds.center.y = Math.round((_tempPanAreaSize.y - realPanElementH) / 2) + item.vGap.top;
2731
2732    // maximum pan position
2733    bounds.max.x = (realPanElementW > _tempPanAreaSize.x) ? 
2734              Math.round(_tempPanAreaSize.x - realPanElementW) : 
2735              bounds.center.x;
2736    
2737    bounds.max.y = (realPanElementH > _tempPanAreaSize.y) ? 
2738              Math.round(_tempPanAreaSize.y - realPanElementH) + item.vGap.top : 
2739              bounds.center.y;
2740    
2741    // minimum pan position
2742    bounds.min.x = (realPanElementW > _tempPanAreaSize.x) ? 0 : bounds.center.x;
2743    bounds.min.y = (realPanElementH > _tempPanAreaSize.y) ? item.vGap.top : bounds.center.y;
2744  },
2745  _calculateItemSize = function(item, viewportSize, zoomLevel) {
2746
2747    if (item.src && !item.loadError) {
2748      var isInitial = !zoomLevel;
2749      
2750      if(isInitial) {
2751        if(!item.vGap) {
2752          item.vGap = {top:0,bottom:0};
2753        }
2754        // allows overriding vertical margin for individual items
2755        _shout('parseVerticalMargin', item);
2756      }
2757
2758
2759      _tempPanAreaSize.x = viewportSize.x;
2760      _tempPanAreaSize.y = viewportSize.y - item.vGap.top - item.vGap.bottom;
2761
2762      if (isInitial) {
2763        var hRatio = _tempPanAreaSize.x / item.w;
2764        var vRatio = _tempPanAreaSize.y / item.h;
2765
2766        item.fitRatio = hRatio < vRatio ? hRatio : vRatio;
2767        //item.fillRatio = hRatio > vRatio ? hRatio : vRatio;
2768
2769        var scaleMode = _options.scaleMode;
2770
2771        if (scaleMode === 'orig') {
2772          zoomLevel = 1;
2773        } else if (scaleMode === 'fit') {
2774          zoomLevel = item.fitRatio;
2775        }
2776
2777        if (zoomLevel > 1) {
2778          zoomLevel = 1;
2779        }
2780
2781        item.initialZoomLevel = zoomLevel;
2782        
2783        if(!item.bounds) {
2784          // reuse bounds object
2785          item.bounds = _getZeroBounds(); 
2786        }
2787      }
2788
2789      if(!zoomLevel) {
2790        return;
2791      }
2792
2793      _calculateSingleItemPanBounds(item, item.w * zoomLevel, item.h * zoomLevel);
2794
2795      if (isInitial && zoomLevel === item.initialZoomLevel) {
2796        item.initialPosition = item.bounds.center;
2797      }
2798
2799      return item.bounds;
2800    } else {
2801      item.w = item.h = 0;
2802      item.initialZoomLevel = item.fitRatio = 1;
2803      item.bounds = _getZeroBounds();
2804      item.initialPosition = item.bounds.center;
2805
2806      // if it's not image, we return zero bounds (content is not zoomable)
2807      return item.bounds;
2808    }
2809    
2810  },
2811
2812  
2813
2814
2815  _appendImage = function(index, item, baseDiv, img, preventAnimation, keepPlaceholder) {
2816    
2817
2818    if(item.loadError) {
2819      return;
2820    }
2821
2822    if(img) {
2823
2824      item.imageAppended = true;
2825      _setImageSize(item, img, (item === self.currItem && _renderMaxResolution) );
2826      
2827      baseDiv.appendChild(img);
2828
2829      if(keepPlaceholder) {
2830        setTimeout(function() {
2831          if(item && item.loaded && item.placeholder) {
2832            item.placeholder.style.display = 'none';
2833            item.placeholder = null;
2834          }
2835        }, 500);
2836      }
2837    }
2838  },
2839  
2840
2841
2842  _preloadImage = function(item) {
2843    item.loading = true;
2844    item.loaded = false;
2845    var img = item.img = framework.createEl('pswp__img', 'img');
2846    var onComplete = function() {
2847      item.loading = false;
2848      item.loaded = true;
2849
2850      if(item.loadComplete) {
2851        item.loadComplete(item);
2852      } else {
2853        item.img = null; // no need to store image object
2854      }
2855      img.onload = img.onerror = null;
2856      img = null;
2857    };
2858    img.onload = onComplete;
2859    img.onerror = function() {
2860      item.loadError = true;
2861      onComplete();
2862    };    
2863
2864    img.src = item.src;// + '?a=' + Math.random();
2865
2866    return img;
2867  },
2868  _checkForError = function(item, cleanUp) {
2869    if(item.src && item.loadError && item.container) {
2870
2871      if(cleanUp) {
2872        item.container.innerHTML = '';
2873      }
2874
2875      item.container.innerHTML = _options.errorMsg.replace('%url%',  item.src );
2876      return true;
2877      
2878    }
2879  },
2880  _setImageSize = function(item, img, maxRes) {
2881    if(!item.src) {
2882      return;
2883    }
2884
2885    if(!img) {
2886      img = item.container.lastChild;
2887    }
2888
2889    var w = maxRes ? item.w : Math.round(item.w * item.fitRatio),
2890      h = maxRes ? item.h : Math.round(item.h * item.fitRatio);
2891    
2892    if(item.placeholder && !item.loaded) {
2893      item.placeholder.style.width = w + 'px';
2894      item.placeholder.style.height = h + 'px';
2895    }
2896
2897    img.style.width = w + 'px';
2898    img.style.height = h + 'px';
2899  },
2900  _appendImagesPool = function() {
2901
2902    if(_imagesToAppendPool.length) {
2903      var poolItem;
2904
2905      for(var i = 0; i < _imagesToAppendPool.length; i++) {
2906        poolItem = _imagesToAppendPool[i];
2907        if( poolItem.holder.index === poolItem.index ) {
2908          _appendImage(poolItem.index, poolItem.item, poolItem.baseDiv, poolItem.img, false, poolItem.clearPlaceholder);
2909        }
2910      }
2911      _imagesToAppendPool = [];
2912    }
2913  };
2914  
2915
2916
2917_registerModule('Controller', {
2918
2919  publicMethods: {
2920
2921    lazyLoadItem: function(index) {
2922      index = _getLoopedId(index);
2923      var item = _getItemAt(index);
2924
2925      if(!item || ((item.loaded || item.loading) && !_itemsNeedUpdate)) {
2926        return;
2927      }
2928
2929      _shout('gettingData', index, item);
2930
2931      if (!item.src) {
2932        return;
2933      }
2934
2935      _preloadImage(item);
2936    },
2937    initController: function() {
2938      framework.extend(_options, _controllerDefaultOptions, true);
2939      self.items = _items = items;
2940      _getItemAt = self.getItemAt;
2941      _getNumItems = _options.getNumItemsFn; //self.getNumItems;
2942
2943
2944
2945      _initialIsLoop = _options.loop;
2946      if(_getNumItems() < 3) {
2947        _options.loop = false; // disable loop if less then 3 items
2948      }
2949
2950      _listen('beforeChange', function(diff) {
2951
2952        var p = _options.preload,
2953          isNext = diff === null ? true : (diff >= 0),
2954          preloadBefore = Math.min(p[0], _getNumItems() ),
2955          preloadAfter = Math.min(p[1], _getNumItems() ),
2956          i;
2957
2958
2959        for(i = 1; i <= (isNext ? preloadAfter : preloadBefore); i++) {
2960          self.lazyLoadItem(_currentItemIndex+i);
2961        }
2962        for(i = 1; i <= (isNext ? preloadBefore : preloadAfter); i++) {
2963          self.lazyLoadItem(_currentItemIndex-i);
2964        }
2965      });
2966
2967      _listen('initialLayout', function() {
2968        self.currItem.initialLayout = _options.getThumbBoundsFn && _options.getThumbBoundsFn(_currentItemIndex);
2969      });
2970
2971      _listen('mainScrollAnimComplete', _appendImagesPool);
2972      _listen('initialZoomInEnd', _appendImagesPool);
2973
2974
2975
2976      _listen('destroy', function() {
2977        var item;
2978        for(var i = 0; i < _items.length; i++) {
2979          item = _items[i];
2980          // remove reference to DOM elements, for GC
2981          if(item.container) {
2982            item.container = null; 
2983          }
2984          if(item.placeholder) {
2985            item.placeholder = null;
2986          }
2987          if(item.img) {
2988            item.img = null;
2989          }
2990          if(item.preloader) {
2991            item.preloader = null;
2992          }
2993          if(item.loadError) {
2994            item.loaded = item.loadError = false;
2995          }
2996        }
2997        _imagesToAppendPool = null;
2998      });
2999    },
3000
3001
3002    getItemAt: function(index) {
3003      if (index >= 0) {
3004        return _items[index] !== undefined ? _items[index] : false;
3005      }
3006      return false;
3007    },
3008
3009    allowProgressiveImg: function() {
3010      // 1. Progressive image loading isn't working on webkit/blink 
3011      //    when hw-acceleration (e.g. translateZ) is applied to IMG element.
3012      //    That's why in PhotoSwipe parent element gets zoom transform, not image itself.
3013      //    
3014      // 2. Progressive image loading sometimes blinks in webkit/blink when applying animation to parent element.
3015      //    That's why it's disabled on touch devices (mainly because of swipe transition)
3016      //    
3017      // 3. Progressive image loading sometimes doesn't work in IE (up to 11).
3018
3019      // Don't allow progressive loading on non-large touch devices
3020      return _options.forceProgressiveLoading || !_likelyTouchDevice || _options.mouseUsed || screen.width > 1200; 
3021      // 1200 - to eliminate touch devices with large screen (like Chromebook Pixel)
3022    },
3023
3024    setContent: function(holder, index) {
3025
3026      if(_options.loop) {
3027        index = _getLoopedId(index);
3028      }
3029
3030      var prevItem = self.getItemAt(holder.index);
3031      if(prevItem) {
3032        prevItem.container = null;
3033      }
3034  
3035      var item = self.getItemAt(index),
3036        img;
3037      
3038      if(!item) {
3039        holder.el.innerHTML = '';
3040        return;
3041      }
3042
3043      // allow to override data
3044      _shout('gettingData', index, item);
3045
3046      holder.index = index;
3047      holder.item = item;
3048
3049      // base container DIV is created only once for each of 3 holders
3050      var baseDiv = item.container = framework.createEl('pswp__zoom-wrap'); 
3051
3052      
3053
3054      if(!item.src && item.html) {
3055        if(item.html.tagName) {
3056          baseDiv.appendChild(item.html);
3057        } else {
3058          baseDiv.innerHTML = item.html;
3059        }
3060      }
3061
3062      _checkForError(item);
3063
3064      _calculateItemSize(item, _viewportSize);
3065      
3066      if(item.src && !item.loadError && !item.loaded) {
3067
3068        item.loadComplete = function(item) {
3069
3070          // gallery closed before image finished loading
3071          if(!_isOpen) {
3072            return;
3073          }
3074
3075          // check if holder hasn't changed while image was loading
3076          if(holder && holder.index === index ) {
3077            if( _checkForError(item, true) ) {
3078              item.loadComplete = item.img = null;
3079              _calculateItemSize(item, _viewportSize);
3080              _applyZoomPanToItem(item);
3081
3082              if(holder.index === _currentItemIndex) {
3083                // recalculate dimensions
3084                self.updateCurrZoomItem();
3085              }
3086              return;
3087            }
3088            if( !item.imageAppended ) {
3089              if(_features.transform && (_mainScrollAnimating || _initialZoomRunning) ) {
3090                _imagesToAppendPool.push({
3091                  item:item,
3092                  baseDiv:baseDiv,
3093                  img:item.img,
3094                  index:index,
3095                  holder:holder,
3096                  clearPlaceholder:true
3097                });
3098              } else {
3099                _appendImage(index, item, baseDiv, item.img, _mainScrollAnimating || _initialZoomRunning, true);
3100              }
3101            } else {
3102              // remove preloader & mini-img
3103              if(!_initialZoomRunning && item.placeholder) {
3104                item.placeholder.style.display = 'none';
3105                item.placeholder = null;
3106              }
3107            }
3108          }
3109
3110          item.loadComplete = null;
3111          item.img = null; // no need to store image element after it's added
3112
3113          _shout('imageLoadComplete', index, item);
3114        };
3115
3116        if(framework.features.transform) {
3117          
3118          var placeholderClassName = 'pswp__img pswp__img--placeholder'; 
3119          placeholderClassName += (item.msrc ? '' : ' pswp__img--placeholder--blank');
3120
3121          var placeholder = framework.createEl(placeholderClassName, item.msrc ? 'img' : '');
3122          if(item.msrc) {
3123            placeholder.src = item.msrc;
3124          }
3125          
3126          _setImageSize(item, placeholder);
3127
3128          baseDiv.appendChild(placeholder);
3129          item.placeholder = placeholder;
3130
3131        }
3132        
3133
3134        
3135
3136        if(!item.loading) {
3137          _preloadImage(item);
3138        }
3139
3140
3141        if( self.allowProgressiveImg() ) {
3142          // just append image
3143          if(!_initialContentSet && _features.transform) {
3144            _imagesToAppendPool.push({
3145              item:item, 
3146              baseDiv:baseDiv, 
3147              img:item.img, 
3148              index:index, 
3149              holder:holder
3150            });
3151          } else {
3152            _appendImage(index, item, baseDiv, item.img, true, true);
3153          }
3154        }
3155        
3156      } else if(item.src && !item.loadError) {
3157        // image object is created every time, due to bugs of image loading & delay when switching images
3158        img = framework.createEl('pswp__img', 'img');
3159        img.style.opacity = 1;
3160        img.src = item.src;
3161        _setImageSize(item, img);
3162        _appendImage(index, item, baseDiv, img, true);
3163      }
3164      
3165
3166      if(!_initialContentSet && index === _currentItemIndex) {
3167        _currZoomElementStyle = baseDiv.style;
3168        _showOrHide(item, (img ||item.img) );
3169      } else {
3170        _applyZoomPanToItem(item);
3171      }
3172
3173      holder.el.innerHTML = '';
3174      holder.el.appendChild(baseDiv);
3175    },
3176
3177    cleanSlide: function( item ) {
3178      if(item.img ) {
3179        item.img.onload = item.img.onerror = null;
3180      }
3181      item.loaded = item.loading = item.img = item.imageAppended = false;
3182    }
3183
3184  }
3185});
3186
3187/*>>items-controller*/
3188
3189/*>>tap*/
3190/**
3191 * tap.js:
3192 *
3193 * Displatches tap and double-tap events.
3194 * 
3195 */
3196
3197var tapTimer,
3198  tapReleasePoint = {},
3199  _dispatchTapEvent = function(origEvent, releasePoint, pointerType) {    
3200    var e = document.createEvent( 'CustomEvent' ),
3201      eDetail = {
3202        origEvent:origEvent, 
3203        target:origEvent.target, 
3204        releasePoint: releasePoint, 
3205        pointerType:pointerType || 'touch'
3206      };
3207
3208    e.initCustomEvent( 'pswpTap', true, true, eDetail );
3209    origEvent.target.dispatchEvent(e);
3210  };
3211
3212_registerModule('Tap', {
3213  publicMethods: {
3214    initTap: function() {
3215      _listen('firstTouchStart', self.onTapStart);
3216      _listen('touchRelease', self.onTapRelease);
3217      _listen('destroy', function() {
3218        tapReleasePoint = {};
3219        tapTimer = null;
3220      });
3221    },
3222    onTapStart: function(touchList) {
3223      if(touchList.length > 1) {
3224        clearTimeout(tapTimer);
3225        tapTimer = null;
3226      }
3227    },
3228    onTapRelease: function(e, releasePoint) {
3229      if(!releasePoint) {
3230        return;
3231      }
3232
3233      if(!_moved && !_isMultitouch && !_numAnimations) {
3234        var p0 = releasePoint;
3235        if(tapTimer) {
3236          clearTimeout(tapTimer);
3237          tapTimer = null;
3238
3239          // Check if taped on the same place
3240          if ( _isNearbyPoints(p0, tapReleasePoint) ) {
3241            _shout('doubleTap', p0);
3242            return;
3243          }
3244        }
3245
3246        if(releasePoint.type === 'mouse') {
3247          _dispatchTapEvent(e, releasePoint, 'mouse');
3248          return;
3249        }
3250
3251        var clickedTagName = e.target.tagName.toUpperCase();
3252        // avoid double tap delay on buttons and elements that have class pswp__single-tap
3253        if(clickedTagName === 'BUTTON' || framework.hasClass(e.target, 'pswp__single-tap') ) {
3254          _dispatchTapEvent(e, releasePoint);
3255          return;
3256        }
3257
3258        _equalizePoints(tapReleasePoint, p0);
3259
3260        tapTimer = setTimeout(function() {
3261          _dispatchTapEvent(e, releasePoint);
3262          tapTimer = null;
3263        }, 300);
3264      }
3265    }
3266  }
3267});
3268
3269/*>>tap*/
3270
3271/*>>desktop-zoom*/
3272/**
3273 *
3274 * desktop-zoom.js:
3275 *
3276 * - Binds mousewheel event for paning zoomed image.
3277 * - Manages "dragging", "zoomed-in", "zoom-out" classes.
3278 *   (which are used for cursors and zoom icon)
3279 * - Adds toggleDesktopZoom function.
3280 * 
3281 */
3282
3283var _wheelDelta;
3284  
3285_registerModule('DesktopZoom', {
3286
3287  publicMethods: {
3288
3289    initDesktopZoom: function() {
3290
3291      if(_oldIE) {
3292        // no zoom for old IE (<=8)
3293        return;
3294      }
3295
3296      if(_likelyTouchDevice) {
3297        // if detected hardware touch support, we wait until mouse is used,
3298        // and only then apply desktop-zoom features
3299        _listen('mouseUsed', function() {
3300          self.setupDesktopZoom();
3301        });
3302      } else {
3303        self.setupDesktopZoom(true);
3304      }
3305
3306    },
3307
3308    setupDesktopZoom: function(onInit) {
3309
3310      _wheelDelta = {};
3311
3312      var events = 'wheel mousewheel DOMMouseScroll';
3313      
3314      _listen('bindEvents', function() {
3315        framework.bind(template, events,  self.handleMouseWheel);
3316      });
3317
3318      _listen('unbindEvents', function() {
3319        if(_wheelDelta) {
3320          framework.unbind(template, events, self.handleMouseWheel);
3321        }
3322      });
3323
3324      self.mouseZoomedIn = false;
3325
3326      var hasDraggingClass,
3327        updateZoomable = function() {
3328          if(self.mouseZoomedIn) {
3329            framework.removeClass(template, 'pswp--zoomed-in');
3330            self.mouseZoomedIn = false;
3331          }
3332          if(_currZoomLevel < 1) {
3333            framework.addClass(template, 'pswp--zoom-allowed');
3334          } else {
3335            framework.removeClass(template, 'pswp--zoom-allowed');
3336          }
3337          removeDraggingClass();
3338        },
3339        removeDraggingClass = function() {
3340          if(hasDraggingClass) {
3341            framework.removeClass(template, 'pswp--dragging');
3342            hasDraggingClass = false;
3343          }
3344        };
3345
3346      _listen('resize' , updateZoomable);
3347      _listen('afterChange' , updateZoomable);
3348      _listen('pointerDown', function() {
3349        if(self.mouseZoomedIn) {
3350          hasDraggingClass = true;
3351          framework.addClass(template, 'pswp--dragging');
3352        }
3353      });
3354      _listen('pointerUp', removeDraggingClass);
3355
3356      if(!onInit) {
3357        updateZoomable();
3358      }
3359      
3360    },
3361
3362    handleMouseWheel: function(e) {
3363
3364      if(_currZoomLevel <= self.currItem.fitRatio) {
3365        if( _options.modal ) {
3366
3367          if (!_options.closeOnScroll || _numAnimations || _isDragging) {
3368            e.preventDefault();
3369          } else if(_transformKey && Math.abs(e.deltaY) > 2) {
3370            // close PhotoSwipe
3371            // if browser supports transforms & scroll changed enough
3372            _closedByScroll = true;
3373            self.close();
3374          }
3375
3376        }
3377        return true;
3378      }
3379
3380      // allow just one event to fire
3381      e.stopPropagation();
3382
3383      // https://developer.mozilla.org/en-US/docs/Web/Events/wheel
3384      _wheelDelta.x = 0;
3385
3386      if('deltaX' in e) {
3387        if(e.deltaMode === 1 /* DOM_DELTA_LINE */) {
3388          // 18 - average line height
3389          _wheelDelta.x = e.deltaX * 18;
3390          _wheelDelta.y = e.deltaY * 18;
3391        } else {
3392          _wheelDelta.x = e.deltaX;
3393          _wheelDelta.y = e.deltaY;
3394        }
3395      } else if('wheelDelta' in e) {
3396        if(e.wheelDeltaX) {
3397          _wheelDelta.x = -0.16 * e.wheelDeltaX;
3398        }
3399        if(e.wheelDeltaY) {
3400          _wheelDelta.y = -0.16 * e.wheelDeltaY;
3401        } else {
3402          _wheelDelta.y = -0.16 * e.wheelDelta;
3403        }
3404      } else if('detail' in e) {
3405        _wheelDelta.y = e.detail;
3406      } else {
3407        return;
3408      }
3409
3410      _calculatePanBounds(_currZoomLevel, true);
3411
3412      var newPanX = _panOffset.x - _wheelDelta.x,
3413        newPanY = _panOffset.y - _wheelDelta.y;
3414
3415      // only prevent scrolling in nonmodal mode when not at edges
3416      if (_options.modal ||
3417        (
3418        newPanX <= _currPanBounds.min.x && newPanX >= _currPanBounds.max.x &&
3419        newPanY <= _currPanBounds.min.y && newPanY >= _currPanBounds.max.y
3420        ) ) {
3421        e.preventDefault();
3422      }
3423
3424      // TODO: use rAF instead of mousewheel?
3425      self.panTo(newPanX, newPanY);
3426    },
3427
3428    toggleDesktopZoom: function(centerPoint) {
3429      centerPoint = centerPoint || {x:_viewportSize.x/2 + _offset.x, y:_viewportSize.y/2 + _offset.y };
3430
3431      var doubleTapZoomLevel = _options.getDoubleTapZoom(true, self.currItem);
3432      var zoomOut = _currZoomLevel === doubleTapZoomLevel;
3433      
3434      self.mouseZoomedIn = !zoomOut;
3435
3436      self.zoomTo(zoomOut ? self.currItem.initialZoomLevel : doubleTapZoomLevel, centerPoint, 333);
3437      framework[ (!zoomOut ? 'add' : 'remove') + 'Class'](template, 'pswp--zoomed-in');
3438    }
3439
3440  }
3441});
3442
3443
3444/*>>desktop-zoom*/
3445
3446/*>>history*/
3447/**
3448 *
3449 * history.js:
3450 *
3451 * - Back button to close gallery.
3452 * 
3453 * - Unique URL for each slide: example.com/&pid=1&gid=3
3454 *   (where PID is picture index, and GID and gallery index)
3455 *   
3456 * - Switch URL when slides change.
3457 * 
3458 */
3459
3460
3461var _historyDefaultOptions = {
3462  history: true,
3463  galleryUID: 1
3464};
3465
3466var _historyUpdateTimeout,
3467  _hashChangeTimeout,
3468  _hashAnimCheckTimeout,
3469  _hashChangedByScript,
3470  _hashChangedByHistory,
3471  _hashReseted,
3472  _initialHash,
3473  _historyChanged,
3474  _closedFromURL,
3475  _urlChangedOnce,
3476  _windowLoc,
3477
3478  _supportsPushState,
3479
3480  _getHash = function() {
3481    return _windowLoc.hash.substring(1);
3482  },
3483  _cleanHistoryTimeouts = function() {
3484
3485    if(_historyUpdateTimeout) {
3486      clearTimeout(_historyUpdateTimeout);
3487    }
3488
3489    if(_hashAnimCheckTimeout) {
3490      clearTimeout(_hashAnimCheckTimeout);
3491    }
3492  },
3493
3494  // pid - Picture index
3495  // gid - Gallery index
3496  _parseItemIndexFromURL = function() {
3497    var hash = _getHash(),
3498      params = {};
3499
3500    if(hash.length < 5) { // pid=1
3501      return params;
3502    }
3503
3504    var i, vars = hash.split('&');
3505    for (i = 0; i < vars.length; i++) {
3506      if(!vars[i]) {
3507        continue;
3508      }
3509      var pair = vars[i].split('=');  
3510      if(pair.length < 2) {
3511        continue;
3512      }
3513      params[pair[0]] = pair[1];
3514    }
3515    if(_options.galleryPIDs) {
3516      // detect custom pid in hash and search for it among the items collection
3517      var searchfor = params.pid;
3518      params.pid = 0; // if custom pid cannot be found, fallback to the first item
3519      for(i = 0; i < _items.length; i++) {
3520        if(_items[i].pid === searchfor) {
3521          params.pid = i;
3522          break;
3523        }
3524      }
3525    } else {
3526      params.pid = parseInt(params.pid,10)-1;
3527    }
3528    if( params.pid < 0 ) {
3529      params.pid = 0;
3530    }
3531    return params;
3532  },
3533  _updateHash = function() {
3534
3535    if(_hashAnimCheckTimeout) {
3536      clearTimeout(_hashAnimCheckTimeout);
3537    }
3538
3539
3540    if(_numAnimations || _isDragging) {
3541      // changing browser URL forces layout/paint in some browsers, which causes noticable lag during animation
3542      // that's why we update hash only when no animations running
3543      _hashAnimCheckTimeout = setTimeout(_updateHash, 500);
3544      return;
3545    }
3546    
3547    if(_hashChangedByScript) {
3548      clearTimeout(_hashChangeTimeout);
3549    } else {
3550      _hashChangedByScript = true;
3551    }
3552
3553
3554    var pid = (_currentItemIndex + 1);
3555    var item = _getItemAt( _currentItemIndex );
3556    if(item.hasOwnProperty('pid')) {
3557      // carry forward any custom pid assigned to the item
3558      pid = item.pid;
3559    }
3560    var newHash = _initialHash + '&'  +  'gid=' + _options.galleryUID + '&' + 'pid=' + pid;
3561
3562    if(!_historyChanged) {
3563      if(_windowLoc.hash.indexOf(newHash) === -1) {
3564        _urlChangedOnce = true;
3565      }
3566      // first time - add new hisory record, then just replace
3567    }
3568
3569    var newURL = _windowLoc.href.split('#')[0] + '#' +  newHash;
3570
3571    if( _supportsPushState ) {
3572
3573      if('#' + newHash !== window.location.hash) {
3574        history[_historyChanged ? 'replaceState' : 'pushState']('', document.title, newURL);
3575      }
3576
3577    } else {
3578      if(_historyChanged) {
3579        _windowLoc.replace( newURL );
3580      } else {
3581        _windowLoc.hash = newHash;
3582      }
3583    }
3584    
3585    
3586
3587    _historyChanged = true;
3588    _hashChangeTimeout = setTimeout(function() {
3589      _hashChangedByScript = false;
3590    }, 60);
3591  };
3592
3593
3594
3595  
3596
3597_registerModule('History', {
3598
3599  
3600
3601  publicMethods: {
3602    initHistory: function() {
3603
3604      framework.extend(_options, _historyDefaultOptions, true);
3605
3606      if( !_options.history ) {
3607        return;
3608      }
3609
3610
3611      _windowLoc = window.location;
3612      _urlChangedOnce = false;
3613      _closedFromURL = false;
3614      _historyChanged = false;
3615      _initialHash = _getHash();
3616      _supportsPushState = ('pushState' in history);
3617
3618
3619      if(_initialHash.indexOf('gid=') > -1) {
3620        _initialHash = _initialHash.split('&gid=')[0];
3621        _initialHash = _initialHash.split('?gid=')[0];
3622      }
3623      
3624
3625      _listen('afterChange', self.updateURL);
3626      _listen('unbindEvents', function() {
3627        framework.unbind(window, 'hashchange', self.onHashChange);
3628      });
3629
3630
3631      var returnToOriginal = function() {
3632        _hashReseted = true;
3633        if(!_closedFromURL) {
3634
3635          if(_urlChangedOnce) {
3636            history.back();
3637          } else {
3638
3639            if(_initialHash) {
3640              _windowLoc.hash = _initialHash;
3641            } else {
3642              if (_supportsPushState) {
3643
3644                // remove hash from url without refreshing it or scrolling to top
3645                history.pushState('', document.title,  _windowLoc.pathname + _windowLoc.search );
3646              } else {
3647                _windowLoc.hash = '';
3648              }
3649            }
3650          }
3651          
3652        }
3653
3654        _cleanHistoryTimeouts();
3655      };
3656
3657
3658      _listen('unbindEvents', function() {
3659        if(_closedByScroll) {
3660          // if PhotoSwipe is closed by scroll, we go "back" before the closing animation starts
3661          // this is done to keep the scroll position
3662          returnToOriginal();
3663        }
3664      });
3665      _listen('destroy', function() {
3666        if(!_hashReseted) {
3667          returnToOriginal();
3668        }
3669      });
3670      _listen('firstUpdate', function() {
3671        _currentItemIndex = _parseItemIndexFromURL().pid;
3672      });
3673
3674      
3675
3676      
3677      var index = _initialHash.indexOf('pid=');
3678      if(index > -1) {
3679        _initialHash = _initialHash.substring(0, index);
3680        if(_initialHash.slice(-1) === '&') {
3681          _initialHash = _initialHash.slice(0, -1);
3682        }
3683      }
3684      
3685
3686      setTimeout(function() {
3687        if(_isOpen) { // hasn't destroyed yet
3688          framework.bind(window, 'hashchange', self.onHashChange);
3689        }
3690      }, 40);
3691      
3692    },
3693    onHashChange: function() {
3694
3695      if(_getHash() === _initialHash) {
3696
3697        _closedFromURL = true;
3698        self.close();
3699        return;
3700      }
3701      if(!_hashChangedByScript) {
3702
3703        _hashChangedByHistory = true;
3704        self.goTo( _parseItemIndexFromURL().pid );
3705        _hashChangedByHistory = false;
3706      }
3707      
3708    },
3709    updateURL: function() {
3710
3711      // Delay the update of URL, to avoid lag during transition, 
3712      // and to not to trigger actions like "refresh page sound" or "blinking favicon" to often
3713      
3714      _cleanHistoryTimeouts();
3715      
3716
3717      if(_hashChangedByHistory) {
3718        return;
3719      }
3720
3721      if(!_historyChanged) {
3722        _updateHash(); // first time
3723      } else {
3724        _historyUpdateTimeout = setTimeout(_updateHash, 800);
3725      }
3726    }
3727  
3728  }
3729});
3730
3731
3732/*>>history*/
3733  framework.extend(self, publicMethods); };
3734  return PhotoSwipe;
3735});

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.