/** * af.scroller * created by appMobi with modifications by Carlos Ouro @ Badoo and Intel * Supports iOS native touch scrolling * Optimizations and bug improvements by Intel * @copyright Intel */ (function ($) { var HIDE_REFRESH_TIME = 325; // hide animation of pull2ref duration in ms var cache = []; var objId = function (obj) { if (!obj.afScrollerId) obj.afScrollerId = $.uuid(); return obj.afScrollerId; }; $.fn["scroller"] = function (opts) { var tmp, id; for (var i = 0; i < this.length; i++) { //cache system id = objId(this[i]); if (!cache[id]) { if (!opts) opts = {}; if (!$.feat.nativeTouchScroll) opts.useJsScroll = true; tmp = scroller(this[i], opts); cache[id] = tmp; } else { tmp = cache[id]; } } return this.length == 1 ? tmp : this; }; var boundTouchLayer = false; function checkConsistency(id) { if (!cache[id].el) { delete cache[id]; return false; } return true; } function bindTouchLayer() { //use a single bind for all scrollers if (af.os.android && !af.os.chrome && af.os.webkit) { var androidFixOn = false; //connect to touchLayer to detect editMode $.bind($.touchLayer, ['cancel-enter-edit', 'exit-edit'], function (focusEl) { if (androidFixOn) { androidFixOn = false; //dehactivate on scroller for (var el in cache) if (checkConsistency(el) && cache[el].androidFormsMode) cache[el].stopFormsMode(); } }); } boundTouchLayer = true; } var scroller = (function () { var translateOpen = $.feat.cssTransformStart; var translateClose = $.feat.cssTransformEnd; var jsScroller, nativeScroller; //initialize and js/native mode selector var scroller = function (elID, opts) { var el; if (!boundTouchLayer && $.touchLayer && $.isObject($.touchLayer)) bindTouchLayer(); else if (!$.touchLayer || !$.isObject($.touchLayer)) $.touchLayer = {}; if (typeof elID == "string" || elID instanceof String) { el = document.getElementById(elID); } else { el = elID; } if (!el) { alert("Could not find element for scroller " + elID); return; } if (af.os.desktop) return new scrollerCore(el, opts); else if (opts.useJsScroll) return new jsScroller(el, opts); return new nativeScroller(el, opts); }; //parent abstract class (common functionality) var scrollerCore = function (el, opts) { this.el = el; this.afEl = $(this.el); for (var j in opts) { this[j] = opts[j]; } }; scrollerCore.prototype = { //core default properties refresh: false, refreshContent: "Pull to Refresh", refreshHangTimeout: 2000, refreshHeight: 60, refreshElement: null, refreshCancelCB: null, refreshRunning: false, scrollTop: 0, scrollLeft: 0, preventHideRefresh: true, verticalScroll: false, horizontalScroll: true, refreshTriggered: false, moved: false, eventsActive: false, rememberEventsActive: false, scrollingLocked: false, autoEnable: true, blockFormsFix: false, loggedPcentY: 0, loggedPcentX: 0, infinite: false, infiniteEndCheck: false, infiniteTriggered: false, scrollSkip: false, scrollTopInterval: null, scrollLeftInterval: null, bubbles:true, lockBounce:false, _scrollTo: function (params, time) { time = parseInt(time, 10); if (time === 0 || isNaN(time)) { this.el.scrollTop = Math.abs(params.y); this.el.scrollLeft = Math.abs(params.x); return; } var singleTick = 10; var distPerTick = (this.el.scrollTop - params.y) / Math.ceil(time / singleTick); var distLPerTick = (this.el.scrollLeft - params.x) / Math.ceil(time / singleTick); var self = this; var toRunY = Math.ceil(this.el.scrollTop - params.y) / distPerTick; var toRunX = Math.ceil(this.el.scrollLeft - params.x) / distPerTick; var xRun =0, yRun = 0; self.scrollTopInterval = window.setInterval(function () { self.el.scrollTop -= distPerTick; yRun++; if (yRun >= toRunY) { self.el.scrollTop = params.y; clearInterval(self.scrollTopInterval); } }, singleTick); self.scrollLeftInterval = window.setInterval(function () { self.el.scrollLeft -= distLPerTick; xRun++; if (xRun >= toRunX) { self.el.scrollLeft = params.x; clearInterval(self.scrollLeftInterval); } }, singleTick); }, enable: function () {}, disable: function () {}, hideScrollbars: function () {}, addPullToRefresh: function () {}, /** * We do step animations for 'native' - iOS is acceptable and desktop browsers are fine * instead of css3 */ _scrollToTop: function (time) { this._scrollTo({ x: 0, y: 0 }, time); }, _scrollToBottom: function (time) { this._scrollTo({ x: 0, y: this.el.scrollHeight - this.el.offsetHeight }, time); }, scrollToBottom: function (time) { return this._scrollToBottom(time); }, scrollToTop: function (time) { return this._scrollToTop(time); }, //methods init: function (el, opts) { this.el = el; this.afEl = $(this.el); this.defaultProperties(); for (var j in opts) { this[j] = opts[j]; } //assign self destruct var that = this; var orientationChangeProxy = function () { //no need to readjust if disabled... if (that.eventsActive&&!$.feat.nativeTouchScroll) that.adjustScroll(); }; this.afEl.bind('destroy', function () { that.disable(true); //with destroy notice var id = that.el.afScrollerId; if (cache[id]) delete cache[id]; $.unbind($.touchLayer, 'orientationchange-reshape', orientationChangeProxy); }); $.bind($.touchLayer, 'orientationchange-reshape', orientationChangeProxy); $(window).bind('resize', orientationChangeProxy); }, needsFormsFix: function (focusEl) { return this.useJsScroll && this.isEnabled() && this.el.style.display != "none" && $(focusEl).closest(this.afEl).size() > 0; }, handleEvent: function (e) { if (!this.scrollingLocked) { switch (e.type) { case 'touchstart': clearInterval(this.scrollTopInterval); this.preventHideRefresh = !this.refreshRunning; // if it's not running why prevent it xD this.moved = false; this.onTouchStart(e); if(!this.bubbles) e.stopPropagation(); break; case 'touchmove': this.onTouchMove(e); if(!this.bubbles) e.stopPropagation(); break; case 'touchend': this.onTouchEnd(e); if(!this.bubbles) e.stopPropagation(); break; case 'scroll': this.onScroll(e); break; } } }, coreAddPullToRefresh: function (rEl) { if (rEl) this.refreshElement = rEl; //Add the pull to refresh text. Not optimal but keeps from others overwriting the content and worrying about italics //add the refresh div var afEl; if (this.refreshElement === null) { var orginalEl = document.getElementById(this.container.id + "_pulldown"); if (orginalEl !== null) { afEl = af(orginalEl); } else { afEl = af("
" + this.refreshContent + "
"); } } else { afEl = af(this.refreshElement); } var el = afEl.get(0); this.refreshContainer = af('
'); $(this.el).prepend(this.refreshContainer.prepend(el)); this.refreshContainer = this.refreshContainer[0]; }, fireRefreshRelease: function (triggered, allowHide) { if (!this.refresh || !triggered) return; this.setRefreshContent("Refreshing..."); var autoCancel = $.trigger(this, 'refresh-release', [triggered]) !== false; this.preventHideRefresh = false; this.refreshRunning = true; if (autoCancel) { var that = this; if (this.refreshHangTimeout > 0) this.refreshCancelCB = setTimeout(function () { that.hideRefresh(); }, this.refreshHangTimeout); } }, setRefreshContent: function (content) { af(this.container).find(".afscroll_refresh").html(content); }, lock: function () { if (this.scrollingLocked) return; this.scrollingLocked = true; this.rememberEventsActive = this.eventsActive; if (this.eventsActive) { this.disable(); } }, unlock: function () { if (!this.scrollingLocked) return; this.scrollingLocked = false; if (this.rememberEventsActive) { this.enable(); } }, scrollToItem: function (el, where) { //TODO: add functionality for x position if (!$.is$(el)) el = $(el); var newTop,itemPos,panelTop,itemTop; if (where == 'bottom') { itemPos = el.offset(); newTop = itemPos.top - this.afEl.offset().bottom + itemPos.height; newTop += 4; //add a small space } else { itemTop = el.offset().top; newTop = itemTop - document.body.scrollTop; panelTop = this.afEl.offset().top; if (document.body.scrollTop < panelTop) { newTop -= panelTop; } newTop -= 4; //add a small space } this.scrollBy({ y: newTop, x: 0 }, 0); }, setPaddings: function (top, bottom) { var el = $(this.el); var curTop = numOnly(el.css('paddingTop')); el.css('paddingTop', top + "px").css('paddingBottom', bottom + "px"); //don't let padding mess with scroll this.scrollBy({ y: top - curTop, x: 0 }); }, //freak of mathematics, but for our cases it works divide: function (a, b) { return b !== 0 ? a / b : 0; }, isEnabled: function () { return this.eventsActive; }, addInfinite: function () { this.infinite = true; }, clearInfinite: function () { this.infiniteTriggered = false; this.scrollSkip = true; }, scrollTo:function (pos, time) { return this._scrollTo(pos, time); } }; //extend to jsScroller and nativeScroller (constructs) jsScroller = function (el, opts) { this.init(el, opts); //test //this.refresh=true; this.container = this.el.parentNode; this.container.afScrollerId = el.afScrollerId; this.afEl = $(this.container); if (this.container.style.overflow != 'hidden') this.container.style.overflow = 'hidden'; this.addPullToRefresh(null, true); if (this.autoEnable) this.enable(true); var scrollDiv; //create vertical scroll if (this.verticalScroll && this.verticalScroll === true && this.scrollBars === true) { scrollDiv = createScrollBar(5, 20); scrollDiv.style.top = "0px"; if (this.vScrollCSS) scrollDiv.className = this.vScrollCSS; //scrollDiv.style.opacity = "0"; scrollDiv.style.display='none'; this.container.appendChild(scrollDiv); this.vscrollBar = scrollDiv; scrollDiv = null; } //create horizontal scroll if (this.horizontalScroll && this.horizontalScroll === true && this.scrollBars === true) { scrollDiv = createScrollBar(20, 5); scrollDiv.style.bottom = "0px"; if (this.hScrollCSS) scrollDiv.className = this.hScrollCSS; //scrollDiv.style.opacity = "0"; scrollDiv.style.display='none'; this.container.appendChild(scrollDiv); this.hscrollBar = scrollDiv; scrollDiv = null; } if (this.horizontalScroll) this.el.style['float'] = "left"; this.el.hasScroller = true; }; nativeScroller = function (el, opts) { if(opts.nativeParent){ el=el.parentNode; } this.init(el, opts); var $el = $(el); if (opts.noParent !== true) { var oldParent = $el.parent(); $el.css('height', oldParent.height()).css("width", oldParent.width()); $el.insertBefore($el.parent()); //$el.parent().parent().append($el); oldParent.remove(); } this.container = this.el; $el.css("-webkit-overflow-scrolling", "touch"); if(opts.autoEnable) this.enable(); }; nativeScroller.prototype = new scrollerCore(); jsScroller.prototype = new scrollerCore(); ///Native scroller nativeScroller.prototype.defaultProperties = function () { this.refreshContainer = null; this.dY = this.cY = 0; this.dX = this.cX = 0; this.cancelPropagation = false; this.loggedPcentY = 0; this.loggedPcentX = 0; var that = this; this.adjustScrollOverflowProxy_ = function () { that.afEl.css('overflow', 'auto'); that.afEl.parent().css("overflow","hidden"); }; }; nativeScroller.prototype.enable = function (firstExecution) { if (this.eventsActive) return; this.eventsActive = true; //unlock overflow this.el.style.overflow = 'auto'; this.el.parentNode.style.overflow="hidden"; //set current scroll if (!firstExecution) this.adjustScroll(); //set events this.el.addEventListener('touchstart', this, false); this.el.addEventListener('scroll', this, false); }; nativeScroller.prototype.disable = function (destroy) { if (!this.eventsActive) return; //log current scroll this.logPos(this.el.scrollLeft, this.el.scrollTop); //lock overflow if (!destroy&&!$.ui) { this.el.style.overflow = 'hidden'; } //remove events this.el.removeEventListener('touchstart', this, false); this.el.removeEventListener('touchmove', this, false); this.el.removeEventListener('touchend', this, false); this.el.removeEventListener('scroll', this, false); this.eventsActive = false; }; nativeScroller.prototype.addPullToRefresh = function (el, leaveRefresh) { this.el.removeEventListener('touchstart', this, false); this.el.addEventListener('touchstart', this, false); if (!leaveRefresh) this.refresh = true; if (this.refresh && this.refresh === true) { this.coreAddPullToRefresh(el); this.refreshContainer.style.position = "absolute"; this.refreshContainer.style.top = "-60px"; this.refreshContainer.style.height = "60px"; this.refreshContainer.style.display = "block"; } }; nativeScroller.prototype.onTouchStart = function (e) { if(this.el.scrollTop===0) this.el.scrollTop=1; if(this.el.scrollTop===(this.el.scrollHeight - this.el.clientHeight)) this.el.scrollTop-=1; if(this.horizontalScroll){ if(this.el.scrollLeft===0) this.el.scrollLeft=1; if(this.el.scrollLeft===(this.el.scrollWidth-this.el.clientWidth)) this.el.scrollLeft-=1; } if (this.refreshCancelCB) clearTimeout(this.refreshCancelCB); //get refresh ready this.el.addEventListener('touchmove', this,false); this.dY = e.touches[0].pageY; if (this.refresh || this.infinite) { if (this.refresh && this.dY < 0) { this.showRefresh(); } } }; nativeScroller.prototype.onTouchMove = function (e) { var newcY = e.touches[0].pageY - this.dY; var newcX = e.touches[0].pageX - this.dX; if(this.hasVertScroll&&this.el.clientHeight==this.el.scrollHeight){ e.preventDefault(); } if(this.hasHorScroll&&this.el.clientWidth==this.el.scrollWidth){ e.preventDefault(); } if (!this.moved) { $.trigger(this, "scrollstart", [this.el]); $.trigger($.touchLayer, "scrollstart", [this.el]); this.el.addEventListener('touchend', this, false); this.moved = true; } var difY = newcY - this.cY; var difX = newcX-this.cX; //check for trigger if (this.refresh && (this.el.scrollTop < -this.refreshHeight)) { this.showRefresh(); //check for cancel when refresh is running } else if (this.refresh && this.refreshTriggered && this.refreshRunning && (this.el.scrollTop > this.refreshHeight)) { this.refreshTriggered = false; this.refreshRunning = false; if (this.refreshCancelCB) clearTimeout(this.refreshCancelCB); this.hideRefresh(false); this.setRefreshContent("Pull to Refresh"); $.trigger(this, 'refresh-cancel'); //check for cancel when refresh is not running } else if (this.refresh && this.refreshTriggered && !this.refreshRunning && (this.el.scrollTop > -this.refreshHeight)) { this.refreshTriggered = false; this.refreshRunning = false; if (this.refreshCancelCB) clearTimeout(this.refreshCancelCB); this.hideRefresh(false); this.setRefreshContent("Pull to Refresh"); $.trigger(this, 'refresh-cancel'); } this.cY = newcY; this.cX = newcX; }; nativeScroller.prototype.showRefresh = function () { if (!this.refreshTriggered) { this.refreshTriggered = true; this.setRefreshContent("Release to Refresh"); $.trigger(this, 'refresh-trigger'); } }; nativeScroller.prototype.onTouchEnd = function (e) { var triggered = this.el.scrollTop <= -(this.refreshHeight); this.fireRefreshRelease(triggered, true); if (triggered&&this.refresh) { //lock in place this.refreshContainer.style.position = "relative"; this.refreshContainer.style.top = "0px"; } this.dY = this.cY = 0; this.el.removeEventListener('touchmove', this, false); this.el.removeEventListener('touchend', this, false); this.infiniteEndCheck = true; if (this.infinite && !this.infiniteTriggered && (Math.abs(this.el.scrollTop) >= (this.el.scrollHeight - this.el.clientHeight))) { this.infiniteTriggered = true; $.trigger(this, "infinite-scroll"); this.infiniteEndCheck = true; } this.touchEndFired = true; //pollyfil for scroll end since webkit doesn't give any events during the "flick" var max = 200; var self = this; var currPos = { top: this.el.scrollTop, left: this.el.scrollLeft }; var counter = 0; self.nativePolling = setInterval(function () { counter++; if (counter >= max) { clearInterval(self.nativePolling); return; } if (self.el.scrollTop != currPos.top || self.el.scrollLeft != currPos.left) { clearInterval(self.nativePolling); $.trigger($.touchLayer, 'scrollend', [self.el]); //notify touchLayer of this elements scrollend $.trigger(self, "scrollend", [self.el]); } }, 20); }; nativeScroller.prototype.hideRefresh = function (animate) { if (this.preventHideRefresh) return; var that = this; var endAnimationCb = function (canceled) { that.refreshContainer.style.top = "-60px"; that.refreshContainer.style.position = "absolute"; that.dY = that.cY = 0; if (!canceled) { //not sure if this should be the correct logic.... that.el.style[$.feat.cssPrefix + "Transform"] = "none"; that.el.style[$.feat.cssPrefix + "TransitionProperty"] = "none"; that.el.scrollTop = 0; that.logPos(that.el.scrollLeft, 0); that.refreshRunning = false; that.setRefreshContent("Pull to Refresh"); $.trigger(that, "refresh-finish"); } }; if (animate === false || !that.afEl.css3Animate) { endAnimationCb(); } else { that.afEl.css3Animate({ y: (that.el.scrollTop - that.refreshHeight) + "px", x: "0%", time: HIDE_REFRESH_TIME + "ms", complete: endAnimationCb }); } this.refreshTriggered = false; //this.el.addEventListener('touchend', this, false); }; nativeScroller.prototype.hideScrollbars = function () {}; nativeScroller.prototype.scrollTo = function (pos, time) { this.logPos(pos.x, pos.y); pos.x *= -1; pos.y *= -1; return this._scrollTo(pos, time); }; nativeScroller.prototype.scrollBy = function (pos, time) { pos.x += this.el.scrollLeft; pos.y += this.el.scrollTop; this.logPos(this.el.scrollLeft, this.el.scrollTop); return this._scrollTo(pos, time); }; nativeScroller.prototype.scrollToBottom = function (time) { //this.el.scrollTop = this.el.scrollHeight; this._scrollToBottom(time); this.logPos(this.el.scrollLeft, this.el.scrollTop); }; nativeScroller.prototype.onScroll = function (e) { if (this.infinite && this.touchEndFired) { this.touchEndFired = false; return; } if (this.scrollSkip) { this.scrollSkip = false; return; } if (this.infinite) { if (!this.infiniteTriggered && (Math.abs(this.el.scrollTop) >= (this.el.scrollHeight - this.el.clientHeight))) { this.infiniteTriggered = true; $.trigger(this, "infinite-scroll"); this.infiniteEndCheck = true; } } var that = this; if (this.infinite && this.infiniteEndCheck && this.infiniteTriggered) { this.infiniteEndCheck = false; $.trigger(that, "infinite-scroll-end"); } }; nativeScroller.prototype.logPos = function (x, y) { this.loggedPcentX = this.divide(x, (this.el.scrollWidth)); this.loggedPcentY = this.divide(y, (this.el.scrollHeight)); this.scrollLeft = x; this.scrollTop = y; if (isNaN(this.loggedPcentX)) this.loggedPcentX = 0; if (isNaN(this.loggedPcentY)) this.loggedPcentY = 0; }; nativeScroller.prototype.adjustScroll = function () { this.adjustScrollOverflowProxy_(); this.el.scrollLeft = this.loggedPcentX * (this.el.scrollWidth); this.el.scrollTop = this.loggedPcentY * (this.el.scrollHeight); this.logPos(this.el.scrollLeft, this.el.scrollTop); }; //JS scroller jsScroller.prototype.defaultProperties = function () { this.boolScrollLock = false; this.currentScrollingObject = null; this.elementInfo = null; this.verticalScroll = true; this.horizontalScroll = false; this.scrollBars = true; this.vscrollBar = null; this.hscrollBar = null; this.hScrollCSS = "scrollBar"; this.vScrollCSS = "scrollBar"; this.firstEventInfo = null; this.moved = false; this.preventPullToRefresh = true; this.isScrolling = false; this.androidFormsMode = false; this.refreshSafeKeep = false; this.lastScrollbar = ""; this.finishScrollingObject = null; this.container = null; this.scrollingFinishCB = null; this.loggedPcentY = 0; this.loggedPcentX = 0; }; function createScrollBar(width, height) { var scrollDiv = document.createElement("div"); scrollDiv.style.position = 'absolute'; scrollDiv.style.width = width + "px"; scrollDiv.style.height = height + "px"; scrollDiv.style[$.feat.cssPrefix + 'BorderRadius'] = "2px"; scrollDiv.style.borderRadius = "2px"; scrollDiv.style.display="none"; scrollDiv.className = 'scrollBar'; scrollDiv.style.background = "black"; return scrollDiv; } jsScroller.prototype.enable = function (firstExecution) { if (this.eventsActive) return; this.eventsActive = true; if (!firstExecution) this.adjustScroll(); else this.scrollerMoveCSS({ x: 0, y: 0 }, 0); //add listeners this.container.addEventListener('touchstart', this, false); this.container.addEventListener('touchmove', this, false); this.container.addEventListener('touchend', this, false); }; jsScroller.prototype.adjustScroll = function () { //set top/left var size = this.getViewportSize(); this.scrollerMoveCSS({ x: Math.round(this.loggedPcentX * (this.el.clientWidth - size.w)), y: Math.round(this.loggedPcentY * (this.el.clientHeight - size.h)) }, 0); }; jsScroller.prototype.disable = function () { if (!this.eventsActive) return; //log top/left var cssMatrix = this.getCSSMatrix(this.el); this.logPos((numOnly(cssMatrix.e) - numOnly(this.container.scrollLeft)), (numOnly(cssMatrix.f) - numOnly(this.container.scrollTop))); //remove event listeners this.container.removeEventListener('touchstart', this, false); this.container.removeEventListener('touchmove', this, false); this.container.removeEventListener('touchend', this, false); this.eventsActive = false; }; jsScroller.prototype.addPullToRefresh = function (el, leaveRefresh) { if (!leaveRefresh) this.refresh = true; if (this.refresh && this.refresh === true) { this.coreAddPullToRefresh(el); this.el.style.overflow = 'visible'; } }; jsScroller.prototype.hideScrollbars = function () { if (this.hscrollBar) { this.hscrollBar.style.display="none"; this.hscrollBar.style[$.feat.cssPrefix + 'TransitionDuration'] = "0ms"; } if (this.vscrollBar) { this.vscrollBar.style.display="none"; this.vscrollBar.style[$.feat.cssPrefix + 'TransitionDuration'] = "0ms"; } }; jsScroller.prototype.getViewportSize = function () { var style = window.getComputedStyle(this.container); if (isNaN(numOnly(style.paddingTop))) alert((typeof style.paddingTop) + '::' + style.paddingTop + ':'); return { h: (this.container.clientHeight > window.innerHeight ? window.innerHeight : this.container.clientHeight - numOnly(style.paddingTop) - numOnly(style.paddingBottom)), w: (this.container.clientWidth > window.innerWidth ? window.innerWidth : this.container.clientWidth - numOnly(style.paddingLeft) - numOnly(style.paddingRight)) }; }; jsScroller.prototype.onTouchStart = function (event) { this.moved = false; this.currentScrollingObject = null; if (!this.container) return; if (this.refreshCancelCB) { clearTimeout(this.refreshCancelCB); this.refreshCancelCB = null; } if (this.scrollingFinishCB) { clearTimeout(this.scrollingFinishCB); this.scrollingFinishCB = null; } //disable if locked if (event.touches.length != 1 || this.boolScrollLock) return; // Allow interaction to legit calls, like select boxes, etc. if (event.touches[0].target && event.touches[0].target.type !== undefined) { var tagname = event.touches[0].target.tagName.toLowerCase(); var tagtype=event.touches[0].target.type.toLowerCase(); if (tagname == "select" ) // stuff we need to allow // access to legit calls return; } //default variables var scrollInfo = { //current position top: 0, left: 0, //current movement speedY: 0, speedX: 0, absSpeedY: 0, absSpeedX: 0, deltaY: 0, deltaX: 0, absDeltaY: 0, absDeltaX: 0, y: 0, x: 0, duration: 0 }; //element info this.elementInfo = {}; var size = this.getViewportSize(); this.elementInfo.bottomMargin = size.h; this.elementInfo.maxTop = (this.el.clientHeight - this.elementInfo.bottomMargin); if (this.elementInfo.maxTop < 0) this.elementInfo.maxTop = 0; this.elementInfo.divHeight = this.el.clientHeight; this.elementInfo.rightMargin = size.w; this.elementInfo.maxLeft = (this.el.clientWidth - this.elementInfo.rightMargin); if (this.elementInfo.maxLeft < 0) this.elementInfo.maxLeft = 0; this.elementInfo.divWidth = this.el.clientWidth; this.elementInfo.hasVertScroll = this.verticalScroll || this.elementInfo.maxTop > 0; this.elementInfo.hasHorScroll = this.elementInfo.maxLeft > 0; this.elementInfo.requiresVScrollBar = this.vscrollBar && this.elementInfo.hasVertScroll; this.elementInfo.requiresHScrollBar = this.hscrollBar && this.elementInfo.hasHorScroll; //save event this.saveEventInfo(event); this.saveFirstEventInfo(event); //get the current top var cssMatrix = this.getCSSMatrix(this.el); scrollInfo.top = numOnly(cssMatrix.f) - numOnly(this.container.scrollTop); scrollInfo.left = numOnly(cssMatrix.e) - numOnly(this.container.scrollLeft); this.container.scrollTop = this.container.scrollLeft = 0; this.currentScrollingObject = this.el; //get refresh ready if (this.refresh && scrollInfo.top === 0) { this.refreshContainer.style.display = "block"; this.refreshHeight = this.refreshContainer.firstChild.clientHeight; this.refreshContainer.firstChild.style.top = (-this.refreshHeight) + 'px'; this.refreshContainer.style.overflow = 'visible'; this.preventPullToRefresh = false; } else if (scrollInfo.top < 0) { this.preventPullToRefresh = true; if (this.refresh) this.refreshContainer.style.overflow = 'hidden'; } //set target scrollInfo.x = scrollInfo.left; scrollInfo.y = scrollInfo.top; //vertical scroll bar if (this.setVScrollBar(scrollInfo, 0, 0)) { if (this.container.clientWidth > window.innerWidth) this.vscrollBar.style.right = "0px"; else this.vscrollBar.style.right = "0px"; this.vscrollBar.style[$.feat.cssPrefix + "Transition"] = ''; // this.vscrollBar.style.opacity = 1; } //horizontal scroll if (this.setHScrollBar(scrollInfo, 0, 0)) { if (this.container.clientHeight > window.innerHeight) this.hscrollBar.style.top = (window.innerHeight - numOnly(this.hscrollBar.style.height)) + "px"; else this.hscrollBar.style.bottom = numOnly(this.hscrollBar.style.height); this.hscrollBar.style[$.feat.cssPrefix + "Transition"] = ''; // this.hscrollBar.style.opacity = 1; } //save scrollInfo this.lastScrollInfo = scrollInfo; this.hasMoved = false; if(this.elementInfo.maxTop==0&&this.elementInfo.maxLeft==0){ this.currentScrollingObject=null; this.scrollToTop(0); }else{ this.scrollerMoveCSS(this.lastScrollInfo, 0); } }; jsScroller.prototype.getCSSMatrix = function (el) { if (this.androidFormsMode) { //absolute mode var top = parseInt(el.style.marginTop,10); var left = parseInt(el.style.marginLeft,10); if (isNaN(top)) top = 0; if (isNaN(left)) left = 0; return { f: top, e: left }; } else { //regular transform var obj = $.getCssMatrix(el); return obj; } }; jsScroller.prototype.saveEventInfo = function (event) { this.lastEventInfo = { pageX: event.touches[0].pageX, pageY: event.touches[0].pageY, time: event.timeStamp }; }; jsScroller.prototype.saveFirstEventInfo = function (event) { this.firstEventInfo = { pageX: event.touches[0].pageX, pageY: event.touches[0].pageY, time: event.timeStamp }; }; jsScroller.prototype.setVScrollBar = function (scrollInfo, time, timingFunction) { if (!this.elementInfo.requiresVScrollBar) return false; var newHeight = (parseFloat(this.elementInfo.bottomMargin / this.elementInfo.divHeight) * this.elementInfo.bottomMargin) + "px"; if(numOnly(newHeight)>this.elementInfo.bottomMargin) newHeight=this.elementInfo.bottomMargin+"px"; if (newHeight != this.vscrollBar.style.height) this.vscrollBar.style.height = newHeight; var pos = (this.elementInfo.bottomMargin - numOnly(this.vscrollBar.style.height)) - (((this.elementInfo.maxTop + scrollInfo.y) / this.elementInfo.maxTop) * (this.elementInfo.bottomMargin - numOnly(this.vscrollBar.style.height))); if (pos > this.elementInfo.bottomMargin) pos = this.elementInfo.bottomMargin; if (pos < 0) pos = 0; this.scrollbarMoveCSS(this.vscrollBar, { x: 0, y: pos }, time, timingFunction); return true; }; jsScroller.prototype.setHScrollBar = function (scrollInfo, time, timingFunction) { if (!this.elementInfo.requiresHScrollBar) return false; var newWidth = (parseFloat(this.elementInfo.rightMargin / this.elementInfo.divWidth) * this.elementInfo.rightMargin) + "px"; if (newWidth != this.hscrollBar.style.width) this.hscrollBar.style.width = newWidth; var pos = (this.elementInfo.rightMargin - numOnly(this.hscrollBar.style.width)) - (((this.elementInfo.maxLeft + scrollInfo.x) / this.elementInfo.maxLeft) * (this.elementInfo.rightMargin - numOnly(this.hscrollBar.style.width))); if (pos > this.elementInfo.rightMargin) pos = this.elementInfo.rightMargin; if (pos < 0) pos = 0; this.scrollbarMoveCSS(this.hscrollBar, { x: pos, y: 0 }, time, timingFunction); return true; }; jsScroller.prototype.onTouchMove = function (event) { if (this.currentScrollingObject === null) return; //event.preventDefault(); var scrollInfo = this.calculateMovement(event); this.calculateTarget(scrollInfo); this.lastScrollInfo = scrollInfo; if (!this.moved) { $.trigger(this, "scrollstart"); $.trigger($.touchLayer, "scrollstart", [this.el]); if (this.elementInfo.requiresVScrollBar) this.vscrollBar.style.display="block"; if (this.elementInfo.requiresHScrollBar) this.hscrollBar.style.display="block"; } this.moved = true; if (this.refresh && scrollInfo.top === 0) { this.refreshContainer.style.display = "block"; this.refreshHeight = this.refreshContainer.firstChild.clientHeight; this.refreshContainer.firstChild.style.top = (-this.refreshHeight) + 'px'; this.refreshContainer.style.overflow = 'visible'; this.preventPullToRefresh = false; } else if (scrollInfo.top < 0) { this.preventPullToRefresh = true; if (this.refresh) this.refreshContainer.style.overflow = 'hidden'; } this.saveEventInfo(event); if (this.isScrolling===false){ // && (this.lastScrollInfo.x != this.lastScrollInfo.left || this.lastScrollInfo.y != this.lastScrollInfo.top)) { this.isScrolling = true; if (this.onScrollStart) this.onScrollStart(); } //proceed normally var cssMatrix = this.getCSSMatrix(this.el); this.lastScrollInfo.top = numOnly(cssMatrix.f); this.lastScrollInfo.left = numOnly(cssMatrix.e); this.recalculateDeltaY(this.lastScrollInfo); this.recalculateDeltaX(this.lastScrollInfo); //boundaries control this.checkYboundary(this.lastScrollInfo); if (this.elementInfo.hasHorScroll) this.checkXboundary(this.lastScrollInfo); //pull to refresh elastic var positiveOverflow = this.lastScrollInfo.y > 0 && this.lastScrollInfo.deltaY > 0; var negativeOverflow = this.lastScrollInfo.y < -this.elementInfo.maxTop && this.lastScrollInfo.deltaY < 0; if (positiveOverflow || negativeOverflow) { var overflow = positiveOverflow ? this.lastScrollInfo.y : -this.lastScrollInfo.y - this.elementInfo.maxTop; var pcent = (this.container.clientHeight - overflow) / this.container.clientHeight; if (pcent < 0.5) pcent = 0.5; //cur top, maxTop or 0? var baseTop = 0; if ((positiveOverflow && this.lastScrollInfo.top > 0) || (negativeOverflow && this.lastScrollInfo.top < -this.elementInfo.maxTop)) { baseTop = this.lastScrollInfo.top; } else if (negativeOverflow) { baseTop = -this.elementInfo.maxTop; } var changeY = this.lastScrollInfo.deltaY * pcent; var absChangeY = Math.abs(this.lastScrollInfo.deltaY * pcent); if (absChangeY < 1) changeY = positiveOverflow ? 1 : -1; this.lastScrollInfo.y = baseTop + changeY; } if(this.elementInfo.hasHorScroll){ positiveOverflow = this.lastScrollInfo.x > 0 && this.lastScrollInfo.deltaX > 0; negativeOverflow = this.lastScrollInfo.x < -this.elementInfo.maxLeft && this.lastScrollInfo.deltaX < 0; if (positiveOverflow || negativeOverflow) { var overflow = positiveOverflow ? this.lastScrollInfo.x : -this.lastScrollInfo.x - this.elementInfo.maxLeft; var pcent = (this.container.clientWidth - overflow) / this.container.clientWidth; if (pcent < 0.5) pcent = 0.5; //cur top, maxTop or 0? var baseTop = 0; if ((positiveOverflow && this.lastScrollInfo.left > 0) || (negativeOverflow && this.lastScrollInfo.left < -this.elementInfo.maxLeft)) { baseTop = this.lastScrollInfo.left; } else if (negativeOverflow) { baseTop = -this.elementInfo.maxLeft; } var changeX = this.lastScrollInfo.deltaX * pcent; var absChangeX = Math.abs(this.lastScrollInfo.deltaX * pcent); if (absChangeX < 1) changeX = positiveOverflow ? 1 : -1; this.lastScrollInfo.x = baseTop + changeX; } } if(this.lockBounce){ if(this.lastScrollInfo.x>0) this.lastScrollInfo.x=0; else if(this.lastScrollInfo.x*-1>this.elementInfo.maxLeft) this.lastScrollInfo.x=this.elementInfo.maxLeft*-1; if(this.lastScrollInfo.y>0) this.lastScrollInfo.y=0; else if(this.lastScrollInfo.y*-1>this.elementInfo.maxTop) this.lastScrollInfo.y=this.elementInfo.maxTop*-1; } //move this.scrollerMoveCSS(this.lastScrollInfo, 0); this.setVScrollBar(this.lastScrollInfo, 0, 0); this.setHScrollBar(this.lastScrollInfo, 0, 0); //check refresh triggering if (this.refresh && !this.preventPullToRefresh) { if (!this.refreshTriggered && this.lastScrollInfo.top > this.refreshHeight) { this.refreshTriggered = true; this.setRefreshContent("Release to Refresh"); $.trigger(this, 'refresh-trigger'); } else if (this.refreshTriggered && this.lastScrollInfo.top < this.refreshHeight) { this.refreshTriggered = false; this.setRefreshContent("Pull to Refresh"); $.trigger(this, 'refresh-cancel'); } } if (this.infinite && !this.infiniteTriggered) { if ((Math.abs(this.lastScrollInfo.top) > (this.el.clientHeight - this.container.clientHeight))) { this.infiniteTriggered = true; $.trigger(this, "infinite-scroll"); } } }; jsScroller.prototype.calculateMovement = function (event, last) { //default variables var scrollInfo = { //current position top: 0, left: 0, //current movement speedY: 0, speedX: 0, absSpeedY: 0, absSpeedX: 0, deltaY: 0, deltaX: 0, absDeltaY: 0, absDeltaX: 0, y: 0, x: 0, duration: 0 }; var prevEventInfo = last ? this.firstEventInfo : this.lastEventInfo; var pageX = last ? event.pageX : event.touches[0].pageX; var pageY = last ? event.pageY : event.touches[0].pageY; var time = last ? event.time : event.timeStamp; scrollInfo.deltaY = this.elementInfo.hasVertScroll ? pageY - prevEventInfo.pageY : 0; scrollInfo.deltaX = this.elementInfo.hasHorScroll ? pageX - prevEventInfo.pageX : 0; scrollInfo.time = time; scrollInfo.duration = time - prevEventInfo.time; return scrollInfo; }; jsScroller.prototype.calculateTarget = function (scrollInfo) { scrollInfo.y = this.lastScrollInfo.y + scrollInfo.deltaY; scrollInfo.x = this.lastScrollInfo.x + scrollInfo.deltaX; }; jsScroller.prototype.checkYboundary = function (scrollInfo) { var minTop = this.container.clientHeight / 2; var maxTop = this.elementInfo.maxTop + minTop; //y boundaries if (scrollInfo.y > minTop) scrollInfo.y = minTop; else if (-scrollInfo.y > maxTop) scrollInfo.y = -maxTop; else return; this.recalculateDeltaY(scrollInfo); }; jsScroller.prototype.checkXboundary = function (scrollInfo) { //x boundaries var minLeft=this.container.clientWidth/2; var maxLeft=this.elementInfo.maxLeft+minLeft; if (scrollInfo.x > minLeft) scrollInfo.x = minLeft; else if (-scrollInfo.x > maxLeft) scrollInfo.x = -maxLeft; else return; this.recalculateDeltaX(scrollInfo); }; jsScroller.prototype.recalculateDeltaY = function (scrollInfo) { //recalculate delta var oldAbsDeltaY = Math.abs(scrollInfo.deltaY); scrollInfo.deltaY = scrollInfo.y - scrollInfo.top; newAbsDeltaY = Math.abs(scrollInfo.deltaY); //recalculate duration at same speed scrollInfo.duration = scrollInfo.duration * newAbsDeltaY / oldAbsDeltaY; }; jsScroller.prototype.recalculateDeltaX = function (scrollInfo) { //recalculate delta var oldAbsDeltaX = Math.abs(scrollInfo.deltaX); scrollInfo.deltaX = scrollInfo.x - scrollInfo.left; newAbsDeltaX = Math.abs(scrollInfo.deltaX); //recalculate duration at same speed scrollInfo.duration = scrollInfo.duration * newAbsDeltaX / oldAbsDeltaX; }; jsScroller.prototype.hideRefresh = function (animate) { if (this.preventHideRefresh) return; var that = this; var endAnimationCb = function () { that.setRefreshContent("Pull to Refresh"); $.trigger(that, "refresh-finish"); }; this.scrollerMoveCSS({x: 0, y: 0}, HIDE_REFRESH_TIME); if (animate === false || !that.afEl.css3Animate) { endAnimationCb(); } else { that.afEl.css3Animate({ time: HIDE_REFRESH_TIME + "ms", complete: endAnimationCb }); } this.refreshTriggered = false; }; jsScroller.prototype.setMomentum = function (scrollInfo) { var deceleration = 0.0012; //calculate movement speed scrollInfo.speedY = this.divide(scrollInfo.deltaY, scrollInfo.duration); scrollInfo.speedX = this.divide(scrollInfo.deltaX, scrollInfo.duration); scrollInfo.absSpeedY = Math.abs(scrollInfo.speedY); scrollInfo.absSpeedX = Math.abs(scrollInfo.speedX); scrollInfo.absDeltaY = Math.abs(scrollInfo.deltaY); scrollInfo.absDeltaX = Math.abs(scrollInfo.deltaX); //set momentum if (scrollInfo.absDeltaY > 0) { scrollInfo.deltaY = (scrollInfo.deltaY < 0 ? -1 : 1) * (scrollInfo.absSpeedY * scrollInfo.absSpeedY) / (2 * deceleration); scrollInfo.absDeltaY = Math.abs(scrollInfo.deltaY); scrollInfo.duration = scrollInfo.absSpeedY / deceleration; scrollInfo.speedY = scrollInfo.deltaY / scrollInfo.duration; scrollInfo.absSpeedY = Math.abs(scrollInfo.speedY); if (scrollInfo.absSpeedY < deceleration * 100 || scrollInfo.absDeltaY < 5) scrollInfo.deltaY = scrollInfo.absDeltaY = scrollInfo.duration = scrollInfo.speedY = scrollInfo.absSpeedY = 0; } else if (scrollInfo.absDeltaX) { scrollInfo.deltaX = (scrollInfo.deltaX < 0 ? -1 : 1) * (scrollInfo.absSpeedX * scrollInfo.absSpeedX) / (2 * deceleration); scrollInfo.absDeltaX = Math.abs(scrollInfo.deltaX); scrollInfo.duration = scrollInfo.absSpeedX / deceleration; scrollInfo.speedX = scrollInfo.deltaX / scrollInfo.duration; scrollInfo.absSpeedX = Math.abs(scrollInfo.speedX); if (scrollInfo.absSpeedX < deceleration * 100 || scrollInfo.absDeltaX < 5) scrollInfo.deltaX = scrollInfo.absDeltaX = scrollInfo.duration = scrollInfo.speedX = scrollInfo.absSpeedX = 0; } else scrollInfo.duration = 0; }; jsScroller.prototype.onTouchEnd = function (event) { if (this.currentScrollingObject === null || !this.moved) return; //event.preventDefault(); this.finishScrollingObject = this.currentScrollingObject; this.currentScrollingObject = null; var scrollInfo = this.calculateMovement(this.lastEventInfo, true); if (!this.androidFormsMode) { this.setMomentum(scrollInfo); } this.calculateTarget(scrollInfo); //get the current top var cssMatrix = this.getCSSMatrix(this.el); scrollInfo.top = numOnly(cssMatrix.f); scrollInfo.left = numOnly(cssMatrix.e); //boundaries control this.checkYboundary(scrollInfo); if (this.elementInfo.hasHorScroll) this.checkXboundary(scrollInfo); var triggered = !this.preventPullToRefresh && (scrollInfo.top > this.refreshHeight || scrollInfo.y > this.refreshHeight); this.fireRefreshRelease(triggered, scrollInfo.top > 0); //refresh hang in if (this.refresh && triggered) { scrollInfo.y = this.refreshHeight; scrollInfo.duration = HIDE_REFRESH_TIME; //top boundary } else if (scrollInfo.y >= 0) { scrollInfo.y = 0; if (scrollInfo.top >= 0) scrollInfo.duration = HIDE_REFRESH_TIME; //lower boundary } else if (-scrollInfo.y > this.elementInfo.maxTop || this.elementInfo.maxTop === 0) { scrollInfo.y = -this.elementInfo.maxTop; if (-scrollInfo.top > this.elementInfo.maxTop) scrollInfo.duration = HIDE_REFRESH_TIME; //all others } ; if(this.elementInfo.hasHorScroll){ if(scrollInfo.x>=0) { scrollInfo.x=0; if(scrollInfo.left>=0) scrollInfo.duration=HIDE_REFRESH_TIME; } else if(-scrollInfo.x>this.elementInfo.maxLeft||this.elementInfo.maxLeft===0){ scrollInfo.x=-this.elementInfo.maxLeft; if(-scrollInfo.left>this.elementInfo.maxLeft) scrollInfo.duration=HIDE_REFRESH_TIME; } } if (this.androidFormsMode) scrollInfo.duration = 0; this.scrollerMoveCSS(scrollInfo, scrollInfo.duration, "cubic-bezier(0.33,0.66,0.66,1)"); this.setVScrollBar(scrollInfo, scrollInfo.duration, "cubic-bezier(0.33,0.66,0.66,1)"); this.setHScrollBar(scrollInfo, scrollInfo.duration, "cubic-bezier(0.33,0.66,0.66,1)"); this.setFinishCalback(scrollInfo.duration); if (this.infinite && !this.infiniteTriggered) { if ((Math.abs(scrollInfo.y) >= (this.el.clientHeight - this.container.clientHeight))) { this.infiniteTriggered = true; $.trigger(this, "infinite-scroll"); } } }; //finish callback jsScroller.prototype.setFinishCalback = function (duration) { var that = this; this.scrollingFinishCB = setTimeout(function () { that.hideScrollbars(); $.trigger($.touchLayer, 'scrollend', [that.el]); //notify touchLayer of this elements scrollend $.trigger(that, "scrollend", [that.el]); that.isScrolling = false; that.elementInfo = null; //reset elementInfo when idle if (that.infinite) $.trigger(that, "infinite-scroll-end"); }, duration); }; //Android Forms Fix jsScroller.prototype.startFormsMode = function () { if (this.blockFormsFix) return; //get prev values var cssMatrix = this.getCSSMatrix(this.el); //toggle vars this.refreshSafeKeep = this.refresh; this.refresh = false; this.androidFormsMode = true; //set new css rules this.el.style[$.feat.cssPrefix + "Transform"] = "none"; this.el.style[$.feat.cssPrefix + "Transition"] = "none"; this.el.style[$.feat.cssPrefix + "Perspective"] = "none"; //set position this.scrollerMoveCSS({ x: numOnly(cssMatrix.e), y: numOnly(cssMatrix.f) }, 0); //container this.container.style[$.feat.cssPrefix + "Perspective"] = "none"; this.container.style[$.feat.cssPrefix + "BackfaceVisibility"] = "visible"; //scrollbars if (this.vscrollBar) { this.vscrollBar.style[$.feat.cssPrefix + "Transform"] = "none"; this.vscrollBar.style[$.feat.cssPrefix + "Transition"] = "none"; this.vscrollBar.style[$.feat.cssPrefix + "Perspective"] = "none"; this.vscrollBar.style[$.feat.cssPrefix + "BackfaceVisibility"] = "visible"; } if (this.hscrollBar) { this.hscrollBar.style[$.feat.cssPrefix + "Transform"] = "none"; this.hscrollBar.style[$.feat.cssPrefix + "Transition"] = "none"; this.hscrollBar.style[$.feat.cssPrefix + "Perspective"] = "none"; this.hscrollBar.style[$.feat.cssPrefix + "BackfaceVisibility"] = "visible"; } }; jsScroller.prototype.stopFormsMode = function () { if (this.blockFormsFix) return; //get prev values var cssMatrix = this.getCSSMatrix(this.el); //toggle vars this.refresh = this.refreshSafeKeep; this.androidFormsMode = false; //set new css rules this.el.style[$.feat.cssPrefix + "Perspective"] = 1000; this.el.style.marginTop = 0; this.el.style.marginLeft = 0; this.el.style[$.feat.cssPrefix + "Transition"] = '0ms linear'; //reactivate transitions //set position this.scrollerMoveCSS({ x: numOnly(cssMatrix.e), y: numOnly(cssMatrix.f) }, 0); //container this.container.style[$.feat.cssPrefix + "Perspective"] = 1000; this.container.style[$.feat.cssPrefix + "BackfaceVisibility"] = "hidden"; //scrollbars if (this.vscrollBar) { this.vscrollBar.style[$.feat.cssPrefix + "Perspective"] = 1000; this.vscrollBar.style[$.feat.cssPrefix + "BackfaceVisibility"] = "hidden"; } if (this.hscrollBar) { this.hscrollBar.style[$.feat.cssPrefix + "Perspective"] = 1000; this.hscrollBar.style[$.feat.cssPrefix + "BackfaceVisibility"] = "hidden"; } }; jsScroller.prototype.scrollerMoveCSS = function (distanceToMove, time, timingFunction) { if (!time) time = 0; if (!timingFunction) timingFunction = "linear"; time = numOnly(time); if (this.el && this.el.style) { //do not touch the DOM if disabled if (this.eventsActive) { if (this.androidFormsMode) { this.el.style.marginTop = Math.round(distanceToMove.y) + "px"; this.el.style.marginLeft = Math.round(distanceToMove.x) + "px"; } else { this.el.style[$.feat.cssPrefix + "Transform"] = "translate" + translateOpen + distanceToMove.x + "px," + distanceToMove.y + "px" + translateClose; this.el.style[$.feat.cssPrefix + "TransitionDuration"] = time + "ms"; this.el.style[$.feat.cssPrefix + "TransitionTimingFunction"] = timingFunction; } } // Position should be updated even when the scroller is disabled so we log the change this.logPos(distanceToMove.x, distanceToMove.y); } }; jsScroller.prototype.logPos = function (x, y) { var size; if (!this.elementInfo) { size = this.getViewportSize(); } else { size = { h: this.elementInfo.bottomMargin, w: this.elementInfo.rightMargin }; } this.loggedPcentX = this.divide(x, (this.el.clientWidth - size.w)); this.loggedPcentY = this.divide(y, (this.el.clientHeight - size.h)); this.scrollTop = y; this.scrollLeft = x; }; jsScroller.prototype.scrollbarMoveCSS = function (el, distanceToMove, time, timingFunction) { if (!time) time = 0; if (!timingFunction) timingFunction = "linear"; if (el && el.style) { if (this.androidFormsMode) { el.style.marginTop = Math.round(distanceToMove.y) + "px"; el.style.marginLeft = Math.round(distanceToMove.x) + "px"; } else { el.style[$.feat.cssPrefix + "Transform"] = "translate" + translateOpen + distanceToMove.x + "px," + distanceToMove.y + "px" + translateClose; el.style[$.feat.cssPrefix + "TransitionDuration"] = time + "ms"; el.style[$.feat.cssPrefix + "TransitionTimingFunction"] = timingFunction; } } }; jsScroller.prototype.scrollTo = function (pos, time) { if (!time) time = 0; this.scrollerMoveCSS(pos, time); }; jsScroller.prototype.scrollBy = function (pos, time) { var cssMatrix = this.getCSSMatrix(this.el); var startTop = numOnly(cssMatrix.f); var startLeft = numOnly(cssMatrix.e); this.scrollTo({ y: startTop - pos.y, x: startLeft - pos.x }, time); }; jsScroller.prototype.scrollToBottom = function (time) { this.scrollTo({ y: -1 * (this.el.clientHeight - this.container.clientHeight), x: 0 }, time); }; jsScroller.prototype.scrollToTop = function (time) { this.scrollTo({ x: 0, y: 0 }, time); }; return scroller; })(); })(af);