PageRenderTime 104ms CodeModel.GetById 3ms app.highlight 91ms RepoModel.GetById 2ms app.codeStats 0ms

/src/iscroll.js

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