Highlights/activity alerts
[KiwiIRC.git] / client / js / iscroll.js
1 /*!
2 * iScroll v4.1.4 ~ Copyright (c) 2011 Matteo Spinelli, http://cubiq.org
3 * Released under MIT license, http://cubiq.org/license
4 */
5
6 (function(){
7 var m = Math,
8 vendor = (/webkit/i).test(navigator.appVersion) ? 'webkit' :
9 (/firefox/i).test(navigator.userAgent) ? 'Moz' :
10 'opera' in window ? 'O' : '',
11
12 // Browser capabilities
13 has3d = 'WebKitCSSMatrix' in window && 'm11' in new WebKitCSSMatrix(),
14 hasTouch = 'ontouchstart' in window,
15 hasTransform = vendor + 'Transform' in document.documentElement.style,
16 isAndroid = (/android/gi).test(navigator.appVersion),
17 isIDevice = (/iphone|ipad/gi).test(navigator.appVersion),
18 isPlaybook = (/playbook/gi).test(navigator.appVersion),
19 hasTransitionEnd = (isIDevice || isPlaybook) && 'onwebkittransitionend' in window,
20 nextFrame = (function() {
21 return window.requestAnimationFrame
22 || window.webkitRequestAnimationFrame
23 || window.mozRequestAnimationFrame
24 || window.oRequestAnimationFrame
25 || window.msRequestAnimationFrame
26 || function(callback) { return setTimeout(callback, 17); }
27 })(),
28 cancelFrame = (function () {
29 return window.cancelRequestAnimationFrame
30 || window.webkitCancelRequestAnimationFrame
31 || window.mozCancelRequestAnimationFrame
32 || window.oCancelRequestAnimationFrame
33 || window.msCancelRequestAnimationFrame
34 || clearTimeout
35 })(),
36
37 // Events
38 RESIZE_EV = 'onorientationchange' in window ? 'orientationchange' : 'resize',
39 START_EV = hasTouch ? 'touchstart' : 'mousedown',
40 MOVE_EV = hasTouch ? 'touchmove' : 'mousemove',
41 END_EV = hasTouch ? 'touchend' : 'mouseup',
42 CANCEL_EV = hasTouch ? 'touchcancel' : 'mouseup',
43 WHEEL_EV = vendor == 'Moz' ? 'DOMMouseScroll' : 'mousewheel',
44
45 // Helpers
46 trnOpen = 'translate' + (has3d ? '3d(' : '('),
47 trnClose = has3d ? ',0)' : ')',
48
49 // Constructor
50 iScroll = function (el, options) {
51 var that = this,
52 doc = document,
53 i;
54
55 that.wrapper = typeof el == 'object' ? el : doc.getElementById(el);
56 that.wrapper.style.overflow = 'hidden';
57 that.scroller = that.wrapper.children[0];
58
59 // Default options
60 that.options = {
61 hScroll: true,
62 vScroll: true,
63 bounce: true,
64 bounceLock: false,
65 momentum: true,
66 lockDirection: true,
67 useTransform: true,
68 useTransition: false,
69
70 // Scrollbar
71 hScrollbar: true,
72 vScrollbar: true,
73 fixedScrollbar: isAndroid,
74 hideScrollbar: isIDevice,
75 fadeScrollbar: isIDevice && has3d,
76 scrollbarClass: '',
77
78 // Zoom
79 zoom: false,
80 zoomMin: 1,
81 zoomMax: 4,
82 doubleTapZoom: 2,
83
84 // Snap
85 snap: false,
86 snapThreshold: 1,
87
88 // Events
89 onRefresh: null,
90 onBeforeScrollStart: function (e) { e.preventDefault(); },
91 onScrollStart: null,
92 onBeforeScrollMove: null,
93 onScrollMove: null,
94 onBeforeScrollEnd: null,
95 onScrollEnd: null,
96 onTouchEnd: null,
97 onDestroy: null
98 };
99
100 // User defined options
101 for (i in options) that.options[i] = options[i];
102
103 // Normalize options
104 that.options.useTransform = hasTransform ? that.options.useTransform : false;
105 that.options.hScrollbar = that.options.hScroll && that.options.hScrollbar;
106 that.options.vScrollbar = that.options.vScroll && that.options.vScrollbar;
107 that.options.zoom = that.options.useTransform && that.options.zoom;
108 that.options.useTransition = hasTransitionEnd && that.options.useTransition;
109
110 // Set some default styles
111 that.scroller.style[vendor + 'TransitionProperty'] = that.options.useTransform ? '-' + vendor.toLowerCase() + '-transform' : 'top left';
112 that.scroller.style[vendor + 'TransitionDuration'] = '0';
113 that.scroller.style[vendor + 'TransformOrigin'] = '0 0';
114 if (that.options.useTransition) that.scroller.style[vendor + 'TransitionTimingFunction'] = 'cubic-bezier(0.33,0.66,0.66,1)';
115
116 if (that.options.useTransform) that.scroller.style[vendor + 'Transform'] = trnOpen + '0,0' + trnClose;
117 else that.scroller.style.cssText += ';top:0;left:0';
118
119 if (that.options.useTransition) that.options.fixedScrollbar = true;
120
121 that.refresh();
122
123 that._bind(RESIZE_EV, window);
124 that._bind(START_EV);
125 if (!hasTouch) {
126 that._bind('mouseout', that.wrapper);
127 that._bind(WHEEL_EV);
128 }
129 };
130
131 // Prototype
132 iScroll.prototype = {
133 enabled: true,
134 x: 0,
135 y: 0,
136 steps: [],
137 scale: 1,
138 currPageX: 0, currPageY: 0,
139 pagesX: [], pagesY: [],
140 aniTime: null,
141
142 handleEvent: function (e) {
143 var that = this;
144 switch(e.type) {
145 case START_EV: that._start(e); break;
146 case MOVE_EV: that._move(e); break;
147 case END_EV:
148 case CANCEL_EV: that._end(e); break;
149 case RESIZE_EV: that._resize(); break;
150 case WHEEL_EV: that._wheel(e); break;
151 case 'mouseout': that._mouseout(e); break;
152 case 'webkitTransitionEnd': that._transitionEnd(e); break;
153 }
154 },
155
156 _scrollbar: function (dir) {
157 var that = this,
158 doc = document,
159 bar;
160
161 if (!that[dir + 'Scrollbar']) {
162 if (that[dir + 'ScrollbarWrapper']) {
163 if (hasTransform) that[dir + 'ScrollbarIndicator'].style[vendor + 'Transform'] = '';
164 that[dir + 'ScrollbarWrapper'].parentNode.removeChild(that[dir + 'ScrollbarWrapper']);
165 that[dir + 'ScrollbarWrapper'] = null;
166 that[dir + 'ScrollbarIndicator'] = null;
167 }
168
169 return;
170 }
171
172 if (!that[dir + 'ScrollbarWrapper']) {
173 // Create the scrollbar wrapper
174 bar = doc.createElement('div');
175
176 if (that.options.scrollbarClass) bar.className = that.options.scrollbarClass + dir.toUpperCase();
177 else bar.style.cssText = 'position:absolute;z-index:100;' + (dir == 'h' ? 'height:7px;bottom:1px;left:2px;right:' + (that.vScrollbar ? '7' : '2') + 'px' : 'width:7px;bottom:' + (that.hScrollbar ? '7' : '2') + 'px;top:2px;right:1px');
178
179 bar.style.cssText += ';pointer-events:none;-' + vendor + '-transition-property:opacity;-' + vendor + '-transition-duration:' + (that.options.fadeScrollbar ? '350ms' : '0') + ';overflow:hidden;opacity:' + (that.options.hideScrollbar ? '0' : '1');
180
181 that.wrapper.appendChild(bar);
182 that[dir + 'ScrollbarWrapper'] = bar;
183
184 // Create the scrollbar indicator
185 bar = doc.createElement('div');
186 if (!that.options.scrollbarClass) {
187 bar.style.cssText = 'position:absolute;z-index:100;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);-' + vendor + '-background-clip:padding-box;-' + vendor + '-box-sizing:border-box;' + (dir == 'h' ? 'height:100%' : 'width:100%') + ';-' + vendor + '-border-radius:3px;border-radius:3px';
188 }
189 bar.style.cssText += ';pointer-events:none;-' + vendor + '-transition-property:-' + vendor + '-transform;-' + vendor + '-transition-timing-function:cubic-bezier(0.33,0.66,0.66,1);-' + vendor + '-transition-duration:0;-' + vendor + '-transform:' + trnOpen + '0,0' + trnClose;
190 if (that.options.useTransition) bar.style.cssText += ';-' + vendor + '-transition-timing-function:cubic-bezier(0.33,0.66,0.66,1)';
191
192 that[dir + 'ScrollbarWrapper'].appendChild(bar);
193 that[dir + 'ScrollbarIndicator'] = bar;
194 }
195
196 if (dir == 'h') {
197 that.hScrollbarSize = that.hScrollbarWrapper.clientWidth;
198 that.hScrollbarIndicatorSize = m.max(m.round(that.hScrollbarSize * that.hScrollbarSize / that.scrollerW), 8);
199 that.hScrollbarIndicator.style.width = that.hScrollbarIndicatorSize + 'px';
200 that.hScrollbarMaxScroll = that.hScrollbarSize - that.hScrollbarIndicatorSize;
201 that.hScrollbarProp = that.hScrollbarMaxScroll / that.maxScrollX;
202 } else {
203 that.vScrollbarSize = that.vScrollbarWrapper.clientHeight;
204 that.vScrollbarIndicatorSize = m.max(m.round(that.vScrollbarSize * that.vScrollbarSize / that.scrollerH), 8);
205 that.vScrollbarIndicator.style.height = that.vScrollbarIndicatorSize + 'px';
206 that.vScrollbarMaxScroll = that.vScrollbarSize - that.vScrollbarIndicatorSize;
207 that.vScrollbarProp = that.vScrollbarMaxScroll / that.maxScrollY;
208 }
209
210 // Reset position
211 that._scrollbarPos(dir, true);
212 },
213
214 _resize: function () {
215 this.refresh();
216 },
217
218 _pos: function (x, y) {
219 x = this.hScroll ? x : 0;
220 y = this.vScroll ? y : 0;
221
222 if (this.options.useTransform) {
223 this.scroller.style[vendor + 'Transform'] = trnOpen + x + 'px,' + y + 'px' + trnClose + ' scale(' + this.scale + ')';
224 } else {
225 x = m.round(x);
226 y = m.round(y);
227 this.scroller.style.left = x + 'px';
228 this.scroller.style.top = y + 'px';
229 }
230
231 this.x = x;
232 this.y = y;
233
234 this._scrollbarPos('h');
235 this._scrollbarPos('v');
236 },
237
238 _scrollbarPos: function (dir, hidden) {
239 var that = this,
240 pos = dir == 'h' ? that.x : that.y,
241 size;
242
243 if (!that[dir + 'Scrollbar']) return;
244
245 pos = that[dir + 'ScrollbarProp'] * pos;
246
247 if (pos < 0) {
248 if (!that.options.fixedScrollbar) {
249 size = that[dir + 'ScrollbarIndicatorSize'] + m.round(pos * 3);
250 if (size < 8) size = 8;
251 that[dir + 'ScrollbarIndicator'].style[dir == 'h' ? 'width' : 'height'] = size + 'px';
252 }
253 pos = 0;
254 } else if (pos > that[dir + 'ScrollbarMaxScroll']) {
255 if (!that.options.fixedScrollbar) {
256 size = that[dir + 'ScrollbarIndicatorSize'] - m.round((pos - that[dir + 'ScrollbarMaxScroll']) * 3);
257 if (size < 8) size = 8;
258 that[dir + 'ScrollbarIndicator'].style[dir == 'h' ? 'width' : 'height'] = size + 'px';
259 pos = that[dir + 'ScrollbarMaxScroll'] + (that[dir + 'ScrollbarIndicatorSize'] - size);
260 } else {
261 pos = that[dir + 'ScrollbarMaxScroll'];
262 }
263 }
264
265 that[dir + 'ScrollbarWrapper'].style[vendor + 'TransitionDelay'] = '0';
266 that[dir + 'ScrollbarWrapper'].style.opacity = hidden && that.options.hideScrollbar ? '0' : '1';
267 that[dir + 'ScrollbarIndicator'].style[vendor + 'Transform'] = trnOpen + (dir == 'h' ? pos + 'px,0' : '0,' + pos + 'px') + trnClose;
268 },
269
270 _start: function (e) {
271 var that = this,
272 point = hasTouch ? e.touches[0] : e,
273 matrix, x, y,
274 c1, c2, target;
275
276 if (!that.enabled) return;
277
278 if (that.options.onBeforeScrollStart) that.options.onBeforeScrollStart.call(that, e);
279
280 if (that.options.useTransition) that._transitionTime(0);
281
282 that.moved = false;
283 that.animating = false;
284 that.zoomed = false;
285 that.distX = 0;
286 that.distY = 0;
287 that.absDistX = 0;
288 that.absDistY = 0;
289 that.dirX = 0;
290 that.dirY = 0;
291
292 // Gesture start
293 if (that.options.zoom && hasTouch && e.touches.length > 1) {
294 c1 = m.abs(e.touches[0].pageX-e.touches[1].pageX);
295 c2 = m.abs(e.touches[0].pageY-e.touches[1].pageY);
296 that.touchesDistStart = m.sqrt(c1 * c1 + c2 * c2);
297
298 that.originX = m.abs(e.touches[0].pageX + e.touches[1].pageX - that.wrapperOffsetLeft * 2) / 2 - that.x;
299 that.originY = m.abs(e.touches[0].pageY + e.touches[1].pageY - that.wrapperOffsetTop * 2) / 2 - that.y;
300 }
301
302 if (that.options.momentum) {
303 if (that.options.useTransform) {
304 // Very lame general purpose alternative to CSSMatrix
305 matrix = getComputedStyle(that.scroller, null)[vendor + 'Transform'].replace(/[^0-9-.,]/g, '').split(',');
306 x = matrix[4] * 1;
307 y = matrix[5] * 1;
308 } else {
309 x = getComputedStyle(that.scroller, null).left.replace(/[^0-9-]/g, '') * 1;
310 y = getComputedStyle(that.scroller, null).top.replace(/[^0-9-]/g, '') * 1;
311 }
312
313 if (x != that.x || y != that.y) {
314 if (that.options.useTransition) that._unbind('webkitTransitionEnd');
315 else cancelFrame(that.aniTime);
316 that.steps = [];
317 that._pos(x, y);
318 }
319 }
320
321 that.absStartX = that.x; // Needed by snap threshold
322 that.absStartY = that.y;
323
324 that.startX = that.x;
325 that.startY = that.y;
326 that.pointX = point.pageX;
327 that.pointY = point.pageY;
328
329 that.startTime = e.timeStamp || (new Date()).getTime();
330
331 if (that.options.onScrollStart) that.options.onScrollStart.call(that, e);
332
333 that._bind(MOVE_EV);
334 that._bind(END_EV);
335 that._bind(CANCEL_EV);
336 },
337
338 _move: function (e) {
339 var that = this,
340 point = hasTouch ? e.touches[0] : e,
341 deltaX = point.pageX - that.pointX,
342 deltaY = point.pageY - that.pointY,
343 newX = that.x + deltaX,
344 newY = that.y + deltaY,
345 c1, c2, scale,
346 timestamp = e.timeStamp || (new Date()).getTime();
347
348 if (that.options.onBeforeScrollMove) that.options.onBeforeScrollMove.call(that, e);
349
350 // Zoom
351 if (that.options.zoom && hasTouch && e.touches.length > 1) {
352 c1 = m.abs(e.touches[0].pageX - e.touches[1].pageX);
353 c2 = m.abs(e.touches[0].pageY - e.touches[1].pageY);
354 that.touchesDist = m.sqrt(c1*c1+c2*c2);
355
356 that.zoomed = true;
357
358 scale = 1 / that.touchesDistStart * that.touchesDist * this.scale;
359 if (scale < 0.5) scale = 0.5;
360 else if (scale > that.options.zoomMax) scale = that.options.zoomMax;
361 that.lastScale = scale / this.scale;
362
363 newX = this.originX - this.originX * that.lastScale + this.x,
364 newY = this.originY - this.originY * that.lastScale + this.y;
365
366 this.scroller.style[vendor + 'Transform'] = trnOpen + newX + 'px,' + newY + 'px' + trnClose + ' scale(' + scale + ')';
367
368 return;
369 }
370
371 that.pointX = point.pageX;
372 that.pointY = point.pageY;
373
374 // Slow down if outside of the boundaries
375 if (newX > 0 || newX < that.maxScrollX) {
376 newX = that.options.bounce ? that.x + (deltaX / 2) : newX >= 0 || that.maxScrollX >= 0 ? 0 : that.maxScrollX;
377 }
378 if (newY > 0 || newY < that.maxScrollY) {
379 newY = that.options.bounce ? that.y + (deltaY / 2) : newY >= 0 || that.maxScrollY >= 0 ? 0 : that.maxScrollY;
380 }
381
382 if (that.absDistX < 6 && that.absDistY < 6) {
383 that.distX += deltaX;
384 that.distY += deltaY;
385 that.absDistX = m.abs(that.distX);
386 that.absDistY = m.abs(that.distY);
387
388 return;
389 }
390
391 // Lock direction
392 if (that.options.lockDirection) {
393 if (that.absDistX > that.absDistY + 5) {
394 newY = that.y;
395 deltaY = 0;
396 } else if (that.absDistY > that.absDistX + 5) {
397 newX = that.x;
398 deltaX = 0;
399 }
400 }
401
402 that.moved = true;
403 that._pos(newX, newY);
404 that.dirX = deltaX > 0 ? -1 : deltaX < 0 ? 1 : 0;
405 that.dirY = deltaY > 0 ? -1 : deltaY < 0 ? 1 : 0;
406
407 if (timestamp - that.startTime > 300) {
408 that.startTime = timestamp;
409 that.startX = that.x;
410 that.startY = that.y;
411 }
412
413 if (that.options.onScrollMove) that.options.onScrollMove.call(that, e);
414 },
415
416 _end: function (e) {
417 if (hasTouch && e.touches.length != 0) return;
418
419 var that = this,
420 point = hasTouch ? e.changedTouches[0] : e,
421 target, ev,
422 momentumX = { dist:0, time:0 },
423 momentumY = { dist:0, time:0 },
424 duration = (e.timeStamp || (new Date()).getTime()) - that.startTime,
425 newPosX = that.x,
426 newPosY = that.y,
427 distX, distY,
428 newDuration;
429
430 that._unbind(MOVE_EV);
431 that._unbind(END_EV);
432 that._unbind(CANCEL_EV);
433
434 if (that.options.onBeforeScrollEnd) that.options.onBeforeScrollEnd.call(that, e);
435
436 if (that.zoomed) {
437 that.scale = that.scale * that.lastScale;
438 that.x = that.originX - that.originX * that.lastScale + that.x;
439 that.y = that.originY - that.originY * that.lastScale + that.y;
440
441 that.scroller.style.webkitTransform = trnOpen + that.x + 'px,' + that.y + 'px' + trnClose + ' scale(' + that.scale + ')';
442
443 that.refresh();
444 return;
445 }
446
447 if (!that.moved) {
448 if (hasTouch) {
449 if (that.doubleTapTimer && that.options.zoom) {
450 // Double tapped
451 clearTimeout(that.doubleTapTimer);
452 that.doubleTapTimer = null;
453 that.zoom(that.pointX, that.pointY, that.scale == 1 ? that.options.doubleTapZoom : 1);
454 } else {
455 that.doubleTapTimer = setTimeout(function () {
456 that.doubleTapTimer = null;
457
458 // Find the last touched element
459 target = point.target;
460 while (target.nodeType != 1) target = target.parentNode;
461
462 if (target.tagName != 'SELECT' && target.tagName != 'INPUT' && target.tagName != 'TEXTAREA') {
463 ev = document.createEvent('MouseEvents');
464 ev.initMouseEvent('click', true, true, e.view, 1,
465 point.screenX, point.screenY, point.clientX, point.clientY,
466 e.ctrlKey, e.altKey, e.shiftKey, e.metaKey,
467 0, null);
468 ev._fake = true;
469 target.dispatchEvent(ev);
470 }
471 }, that.options.zoom ? 250 : 0);
472 }
473 }
474
475 that._resetPos(200);
476
477 if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);
478 return;
479 }
480
481 if (duration < 300 && that.options.momentum) {
482 momentumX = newPosX ? that._momentum(newPosX - that.startX, duration, -that.x, that.scrollerW - that.wrapperW + that.x, that.options.bounce ? that.wrapperW : 0) : momentumX;
483 momentumY = newPosY ? that._momentum(newPosY - that.startY, duration, -that.y, (that.maxScrollY < 0 ? that.scrollerH - that.wrapperH + that.y : 0), that.options.bounce ? that.wrapperH : 0) : momentumY;
484
485 newPosX = that.x + momentumX.dist;
486 newPosY = that.y + momentumY.dist;
487
488 if ((that.x > 0 && newPosX > 0) || (that.x < that.maxScrollX && newPosX < that.maxScrollX)) momentumX = { dist:0, time:0 };
489 if ((that.y > 0 && newPosY > 0) || (that.y < that.maxScrollY && newPosY < that.maxScrollY)) momentumY = { dist:0, time:0 };
490 }
491
492 if (momentumX.dist || momentumY.dist) {
493 newDuration = m.max(m.max(momentumX.time, momentumY.time), 10);
494
495 // Do we need to snap?
496 if (that.options.snap) {
497 distX = newPosX - that.absStartX;
498 distY = newPosY - that.absStartY;
499 if (m.abs(distX) < that.options.snapThreshold && m.abs(distY) < that.options.snapThreshold) { that.scrollTo(that.absStartX, that.absStartY, 200); }
500 else {
501 snap = that._snap(newPosX, newPosY);
502 newPosX = snap.x;
503 newPosY = snap.y;
504 newDuration = m.max(snap.time, newDuration);
505 }
506 }
507
508 that.scrollTo(newPosX, newPosY, newDuration);
509
510 if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);
511 return;
512 }
513
514 // Do we need to snap?
515 if (that.options.snap) {
516 distX = newPosX - that.absStartX;
517 distY = newPosY - that.absStartY;
518 if (m.abs(distX) < that.options.snapThreshold && m.abs(distY) < that.options.snapThreshold) that.scrollTo(that.absStartX, that.absStartY, 200);
519 else {
520 snap = that._snap(that.x, that.y);
521 if (snap.x != that.x || snap.y != that.y) that.scrollTo(snap.x, snap.y, snap.time);
522 }
523
524 if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);
525 return;
526 }
527
528 that._resetPos(200);
529 if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);
530 },
531
532 _resetPos: function (time) {
533 var that = this,
534 resetX = that.x >= 0 ? 0 : that.x < that.maxScrollX ? that.maxScrollX : that.x,
535 resetY = that.y >= 0 || that.maxScrollY > 0 ? 0 : that.y < that.maxScrollY ? that.maxScrollY : that.y;
536
537 if (resetX == that.x && resetY == that.y) {
538 if (that.moved) {
539 that.moved = false;
540 if (that.options.onScrollEnd) that.options.onScrollEnd.call(that); // Execute custom code on scroll end
541 }
542
543 if (that.hScrollbar && that.options.hideScrollbar) {
544 if (vendor == 'webkit') that.hScrollbarWrapper.style[vendor + 'TransitionDelay'] = '300ms';
545 that.hScrollbarWrapper.style.opacity = '0';
546 }
547 if (that.vScrollbar && that.options.hideScrollbar) {
548 if (vendor == 'webkit') that.vScrollbarWrapper.style[vendor + 'TransitionDelay'] = '300ms';
549 that.vScrollbarWrapper.style.opacity = '0';
550 }
551
552 return;
553 }
554
555 that.scrollTo(resetX, resetY, time || 0);
556 },
557
558 _wheel: function (e) {
559 var that = this,
560 deltaX, deltaY;
561
562 if ('wheelDeltaX' in e) {
563 deltaX = that.x + e.wheelDeltaX / 12,
564 deltaY = that.y + e.wheelDeltaY / 12;
565 } else if ('detail' in e) {
566 deltaX = that.x - e.detail * 3,
567 deltaY = that.y - e.detail * 3;
568 } else {
569 deltaX = that.x - e.wheelDelta,
570 deltaY = that.y - e.wheelDelta;
571 }
572
573 if (deltaX > 0) deltaX = 0;
574 else if (deltaX < that.maxScrollX) deltaX = that.maxScrollX;
575
576 if (deltaY > 0) deltaY = 0;
577 else if (deltaY < that.maxScrollY) deltaY = that.maxScrollY;
578
579 that.scrollTo(deltaX, deltaY, 0);
580 },
581
582 _mouseout: function (e) {
583 var t = e.relatedTarget;
584
585 if (!t) {
586 this._end(e);
587 return;
588 }
589
590 while (t = t.parentNode) if (t == this.wrapper) return;
591
592 this._end(e);
593 },
594
595 _transitionEnd: function (e) {
596 var that = this;
597
598 if (e.target != that.scroller) return;
599
600 that._unbind('webkitTransitionEnd');
601
602 that._startAni();
603 },
604
605
606 /**
607 *
608 * Utilities
609 *
610 */
611 _startAni: function () {
612 var that = this,
613 startX = that.x, startY = that.y,
614 startTime = (new Date).getTime(),
615 step, easeOut;
616
617 if (that.animating) return;
618
619 if (!that.steps.length) {
620 that._resetPos(400);
621 return;
622 }
623
624 step = that.steps.shift();
625
626 if (step.x == startX && step.y == startY) step.time = 0;
627
628 that.animating = true;
629 that.moved = true;
630
631 if (that.options.useTransition) {
632 that._transitionTime(step.time);
633 that._pos(step.x, step.y);
634 that.animating = false;
635 if (step.time) that._bind('webkitTransitionEnd');
636 return;
637 }
638
639 (function animate () {
640 var now = (new Date).getTime(),
641 newX, newY;
642
643 if (now >= startTime + step.time) {
644 that._pos(step.x, step.y);
645 that.animating = false;
646 if (that.options.onAnimationEnd) that.options.onAnimationEnd.call(that); // Execute custom code on animation end
647 that._startAni();
648 return;
649 }
650
651 now = (now - startTime) / step.time - 1;
652 easeOut = m.sqrt(1 - now * now);
653 newX = (step.x - startX) * easeOut + startX;
654 newY = (step.y - startY) * easeOut + startY;
655 that._pos(newX, newY);
656 if (that.animating) that.aniTime = nextFrame(animate);
657 })();
658 },
659
660 _transitionTime: function (time) {
661 time += 'ms';
662 this.scroller.style[vendor + 'TransitionDuration'] = time;
663 if (this.hScrollbar) this.hScrollbarIndicator.style[vendor + 'TransitionDuration'] = time;
664 if (this.vScrollbar) this.vScrollbarIndicator.style[vendor + 'TransitionDuration'] = time;
665 },
666
667 _momentum: function (dist, time, maxDistUpper, maxDistLower, size) {
668 var deceleration = 0.0006,
669 speed = m.abs(dist) / time,
670 newDist = (speed * speed) / (2 * deceleration),
671 newTime = 0, outsideDist = 0;
672
673 // Proportinally reduce speed if we are outside of the boundaries
674 if (dist > 0 && newDist > maxDistUpper) {
675 outsideDist = size / (6 / (newDist / speed * deceleration));
676 maxDistUpper = maxDistUpper + outsideDist;
677 speed = speed * maxDistUpper / newDist;
678 newDist = maxDistUpper;
679 } else if (dist < 0 && newDist > maxDistLower) {
680 outsideDist = size / (6 / (newDist / speed * deceleration));
681 maxDistLower = maxDistLower + outsideDist;
682 speed = speed * maxDistLower / newDist;
683 newDist = maxDistLower;
684 }
685
686 newDist = newDist * (dist < 0 ? -1 : 1);
687 newTime = speed / deceleration;
688
689 return { dist: newDist, time: m.round(newTime) };
690 },
691
692 _offset: function (el) {
693 var left = -el.offsetLeft,
694 top = -el.offsetTop;
695
696 while (el = el.offsetParent) {
697 left -= el.offsetLeft;
698 top -= el.offsetTop;
699 }
700
701 if (el != this.wrapper) {
702 left *= this.scale;
703 top *= this.scale;
704 }
705
706 return { left: left, top: top };
707 },
708
709 _snap: function (x, y) {
710 var that = this,
711 i, l,
712 page, time,
713 sizeX, sizeY;
714
715 // Check page X
716 page = that.pagesX.length - 1;
717 for (i=0, l=that.pagesX.length; i<l; i++) {
718 if (x >= that.pagesX[i]) {
719 page = i;
720 break;
721 }
722 }
723 if (page == that.currPageX && page > 0 && that.dirX < 0) page--;
724 x = that.pagesX[page];
725 sizeX = m.abs(x - that.pagesX[that.currPageX]);
726 sizeX = sizeX ? m.abs(that.x - x) / sizeX * 500 : 0;
727 that.currPageX = page;
728
729 // Check page Y
730 page = that.pagesY.length-1;
731 for (i=0; i<page; i++) {
732 if (y >= that.pagesY[i]) {
733 page = i;
734 break;
735 }
736 }
737 if (page == that.currPageY && page > 0 && that.dirY < 0) page--;
738 y = that.pagesY[page];
739 sizeY = m.abs(y - that.pagesY[that.currPageY]);
740 sizeY = sizeY ? m.abs(that.y - y) / sizeY * 500 : 0;
741 that.currPageY = page;
742
743 // Snap with constant speed (proportional duration)
744 time = m.round(m.max(sizeX, sizeY)) || 200;
745
746 return { x: x, y: y, time: time };
747 },
748
749 _bind: function (type, el, bubble) {
750 (el || this.scroller).addEventListener(type, this, !!bubble);
751 },
752
753 _unbind: function (type, el, bubble) {
754 (el || this.scroller).removeEventListener(type, this, !!bubble);
755 },
756
757
758 /**
759 *
760 * Public methods
761 *
762 */
763 destroy: function () {
764 var that = this;
765
766 that.scroller.style[vendor + 'Transform'] = '';
767
768 // Remove the scrollbars
769 that.hScrollbar = false;
770 that.vScrollbar = false;
771 that._scrollbar('h');
772 that._scrollbar('v');
773
774 // Remove the event listeners
775 that._unbind(RESIZE_EV);
776 that._unbind(START_EV);
777 that._unbind(MOVE_EV);
778 that._unbind(END_EV);
779 that._unbind(CANCEL_EV);
780
781 if (that.options.hasTouch) {
782 that._unbind('mouseout', that.wrapper);
783 that._unbind(WHEEL_EV);
784 }
785
786 if (that.options.useTransition) that._unbind('webkitTransitionEnd');
787
788 if (that.options.onDestroy) that.options.onDestroy.call(that);
789 },
790
791 refresh: function () {
792 var that = this,
793 offset,
794 pos = 0,
795 page = 0;
796
797 if (that.scale < that.options.zoomMin) that.scale = that.options.zoomMin;
798 that.wrapperW = that.wrapper.clientWidth || 1;
799 that.wrapperH = that.wrapper.clientHeight || 1;
800
801 that.scrollerW = m.round(that.scroller.offsetWidth * that.scale);
802 that.scrollerH = m.round(that.scroller.offsetHeight * that.scale);
803 that.maxScrollX = that.wrapperW - that.scrollerW;
804 that.maxScrollY = that.wrapperH - that.scrollerH;
805 that.dirX = 0;
806 that.dirY = 0;
807
808 that.hScroll = that.options.hScroll && that.maxScrollX < 0;
809 that.vScroll = that.options.vScroll && (!that.options.bounceLock && !that.hScroll || that.scrollerH > that.wrapperH);
810
811 that.hScrollbar = that.hScroll && that.options.hScrollbar;
812 that.vScrollbar = that.vScroll && that.options.vScrollbar && that.scrollerH > that.wrapperH;
813
814 offset = that._offset(that.wrapper);
815 that.wrapperOffsetLeft = -offset.left;
816 that.wrapperOffsetTop = -offset.top;
817
818 // Prepare snap
819 if (typeof that.options.snap == 'string') {
820 that.pagesX = [];
821 that.pagesY = [];
822 els = that.scroller.querySelectorAll(that.options.snap);
823 for (i=0, l=els.length; i<l; i++) {
824 pos = that._offset(els[i]);
825 pos.left += that.wrapperOffsetLeft;
826 pos.top += that.wrapperOffsetTop;
827 that.pagesX[i] = pos.left < that.maxScrollX ? that.maxScrollX : pos.left * that.scale;
828 that.pagesY[i] = pos.top < that.maxScrollY ? that.maxScrollY : pos.top * that.scale;
829 }
830 } else if (that.options.snap) {
831 that.pagesX = [];
832 while (pos >= that.maxScrollX) {
833 that.pagesX[page] = pos;
834 pos = pos - that.wrapperW;
835 page++;
836 }
837 if (that.maxScrollX%that.wrapperW) that.pagesX[that.pagesX.length] = that.maxScrollX - that.pagesX[that.pagesX.length-1] + that.pagesX[that.pagesX.length-1];
838
839 pos = 0;
840 page = 0;
841 that.pagesY = [];
842 while (pos >= that.maxScrollY) {
843 that.pagesY[page] = pos;
844 pos = pos - that.wrapperH;
845 page++;
846 }
847 if (that.maxScrollY%that.wrapperH) that.pagesY[that.pagesY.length] = that.maxScrollY - that.pagesY[that.pagesY.length-1] + that.pagesY[that.pagesY.length-1];
848 }
849
850 // Prepare the scrollbars
851 that._scrollbar('h');
852 that._scrollbar('v');
853
854 that.scroller.style[vendor + 'TransitionDuration'] = '0';
855
856 that._resetPos(200);
857 },
858
859 scrollTo: function (x, y, time, relative) {
860 var that = this,
861 step = x,
862 i, l;
863
864 that.stop();
865
866 if (!step.length) step = [{ x: x, y: y, time: time, relative: relative }];
867
868 for (i=0, l=step.length; i<l; i++) {
869 if (step[i].relative) { step[i].x = that.x - step[i].x; step[i].y = that.y - step[i].y; }
870 that.steps.push({ x: step[i].x, y: step[i].y, time: step[i].time || 0 });
871 }
872
873 that._startAni();
874 },
875
876 scrollToElement: function (el, time) {
877 var that = this, pos;
878 el = el.nodeType ? el : that.scroller.querySelector(el);
879 if (!el) return;
880
881 pos = that._offset(el);
882 pos.left += that.wrapperOffsetLeft;
883 pos.top += that.wrapperOffsetTop;
884
885 pos.left = pos.left > 0 ? 0 : pos.left < that.maxScrollX ? that.maxScrollX : pos.left;
886 pos.top = pos.top > 0 ? 0 : pos.top < that.maxScrollY ? that.maxScrollY : pos.top;
887 time = time === undefined ? m.max(m.abs(pos.left)*2, m.abs(pos.top)*2) : time;
888
889 that.scrollTo(pos.left, pos.top, time);
890 },
891
892 scrollToPage: function (pageX, pageY, time) {
893 var that = this, x, y;
894
895 if (that.options.snap) {
896 pageX = pageX == 'next' ? that.currPageX+1 : pageX == 'prev' ? that.currPageX-1 : pageX;
897 pageY = pageY == 'next' ? that.currPageY+1 : pageY == 'prev' ? that.currPageY-1 : pageY;
898
899 pageX = pageX < 0 ? 0 : pageX > that.pagesX.length-1 ? that.pagesX.length-1 : pageX;
900 pageY = pageY < 0 ? 0 : pageY > that.pagesY.length-1 ? that.pagesY.length-1 : pageY;
901
902 that.currPageX = pageX;
903 that.currPageY = pageY;
904 x = that.pagesX[pageX];
905 y = that.pagesY[pageY];
906 } else {
907 x = -that.wrapperW * pageX;
908 y = -that.wrapperH * pageY;
909 if (x < that.maxScrollX) x = that.maxScrollX;
910 if (y < that.maxScrollY) y = that.maxScrollY;
911 }
912
913 that.scrollTo(x, y, time || 400);
914 },
915
916 disable: function () {
917 this.stop();
918 this._resetPos(0);
919 this.enabled = false;
920
921 // If disabled after touchstart we make sure that there are no left over events
922 this._unbind(MOVE_EV);
923 this._unbind(END_EV);
924 this._unbind(CANCEL_EV);
925 },
926
927 enable: function () {
928 this.enabled = true;
929 },
930
931 stop: function () {
932 if (this.options.useTransition) this._unbind('webkitTransitionEnd');
933 else cancelFrame(this.aniTime);
934 this.steps = [];
935 this.moved = false;
936 this.animating = false;
937 },
938
939 zoom: function (x, y, scale, time) {
940 var that = this,
941 relScale = scale / that.scale;
942
943 if (!that.options.useTransform) return;
944
945 time = (time || 200) + 'ms'
946 x = x - that.wrapperOffsetLeft - that.x;
947 y = y - that.wrapperOffsetTop - that.y;
948 that.x = x - x * relScale + that.x;
949 that.y = y - y * relScale + that.y;
950
951 that.scale = scale;
952
953 that.scroller.style[vendor + 'TransitionDuration'] = time;
954 that.scroller.style[vendor + 'Transform'] = trnOpen + that.x + 'px,' + that.y + 'px' + trnClose + ' scale(' + scale + ')';
955
956 that.refresh();
957 }
958 };
959
960 if (typeof exports !== 'undefined') exports.iScroll = iScroll;
961 else window.iScroll = iScroll;
962
963 })();