xinyb
2024-04-23 2ef61846432762b570d5f2004a4551225da01324
提交 | 用户 | age
a6a76f 1 /*! iScroll v5.1.3 ~ (c) 2008-2014 Matteo Spinelli ~ http://cubiq.org/license */
F 2 (function (window, document, Math) {
3 var rAF = window.requestAnimationFrame    ||
4     window.webkitRequestAnimationFrame    ||
5     window.mozRequestAnimationFrame        ||
6     window.oRequestAnimationFrame        ||
7     window.msRequestAnimationFrame        ||
8     function (callback) { window.setTimeout(callback, 1000 / 60); };
9
10 var utils = (function () {
11     var me = {};
12
13     var _elementStyle = document.createElement('div').style;
14     var _vendor = (function () {
15         var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'],
16             transform,
17             i = 0,
18             l = vendors.length;
19
20         for ( ; i < l; i++ ) {
21             transform = vendors[i] + 'ransform';
22             if ( transform in _elementStyle ) return vendors[i].substr(0, vendors[i].length-1);
23         }
24
25         return false;
26     })();
27
28     function _prefixStyle (style) {
29         if ( _vendor === false ) return false;
30         if ( _vendor === '' ) return style;
31         return _vendor + style.charAt(0).toUpperCase() + style.substr(1);
32     }
33
34     me.getTime = Date.now || function getTime () { return new Date().getTime(); };
35
36     me.extend = function (target, obj) {
37         for ( var i in obj ) {
38             target[i] = obj[i];
39         }
40     };
41
42     me.addEvent = function (el, type, fn, capture) {
43         el.addEventListener(type, fn, !!capture);
44     };
45
46     me.removeEvent = function (el, type, fn, capture) {
47         el.removeEventListener(type, fn, !!capture);
48     };
49
50     me.prefixPointerEvent = function (pointerEvent) {
51         return window.MSPointerEvent ? 
52             'MSPointer' + pointerEvent.charAt(9).toUpperCase() + pointerEvent.substr(10):
53             pointerEvent;
54     };
55
56     me.momentum = function (current, start, time, lowerMargin, wrapperSize, deceleration) {
57         var distance = current - start,
58             speed = Math.abs(distance) / time,
59             destination,
60             duration;
61
62         deceleration = deceleration === undefined ? 0.0006 : deceleration;
63
64         destination = current + ( speed * speed ) / ( 2 * deceleration ) * ( distance < 0 ? -1 : 1 );
65         duration = speed / deceleration;
66
67         if ( destination < lowerMargin ) {
68             destination = wrapperSize ? lowerMargin - ( wrapperSize / 2.5 * ( speed / 8 ) ) : lowerMargin;
69             distance = Math.abs(destination - current);
70             duration = distance / speed;
71         } else if ( destination > 0 ) {
72             destination = wrapperSize ? wrapperSize / 2.5 * ( speed / 8 ) : 0;
73             distance = Math.abs(current) + destination;
74             duration = distance / speed;
75         }
76
77         return {
78             destination: Math.round(destination),
79             duration: duration
80         };
81     };
82
83     var _transform = _prefixStyle('transform');
84
85     me.extend(me, {
86         hasTransform: _transform !== false,
87         hasPerspective: _prefixStyle('perspective') in _elementStyle,
88         hasTouch: 'ontouchstart' in window,
89         hasPointer: window.PointerEvent || window.MSPointerEvent, // IE10 is prefixed
90         hasTransition: _prefixStyle('transition') in _elementStyle
91     });
92
93     // This should find all Android browsers lower than build 535.19 (both stock browser and webview)
94     me.isBadAndroid = /Android /.test(window.navigator.appVersion) && !(/Chrome\/\d/.test(window.navigator.appVersion));
95
96     me.extend(me.style = {}, {
97         transform: _transform,
98         transitionTimingFunction: _prefixStyle('transitionTimingFunction'),
99         transitionDuration: _prefixStyle('transitionDuration'),
100         transitionDelay: _prefixStyle('transitionDelay'),
101         transformOrigin: _prefixStyle('transformOrigin')
102     });
103
104     me.hasClass = function (e, c) {
105         var re = new RegExp("(^|\\s)" + c + "(\\s|$)");
106         return re.test(e.className);
107     };
108
109     me.addClass = function (e, c) {
110         if ( me.hasClass(e, c) ) {
111             return;
112         }
113
114         var newclass = e.className.split(' ');
115         newclass.push(c);
116         e.className = newclass.join(' ');
117     };
118
119     me.removeClass = function (e, c) {
120         if ( !me.hasClass(e, c) ) {
121             return;
122         }
123
124         var re = new RegExp("(^|\\s)" + c + "(\\s|$)", 'g');
125         e.className = e.className.replace(re, ' ');
126     };
127
128     me.offset = function (el) {
129         var left = -el.offsetLeft,
130             top = -el.offsetTop;
131
132         // jshint -W084
133         while (el = el.offsetParent) {
134             left -= el.offsetLeft;
135             top -= el.offsetTop;
136         }
137         // jshint +W084
138
139         return {
140             left: left,
141             top: top
142         };
143     };
144
145     me.preventDefaultException = function (el, exceptions) {
146         for ( var i in exceptions ) {
147             if ( exceptions[i].test(el[i]) ) {
148                 return true;
149             }
150         }
151
152         return false;
153     };
154
155     me.extend(me.eventType = {}, {
156         touchstart: 1,
157         touchmove: 1,
158         touchend: 1,
159
160         mousedown: 2,
161         mousemove: 2,
162         mouseup: 2,
163
164         pointerdown: 3,
165         pointermove: 3,
166         pointerup: 3,
167
168         MSPointerDown: 3,
169         MSPointerMove: 3,
170         MSPointerUp: 3
171     });
172
173     me.extend(me.ease = {}, {
174         quadratic: {
175             style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
176             fn: function (k) {
177                 return k * ( 2 - k );
178             }
179         },
180         circular: {
181             style: 'cubic-bezier(0.1, 0.57, 0.1, 1)',    // Not properly "circular" but this looks better, it should be (0.075, 0.82, 0.165, 1)
182             fn: function (k) {
183                 return Math.sqrt( 1 - ( --k * k ) );
184             }
185         },
186         back: {
187             style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
188             fn: function (k) {
189                 var b = 4;
190                 return ( k = k - 1 ) * k * ( ( b + 1 ) * k + b ) + 1;
191             }
192         },
193         bounce: {
194             style: '',
195             fn: function (k) {
196                 if ( ( k /= 1 ) < ( 1 / 2.75 ) ) {
197                     return 7.5625 * k * k;
198                 } else if ( k < ( 2 / 2.75 ) ) {
199                     return 7.5625 * ( k -= ( 1.5 / 2.75 ) ) * k + 0.75;
200                 } else if ( k < ( 2.5 / 2.75 ) ) {
201                     return 7.5625 * ( k -= ( 2.25 / 2.75 ) ) * k + 0.9375;
202                 } else {
203                     return 7.5625 * ( k -= ( 2.625 / 2.75 ) ) * k + 0.984375;
204                 }
205             }
206         },
207         elastic: {
208             style: '',
209             fn: function (k) {
210                 var f = 0.22,
211                     e = 0.4;
212
213                 if ( k === 0 ) { return 0; }
214                 if ( k == 1 ) { return 1; }
215
216                 return ( e * Math.pow( 2, - 10 * k ) * Math.sin( ( k - f / 4 ) * ( 2 * Math.PI ) / f ) + 1 );
217             }
218         }
219     });
220
221     me.tap = function (e, eventName) {
222         var ev = document.createEvent('Event');
223         ev.initEvent(eventName, true, true);
224         ev.pageX = e.pageX;
225         ev.pageY = e.pageY;
226         e.target.dispatchEvent(ev);
227     };
228
229     me.click = function (e) {
230         var target = e.target,
231             ev;
232
233         if ( !(/(SELECT|INPUT|TEXTAREA)/i).test(target.tagName) ) {
234             ev = document.createEvent('MouseEvents');
235             ev.initMouseEvent('click', true, true, e.view, 1,
236                 target.screenX, target.screenY, target.clientX, target.clientY,
237                 e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
238                 0, null);
239
240             ev._constructed = true;
241             target.dispatchEvent(ev);
242         }
243     };
244
245     return me;
246 })();
247
248 function IScroll (el, options) {
249     this.wrapper = typeof el == 'string' ? document.querySelector(el) : el;
250     this.scroller = this.wrapper.children[0];
251     this.scrollerStyle = this.scroller.style;        // cache style for better performance
252
253     this.options = {
254
255         resizeScrollbars: true,
256
257         mouseWheelSpeed: 20,
258
259         snapThreshold: 0.334,
260
261 // INSERT POINT: OPTIONS 
262
263         startX: 0,
264         startY: 0,
265         scrollY: true,
266         directionLockThreshold: 5,
267         momentum: true,
268
269         bounce: true,
270         bounceTime: 600,
271         bounceEasing: '',
272
273         preventDefault: true,
274         preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT)$/ },
275
276         HWCompositing: true,
277         useTransition: true,
278         useTransform: true
279     };
280
281     for ( var i in options ) {
282         this.options[i] = options[i];
283     }
284
285     // Normalize options
286     this.translateZ = this.options.HWCompositing && utils.hasPerspective ? ' translateZ(0)' : '';
287
288     this.options.useTransition = utils.hasTransition && this.options.useTransition;
289     this.options.useTransform = utils.hasTransform && this.options.useTransform;
290
291     this.options.eventPassthrough = this.options.eventPassthrough === true ? 'vertical' : this.options.eventPassthrough;
292     this.options.preventDefault = !this.options.eventPassthrough && this.options.preventDefault;
293
294     // If you want eventPassthrough I have to lock one of the axes
295     this.options.scrollY = this.options.eventPassthrough == 'vertical' ? false : this.options.scrollY;
296     this.options.scrollX = this.options.eventPassthrough == 'horizontal' ? false : this.options.scrollX;
297
298     // With eventPassthrough we also need lockDirection mechanism
299     this.options.freeScroll = this.options.freeScroll && !this.options.eventPassthrough;
300     this.options.directionLockThreshold = this.options.eventPassthrough ? 0 : this.options.directionLockThreshold;
301
302     this.options.bounceEasing = typeof this.options.bounceEasing == 'string' ? utils.ease[this.options.bounceEasing] || utils.ease.circular : this.options.bounceEasing;
303
304     this.options.resizePolling = this.options.resizePolling === undefined ? 60 : this.options.resizePolling;
305
306     if ( this.options.tap === true ) {
307         this.options.tap = 'tap';
308     }
309
310     if ( this.options.shrinkScrollbars == 'scale' ) {
311         this.options.useTransition = false;
312     }
313
314     this.options.invertWheelDirection = this.options.invertWheelDirection ? -1 : 1;
315
316 // INSERT POINT: NORMALIZATION
317
318     // Some defaults    
319     this.x = 0;
320     this.y = 0;
321     this.directionX = 0;
322     this.directionY = 0;
323     this._events = {};
324
325 // INSERT POINT: DEFAULTS
326
327     this._init();
328     this.refresh();
329
330     this.scrollTo(this.options.startX, this.options.startY);
331     this.enable();
332 }
333
334 IScroll.prototype = {
335     version: '5.1.3',
336
337     _init: function () {
338         this._initEvents();
339
340         if ( this.options.scrollbars || this.options.indicators ) {
341             this._initIndicators();
342         }
343
344         if ( this.options.mouseWheel ) {
345             this._initWheel();
346         }
347
348         if ( this.options.snap ) {
349             this._initSnap();
350         }
351
352         if ( this.options.keyBindings ) {
353             this._initKeys();
354         }
355
356 // INSERT POINT: _init
357
358     },
359
360     destroy: function () {
361         this._initEvents(true);
362
363         this._execEvent('destroy');
364     },
365
366     _transitionEnd: function (e) {
367         if ( e.target != this.scroller || !this.isInTransition ) {
368             return;
369         }
370
371         this._transitionTime();
372         if ( !this.resetPosition(this.options.bounceTime) ) {
373             this.isInTransition = false;
374             this._execEvent('scrollEnd');
375         }
376     },
377
378     _start: function (e) {
379         // React to left mouse button only
380         if ( utils.eventType[e.type] != 1 ) {
381             if ( e.button !== 0 ) {
382                 return;
383             }
384         }
385
386         if ( !this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated) ) {
387             return;
388         }
389
390         if ( this.options.preventDefault && !utils.isBadAndroid && !utils.preventDefaultException(e.target, this.options.preventDefaultException) ) {
391             e.preventDefault();
392         }
393
394         var point = e.touches ? e.touches[0] : e,
395             pos;
396
397         this.initiated    = utils.eventType[e.type];
398         this.moved        = false;
399         this.distX        = 0;
400         this.distY        = 0;
401         this.directionX = 0;
402         this.directionY = 0;
403         this.directionLocked = 0;
404
405         this._transitionTime();
406
407         this.startTime = utils.getTime();
408
409         if ( this.options.useTransition && this.isInTransition ) {
410             this.isInTransition = false;
411             pos = this.getComputedPosition();
412             this._translate(Math.round(pos.x), Math.round(pos.y));
413             this._execEvent('scrollEnd');
414         } else if ( !this.options.useTransition && this.isAnimating ) {
415             this.isAnimating = false;
416             this._execEvent('scrollEnd');
417         }
418
419         this.startX    = this.x;
420         this.startY    = this.y;
421         this.absStartX = this.x;
422         this.absStartY = this.y;
423         this.pointX    = point.pageX;
424         this.pointY    = point.pageY;
425
426         this._execEvent('beforeScrollStart');
427     },
428
429     _move: function (e) {
430         if ( !this.enabled || utils.eventType[e.type] !== this.initiated ) {
431             return;
432         }
433
434         if ( this.options.preventDefault ) {    // increases performance on Android? TODO: check!
435             e.preventDefault();
436         }
437
438         var point        = e.touches ? e.touches[0] : e,
439             deltaX        = point.pageX - this.pointX,
440             deltaY        = point.pageY - this.pointY,
441             timestamp    = utils.getTime(),
442             newX, newY,
443             absDistX, absDistY;
444
445         this.pointX        = point.pageX;
446         this.pointY        = point.pageY;
447
448         this.distX        += deltaX;
449         this.distY        += deltaY;
450         absDistX        = Math.abs(this.distX);
451         absDistY        = Math.abs(this.distY);
452
453         // We need to move at least 10 pixels for the scrolling to initiate
454         if ( timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10) ) {
455             return;
456         }
457
458         // If you are scrolling in one direction lock the other
459         if ( !this.directionLocked && !this.options.freeScroll ) {
460             if ( absDistX > absDistY + this.options.directionLockThreshold ) {
461                 this.directionLocked = 'h';        // lock horizontally
462             } else if ( absDistY >= absDistX + this.options.directionLockThreshold ) {
463                 this.directionLocked = 'v';        // lock vertically
464             } else {
465                 this.directionLocked = 'n';        // no lock
466             }
467         }
468
469         if ( this.directionLocked == 'h' ) {
470             if ( this.options.eventPassthrough == 'vertical' ) {
471                 e.preventDefault();
472             } else if ( this.options.eventPassthrough == 'horizontal' ) {
473                 this.initiated = false;
474                 return;
475             }
476
477             deltaY = 0;
478         } else if ( this.directionLocked == 'v' ) {
479             if ( this.options.eventPassthrough == 'horizontal' ) {
480                 e.preventDefault();
481             } else if ( this.options.eventPassthrough == 'vertical' ) {
482                 this.initiated = false;
483                 return;
484             }
485
486             deltaX = 0;
487         }
488
489         deltaX = this.hasHorizontalScroll ? deltaX : 0;
490         deltaY = this.hasVerticalScroll ? deltaY : 0;
491
492         newX = this.x + deltaX;
493         newY = this.y + deltaY;
494
495         // Slow down if outside of the boundaries
496         if ( newX > 0 || newX < this.maxScrollX ) {
497             newX = this.options.bounce ? this.x + deltaX / 3 : newX > 0 ? 0 : this.maxScrollX;
498         }
499         if ( newY > 0 || newY < this.maxScrollY ) {
500             newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY;
501         }
502
503         this.directionX = deltaX > 0 ? -1 : deltaX < 0 ? 1 : 0;
504         this.directionY = deltaY > 0 ? -1 : deltaY < 0 ? 1 : 0;
505
506         if ( !this.moved ) {
507             this._execEvent('scrollStart');
508         }
509
510         this.moved = true;
511
512         this._translate(newX, newY);
513
514 /* REPLACE START: _move */
515
516         if ( timestamp - this.startTime > 300 ) {
517             this.startTime = timestamp;
518             this.startX = this.x;
519             this.startY = this.y;
520         }
521
522 /* REPLACE END: _move */
523
524     },
525
526     _end: function (e) {
527         if ( !this.enabled || utils.eventType[e.type] !== this.initiated ) {
528             return;
529         }
530
531         if ( this.options.preventDefault && !utils.preventDefaultException(e.target, this.options.preventDefaultException) ) {
532             e.preventDefault();
533         }
534
535         var point = e.changedTouches ? e.changedTouches[0] : e,
536             momentumX,
537             momentumY,
538             duration = utils.getTime() - this.startTime,
539             newX = Math.round(this.x),
540             newY = Math.round(this.y),
541             distanceX = Math.abs(newX - this.startX),
542             distanceY = Math.abs(newY - this.startY),
543             time = 0,
544             easing = '';
545
546         this.isInTransition = 0;
547         this.initiated = 0;
548         this.endTime = utils.getTime();
549
550         // reset if we are outside of the boundaries
551         if ( this.resetPosition(this.options.bounceTime) ) {
552             return;
553         }
554
555         this.scrollTo(newX, newY);    // ensures that the last position is rounded
556
557         // we scrolled less than 10 pixels
558         if ( !this.moved ) {
559             if ( this.options.tap ) {
560                 utils.tap(e, this.options.tap);
561             }
562
563             if ( this.options.click ) {
564                 utils.click(e);
565             }
566
567             this._execEvent('scrollCancel');
568             return;
569         }
570
571         if ( this._events.flick && duration < 200 && distanceX < 100 && distanceY < 100 ) {
572             this._execEvent('flick');
573             return;
574         }
575
576         // start momentum animation if needed
577         if ( this.options.momentum && duration < 300 ) {
578             momentumX = this.hasHorizontalScroll ? utils.momentum(this.x, this.startX, duration, this.maxScrollX, this.options.bounce ? this.wrapperWidth : 0, this.options.deceleration) : { destination: newX, duration: 0 };
579             momentumY = this.hasVerticalScroll ? utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0, this.options.deceleration) : { destination: newY, duration: 0 };
580             newX = momentumX.destination;
581             newY = momentumY.destination;
582             time = Math.max(momentumX.duration, momentumY.duration);
583             this.isInTransition = 1;
584         }
585
586
587         if ( this.options.snap ) {
588             var snap = this._nearestSnap(newX, newY);
589             this.currentPage = snap;
590             time = this.options.snapSpeed || Math.max(
591                     Math.max(
592                         Math.min(Math.abs(newX - snap.x), 1000),
593                         Math.min(Math.abs(newY - snap.y), 1000)
594                     ), 300);
595             newX = snap.x;
596             newY = snap.y;
597
598             this.directionX = 0;
599             this.directionY = 0;
600             easing = this.options.bounceEasing;
601         }
602
603 // INSERT POINT: _end
604
605         if ( newX != this.x || newY != this.y ) {
606             // change easing function when scroller goes out of the boundaries
607             if ( newX > 0 || newX < this.maxScrollX || newY > 0 || newY < this.maxScrollY ) {
608                 easing = utils.ease.quadratic;
609             }
610
611             this.scrollTo(newX, newY, time, easing);
612             return;
613         }
614
615         this._execEvent('scrollEnd');
616     },
617
618     _resize: function () {
619         var that = this;
620
621         clearTimeout(this.resizeTimeout);
622
623         this.resizeTimeout = setTimeout(function () {
624             that.refresh();
625         }, this.options.resizePolling);
626     },
627
628     resetPosition: function (time) {
629         var x = this.x,
630             y = this.y;
631
632         time = time || 0;
633
634         if ( !this.hasHorizontalScroll || this.x > 0 ) {
635             x = 0;
636         } else if ( this.x < this.maxScrollX ) {
637             x = this.maxScrollX;
638         }
639
640         if ( !this.hasVerticalScroll || this.y > 0 ) {
641             y = 0;
642         } else if ( this.y < this.maxScrollY ) {
643             y = this.maxScrollY;
644         }
645
646         if ( x == this.x && y == this.y ) {
647             return false;
648         }
649
650         this.scrollTo(x, y, time, this.options.bounceEasing);
651
652         return true;
653     },
654
655     disable: function () {
656         this.enabled = false;
657     },
658
659     enable: function () {
660         this.enabled = true;
661     },
662
663     refresh: function () {
664         var rf = this.wrapper.offsetHeight;        // Force reflow
665
666         this.wrapperWidth    = this.wrapper.clientWidth;
667         this.wrapperHeight    = this.wrapper.clientHeight;
668
669 /* REPLACE START: refresh */
670
671         this.scrollerWidth    = this.scroller.offsetWidth;
672         this.scrollerHeight    = this.scroller.offsetHeight;
673
674         this.maxScrollX        = this.wrapperWidth - this.scrollerWidth;
675         this.maxScrollY        = this.wrapperHeight - this.scrollerHeight;
676
677 /* REPLACE END: refresh */
678
679         this.hasHorizontalScroll    = this.options.scrollX && this.maxScrollX < 0;
680         this.hasVerticalScroll        = this.options.scrollY && this.maxScrollY < 0;
681
682         if ( !this.hasHorizontalScroll ) {
683             this.maxScrollX = 0;
684             this.scrollerWidth = this.wrapperWidth;
685         }
686
687         if ( !this.hasVerticalScroll ) {
688             this.maxScrollY = 0;
689             this.scrollerHeight = this.wrapperHeight;
690         }
691
692         this.endTime = 0;
693         this.directionX = 0;
694         this.directionY = 0;
695
696         this.wrapperOffset = utils.offset(this.wrapper);
697
698         this._execEvent('refresh');
699
700         this.resetPosition();
701
702 // INSERT POINT: _refresh
703
704     },
705
706     on: function (type, fn) {
707         if ( !this._events[type] ) {
708             this._events[type] = [];
709         }
710
711         this._events[type].push(fn);
712     },
713
714     off: function (type, fn) {
715         if ( !this._events[type] ) {
716             return;
717         }
718
719         var index = this._events[type].indexOf(fn);
720
721         if ( index > -1 ) {
722             this._events[type].splice(index, 1);
723         }
724     },
725
726     _execEvent: function (type) {
727         if ( !this._events[type] ) {
728             return;
729         }
730
731         var i = 0,
732             l = this._events[type].length;
733
734         if ( !l ) {
735             return;
736         }
737
738         for ( ; i < l; i++ ) {
739             this._events[type][i].apply(this, [].slice.call(arguments, 1));
740         }
741     },
742
743     scrollBy: function (x, y, time, easing) {
744         x = this.x + x;
745         y = this.y + y;
746         time = time || 0;
747
748         this.scrollTo(x, y, time, easing);
749     },
750
751     scrollTo: function (x, y, time, easing) {
752         easing = easing || utils.ease.circular;
753
754         this.isInTransition = this.options.useTransition && time > 0;
755
756         if ( !time || (this.options.useTransition && easing.style) ) {
757             this._transitionTimingFunction(easing.style);
758             this._transitionTime(time);
759             this._translate(x, y);
760         } else {
761             this._animate(x, y, time, easing.fn);
762         }
763     },
764
765     scrollToElement: function (el, time, offsetX, offsetY, easing) {
766         el = el.nodeType ? el : this.scroller.querySelector(el);
767
768         if ( !el ) {
769             return;
770         }
771
772         var pos = utils.offset(el);
773
774         pos.left -= this.wrapperOffset.left;
775         pos.top  -= this.wrapperOffset.top;
776
777         // if offsetX/Y are true we center the element to the screen
778         if ( offsetX === true ) {
779             offsetX = Math.round(el.offsetWidth / 2 - this.wrapper.offsetWidth / 2);
780         }
781         if ( offsetY === true ) {
782             offsetY = Math.round(el.offsetHeight / 2 - this.wrapper.offsetHeight / 2);
783         }
784
785         pos.left -= offsetX || 0;
786         pos.top  -= offsetY || 0;
787
788         pos.left = pos.left > 0 ? 0 : pos.left < this.maxScrollX ? this.maxScrollX : pos.left;
789         pos.top  = pos.top  > 0 ? 0 : pos.top  < this.maxScrollY ? this.maxScrollY : pos.top;
790
791         time = time === undefined || time === null || time === 'auto' ? Math.max(Math.abs(this.x-pos.left), Math.abs(this.y-pos.top)) : time;
792
793         this.scrollTo(pos.left, pos.top, time, easing);
794     },
795
796     _transitionTime: function (time) {
797         time = time || 0;
798
799         this.scrollerStyle[utils.style.transitionDuration] = time + 'ms';
800
801         if ( !time && utils.isBadAndroid ) {
802             this.scrollerStyle[utils.style.transitionDuration] = '0.001s';
803         }
804
805
806         if ( this.indicators ) {
807             for ( var i = this.indicators.length; i--; ) {
808                 this.indicators[i].transitionTime(time);
809             }
810         }
811
812
813 // INSERT POINT: _transitionTime
814
815     },
816
817     _transitionTimingFunction: function (easing) {
818         this.scrollerStyle[utils.style.transitionTimingFunction] = easing;
819
820
821         if ( this.indicators ) {
822             for ( var i = this.indicators.length; i--; ) {
823                 this.indicators[i].transitionTimingFunction(easing);
824             }
825         }
826
827
828 // INSERT POINT: _transitionTimingFunction
829
830     },
831
832     _translate: function (x, y) {
833         if ( this.options.useTransform ) {
834
835 /* REPLACE START: _translate */
836
837             this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
838
839 /* REPLACE END: _translate */
840
841         } else {
842             x = Math.round(x);
843             y = Math.round(y);
844             this.scrollerStyle.left = x + 'px';
845             this.scrollerStyle.top = y + 'px';
846         }
847
848         this.x = x;
849         this.y = y;
850
851
852     if ( this.indicators ) {
853         for ( var i = this.indicators.length; i--; ) {
854             this.indicators[i].updatePosition();
855         }
856     }
857
858
859 // INSERT POINT: _translate
860
861     },
862
863     _initEvents: function (remove) {
864         var eventType = remove ? utils.removeEvent : utils.addEvent,
865             target = this.options.bindToWrapper ? this.wrapper : window;
866
867         eventType(window, 'orientationchange', this);
868         eventType(window, 'resize', this);
869
870         if ( this.options.click ) {
871             eventType(this.wrapper, 'click', this, true);
872         }
873
874         if ( !this.options.disableMouse ) {
875             eventType(this.wrapper, 'mousedown', this);
876             eventType(target, 'mousemove', this);
877             eventType(target, 'mousecancel', this);
878             eventType(target, 'mouseup', this);
879         }
880
881         if ( utils.hasPointer && !this.options.disablePointer ) {
882             eventType(this.wrapper, utils.prefixPointerEvent('pointerdown'), this);
883             eventType(target, utils.prefixPointerEvent('pointermove'), this);
884             eventType(target, utils.prefixPointerEvent('pointercancel'), this);
885             eventType(target, utils.prefixPointerEvent('pointerup'), this);
886         }
887
888         if ( utils.hasTouch && !this.options.disableTouch ) {
889             eventType(this.wrapper, 'touchstart', this);
890             eventType(target, 'touchmove', this);
891             eventType(target, 'touchcancel', this);
892             eventType(target, 'touchend', this);
893         }
894
895         eventType(this.scroller, 'transitionend', this);
896         eventType(this.scroller, 'webkitTransitionEnd', this);
897         eventType(this.scroller, 'oTransitionEnd', this);
898         eventType(this.scroller, 'MSTransitionEnd', this);
899     },
900
901     getComputedPosition: function () {
902         var matrix = window.getComputedStyle(this.scroller, null),
903             x, y;
904
905         if ( this.options.useTransform ) {
906             matrix = matrix[utils.style.transform].split(')')[0].split(', ');
907             x = +(matrix[12] || matrix[4]);
908             y = +(matrix[13] || matrix[5]);
909         } else {
910             x = +matrix.left.replace(/[^-\d.]/g, '');
911             y = +matrix.top.replace(/[^-\d.]/g, '');
912         }
913
914         return { x: x, y: y };
915     },
916
917     _initIndicators: function () {
918         var interactive = this.options.interactiveScrollbars,
919             customStyle = typeof this.options.scrollbars != 'string',
920             indicators = [],
921             indicator;
922
923         var that = this;
924
925         this.indicators = [];
926
927         if ( this.options.scrollbars ) {
928             // Vertical scrollbar
929             if ( this.options.scrollY ) {
930                 indicator = {
931                     el: createDefaultScrollbar('v', interactive, this.options.scrollbars),
932                     interactive: interactive,
933                     defaultScrollbars: true,
934                     customStyle: customStyle,
935                     resize: this.options.resizeScrollbars,
936                     shrink: this.options.shrinkScrollbars,
937                     fade: this.options.fadeScrollbars,
938                     listenX: false
939                 };
940
941                 this.wrapper.appendChild(indicator.el);
942                 indicators.push(indicator);
943             }
944
945             // Horizontal scrollbar
946             if ( this.options.scrollX ) {
947                 indicator = {
948                     el: createDefaultScrollbar('h', interactive, this.options.scrollbars),
949                     interactive: interactive,
950                     defaultScrollbars: true,
951                     customStyle: customStyle,
952                     resize: this.options.resizeScrollbars,
953                     shrink: this.options.shrinkScrollbars,
954                     fade: this.options.fadeScrollbars,
955                     listenY: false
956                 };
957
958                 this.wrapper.appendChild(indicator.el);
959                 indicators.push(indicator);
960             }
961         }
962
963         if ( this.options.indicators ) {
964             // TODO: check concat compatibility
965             indicators = indicators.concat(this.options.indicators);
966         }
967
968         for ( var i = indicators.length; i--; ) {
969             this.indicators.push( new Indicator(this, indicators[i]) );
970         }
971
972         // TODO: check if we can use array.map (wide compatibility and performance issues)
973         function _indicatorsMap (fn) {
974             for ( var i = that.indicators.length; i--; ) {
975                 fn.call(that.indicators[i]);
976             }
977         }
978
979         if ( this.options.fadeScrollbars ) {
980             this.on('scrollEnd', function () {
981                 _indicatorsMap(function () {
982                     this.fade();
983                 });
984             });
985
986             this.on('scrollCancel', function () {
987                 _indicatorsMap(function () {
988                     this.fade();
989                 });
990             });
991
992             this.on('scrollStart', function () {
993                 _indicatorsMap(function () {
994                     this.fade(1);
995                 });
996             });
997
998             this.on('beforeScrollStart', function () {
999                 _indicatorsMap(function () {
1000                     this.fade(1, true);
1001                 });
1002             });
1003         }
1004
1005
1006         this.on('refresh', function () {
1007             _indicatorsMap(function () {
1008                 this.refresh();
1009             });
1010         });
1011
1012         this.on('destroy', function () {
1013             _indicatorsMap(function () {
1014                 this.destroy();
1015             });
1016
1017             delete this.indicators;
1018         });
1019     },
1020
1021     _initWheel: function () {
1022         utils.addEvent(this.wrapper, 'wheel', this);
1023         utils.addEvent(this.wrapper, 'mousewheel', this);
1024         utils.addEvent(this.wrapper, 'DOMMouseScroll', this);
1025
1026         this.on('destroy', function () {
1027             utils.removeEvent(this.wrapper, 'wheel', this);
1028             utils.removeEvent(this.wrapper, 'mousewheel', this);
1029             utils.removeEvent(this.wrapper, 'DOMMouseScroll', this);
1030         });
1031     },
1032
1033     _wheel: function (e) {
1034         if ( !this.enabled ) {
1035             return;
1036         }
1037
1038         e.preventDefault();
1039         e.stopPropagation();
1040
1041         var wheelDeltaX, wheelDeltaY,
1042             newX, newY,
1043             that = this;
1044
1045         if ( this.wheelTimeout === undefined ) {
1046             that._execEvent('scrollStart');
1047         }
1048
1049         // Execute the scrollEnd event after 400ms the wheel stopped scrolling
1050         clearTimeout(this.wheelTimeout);
1051         this.wheelTimeout = setTimeout(function () {
1052             that._execEvent('scrollEnd');
1053             that.wheelTimeout = undefined;
1054         }, 400);
1055
1056         if ( 'deltaX' in e ) {
1057             if (e.deltaMode === 1) {
1058                 wheelDeltaX = -e.deltaX * this.options.mouseWheelSpeed;
1059                 wheelDeltaY = -e.deltaY * this.options.mouseWheelSpeed;
1060             } else {
1061                 wheelDeltaX = -e.deltaX;
1062                 wheelDeltaY = -e.deltaY;
1063             }
1064         } else if ( 'wheelDeltaX' in e ) {
1065             wheelDeltaX = e.wheelDeltaX / 120 * this.options.mouseWheelSpeed;
1066             wheelDeltaY = e.wheelDeltaY / 120 * this.options.mouseWheelSpeed;
1067         } else if ( 'wheelDelta' in e ) {
1068             wheelDeltaX = wheelDeltaY = e.wheelDelta / 120 * this.options.mouseWheelSpeed;
1069         } else if ( 'detail' in e ) {
1070             wheelDeltaX = wheelDeltaY = -e.detail / 3 * this.options.mouseWheelSpeed;
1071         } else {
1072             return;
1073         }
1074
1075         wheelDeltaX *= this.options.invertWheelDirection;
1076         wheelDeltaY *= this.options.invertWheelDirection;
1077
1078         if ( !this.hasVerticalScroll ) {
1079             wheelDeltaX = wheelDeltaY;
1080             wheelDeltaY = 0;
1081         }
1082
1083         if ( this.options.snap ) {
1084             newX = this.currentPage.pageX;
1085             newY = this.currentPage.pageY;
1086
1087             if ( wheelDeltaX > 0 ) {
1088                 newX--;
1089             } else if ( wheelDeltaX < 0 ) {
1090                 newX++;
1091             }
1092
1093             if ( wheelDeltaY > 0 ) {
1094                 newY--;
1095             } else if ( wheelDeltaY < 0 ) {
1096                 newY++;
1097             }
1098
1099             this.goToPage(newX, newY);
1100
1101             return;
1102         }
1103
1104         newX = this.x + Math.round(this.hasHorizontalScroll ? wheelDeltaX : 0);
1105         newY = this.y + Math.round(this.hasVerticalScroll ? wheelDeltaY : 0);
1106
1107         if ( newX > 0 ) {
1108             newX = 0;
1109         } else if ( newX < this.maxScrollX ) {
1110             newX = this.maxScrollX;
1111         }
1112
1113         if ( newY > 0 ) {
1114             newY = 0;
1115         } else if ( newY < this.maxScrollY ) {
1116             newY = this.maxScrollY;
1117         }
1118
1119         this.scrollTo(newX, newY, 0);
1120
1121 // INSERT POINT: _wheel
1122     },
1123
1124     _initSnap: function () {
1125         this.currentPage = {};
1126
1127         if ( typeof this.options.snap == 'string' ) {
1128             this.options.snap = this.scroller.querySelectorAll(this.options.snap);
1129         }
1130
1131         this.on('refresh', function () {
1132             var i = 0, l,
1133                 m = 0, n,
1134                 cx, cy,
1135                 x = 0, y,
1136                 stepX = this.options.snapStepX || this.wrapperWidth,
1137                 stepY = this.options.snapStepY || this.wrapperHeight,
1138                 el;
1139
1140             this.pages = [];
1141
1142             if ( !this.wrapperWidth || !this.wrapperHeight || !this.scrollerWidth || !this.scrollerHeight ) {
1143                 return;
1144             }
1145
1146             if ( this.options.snap === true ) {
1147                 cx = Math.round( stepX / 2 );
1148                 cy = Math.round( stepY / 2 );
1149
1150                 while ( x > -this.scrollerWidth ) {
1151                     this.pages[i] = [];
1152                     l = 0;
1153                     y = 0;
1154
1155                     while ( y > -this.scrollerHeight ) {
1156                         this.pages[i][l] = {
1157                             x: Math.max(x, this.maxScrollX),
1158                             y: Math.max(y, this.maxScrollY),
1159                             width: stepX,
1160                             height: stepY,
1161                             cx: x - cx,
1162                             cy: y - cy
1163                         };
1164
1165                         y -= stepY;
1166                         l++;
1167                     }
1168
1169                     x -= stepX;
1170                     i++;
1171                 }
1172             } else {
1173                 el = this.options.snap;
1174                 l = el.length;
1175                 n = -1;
1176
1177                 for ( ; i < l; i++ ) {
1178                     if ( i === 0 || el[i].offsetLeft <= el[i-1].offsetLeft ) {
1179                         m = 0;
1180                         n++;
1181                     }
1182
1183                     if ( !this.pages[m] ) {
1184                         this.pages[m] = [];
1185                     }
1186
1187                     x = Math.max(-el[i].offsetLeft, this.maxScrollX);
1188                     y = Math.max(-el[i].offsetTop, this.maxScrollY);
1189                     cx = x - Math.round(el[i].offsetWidth / 2);
1190                     cy = y - Math.round(el[i].offsetHeight / 2);
1191
1192                     this.pages[m][n] = {
1193                         x: x,
1194                         y: y,
1195                         width: el[i].offsetWidth,
1196                         height: el[i].offsetHeight,
1197                         cx: cx,
1198                         cy: cy
1199                     };
1200
1201                     if ( x > this.maxScrollX ) {
1202                         m++;
1203                     }
1204                 }
1205             }
1206
1207             this.goToPage(this.currentPage.pageX || 0, this.currentPage.pageY || 0, 0);
1208
1209             // Update snap threshold if needed
1210             if ( this.options.snapThreshold % 1 === 0 ) {
1211                 this.snapThresholdX = this.options.snapThreshold;
1212                 this.snapThresholdY = this.options.snapThreshold;
1213             } else {
1214                 this.snapThresholdX = Math.round(this.pages[this.currentPage.pageX][this.currentPage.pageY].width * this.options.snapThreshold);
1215                 this.snapThresholdY = Math.round(this.pages[this.currentPage.pageX][this.currentPage.pageY].height * this.options.snapThreshold);
1216             }
1217         });
1218
1219         this.on('flick', function () {
1220             var time = this.options.snapSpeed || Math.max(
1221                     Math.max(
1222                         Math.min(Math.abs(this.x - this.startX), 1000),
1223                         Math.min(Math.abs(this.y - this.startY), 1000)
1224                     ), 300);
1225
1226             this.goToPage(
1227                 this.currentPage.pageX + this.directionX,
1228                 this.currentPage.pageY + this.directionY,
1229                 time
1230             );
1231         });
1232     },
1233
1234     _nearestSnap: function (x, y) {
1235         if ( !this.pages.length ) {
1236             return { x: 0, y: 0, pageX: 0, pageY: 0 };
1237         }
1238
1239         var i = 0,
1240             l = this.pages.length,
1241             m = 0;
1242
1243         // Check if we exceeded the snap threshold
1244         if ( Math.abs(x - this.absStartX) < this.snapThresholdX &&
1245             Math.abs(y - this.absStartY) < this.snapThresholdY ) {
1246             return this.currentPage;
1247         }
1248
1249         if ( x > 0 ) {
1250             x = 0;
1251         } else if ( x < this.maxScrollX ) {
1252             x = this.maxScrollX;
1253         }
1254
1255         if ( y > 0 ) {
1256             y = 0;
1257         } else if ( y < this.maxScrollY ) {
1258             y = this.maxScrollY;
1259         }
1260
1261         for ( ; i < l; i++ ) {
1262             if ( x >= this.pages[i][0].cx ) {
1263                 x = this.pages[i][0].x;
1264                 break;
1265             }
1266         }
1267
1268         l = this.pages[i].length;
1269
1270         for ( ; m < l; m++ ) {
1271             if ( y >= this.pages[0][m].cy ) {
1272                 y = this.pages[0][m].y;
1273                 break;
1274             }
1275         }
1276
1277         if ( i == this.currentPage.pageX ) {
1278             i += this.directionX;
1279
1280             if ( i < 0 ) {
1281                 i = 0;
1282             } else if ( i >= this.pages.length ) {
1283                 i = this.pages.length - 1;
1284             }
1285
1286             x = this.pages[i][0].x;
1287         }
1288
1289         if ( m == this.currentPage.pageY ) {
1290             m += this.directionY;
1291
1292             if ( m < 0 ) {
1293                 m = 0;
1294             } else if ( m >= this.pages[0].length ) {
1295                 m = this.pages[0].length - 1;
1296             }
1297
1298             y = this.pages[0][m].y;
1299         }
1300
1301         return {
1302             x: x,
1303             y: y,
1304             pageX: i,
1305             pageY: m
1306         };
1307     },
1308
1309     goToPage: function (x, y, time, easing) {
1310         easing = easing || this.options.bounceEasing;
1311
1312         if ( x >= this.pages.length ) {
1313             x = this.pages.length - 1;
1314         } else if ( x < 0 ) {
1315             x = 0;
1316         }
1317
1318         if ( y >= this.pages[x].length ) {
1319             y = this.pages[x].length - 1;
1320         } else if ( y < 0 ) {
1321             y = 0;
1322         }
1323
1324         var posX = this.pages[x][y].x,
1325             posY = this.pages[x][y].y;
1326
1327         time = time === undefined ? this.options.snapSpeed || Math.max(
1328             Math.max(
1329                 Math.min(Math.abs(posX - this.x), 1000),
1330                 Math.min(Math.abs(posY - this.y), 1000)
1331             ), 300) : time;
1332
1333         this.currentPage = {
1334             x: posX,
1335             y: posY,
1336             pageX: x,
1337             pageY: y
1338         };
1339
1340         this.scrollTo(posX, posY, time, easing);
1341     },
1342
1343     next: function (time, easing) {
1344         var x = this.currentPage.pageX,
1345             y = this.currentPage.pageY;
1346
1347         x++;
1348
1349         if ( x >= this.pages.length && this.hasVerticalScroll ) {
1350             x = 0;
1351             y++;
1352         }
1353
1354         this.goToPage(x, y, time, easing);
1355     },
1356
1357     prev: function (time, easing) {
1358         var x = this.currentPage.pageX,
1359             y = this.currentPage.pageY;
1360
1361         x--;
1362
1363         if ( x < 0 && this.hasVerticalScroll ) {
1364             x = 0;
1365             y--;
1366         }
1367
1368         this.goToPage(x, y, time, easing);
1369     },
1370
1371     _initKeys: function (e) {
1372         // default key bindings
1373         var keys = {
1374             pageUp: 33,
1375             pageDown: 34,
1376             end: 35,
1377             home: 36,
1378             left: 37,
1379             up: 38,
1380             right: 39,
1381             down: 40
1382         };
1383         var i;
1384
1385         // if you give me characters I give you keycode
1386         if ( typeof this.options.keyBindings == 'object' ) {
1387             for ( i in this.options.keyBindings ) {
1388                 if ( typeof this.options.keyBindings[i] == 'string' ) {
1389                     this.options.keyBindings[i] = this.options.keyBindings[i].toUpperCase().charCodeAt(0);
1390                 }
1391             }
1392         } else {
1393             this.options.keyBindings = {};
1394         }
1395
1396         for ( i in keys ) {
1397             this.options.keyBindings[i] = this.options.keyBindings[i] || keys[i];
1398         }
1399
1400         utils.addEvent(window, 'keydown', this);
1401
1402         this.on('destroy', function () {
1403             utils.removeEvent(window, 'keydown', this);
1404         });
1405     },
1406
1407     _key: function (e) {
1408         if ( !this.enabled ) {
1409             return;
1410         }
1411
1412         var snap = this.options.snap,    // we are using this alot, better to cache it
1413             newX = snap ? this.currentPage.pageX : this.x,
1414             newY = snap ? this.currentPage.pageY : this.y,
1415             now = utils.getTime(),
1416             prevTime = this.keyTime || 0,
1417             acceleration = 0.250,
1418             pos;
1419
1420         if ( this.options.useTransition && this.isInTransition ) {
1421             pos = this.getComputedPosition();
1422
1423             this._translate(Math.round(pos.x), Math.round(pos.y));
1424             this.isInTransition = false;
1425         }
1426
1427         this.keyAcceleration = now - prevTime < 200 ? Math.min(this.keyAcceleration + acceleration, 50) : 0;
1428
1429         switch ( e.keyCode ) {
1430             case this.options.keyBindings.pageUp:
1431                 if ( this.hasHorizontalScroll && !this.hasVerticalScroll ) {
1432                     newX += snap ? 1 : this.wrapperWidth;
1433                 } else {
1434                     newY += snap ? 1 : this.wrapperHeight;
1435                 }
1436                 break;
1437             case this.options.keyBindings.pageDown:
1438                 if ( this.hasHorizontalScroll && !this.hasVerticalScroll ) {
1439                     newX -= snap ? 1 : this.wrapperWidth;
1440                 } else {
1441                     newY -= snap ? 1 : this.wrapperHeight;
1442                 }
1443                 break;
1444             case this.options.keyBindings.end:
1445                 newX = snap ? this.pages.length-1 : this.maxScrollX;
1446                 newY = snap ? this.pages[0].length-1 : this.maxScrollY;
1447                 break;
1448             case this.options.keyBindings.home:
1449                 newX = 0;
1450                 newY = 0;
1451                 break;
1452             case this.options.keyBindings.left:
1453                 newX += snap ? -1 : 5 + this.keyAcceleration>>0;
1454                 break;
1455             case this.options.keyBindings.up:
1456                 newY += snap ? 1 : 5 + this.keyAcceleration>>0;
1457                 break;
1458             case this.options.keyBindings.right:
1459                 newX -= snap ? -1 : 5 + this.keyAcceleration>>0;
1460                 break;
1461             case this.options.keyBindings.down:
1462                 newY -= snap ? 1 : 5 + this.keyAcceleration>>0;
1463                 break;
1464             default:
1465                 return;
1466         }
1467
1468         if ( snap ) {
1469             this.goToPage(newX, newY);
1470             return;
1471         }
1472
1473         if ( newX > 0 ) {
1474             newX = 0;
1475             this.keyAcceleration = 0;
1476         } else if ( newX < this.maxScrollX ) {
1477             newX = this.maxScrollX;
1478             this.keyAcceleration = 0;
1479         }
1480
1481         if ( newY > 0 ) {
1482             newY = 0;
1483             this.keyAcceleration = 0;
1484         } else if ( newY < this.maxScrollY ) {
1485             newY = this.maxScrollY;
1486             this.keyAcceleration = 0;
1487         }
1488
1489         this.scrollTo(newX, newY, 0);
1490
1491         this.keyTime = now;
1492     },
1493
1494     _animate: function (destX, destY, duration, easingFn) {
1495         var that = this,
1496             startX = this.x,
1497             startY = this.y,
1498             startTime = utils.getTime(),
1499             destTime = startTime + duration;
1500
1501         function step () {
1502             var now = utils.getTime(),
1503                 newX, newY,
1504                 easing;
1505
1506             if ( now >= destTime ) {
1507                 that.isAnimating = false;
1508                 that._translate(destX, destY);
1509
1510                 if ( !that.resetPosition(that.options.bounceTime) ) {
1511                     that._execEvent('scrollEnd');
1512                 }
1513
1514                 return;
1515             }
1516
1517             now = ( now - startTime ) / duration;
1518             easing = easingFn(now);
1519             newX = ( destX - startX ) * easing + startX;
1520             newY = ( destY - startY ) * easing + startY;
1521             that._translate(newX, newY);
1522
1523             if ( that.isAnimating ) {
1524                 rAF(step);
1525             }
1526         }
1527
1528         this.isAnimating = true;
1529         step();
1530     },
1531     handleEvent: function (e) {
1532         switch ( e.type ) {
1533             case 'touchstart':
1534             case 'pointerdown':
1535             case 'MSPointerDown':
1536             case 'mousedown':
1537                 this._start(e);
1538                 break;
1539             case 'touchmove':
1540             case 'pointermove':
1541             case 'MSPointerMove':
1542             case 'mousemove':
1543                 this._move(e);
1544                 break;
1545             case 'touchend':
1546             case 'pointerup':
1547             case 'MSPointerUp':
1548             case 'mouseup':
1549             case 'touchcancel':
1550             case 'pointercancel':
1551             case 'MSPointerCancel':
1552             case 'mousecancel':
1553                 this._end(e);
1554                 break;
1555             case 'orientationchange':
1556             case 'resize':
1557                 this._resize();
1558                 break;
1559             case 'transitionend':
1560             case 'webkitTransitionEnd':
1561             case 'oTransitionEnd':
1562             case 'MSTransitionEnd':
1563                 this._transitionEnd(e);
1564                 break;
1565             case 'wheel':
1566             case 'DOMMouseScroll':
1567             case 'mousewheel':
1568                 this._wheel(e);
1569                 break;
1570             case 'keydown':
1571                 this._key(e);
1572                 break;
1573             case 'click':
1574                 if ( !e._constructed ) {
1575                     e.preventDefault();
1576                     e.stopPropagation();
1577                 }
1578                 break;
1579         }
1580     }
1581 };
1582 function createDefaultScrollbar (direction, interactive, type) {
1583     var scrollbar = document.createElement('div'),
1584         indicator = document.createElement('div');
1585
1586     if ( type === true ) {
1587         scrollbar.style.cssText = 'position:absolute;z-index:9999';
1588         indicator.style.cssText = '-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;position:absolute;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);border-radius:3px';
1589     }
1590
1591     indicator.className = 'iScrollIndicator';
1592
1593     if ( direction == 'h' ) {
1594         if ( type === true ) {
1595             scrollbar.style.cssText += ';height:7px;left:2px;right:2px;bottom:0';
1596             indicator.style.height = '100%';
1597         }
1598         scrollbar.className = 'iScrollHorizontalScrollbar';
1599     } else {
1600         if ( type === true ) {
1601             scrollbar.style.cssText += ';width:7px;bottom:2px;top:2px;right:1px';
1602             indicator.style.width = '100%';
1603         }
1604         scrollbar.className = 'iScrollVerticalScrollbar';
1605     }
1606
1607     scrollbar.style.cssText += ';overflow:hidden';
1608
1609     if ( !interactive ) {
1610         scrollbar.style.pointerEvents = 'none';
1611     }
1612
1613     scrollbar.appendChild(indicator);
1614
1615     return scrollbar;
1616 }
1617
1618 function Indicator (scroller, options) {
1619     this.wrapper = typeof options.el == 'string' ? document.querySelector(options.el) : options.el;
1620     this.wrapperStyle = this.wrapper.style;
1621     this.indicator = this.wrapper.children[0];
1622     this.indicatorStyle = this.indicator.style;
1623     this.scroller = scroller;
1624
1625     this.options = {
1626         listenX: true,
1627         listenY: true,
1628         interactive: false,
1629         resize: true,
1630         defaultScrollbars: false,
1631         shrink: false,
1632         fade: false,
1633         speedRatioX: 0,
1634         speedRatioY: 0
1635     };
1636
1637     for ( var i in options ) {
1638         this.options[i] = options[i];
1639     }
1640
1641     this.sizeRatioX = 1;
1642     this.sizeRatioY = 1;
1643     this.maxPosX = 0;
1644     this.maxPosY = 0;
1645
1646     if ( this.options.interactive ) {
1647         if ( !this.options.disableTouch ) {
1648             utils.addEvent(this.indicator, 'touchstart', this);
1649             utils.addEvent(window, 'touchend', this);
1650         }
1651         if ( !this.options.disablePointer ) {
1652             utils.addEvent(this.indicator, utils.prefixPointerEvent('pointerdown'), this);
1653             utils.addEvent(window, utils.prefixPointerEvent('pointerup'), this);
1654         }
1655         if ( !this.options.disableMouse ) {
1656             utils.addEvent(this.indicator, 'mousedown', this);
1657             utils.addEvent(window, 'mouseup', this);
1658         }
1659     }
1660
1661     if ( this.options.fade ) {
1662         this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;
1663         this.wrapperStyle[utils.style.transitionDuration] = utils.isBadAndroid ? '0.001s' : '0ms';
1664         this.wrapperStyle.opacity = '0';
1665     }
1666 }
1667
1668 Indicator.prototype = {
1669     handleEvent: function (e) {
1670         switch ( e.type ) {
1671             case 'touchstart':
1672             case 'pointerdown':
1673             case 'MSPointerDown':
1674             case 'mousedown':
1675                 this._start(e);
1676                 break;
1677             case 'touchmove':
1678             case 'pointermove':
1679             case 'MSPointerMove':
1680             case 'mousemove':
1681                 this._move(e);
1682                 break;
1683             case 'touchend':
1684             case 'pointerup':
1685             case 'MSPointerUp':
1686             case 'mouseup':
1687             case 'touchcancel':
1688             case 'pointercancel':
1689             case 'MSPointerCancel':
1690             case 'mousecancel':
1691                 this._end(e);
1692                 break;
1693         }
1694     },
1695
1696     destroy: function () {
1697         if ( this.options.interactive ) {
1698             utils.removeEvent(this.indicator, 'touchstart', this);
1699             utils.removeEvent(this.indicator, utils.prefixPointerEvent('pointerdown'), this);
1700             utils.removeEvent(this.indicator, 'mousedown', this);
1701
1702             utils.removeEvent(window, 'touchmove', this);
1703             utils.removeEvent(window, utils.prefixPointerEvent('pointermove'), this);
1704             utils.removeEvent(window, 'mousemove', this);
1705
1706             utils.removeEvent(window, 'touchend', this);
1707             utils.removeEvent(window, utils.prefixPointerEvent('pointerup'), this);
1708             utils.removeEvent(window, 'mouseup', this);
1709         }
1710
1711         if ( this.options.defaultScrollbars ) {
1712             this.wrapper.parentNode.removeChild(this.wrapper);
1713         }
1714     },
1715
1716     _start: function (e) {
1717         var point = e.touches ? e.touches[0] : e;
1718
1719         e.preventDefault();
1720         e.stopPropagation();
1721
1722         this.transitionTime();
1723
1724         this.initiated = true;
1725         this.moved = false;
1726         this.lastPointX    = point.pageX;
1727         this.lastPointY    = point.pageY;
1728
1729         this.startTime    = utils.getTime();
1730
1731         if ( !this.options.disableTouch ) {
1732             utils.addEvent(window, 'touchmove', this);
1733         }
1734         if ( !this.options.disablePointer ) {
1735             utils.addEvent(window, utils.prefixPointerEvent('pointermove'), this);
1736         }
1737         if ( !this.options.disableMouse ) {
1738             utils.addEvent(window, 'mousemove', this);
1739         }
1740
1741         this.scroller._execEvent('beforeScrollStart');
1742     },
1743
1744     _move: function (e) {
1745         var point = e.touches ? e.touches[0] : e,
1746             deltaX, deltaY,
1747             newX, newY,
1748             timestamp = utils.getTime();
1749
1750         if ( !this.moved ) {
1751             this.scroller._execEvent('scrollStart');
1752         }
1753
1754         this.moved = true;
1755
1756         deltaX = point.pageX - this.lastPointX;
1757         this.lastPointX = point.pageX;
1758
1759         deltaY = point.pageY - this.lastPointY;
1760         this.lastPointY = point.pageY;
1761
1762         newX = this.x + deltaX;
1763         newY = this.y + deltaY;
1764
1765         this._pos(newX, newY);
1766
1767 // INSERT POINT: indicator._move
1768
1769         e.preventDefault();
1770         e.stopPropagation();
1771     },
1772
1773     _end: function (e) {
1774         if ( !this.initiated ) {
1775             return;
1776         }
1777
1778         this.initiated = false;
1779
1780         e.preventDefault();
1781         e.stopPropagation();
1782
1783         utils.removeEvent(window, 'touchmove', this);
1784         utils.removeEvent(window, utils.prefixPointerEvent('pointermove'), this);
1785         utils.removeEvent(window, 'mousemove', this);
1786
1787         if ( this.scroller.options.snap ) {
1788             var snap = this.scroller._nearestSnap(this.scroller.x, this.scroller.y);
1789
1790             var time = this.options.snapSpeed || Math.max(
1791                     Math.max(
1792                         Math.min(Math.abs(this.scroller.x - snap.x), 1000),
1793                         Math.min(Math.abs(this.scroller.y - snap.y), 1000)
1794                     ), 300);
1795
1796             if ( this.scroller.x != snap.x || this.scroller.y != snap.y ) {
1797                 this.scroller.directionX = 0;
1798                 this.scroller.directionY = 0;
1799                 this.scroller.currentPage = snap;
1800                 this.scroller.scrollTo(snap.x, snap.y, time, this.scroller.options.bounceEasing);
1801             }
1802         }
1803
1804         if ( this.moved ) {
1805             this.scroller._execEvent('scrollEnd');
1806         }
1807     },
1808
1809     transitionTime: function (time) {
1810         time = time || 0;
1811         this.indicatorStyle[utils.style.transitionDuration] = time + 'ms';
1812
1813         if ( !time && utils.isBadAndroid ) {
1814             this.indicatorStyle[utils.style.transitionDuration] = '0.001s';
1815         }
1816     },
1817
1818     transitionTimingFunction: function (easing) {
1819         this.indicatorStyle[utils.style.transitionTimingFunction] = easing;
1820     },
1821
1822     refresh: function () {
1823         this.transitionTime();
1824
1825         if ( this.options.listenX && !this.options.listenY ) {
1826             this.indicatorStyle.display = this.scroller.hasHorizontalScroll ? 'block' : 'none';
1827         } else if ( this.options.listenY && !this.options.listenX ) {
1828             this.indicatorStyle.display = this.scroller.hasVerticalScroll ? 'block' : 'none';
1829         } else {
1830             this.indicatorStyle.display = this.scroller.hasHorizontalScroll || this.scroller.hasVerticalScroll ? 'block' : 'none';
1831         }
1832
1833         if ( this.scroller.hasHorizontalScroll && this.scroller.hasVerticalScroll ) {
1834             utils.addClass(this.wrapper, 'iScrollBothScrollbars');
1835             utils.removeClass(this.wrapper, 'iScrollLoneScrollbar');
1836
1837             if ( this.options.defaultScrollbars && this.options.customStyle ) {
1838                 if ( this.options.listenX ) {
1839                     this.wrapper.style.right = '8px';
1840                 } else {
1841                     this.wrapper.style.bottom = '8px';
1842                 }
1843             }
1844         } else {
1845             utils.removeClass(this.wrapper, 'iScrollBothScrollbars');
1846             utils.addClass(this.wrapper, 'iScrollLoneScrollbar');
1847
1848             if ( this.options.defaultScrollbars && this.options.customStyle ) {
1849                 if ( this.options.listenX ) {
1850                     this.wrapper.style.right = '2px';
1851                 } else {
1852                     this.wrapper.style.bottom = '2px';
1853                 }
1854             }
1855         }
1856
1857         var r = this.wrapper.offsetHeight;    // force refresh
1858
1859         if ( this.options.listenX ) {
1860             this.wrapperWidth = this.wrapper.clientWidth;
1861             if ( this.options.resize ) {
1862                 this.indicatorWidth = Math.max(Math.round(this.wrapperWidth * this.wrapperWidth / (this.scroller.scrollerWidth || this.wrapperWidth || 1)), 8);
1863                 this.indicatorStyle.width = this.indicatorWidth + 'px';
1864             } else {
1865                 this.indicatorWidth = this.indicator.clientWidth;
1866             }
1867
1868             this.maxPosX = this.wrapperWidth - this.indicatorWidth;
1869
1870             if ( this.options.shrink == 'clip' ) {
1871                 this.minBoundaryX = -this.indicatorWidth + 8;
1872                 this.maxBoundaryX = this.wrapperWidth - 8;
1873             } else {
1874                 this.minBoundaryX = 0;
1875                 this.maxBoundaryX = this.maxPosX;
1876             }
1877
1878             this.sizeRatioX = this.options.speedRatioX || (this.scroller.maxScrollX && (this.maxPosX / this.scroller.maxScrollX));    
1879         }
1880
1881         if ( this.options.listenY ) {
1882             this.wrapperHeight = this.wrapper.clientHeight;
1883             if ( this.options.resize ) {
1884                 this.indicatorHeight = Math.max(Math.round(this.wrapperHeight * this.wrapperHeight / (this.scroller.scrollerHeight || this.wrapperHeight || 1)), 8);
1885                 this.indicatorStyle.height = this.indicatorHeight + 'px';
1886             } else {
1887                 this.indicatorHeight = this.indicator.clientHeight;
1888             }
1889
1890             this.maxPosY = this.wrapperHeight - this.indicatorHeight;
1891
1892             if ( this.options.shrink == 'clip' ) {
1893                 this.minBoundaryY = -this.indicatorHeight + 8;
1894                 this.maxBoundaryY = this.wrapperHeight - 8;
1895             } else {
1896                 this.minBoundaryY = 0;
1897                 this.maxBoundaryY = this.maxPosY;
1898             }
1899
1900             this.maxPosY = this.wrapperHeight - this.indicatorHeight;
1901             this.sizeRatioY = this.options.speedRatioY || (this.scroller.maxScrollY && (this.maxPosY / this.scroller.maxScrollY));
1902         }
1903
1904         this.updatePosition();
1905     },
1906
1907     updatePosition: function () {
1908         var x = this.options.listenX && Math.round(this.sizeRatioX * this.scroller.x) || 0,
1909             y = this.options.listenY && Math.round(this.sizeRatioY * this.scroller.y) || 0;
1910
1911         if ( !this.options.ignoreBoundaries ) {
1912             if ( x < this.minBoundaryX ) {
1913                 if ( this.options.shrink == 'scale' ) {
1914                     this.width = Math.max(this.indicatorWidth + x, 8);
1915                     this.indicatorStyle.width = this.width + 'px';
1916                 }
1917                 x = this.minBoundaryX;
1918             } else if ( x > this.maxBoundaryX ) {
1919                 if ( this.options.shrink == 'scale' ) {
1920                     this.width = Math.max(this.indicatorWidth - (x - this.maxPosX), 8);
1921                     this.indicatorStyle.width = this.width + 'px';
1922                     x = this.maxPosX + this.indicatorWidth - this.width;
1923                 } else {
1924                     x = this.maxBoundaryX;
1925                 }
1926             } else if ( this.options.shrink == 'scale' && this.width != this.indicatorWidth ) {
1927                 this.width = this.indicatorWidth;
1928                 this.indicatorStyle.width = this.width + 'px';
1929             }
1930
1931             if ( y < this.minBoundaryY ) {
1932                 if ( this.options.shrink == 'scale' ) {
1933                     this.height = Math.max(this.indicatorHeight + y * 3, 8);
1934                     this.indicatorStyle.height = this.height + 'px';
1935                 }
1936                 y = this.minBoundaryY;
1937             } else if ( y > this.maxBoundaryY ) {
1938                 if ( this.options.shrink == 'scale' ) {
1939                     this.height = Math.max(this.indicatorHeight - (y - this.maxPosY) * 3, 8);
1940                     this.indicatorStyle.height = this.height + 'px';
1941                     y = this.maxPosY + this.indicatorHeight - this.height;
1942                 } else {
1943                     y = this.maxBoundaryY;
1944                 }
1945             } else if ( this.options.shrink == 'scale' && this.height != this.indicatorHeight ) {
1946                 this.height = this.indicatorHeight;
1947                 this.indicatorStyle.height = this.height + 'px';
1948             }
1949         }
1950
1951         this.x = x;
1952         this.y = y;
1953
1954         if ( this.scroller.options.useTransform ) {
1955             this.indicatorStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.scroller.translateZ;
1956         } else {
1957             this.indicatorStyle.left = x + 'px';
1958             this.indicatorStyle.top = y + 'px';
1959         }
1960     },
1961
1962     _pos: function (x, y) {
1963         if ( x < 0 ) {
1964             x = 0;
1965         } else if ( x > this.maxPosX ) {
1966             x = this.maxPosX;
1967         }
1968
1969         if ( y < 0 ) {
1970             y = 0;
1971         } else if ( y > this.maxPosY ) {
1972             y = this.maxPosY;
1973         }
1974
1975         x = this.options.listenX ? Math.round(x / this.sizeRatioX) : this.scroller.x;
1976         y = this.options.listenY ? Math.round(y / this.sizeRatioY) : this.scroller.y;
1977
1978         this.scroller.scrollTo(x, y);
1979     },
1980
1981     fade: function (val, hold) {
1982         if ( hold && !this.visible ) {
1983             return;
1984         }
1985
1986         clearTimeout(this.fadeTimeout);
1987         this.fadeTimeout = null;
1988
1989         var time = val ? 250 : 500,
1990             delay = val ? 0 : 300;
1991
1992         val = val ? '1' : '0';
1993
1994         this.wrapperStyle[utils.style.transitionDuration] = time + 'ms';
1995
1996         this.fadeTimeout = setTimeout((function (val) {
1997             this.wrapperStyle.opacity = val;
1998             this.visible = +val;
1999         }).bind(this, val), delay);
2000     }
2001 };
2002
2003 IScroll.utils = utils;
2004
2005 if ( typeof module != 'undefined' && module.exports ) {
2006     module.exports = IScroll;
2007 } else {
2008     window.IScroll = IScroll;
2009 }
2010
2011 })(window, document, Math);