├── .babelrc ├── .gitignore ├── demo ├── assets │ ├── arrow-back.png │ └── arrow-right.png ├── App.vue ├── views │ ├── MasterDetail │ │ ├── Detail.vue │ │ └── Master.vue │ ├── NoContent.vue │ ├── Snapping.vue │ ├── NavBar.vue │ ├── LoadMoreAndNoData.vue │ ├── SmoothingScroll.vue │ ├── LoadMoreAndNoData2.vue │ ├── RefreshAndInfinite.vue │ ├── Custom.vue │ ├── MultiScrollers.vue │ ├── CustomSpinner.vue │ └── Index.vue └── main.js ├── .npmrc ├── index.html ├── src ├── index.js ├── module │ ├── render.js │ └── core.js └── components │ ├── Arrow.vue │ ├── Spinner.vue │ └── Scroller.vue ├── docs └── index.html ├── package.json ├── README.md ├── webpack.config.js └── dist ├── example └── index.html └── vue-scroller.min.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | bower_components/ 4 | npm-debug.log 5 | 6 | .idea/ 7 | .vscode/ 8 | -------------------------------------------------------------------------------- /demo/assets/arrow-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdahoo/vue-scroller/HEAD/demo/assets/arrow-back.png -------------------------------------------------------------------------------- /demo/assets/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangdahoo/vue-scroller/HEAD/demo/assets/arrow-right.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | phantomjs_cdnurl=http://cnpmjs.org/downloads 2 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ 3 | registry=https://registry.npm.taobao.org -------------------------------------------------------------------------------- /demo/App.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vue-scroller 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Scroller from './components/Scroller.vue' 2 | 3 | function install (Vue) { 4 | if (install.installed) return 5 | install.installed = true 6 | Vue.component('scroller', Scroller) 7 | } 8 | 9 | const VueScroller = { 10 | install: install, 11 | Scroller 12 | } 13 | 14 | if (typeof window !== undefined && window.Vue) { 15 | window.Vue.use(VueScroller) 16 | } 17 | 18 | export default VueScroller 19 | -------------------------------------------------------------------------------- /demo/views/MasterDetail/Detail.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-scroller 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /demo/views/NoContent.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /demo/views/Snapping.vue: -------------------------------------------------------------------------------- 1 | 18 | 21 | 45 | -------------------------------------------------------------------------------- /demo/views/NavBar.vue: -------------------------------------------------------------------------------- 1 | 12 | 45 | 46 | 66 | -------------------------------------------------------------------------------- /demo/views/MasterDetail/Master.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-scroller", 3 | "version": "2.2.4", 4 | "description": "Vue component for smooth scrolling, pull to refresh & infinite loading.", 5 | "main": "dist/vue-scroller.min.js", 6 | "author": "wangdahoo <157195705@qq.com>", 7 | "homepage": "https://github.com/wangdahoo/vue-scroller", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/wangdahoo/vue-scroller.git" 11 | }, 12 | "scripts": { 13 | "dev": "webpack-dev-server --inline --hot --host=0.0.0.0 --port=8080", 14 | "doc": "cross-env NODE_ENV=production BUILD=doc webpack --progress --hide-modules", 15 | "build": "cross-env NODE_ENV=production BUILD=publish webpack --progress --hide-modules" 16 | }, 17 | "keywords": [ 18 | "vue", 19 | "scroll", 20 | "pulltorefresh", 21 | "pull to refresh", 22 | "pull-to-refresh", 23 | "infinite load" 24 | ], 25 | "dependencies": { 26 | "vue": "^2.2.6" 27 | }, 28 | "devDependencies": { 29 | "vue-router": "^2.4.0", 30 | "babel-core": "^6.0.0", 31 | "babel-loader": "^6.0.0", 32 | "babel-preset-es2015": "^6.0.0", 33 | "cross-env": "^3.0.0", 34 | "css-loader": "^0.25.0", 35 | "file-loader": "^0.9.0", 36 | "moment": "^2.17.1", 37 | "url-loader": "^0.5.7", 38 | "vue-loader": "^11.3.4", 39 | "vue-template-compiler": "^2.2.6", 40 | "webpack": "^2.4.1", 41 | "webpack-dev-server": "^2.4.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /demo/views/LoadMoreAndNoData.vue: -------------------------------------------------------------------------------- 1 | 13 | 58 | -------------------------------------------------------------------------------- /demo/views/SmoothingScroll.vue: -------------------------------------------------------------------------------- 1 | 15 | 30 | 65 | -------------------------------------------------------------------------------- /demo/views/LoadMoreAndNoData2.vue: -------------------------------------------------------------------------------- 1 | 13 | 61 | -------------------------------------------------------------------------------- /src/module/render.js: -------------------------------------------------------------------------------- 1 | function getContentRender(content) { 2 | var global = window; 3 | 4 | var docStyle = document.documentElement.style; 5 | 6 | var engine; 7 | if (global.opera && Object.prototype.toString.call(opera) === '[object Opera]') { 8 | engine = 'presto'; 9 | } else if ('MozAppearance' in docStyle) { 10 | engine = 'gecko'; 11 | } else if ('WebkitAppearance' in docStyle) { 12 | engine = 'webkit'; 13 | } else if (typeof navigator.cpuClass === 'string') { 14 | engine = 'trident'; 15 | } 16 | 17 | var vendorPrefix = { 18 | trident: 'ms', 19 | gecko: 'Moz', 20 | webkit: 'Webkit', 21 | presto: 'O' 22 | }[engine]; 23 | 24 | var helperElem = document.createElement("div"); 25 | var undef; 26 | 27 | var perspectiveProperty = vendorPrefix + "Perspective"; 28 | var transformProperty = vendorPrefix + "Transform"; 29 | 30 | if (helperElem.style[perspectiveProperty] !== undef) { 31 | 32 | return function(left, top, zoom) { 33 | content.style[transformProperty] = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0) scale(' + zoom + ')'; 34 | }; 35 | 36 | } else if (helperElem.style[transformProperty] !== undef) { 37 | 38 | return function(left, top, zoom) { 39 | content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px) scale(' + zoom + ')'; 40 | }; 41 | 42 | } else { 43 | 44 | return function(left, top, zoom) { 45 | content.style.marginLeft = left ? (-left/zoom) + 'px' : ''; 46 | content.style.marginTop = top ? (-top/zoom) + 'px' : ''; 47 | content.style.zoom = zoom || ''; 48 | }; 49 | 50 | } 51 | } 52 | 53 | module.exports = getContentRender; 54 | -------------------------------------------------------------------------------- /demo/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | Vue.use(VueRouter) 4 | 5 | import VueScroller from 'vue-scroller' 6 | Vue.use(VueScroller) 7 | 8 | // Vue.config.silent = true 9 | 10 | import App from './App.vue' 11 | 12 | import Index from './views/Index.vue' 13 | import RefreshAndInfinite from './views/RefreshAndInfinite.vue' 14 | import SmoothingScroll from './views/SmoothingScroll.vue' 15 | import Custom from './views/Custom.vue' 16 | import MultiScrollers from './views/MultiScrollers.vue' 17 | import LoadMoreAndNoData from './views/LoadMoreAndNoData.vue' 18 | import Snapping from './views/Snapping.vue' 19 | import CustomSpinner from './views/CustomSpinner.vue' 20 | import NoContent from './views/NoContent.vue' 21 | import LoadMoreAndNoData2 from './views/LoadMoreAndNoData2.vue' 22 | import Master from './views/MasterDetail/Master.vue' 23 | import Detail from './views/MasterDetail/Detail.vue' 24 | 25 | const routes = [ 26 | { path: '/', component: Index }, 27 | { path: '/refreshAndInfinite', component: RefreshAndInfinite }, 28 | { path: '/smoothingScroll', component: SmoothingScroll }, 29 | { path: '/custom', component: Custom }, 30 | { path: '/customSpinner', component: CustomSpinner }, 31 | { path: '/multiScrollers', component: MultiScrollers }, 32 | { path: '/loadMoreAndNoData', component: LoadMoreAndNoData }, 33 | { path: '/snapping', component: Snapping }, 34 | { path: '/noContent', component: NoContent }, 35 | { path: '/loadMoreAndNoData2', component: LoadMoreAndNoData2 }, 36 | { path: '/master-detail', component: Master }, 37 | { path: '/master-detail/:id', component: Detail }, 38 | ] 39 | 40 | const router = new VueRouter({ 41 | routes 42 | }) 43 | 44 | new Vue({ 45 | router, 46 | el: '#app', 47 | template: '', 48 | components: { 49 | App 50 | } 51 | }) 52 | 53 | -------------------------------------------------------------------------------- /demo/views/RefreshAndInfinite.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 77 | -------------------------------------------------------------------------------- /demo/views/Custom.vue: -------------------------------------------------------------------------------- 1 | 15 | 43 | 80 | -------------------------------------------------------------------------------- /demo/views/MultiScrollers.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 74 | -------------------------------------------------------------------------------- /src/components/Arrow.vue: -------------------------------------------------------------------------------- 1 | 79 | -------------------------------------------------------------------------------- /src/components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue Scroller ![version](https://img.shields.io/badge/version-%20v2.2.0%20-green.svg) ![vue](https://img.shields.io/badge/vue-%20v2.1%20-green.svg) 2 | 3 | [Vue Scroller](https://github.com/wangdahoo/vue-scroller) is a foundational component of [Vonic](https://github.com/wangdahoo/vonic) UI. 4 | In purpose of smooth scrolling, pull to refresh and infinite loading. 5 | 6 | > For vue 1.0, please refer to branch v1. 7 | 8 | ## Demo 9 | 10 | [https://wangdahoo.github.io/vue-scroller/](https://wangdahoo.github.io/vue-scroller/) 11 | 12 | ## How to use 13 | 14 | ```bash 15 | npm i vue-scroller -S 16 | ``` 17 | 18 | ```js 19 | /* ignore this if you include vue-scroller.js by 7 | 8 | Vue Scroller 9 | 52 | 53 | 54 |
55 |
56 |

Refresh & Infinite

57 |
58 | 61 |
62 | {{ item }} 63 |
64 |
65 |
66 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /demo/views/CustomSpinner.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 84 | -------------------------------------------------------------------------------- /demo/views/Index.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 115 | -------------------------------------------------------------------------------- /src/components/Scroller.vue: -------------------------------------------------------------------------------- 1 | 44 | 163 | 500 | -------------------------------------------------------------------------------- /dist/vue-scroller.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue Scroller 3 | * version: 2.2.4 4 | * repo: https://github.com/wangdahoo/vue-scroller 5 | * build: 2017-09-21 16:25:35 6 | */ 7 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("VueScroller",[],e):"object"==typeof exports?exports.VueScroller=e():t.VueScroller=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var i=n[o]={i:o,l:!1,exports:{}};return t[o].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=4)}([function(t,e){t.exports=function(t,e,n,o){var i,r=t=t||{},a=typeof t.default;"object"!==a&&"function"!==a||(i=t,r=t.default);var l="function"==typeof r?r.options:r;if(e&&(l.render=e.render,l.staticRenderFns=e.staticRenderFns),n&&(l._scopeId=n),o){var s=Object.create(l.computed||null);Object.keys(o).forEach(function(t){var e=o[t];s[t]=function(){return e}}),l.computed=s}return{esModule:i,exports:r,options:l}}},function(t,e,n){n(14);var o=n(0)(n(3),n(13),"data-v-ecaca2b0",null);t.exports=o.exports},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.default={props:{fillColor:{type:String,default:"#AAA"}}}},function(t,e,n){"use strict";function o(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(e,"__esModule",{value:!0});var i=n(5),r=o(i),a=n(6),l=o(a),s=n(10),c=o(s),_=n(9),u=o(_),f=/^[\d]+(\%)?$/,p=function(t){return"%"!=t[t.length-1]?t+"px":t},h=function(t){return f.test(t)};e.default={components:{Spinner:c.default,Arrow:u.default},props:{onRefresh:Function,onInfinite:Function,refreshText:{type:String,default:"下拉刷新"},noDataText:{type:String,default:"没有更多数据"},width:{type:String,default:"100%",validator:h},height:{type:String,default:"100%",validator:h},snapping:{type:Boolean,default:!1},snapWidth:{type:Number,default:100},snapHeight:{type:Number,default:100},animating:{type:Boolean,default:!0},animationDuration:{type:Number,default:250},bouncing:{type:Boolean,default:!0},refreshLayerColor:{type:String,default:"#AAA"},loadingLayerColor:{type:String,default:"#AAA"},cssClass:String,minContentHeight:{type:Number,default:0}},computed:{w:function(){return p(this.width)},h:function(){return p(this.height)},showInfiniteLayer:function(){var t=0;return this.content&&(t=this.content.offsetHeight),!!this.onInfinite&&t>this.minContentHeight}},data:function(){return{containerId:"outer-"+Math.random().toString(36).substring(3,8),contentId:"inner-"+Math.random().toString(36).substring(3,8),state:0,loadingState:0,showLoading:!1,container:void 0,content:void 0,scroller:void 0,pullToRefreshLayer:void 0,mousedown:!1,infiniteTimer:void 0,resizeTimer:void 0}},mounted:function(){var t=this;this.container=document.getElementById(this.containerId),this.container.style.width=this.w,this.container.style.height=this.h,this.content=document.getElementById(this.contentId),this.cssClass&&this.content.classList.add(this.cssClass),this.pullToRefreshLayer=this.content.getElementsByTagName("div")[0];var e=(0,l.default)(this.content);this.scroller=new r.default(e,{scrollingX:!1,snapping:this.snapping,animating:this.animating,animationDuration:this.animationDuration,bouncing:this.bouncing}),this.onRefresh&&this.scroller.activatePullToRefresh(60,function(){t.state=1},function(){t.state=0},function(){t.state=2,t.$on("$finishPullToRefresh",function(){setTimeout(function(){t.state=0,t.finishPullToRefresh()})}),t.onRefresh(t.finishPullToRefresh)}),this.onInfinite&&(this.infiniteTimer=setInterval(function(){var e=t.scroller.getValues(),n=(e.left,e.top);e.zoom;if(t.content.offsetHeight>0&&n+60>t.content.offsetHeight-t.container.clientHeight){if(t.loadingState)return;t.loadingState=1,t.showLoading=!0,t.onInfinite(t.finishInfinite)}},10));var n=this.container.getBoundingClientRect();this.scroller.setPosition(n.left+this.container.clientLeft,n.top+this.container.clientTop),this.snapping&&this.scroller.setSnapSize(this.snapWidth,this.snapHeight);var o=function(){return{width:t.content.offsetWidth,height:t.content.offsetHeight}},i=o(),a=i.content_width,s=i.content_height;this.resizeTimer=setInterval(function(){var e=o(),n=e.width,i=e.height;n===a&&i===s||(a=n,s=i,t.resize())},10)},destroyed:function(){clearInterval(this.resizeTimer),this.infiniteTimer&&clearInterval(this.infiniteTimer)},methods:{resize:function(){var t=this.container,e=this.content;this.scroller.setDimensions(t.clientWidth,t.clientHeight,e.offsetWidth,e.offsetHeight)},finishPullToRefresh:function(){this.scroller.finishPullToRefresh()},finishInfinite:function(t){this.loadingState=t?2:0,this.showLoading=!1,2==this.loadingState&&this.resetLoadingState()},triggerPullToRefresh:function(){this.scroller.triggerPullToRefresh()},scrollTo:function(t,e,n){this.scroller.scrollTo(t,e,n)},scrollBy:function(t,e,n){this.scroller.scrollBy(t,e,n)},touchStart:function(t){t.target.tagName.match(/input|textarea|select/i)||this.scroller.doTouchStart(t.touches,t.timeStamp)},touchMove:function(t){t.preventDefault(),this.scroller.doTouchMove(t.touches,t.timeStamp)},touchEnd:function(t){this.scroller.doTouchEnd(t.timeStamp)},mouseDown:function(t){t.target.tagName.match(/input|textarea|select/i)||(this.scroller.doTouchStart([{pageX:t.pageX,pageY:t.pageY}],t.timeStamp),this.mousedown=!0)},mouseMove:function(t){this.mousedown&&(this.scroller.doTouchMove([{pageX:t.pageX,pageY:t.pageY}],t.timeStamp),this.mousedown=!0)},mouseUp:function(t){this.mousedown&&(this.scroller.doTouchEnd(t.timeStamp),this.mousedown=!1)},getPosition:function(){var t=this.scroller.getValues();return{left:parseInt(t.left),top:parseInt(t.top)}},resetLoadingState:function(){var t=this,e=this.scroller.getValues(),n=(e.left,e.top);e.zoom,this.container,this.content;n+60>this.content.offsetHeight-this.container.clientHeight?setTimeout(function(){t.resetLoadingState()},1e3):this.loadingState=0}}}},function(t,e,n){"use strict";function o(t){o.installed||(o.installed=!0,t.component("scroller",a.default))}Object.defineProperty(e,"__esModule",{value:!0});var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r=n(1),a=function(t){return t&&t.__esModule?t:{default:t}}(r),l={install:o,Scroller:a.default};void 0!==("undefined"==typeof window?"undefined":i(window))&&window.Vue&&window.Vue.use(l),e.default=l},function(t,e,n){"use strict";var o;!function(i){var r=function(){},a=function(t){var e=Date.now||function(){return+new Date},n={},o=1,i={effect:{}};return i.effect.Animate={requestAnimationFrame:function(){var e=t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame,n=!!e;if(e&&!/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(e.toString())&&(n=!1),n)return function(t,n){e(t,n)};var o={},i=0,r=1,a=null,l=+new Date;return function(t,e){var n=r++;return o[n]=t,i++,null===a&&(a=setInterval(function(){var t=+new Date,e=o;o={},i=0;for(var n in e)e.hasOwnProperty(n)&&(e[n](t),l=t);t-l>2500&&(clearInterval(a),a=null)},1e3/60)),n}}(),stop:function(t){var e=null!=n[t];return e&&(n[t]=null),e},isRunning:function(t){return null!=n[t]},start:function(t,r,a,l,s,c){var _=e(),u=_,f=0,p=0,h=o++;if(c||(c=document.body),h%20==0){var d={};for(var m in n)d[m]=!0;n=d}var g=function o(d){var m=!0!==d,g=e();if(!n[h]||r&&!r(h))return n[h]=null,void(a&&a(60-p/((g-_)/1e3),h,!1));if(m)for(var v=Math.round((g-u)/(1e3/60))-1,y=0;y1&&(f=1);var T=s?s(f):f;!1!==t(T,g,m)&&1!==f||!m?m&&(u=g,i.effect.Animate.requestAnimationFrame(o,c)):(n[h]=null,a&&a(60-p/((g-_)/1e3),h,1===f||null==l))};return n[h]=!0,i.effect.Animate.requestAnimationFrame(g,c),h}},i}(i),l=function(t,e){this.__callback=t,this.options={scrollingX:!0,scrollingY:!0,animating:!0,animationDuration:250,bouncing:!0,locking:!0,paging:!1,snapping:!1,zooming:!1,minZoom:.5,maxZoom:3,speedMultiplier:1,scrollingComplete:r,penetrationDeceleration:.03,penetrationAcceleration:.08};for(var n in e)this.options[n]=e[n]},s=function(t){return Math.pow(t-1,3)+1},c=function(t){return(t/=.5)<1?.5*Math.pow(t,3):.5*(Math.pow(t-2,3)+2)},_={__isSingleTouch:!1,__isTracking:!1,__didDecelerationComplete:!1,__isGesturing:!1,__isDragging:!1,__isDecelerating:!1,__isAnimating:!1,__clientLeft:0,__clientTop:0,__clientWidth:0,__clientHeight:0,__contentWidth:0,__contentHeight:0,__snapWidth:100,__snapHeight:100,__refreshHeight:null,__refreshActive:!1,__refreshActivate:null,__refreshDeactivate:null,__refreshStart:null,__zoomLevel:1,__scrollLeft:0,__scrollTop:0,__maxScrollLeft:0,__maxScrollTop:0,__scheduledLeft:0,__scheduledTop:0,__scheduledZoom:0,__lastTouchLeft:null,__lastTouchTop:null,__lastTouchMove:null,__positions:null,__minDecelerationScrollLeft:null,__minDecelerationScrollTop:null,__maxDecelerationScrollLeft:null,__maxDecelerationScrollTop:null,__decelerationVelocityX:null,__decelerationVelocityY:null,setDimensions:function(t,e,n,o){var i=this;t===+t&&(i.__clientWidth=t),e===+e&&(i.__clientHeight=e),n===+n&&(i.__contentWidth=n),o===+o&&(i.__contentHeight=o),i.__computeScrollMax(),i.scrollTo(i.__scrollLeft,i.__scrollTop,!0)},setPosition:function(t,e){var n=this;n.__clientLeft=t||0,n.__clientTop=e||0},setSnapSize:function(t,e){var n=this;n.__snapWidth=t,n.__snapHeight=e},activatePullToRefresh:function(t,e,n,o){var i=this;i.__refreshHeight=t,i.__refreshActivate=e,i.__refreshDeactivate=n,i.__refreshStart=o},triggerPullToRefresh:function(){this.__publish(this.__scrollLeft,-this.__refreshHeight,this.__zoomLevel,!0),this.__refreshStart&&this.__refreshStart()},finishPullToRefresh:function(){var t=this;t.__refreshActive=!1,t.__refreshDeactivate&&t.__refreshDeactivate(),t.scrollTo(t.__scrollLeft,t.__scrollTop,!0)},getValues:function(){var t=this;return{left:t.__scrollLeft,top:t.__scrollTop,zoom:t.__zoomLevel}},getScrollMax:function(){var t=this;return{left:t.__maxScrollLeft,top:t.__maxScrollTop}},zoomTo:function(t,e,n,o,i){var r=this;if(!r.options.zooming)throw new Error("Zooming is not enabled!");i&&(r.__zoomComplete=i),r.__isDecelerating&&(a.effect.Animate.stop(r.__isDecelerating),r.__isDecelerating=!1);var l=r.__zoomLevel;null==n&&(n=r.__clientWidth/2),null==o&&(o=r.__clientHeight/2),t=Math.max(Math.min(t,r.options.maxZoom),r.options.minZoom),r.__computeScrollMax(t);var s=(n+r.__scrollLeft)*t/l-n,c=(o+r.__scrollTop)*t/l-o;s>r.__maxScrollLeft?s=r.__maxScrollLeft:s<0&&(s=0),c>r.__maxScrollTop?c=r.__maxScrollTop:c<0&&(c=0),r.__publish(s,c,t,e)},zoomBy:function(t,e,n,o,i){var r=this;r.zoomTo(r.__zoomLevel*t,e,n,o,i)},scrollTo:function(t,e,n,o){var i=this;if(i.__isDecelerating&&(a.effect.Animate.stop(i.__isDecelerating),i.__isDecelerating=!1),null!=o&&o!==i.__zoomLevel){if(!i.options.zooming)throw new Error("Zooming is not enabled!");t*=o,e*=o,i.__computeScrollMax(o)}else o=i.__zoomLevel;i.options.scrollingX?i.options.paging?t=Math.round(t/i.__clientWidth)*i.__clientWidth:i.options.snapping&&(t=Math.round(t/i.__snapWidth)*i.__snapWidth):t=i.__scrollLeft,i.options.scrollingY?i.options.paging?e=Math.round(e/i.__clientHeight)*i.__clientHeight:i.options.snapping&&(e=Math.round(e/i.__snapHeight)*i.__snapHeight):e=i.__scrollTop,t=Math.max(Math.min(i.__maxScrollLeft,t),0),e=Math.max(Math.min(i.__maxScrollTop,e),0),t===i.__scrollLeft&&e===i.__scrollTop&&(n=!1),i.__isTracking||i.__publish(t,e,o,n)},scrollBy:function(t,e,n){var o=this,i=o.__isAnimating?o.__scheduledLeft:o.__scrollLeft,r=o.__isAnimating?o.__scheduledTop:o.__scrollTop;o.scrollTo(i+(t||0),r+(e||0),n)},doMouseZoom:function(t,e,n,o){var i=this,r=t>0?.97:1.03;return i.zoomTo(i.__zoomLevel*r,!1,n-i.__clientLeft,o-i.__clientTop)},doTouchStart:function(t,e){if(null==t.length)throw new Error("Invalid touch list: "+t);if(e instanceof Date&&(e=e.valueOf()),"number"!=typeof e)throw new Error("Invalid timestamp value: "+e);var n=this;n.__interruptedAnimation=!0,n.__isDecelerating&&(a.effect.Animate.stop(n.__isDecelerating),n.__isDecelerating=!1,n.__interruptedAnimation=!0),n.__isAnimating&&(a.effect.Animate.stop(n.__isAnimating),n.__isAnimating=!1,n.__interruptedAnimation=!0);var o,i,r=1===t.length;r?(o=t[0].pageX,i=t[0].pageY):(o=Math.abs(t[0].pageX+t[1].pageX)/2,i=Math.abs(t[0].pageY+t[1].pageY)/2),n.__initialTouchLeft=o,n.__initialTouchTop=i,n.__zoomLevelStart=n.__zoomLevel,n.__lastTouchLeft=o,n.__lastTouchTop=i,n.__lastTouchMove=e,n.__lastScale=1,n.__enableScrollX=!r&&n.options.scrollingX,n.__enableScrollY=!r&&n.options.scrollingY,n.__isTracking=!0,n.__didDecelerationComplete=!1,n.__isDragging=!r,n.__isSingleTouch=r,n.__positions=[]},doTouchMove:function(t,e,n){if(null==t.length)throw new Error("Invalid touch list: "+t);if(e instanceof Date&&(e=e.valueOf()),"number"!=typeof e)throw new Error("Invalid timestamp value: "+e);var o=this;if(o.__isTracking){var i,r;2===t.length?(i=Math.abs(t[0].pageX+t[1].pageX)/2,r=Math.abs(t[0].pageY+t[1].pageY)/2):(i=t[0].pageX,r=t[0].pageY);var a=o.__positions;if(o.__isDragging){var l=i-o.__lastTouchLeft,s=r-o.__lastTouchTop,c=o.__scrollLeft,_=o.__scrollTop,u=o.__zoomLevel;if(null!=n&&o.options.zooming){var f=u;if(u=u/o.__lastScale*n,u=Math.max(Math.min(u,o.options.maxZoom),o.options.minZoom),f!==u){var p=i-o.__clientLeft,h=r-o.__clientTop;c=(p+c)*u/f-p,_=(h+_)*u/f-h,o.__computeScrollMax(u)}}if(o.__enableScrollX){c-=l*this.options.speedMultiplier;var d=o.__maxScrollLeft;(c>d||c<0)&&(o.options.bouncing?c+=l/2*this.options.speedMultiplier:c=c>d?d:0)}if(o.__enableScrollY){_-=s*this.options.speedMultiplier;var m=o.__maxScrollTop;(_>m||_<0)&&(o.options.bouncing?(_+=s/2*this.options.speedMultiplier,o.__enableScrollX||null==o.__refreshHeight||(!o.__refreshActive&&_<=-o.__refreshHeight?(o.__refreshActive=!0,o.__refreshActivate&&o.__refreshActivate()):o.__refreshActive&&_>-o.__refreshHeight&&(o.__refreshActive=!1,o.__refreshDeactivate&&o.__refreshDeactivate()))):_=_>m?m:0)}a.length>60&&a.splice(0,30),a.push(c,_,e),o.__publish(c,_,u)}else{var g=o.options.locking?3:0,v=Math.abs(i-o.__initialTouchLeft),y=Math.abs(r-o.__initialTouchTop);o.__enableScrollX=o.options.scrollingX&&v>=g,o.__enableScrollY=o.options.scrollingY&&y>=g,a.push(o.__scrollLeft,o.__scrollTop,e),o.__isDragging=(o.__enableScrollX||o.__enableScrollY)&&(v>=5||y>=5),o.__isDragging&&(o.__interruptedAnimation=!1)}o.__lastTouchLeft=i,o.__lastTouchTop=r,o.__lastTouchMove=e,o.__lastScale=n}},doTouchEnd:function(t){if(t instanceof Date&&(t=t.valueOf()),"number"!=typeof t)throw new Error("Invalid timestamp value: "+t);var e=this;if(e.__isTracking){if(e.__isTracking=!1,e.__isDragging)if(e.__isDragging=!1,e.__isSingleTouch&&e.options.animating&&t-e.__lastTouchMove<=100){for(var n=e.__positions,o=n.length-1,i=o,r=o;r>0&&n[r]>e.__lastTouchMove-100;r-=3)i=r;if(i!==o){var a=n[o]-n[i],l=e.__scrollLeft-n[i-2],s=e.__scrollTop-n[i-1];e.__decelerationVelocityX=l/a*(1e3/60),e.__decelerationVelocityY=s/a*(1e3/60);var c=e.options.paging||e.options.snapping?4:1;Math.abs(e.__decelerationVelocityX)>c||Math.abs(e.__decelerationVelocityY)>c?e.__refreshActive||e.__startDeceleration(t):e.options.scrollingComplete()}else e.options.scrollingComplete()}else t-e.__lastTouchMove>100&&e.options.scrollingComplete();e.__isDecelerating||(e.__refreshActive&&e.__refreshStart?(e.__publish(e.__scrollLeft,-e.__refreshHeight,e.__zoomLevel,!0),e.__refreshStart&&e.__refreshStart()):((e.__interruptedAnimation||e.__isDragging)&&e.options.scrollingComplete(),e.scrollTo(e.__scrollLeft,e.__scrollTop,!0,e.__zoomLevel),e.__refreshActive&&(e.__refreshActive=!1,e.__refreshDeactivate&&e.__refreshDeactivate()))),e.__positions.length=0}},__publish:function(t,e,n,o){var i=this,r=i.__isAnimating;if(r&&(a.effect.Animate.stop(r),i.__isAnimating=!1),o&&i.options.animating){i.__scheduledLeft=t,i.__scheduledTop=e,i.__scheduledZoom=n;var l=i.__scrollLeft,_=i.__scrollTop,u=i.__zoomLevel,f=t-l,p=e-_,h=n-u,d=function(t,e,n){n&&(i.__scrollLeft=l+f*t,i.__scrollTop=_+p*t,i.__zoomLevel=u+h*t,i.__callback&&i.__callback(i.__scrollLeft,i.__scrollTop,i.__zoomLevel))},m=function(t){return i.__isAnimating===t},g=function(t,e,n){e===i.__isAnimating&&(i.__isAnimating=!1),(i.__didDecelerationComplete||n)&&i.options.scrollingComplete(),i.options.zooming&&(i.__computeScrollMax(),i.__zoomComplete&&(i.__zoomComplete(),i.__zoomComplete=null))};i.__isAnimating=a.effect.Animate.start(d,m,g,i.options.animationDuration,r?s:c)}else i.__scheduledLeft=i.__scrollLeft=t,i.__scheduledTop=i.__scrollTop=e,i.__scheduledZoom=i.__zoomLevel=n,i.__callback&&i.__callback(t,e,n),i.options.zooming&&(i.__computeScrollMax(),i.__zoomComplete&&(i.__zoomComplete(),i.__zoomComplete=null))},__computeScrollMax:function(t){var e=this;null==t&&(t=e.__zoomLevel),e.__maxScrollLeft=Math.max(e.__contentWidth*t-e.__clientWidth,0),e.__maxScrollTop=Math.max(e.__contentHeight*t-e.__clientHeight,0)},__startDeceleration:function(t){var e=this;if(e.options.paging){var n=Math.max(Math.min(e.__scrollLeft,e.__maxScrollLeft),0),o=Math.max(Math.min(e.__scrollTop,e.__maxScrollTop),0),i=e.__clientWidth,r=e.__clientHeight;e.__minDecelerationScrollLeft=Math.floor(n/i)*i,e.__minDecelerationScrollTop=Math.floor(o/r)*r,e.__maxDecelerationScrollLeft=Math.ceil(n/i)*i,e.__maxDecelerationScrollTop=Math.ceil(o/r)*r}else e.__minDecelerationScrollLeft=0,e.__minDecelerationScrollTop=0,e.__maxDecelerationScrollLeft=e.__maxScrollLeft,e.__maxDecelerationScrollTop=e.__maxScrollTop;var l=function(t,n,o){e.__stepThroughDeceleration(o)},s=e.options.snapping?4:.001,c=function(){var t=Math.abs(e.__decelerationVelocityX)>=s||Math.abs(e.__decelerationVelocityY)>=s;return t||(e.__didDecelerationComplete=!0),t},_=function(t,n,o){e.__isDecelerating=!1,e.__didDecelerationComplete&&e.options.scrollingComplete(),e.scrollTo(e.__scrollLeft,e.__scrollTop,e.options.snapping)};e.__isDecelerating=a.effect.Animate.start(l,c,_)},__stepThroughDeceleration:function(t){var e=this,n=e.__scrollLeft+e.__decelerationVelocityX,o=e.__scrollTop+e.__decelerationVelocityY;if(!e.options.bouncing){var i=Math.max(Math.min(e.__maxDecelerationScrollLeft,n),e.__minDecelerationScrollLeft);i!==n&&(n=i,e.__decelerationVelocityX=0);var r=Math.max(Math.min(e.__maxDecelerationScrollTop,o),e.__minDecelerationScrollTop);r!==o&&(o=r,e.__decelerationVelocityY=0)}if(t?e.__publish(n,o,e.__zoomLevel):(e.__scrollLeft=n,e.__scrollTop=o),!e.options.paging){e.__decelerationVelocityX*=.95,e.__decelerationVelocityY*=.95}if(e.options.bouncing){var a=0,l=0,s=e.options.penetrationDeceleration,c=e.options.penetrationAcceleration;ne.__maxDecelerationScrollLeft&&(a=e.__maxDecelerationScrollLeft-n),oe.__maxDecelerationScrollTop&&(l=e.__maxDecelerationScrollTop-o),0!==a&&(a*e.__decelerationVelocityX<=0?e.__decelerationVelocityX+=a*s:e.__decelerationVelocityX=a*c),0!==l&&(l*e.__decelerationVelocityY<=0?e.__decelerationVelocityY+=l*s:e.__decelerationVelocityY=l*c)}}};for(var u in _)l.prototype[u]=_[u];void 0!==t&&t.exports?t.exports=l:void 0!==(o=function(){return l}.call(e,n,e,t))&&(t.exports=o)}(window)},function(t,e,n){"use strict";function o(t){var e,n=window,o=document.documentElement.style;n.opera&&"[object Opera]"===Object.prototype.toString.call(opera)?e="presto":"MozAppearance"in o?e="gecko":"WebkitAppearance"in o?e="webkit":"string"==typeof navigator.cpuClass&&(e="trident");var i={trident:"ms",gecko:"Moz",webkit:"Webkit",presto:"O"}[e],r=document.createElement("div"),a=i+"Perspective",l=i+"Transform";return void 0!==r.style[a]?function(e,n,o){t.style[l]="translate3d("+-e+"px,"+-n+"px,0) scale("+o+")"}:void 0!==r.style[l]?function(e,n,o){t.style[l]="translate("+-e+"px,"+-n+"px) scale("+o+")"}:function(e,n,o){t.style.marginLeft=e?-e/o+"px":"",t.style.marginTop=n?-n/o+"px":"",t.style.zoom=o||""}}t.exports=o},function(t,e,n){e=t.exports=n(8)(),e.push([t.i,"._v-container[data-v-ecaca2b0]{-webkit-tap-highlight-color:rgba(0,0,0,0);width:100%;height:100%;position:absolute;top:0;left:0;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}._v-container>._v-content[data-v-ecaca2b0]{width:100%;-webkit-transform-origin:left top;-webkit-transform:translateZ(0);-moz-transform-origin:left top;-moz-transform:translateZ(0);-ms-transform-origin:left top;-ms-transform:translateZ(0);-o-transform-origin:left top;-o-transform:translateZ(0);transform-origin:left top;transform:translateZ(0)}._v-container>._v-content>.pull-to-refresh-layer[data-v-ecaca2b0]{width:100%;height:60px;margin-top:-60px;text-align:center;font-size:16px;color:#aaa}._v-container>._v-content>.loading-layer[data-v-ecaca2b0]{width:100%;height:60px;text-align:center;font-size:16px;line-height:60px;color:#aaa;position:relative}._v-container>._v-content>.loading-layer>.no-data-text[data-v-ecaca2b0]{position:absolute;left:0;top:0;width:100%;height:100%;z-index:1}._v-container>._v-content>.loading-layer>.no-data-text[data-v-ecaca2b0],._v-container>._v-content>.loading-layer>.spinner-holder[data-v-ecaca2b0]{opacity:0;transition:opacity .15s linear;-webkit-transition:opacity .15s linear}._v-container>._v-content>.loading-layer>.no-data-text.active[data-v-ecaca2b0],._v-container>._v-content>.loading-layer>.spinner-holder.active[data-v-ecaca2b0]{opacity:1}._v-container>._v-content>.loading-layer .spinner-holder[data-v-ecaca2b0],._v-container>._v-content>.pull-to-refresh-layer .spinner-holder[data-v-ecaca2b0]{text-align:center;-webkit-font-smoothing:antialiased}._v-container>._v-content>.loading-layer .spinner-holder .arrow[data-v-ecaca2b0],._v-container>._v-content>.pull-to-refresh-layer .spinner-holder .arrow[data-v-ecaca2b0]{width:20px;height:20px;margin:8px auto 0;-webkit-transform:translateZ(0) rotate(0deg);transform:translateZ(0) rotate(0deg);transition:transform .2s linear}._v-container>._v-content>.loading-layer .spinner-holder .text[data-v-ecaca2b0],._v-container>._v-content>.pull-to-refresh-layer .spinner-holder .text[data-v-ecaca2b0]{display:block;margin:0 auto;font-size:14px;line-height:20px;color:#aaa}._v-container>._v-content>.loading-layer .spinner-holder .spinner[data-v-ecaca2b0],._v-container>._v-content>.pull-to-refresh-layer .spinner-holder .spinner[data-v-ecaca2b0]{margin-top:14px;width:32px;height:32px;fill:#444;stroke:#69717d}._v-container>._v-content>.pull-to-refresh-layer.active .spinner-holder .arrow[data-v-ecaca2b0]{-webkit-transform:translateZ(0) rotate(180deg);transform:translateZ(0) rotate(180deg)}",""])},function(t,e){t.exports=function(){var t=[];return t.toString=function(){for(var t=[],e=0;en.parts.length&&(o.parts.length=n.parts.length)}else{for(var a=[],i=0;i 2500) { 77 | clearInterval(intervalHandle); 78 | intervalHandle = null; 79 | } 80 | 81 | }, 1000 / TARGET_FPS); 82 | } 83 | 84 | return callbackHandle; 85 | }; 86 | 87 | })(), 88 | 89 | 90 | /** 91 | * Stops the given animation. 92 | * 93 | * @param id {Integer} Unique animation ID 94 | * @return {Boolean} Whether the animation was stopped (aka, was running before) 95 | */ 96 | stop: function(id) { 97 | var cleared = running[id] != null; 98 | if (cleared) { 99 | running[id] = null; 100 | } 101 | 102 | return cleared; 103 | }, 104 | 105 | 106 | /** 107 | * Whether the given animation is still running. 108 | * 109 | * @param id {Integer} Unique animation ID 110 | * @return {Boolean} Whether the animation is still running 111 | */ 112 | isRunning: function(id) { 113 | return running[id] != null; 114 | }, 115 | 116 | 117 | /** 118 | * Start the animation. 119 | * 120 | * @param stepCallback {Function} Pointer to function which is executed on every step. 121 | * Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }` 122 | * @param verifyCallback {Function} Executed before every animation step. 123 | * Signature of the method should be `function() { return continueWithAnimation; }` 124 | * @param completedCallback {Function} 125 | * Signature of the method should be `function(droppedFrames, finishedAnimation) {}` 126 | * @param duration {Integer} Milliseconds to run the animation 127 | * @param easingMethod {Function} Pointer to easing function 128 | * Signature of the method should be `function(percent) { return modifiedValue; }` 129 | * @param root {Element ? document.body} Render root, when available. Used for internal 130 | * usage of requestAnimationFrame. 131 | * @return {Integer} Identifier of animation. Can be used to stop it any time. 132 | */ 133 | start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) { 134 | 135 | var start = time(); 136 | var lastFrame = start; 137 | var percent = 0; 138 | var dropCounter = 0; 139 | var id = counter++; 140 | 141 | if (!root) { 142 | root = document.body; 143 | } 144 | 145 | // Compacting running db automatically every few new animations 146 | if (id % 20 === 0) { 147 | var newRunning = {}; 148 | for (var usedId in running) { 149 | newRunning[usedId] = true; 150 | } 151 | running = newRunning; 152 | } 153 | 154 | // This is the internal step method which is called every few milliseconds 155 | var step = function(virtual) { 156 | 157 | // Normalize virtual value 158 | var render = virtual !== true; 159 | 160 | // Get current time 161 | var now = time(); 162 | 163 | // Verification is executed before next animation step 164 | if (!running[id] || (verifyCallback && !verifyCallback(id))) { 165 | 166 | running[id] = null; 167 | completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false); 168 | return; 169 | 170 | } 171 | 172 | // For the current rendering to apply let's update omitted steps in memory. 173 | // This is important to bring internal state variables up-to-date with progress in time. 174 | if (render) { 175 | 176 | var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1; 177 | for (var j = 0; j < Math.min(droppedFrames, 4); j++) { 178 | step(true); 179 | dropCounter++; 180 | } 181 | 182 | } 183 | 184 | // Compute percent value 185 | if (duration) { 186 | percent = (now - start) / duration; 187 | if (percent > 1) { 188 | percent = 1; 189 | } 190 | } 191 | 192 | // Execute step callback, then... 193 | var value = easingMethod ? easingMethod(percent) : percent; 194 | if ((stepCallback(value, now, render) === false || percent === 1) && render) { 195 | running[id] = null; 196 | completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null); 197 | } else if (render) { 198 | lastFrame = now; 199 | core.effect.Animate.requestAnimationFrame(step, root); 200 | } 201 | }; 202 | 203 | // Mark as running 204 | running[id] = true; 205 | 206 | // Init first step 207 | core.effect.Animate.requestAnimationFrame(step, root); 208 | 209 | // Return unique animation ID 210 | return id; 211 | } 212 | }; 213 | 214 | return core; 215 | 216 | })(window); 217 | 218 | /** 219 | * A pure logic 'component' for 'virtual' scrolling/zooming. 220 | */ 221 | var Scroller = function(callback, options) { 222 | 223 | this.__callback = callback; 224 | // core = animate; 225 | 226 | this.options = { 227 | 228 | /** Enable scrolling on x-axis */ 229 | scrollingX: true, 230 | 231 | /** Enable scrolling on y-axis */ 232 | scrollingY: true, 233 | 234 | /** Enable animations for deceleration, snap back, zooming and scrolling */ 235 | animating: true, 236 | 237 | /** duration for animations triggered by scrollTo/zoomTo */ 238 | animationDuration: 250, 239 | 240 | /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */ 241 | bouncing: true, 242 | 243 | /** Enable locking to the main axis if user moves only slightly on one of them at start */ 244 | locking: true, 245 | 246 | /** Enable pagination mode (switching between full page content panes) */ 247 | paging: false, 248 | 249 | /** Enable snapping of content to a configured pixel grid */ 250 | snapping: false, 251 | 252 | /** Enable zooming of content via API, fingers and mouse wheel */ 253 | zooming: false, 254 | 255 | /** Minimum zoom level */ 256 | minZoom: 0.5, 257 | 258 | /** Maximum zoom level */ 259 | maxZoom: 3, 260 | 261 | /** Multiply or decrease scrolling speed **/ 262 | speedMultiplier: 1, 263 | 264 | /** Callback that is fired on the later of touch end or deceleration end, 265 | provided that another scrolling action has not begun. Used to know 266 | when to fade out a scrollbar. */ 267 | scrollingComplete: NOOP, 268 | 269 | /** This configures the amount of change applied to deceleration when reaching boundaries **/ 270 | penetrationDeceleration : 0.03, 271 | 272 | /** This configures the amount of change applied to acceleration when reaching boundaries **/ 273 | penetrationAcceleration : 0.08 274 | 275 | }; 276 | 277 | for (var key in options) { 278 | this.options[key] = options[key]; 279 | } 280 | 281 | }; 282 | 283 | 284 | // Easing Equations (c) 2003 Robert Penner, all rights reserved. 285 | // Open source under the BSD License. 286 | 287 | /** 288 | * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) 289 | **/ 290 | var easeOutCubic = function(pos) { 291 | return (Math.pow((pos - 1), 3) + 1); 292 | }; 293 | 294 | /** 295 | * @param pos {Number} position between 0 (start of effect) and 1 (end of effect) 296 | **/ 297 | var easeInOutCubic = function(pos) { 298 | if ((pos /= 0.5) < 1) { 299 | return 0.5 * Math.pow(pos, 3); 300 | } 301 | 302 | return 0.5 * (Math.pow((pos - 2), 3) + 2); 303 | }; 304 | 305 | 306 | var members = { 307 | 308 | /* 309 | --------------------------------------------------------------------------- 310 | INTERNAL FIELDS :: STATUS 311 | --------------------------------------------------------------------------- 312 | */ 313 | 314 | /** {Boolean} Whether only a single finger is used in touch handling */ 315 | __isSingleTouch: false, 316 | 317 | /** {Boolean} Whether a touch event sequence is in progress */ 318 | __isTracking: false, 319 | 320 | /** {Boolean} Whether a deceleration animation went to completion. */ 321 | __didDecelerationComplete: false, 322 | 323 | /** 324 | * {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when 325 | * a gesturestart event happens. This has higher priority than dragging. 326 | */ 327 | __isGesturing: false, 328 | 329 | /** 330 | * {Boolean} Whether the user has moved by such a distance that we have enabled 331 | * dragging mode. Hint: It's only enabled after some pixels of movement to 332 | * not interrupt with clicks etc. 333 | */ 334 | __isDragging: false, 335 | 336 | /** 337 | * {Boolean} Not touching and dragging anymore, and smoothly animating the 338 | * touch sequence using deceleration. 339 | */ 340 | __isDecelerating: false, 341 | 342 | /** 343 | * {Boolean} Smoothly animating the currently configured change 344 | */ 345 | __isAnimating: false, 346 | 347 | 348 | 349 | /* 350 | --------------------------------------------------------------------------- 351 | INTERNAL FIELDS :: DIMENSIONS 352 | --------------------------------------------------------------------------- 353 | */ 354 | 355 | /** {Integer} Available outer left position (from document perspective) */ 356 | __clientLeft: 0, 357 | 358 | /** {Integer} Available outer top position (from document perspective) */ 359 | __clientTop: 0, 360 | 361 | /** {Integer} Available outer width */ 362 | __clientWidth: 0, 363 | 364 | /** {Integer} Available outer height */ 365 | __clientHeight: 0, 366 | 367 | /** {Integer} Outer width of content */ 368 | __contentWidth: 0, 369 | 370 | /** {Integer} Outer height of content */ 371 | __contentHeight: 0, 372 | 373 | /** {Integer} Snapping width for content */ 374 | __snapWidth: 100, 375 | 376 | /** {Integer} Snapping height for content */ 377 | __snapHeight: 100, 378 | 379 | /** {Integer} Height to assign to refresh area */ 380 | __refreshHeight: null, 381 | 382 | /** {Boolean} Whether the refresh process is enabled when the event is released now */ 383 | __refreshActive: false, 384 | 385 | /** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */ 386 | __refreshActivate: null, 387 | 388 | /** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */ 389 | __refreshDeactivate: null, 390 | 391 | /** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */ 392 | __refreshStart: null, 393 | 394 | /** {Number} Zoom level */ 395 | __zoomLevel: 1, 396 | 397 | /** {Number} Scroll position on x-axis */ 398 | __scrollLeft: 0, 399 | 400 | /** {Number} Scroll position on y-axis */ 401 | __scrollTop: 0, 402 | 403 | /** {Integer} Maximum allowed scroll position on x-axis */ 404 | __maxScrollLeft: 0, 405 | 406 | /** {Integer} Maximum allowed scroll position on y-axis */ 407 | __maxScrollTop: 0, 408 | 409 | /* {Number} Scheduled left position (final position when animating) */ 410 | __scheduledLeft: 0, 411 | 412 | /* {Number} Scheduled top position (final position when animating) */ 413 | __scheduledTop: 0, 414 | 415 | /* {Number} Scheduled zoom level (final scale when animating) */ 416 | __scheduledZoom: 0, 417 | 418 | 419 | 420 | /* 421 | --------------------------------------------------------------------------- 422 | INTERNAL FIELDS :: LAST POSITIONS 423 | --------------------------------------------------------------------------- 424 | */ 425 | 426 | /** {Number} Left position of finger at start */ 427 | __lastTouchLeft: null, 428 | 429 | /** {Number} Top position of finger at start */ 430 | __lastTouchTop: null, 431 | 432 | /** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */ 433 | __lastTouchMove: null, 434 | 435 | /** {Array} List of positions, uses three indexes for each state: left, top, timestamp */ 436 | __positions: null, 437 | 438 | 439 | 440 | /* 441 | --------------------------------------------------------------------------- 442 | INTERNAL FIELDS :: DECELERATION SUPPORT 443 | --------------------------------------------------------------------------- 444 | */ 445 | 446 | /** {Integer} Minimum left scroll position during deceleration */ 447 | __minDecelerationScrollLeft: null, 448 | 449 | /** {Integer} Minimum top scroll position during deceleration */ 450 | __minDecelerationScrollTop: null, 451 | 452 | /** {Integer} Maximum left scroll position during deceleration */ 453 | __maxDecelerationScrollLeft: null, 454 | 455 | /** {Integer} Maximum top scroll position during deceleration */ 456 | __maxDecelerationScrollTop: null, 457 | 458 | /** {Number} Current factor to modify horizontal scroll position with on every step */ 459 | __decelerationVelocityX: null, 460 | 461 | /** {Number} Current factor to modify vertical scroll position with on every step */ 462 | __decelerationVelocityY: null, 463 | 464 | 465 | 466 | /* 467 | --------------------------------------------------------------------------- 468 | PUBLIC API 469 | --------------------------------------------------------------------------- 470 | */ 471 | 472 | /** 473 | * Configures the dimensions of the client (outer) and content (inner) elements. 474 | * Requires the available space for the outer element and the outer size of the inner element. 475 | * All values which are falsy (null or zero etc.) are ignored and the old value is kept. 476 | * 477 | * @param clientWidth {Integer ? null} Inner width of outer element 478 | * @param clientHeight {Integer ? null} Inner height of outer element 479 | * @param contentWidth {Integer ? null} Outer width of inner element 480 | * @param contentHeight {Integer ? null} Outer height of inner element 481 | */ 482 | setDimensions: function(clientWidth, clientHeight, contentWidth, contentHeight) { 483 | 484 | var self = this; 485 | 486 | // Only update values which are defined 487 | if (clientWidth === +clientWidth) { 488 | self.__clientWidth = clientWidth; 489 | } 490 | 491 | if (clientHeight === +clientHeight) { 492 | self.__clientHeight = clientHeight; 493 | } 494 | 495 | if (contentWidth === +contentWidth) { 496 | self.__contentWidth = contentWidth; 497 | } 498 | 499 | if (contentHeight === +contentHeight) { 500 | self.__contentHeight = contentHeight; 501 | } 502 | 503 | // Refresh maximums 504 | self.__computeScrollMax(); 505 | 506 | // Refresh scroll position 507 | self.scrollTo(self.__scrollLeft, self.__scrollTop, true); 508 | 509 | }, 510 | 511 | 512 | /** 513 | * Sets the client coordinates in relation to the document. 514 | * 515 | * @param left {Integer ? 0} Left position of outer element 516 | * @param top {Integer ? 0} Top position of outer element 517 | */ 518 | setPosition: function(left, top) { 519 | 520 | var self = this; 521 | 522 | self.__clientLeft = left || 0; 523 | self.__clientTop = top || 0; 524 | 525 | }, 526 | 527 | 528 | /** 529 | * Configures the snapping (when snapping is active) 530 | * 531 | * @param width {Integer} Snapping width 532 | * @param height {Integer} Snapping height 533 | */ 534 | setSnapSize: function(width, height) { 535 | 536 | var self = this; 537 | 538 | self.__snapWidth = width; 539 | self.__snapHeight = height; 540 | 541 | }, 542 | 543 | 544 | /** 545 | * Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever 546 | * the user event is released during visibility of this zone. This was introduced by some apps on iOS like 547 | * the official Twitter client. 548 | * 549 | * @param height {Integer} Height of pull-to-refresh zone on top of rendered list 550 | * @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release. 551 | * @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled. 552 | * @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh. 553 | */ 554 | activatePullToRefresh: function(height, activateCallback, deactivateCallback, startCallback) { 555 | 556 | var self = this; 557 | 558 | self.__refreshHeight = height; 559 | self.__refreshActivate = activateCallback; 560 | self.__refreshDeactivate = deactivateCallback; 561 | self.__refreshStart = startCallback; 562 | 563 | }, 564 | 565 | 566 | /** 567 | * Starts pull-to-refresh manually. 568 | */ 569 | triggerPullToRefresh: function() { 570 | // Use publish instead of scrollTo to allow scrolling to out of boundary position 571 | // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled 572 | this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true); 573 | 574 | if (this.__refreshStart) { 575 | this.__refreshStart(); 576 | } 577 | }, 578 | 579 | 580 | /** 581 | * Signalizes that pull-to-refresh is finished. 582 | */ 583 | finishPullToRefresh: function() { 584 | 585 | var self = this; 586 | 587 | self.__refreshActive = false; 588 | if (self.__refreshDeactivate) { 589 | self.__refreshDeactivate(); 590 | } 591 | 592 | self.scrollTo(self.__scrollLeft, self.__scrollTop, true); 593 | 594 | }, 595 | 596 | 597 | /** 598 | * Returns the scroll position and zooming values 599 | * 600 | * @return {Map} `left` and `top` scroll position and `zoom` level 601 | */ 602 | getValues: function() { 603 | 604 | var self = this; 605 | 606 | return { 607 | left: self.__scrollLeft, 608 | top: self.__scrollTop, 609 | zoom: self.__zoomLevel 610 | }; 611 | 612 | }, 613 | 614 | 615 | /** 616 | * Returns the maximum scroll values 617 | * 618 | * @return {Map} `left` and `top` maximum scroll values 619 | */ 620 | getScrollMax: function() { 621 | 622 | var self = this; 623 | 624 | return { 625 | left: self.__maxScrollLeft, 626 | top: self.__maxScrollTop 627 | }; 628 | 629 | }, 630 | 631 | 632 | /** 633 | * Zooms to the given level. Supports optional animation. Zooms 634 | * the center when no coordinates are given. 635 | * 636 | * @param level {Number} Level to zoom to 637 | * @param animate {Boolean ? false} Whether to use animation 638 | * @param originLeft {Number ? null} Zoom in at given left coordinate 639 | * @param originTop {Number ? null} Zoom in at given top coordinate 640 | * @param callback {Function ? null} A callback that gets fired when the zoom is complete. 641 | */ 642 | zoomTo: function(level, animate, originLeft, originTop, callback) { 643 | 644 | var self = this; 645 | 646 | if (!self.options.zooming) { 647 | throw new Error("Zooming is not enabled!"); 648 | } 649 | 650 | // Add callback if exists 651 | if(callback) { 652 | self.__zoomComplete = callback; 653 | } 654 | 655 | // Stop deceleration 656 | if (self.__isDecelerating) { 657 | core.effect.Animate.stop(self.__isDecelerating); 658 | self.__isDecelerating = false; 659 | } 660 | 661 | var oldLevel = self.__zoomLevel; 662 | 663 | // Normalize input origin to center of viewport if not defined 664 | if (originLeft == null) { 665 | originLeft = self.__clientWidth / 2; 666 | } 667 | 668 | if (originTop == null) { 669 | originTop = self.__clientHeight / 2; 670 | } 671 | 672 | // Limit level according to configuration 673 | level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom); 674 | 675 | // Recompute maximum values while temporary tweaking maximum scroll ranges 676 | self.__computeScrollMax(level); 677 | 678 | // Recompute left and top coordinates based on new zoom level 679 | var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft; 680 | var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop; 681 | 682 | // Limit x-axis 683 | if (left > self.__maxScrollLeft) { 684 | left = self.__maxScrollLeft; 685 | } else if (left < 0) { 686 | left = 0; 687 | } 688 | 689 | // Limit y-axis 690 | if (top > self.__maxScrollTop) { 691 | top = self.__maxScrollTop; 692 | } else if (top < 0) { 693 | top = 0; 694 | } 695 | 696 | // Push values out 697 | self.__publish(left, top, level, animate); 698 | 699 | }, 700 | 701 | 702 | /** 703 | * Zooms the content by the given factor. 704 | * 705 | * @param factor {Number} Zoom by given factor 706 | * @param animate {Boolean ? false} Whether to use animation 707 | * @param originLeft {Number ? 0} Zoom in at given left coordinate 708 | * @param originTop {Number ? 0} Zoom in at given top coordinate 709 | * @param callback {Function ? null} A callback that gets fired when the zoom is complete. 710 | */ 711 | zoomBy: function(factor, animate, originLeft, originTop, callback) { 712 | 713 | var self = this; 714 | 715 | self.zoomTo(self.__zoomLevel * factor, animate, originLeft, originTop, callback); 716 | 717 | }, 718 | 719 | 720 | /** 721 | * Scrolls to the given position. Respect limitations and snapping automatically. 722 | * 723 | * @param left {Number?null} Horizontal scroll position, keeps current if value is null 724 | * @param top {Number?null} Vertical scroll position, keeps current if value is null 725 | * @param animate {Boolean?false} Whether the scrolling should happen using an animation 726 | * @param zoom {Number?null} Zoom level to go to 727 | */ 728 | scrollTo: function(left, top, animate, zoom) { 729 | 730 | var self = this; 731 | 732 | // Stop deceleration 733 | if (self.__isDecelerating) { 734 | core.effect.Animate.stop(self.__isDecelerating); 735 | self.__isDecelerating = false; 736 | } 737 | 738 | // Correct coordinates based on new zoom level 739 | if (zoom != null && zoom !== self.__zoomLevel) { 740 | 741 | if (!self.options.zooming) { 742 | throw new Error("Zooming is not enabled!"); 743 | } 744 | 745 | left *= zoom; 746 | top *= zoom; 747 | 748 | // Recompute maximum values while temporary tweaking maximum scroll ranges 749 | self.__computeScrollMax(zoom); 750 | 751 | } else { 752 | 753 | // Keep zoom when not defined 754 | zoom = self.__zoomLevel; 755 | 756 | } 757 | 758 | if (!self.options.scrollingX) { 759 | 760 | left = self.__scrollLeft; 761 | 762 | } else { 763 | 764 | if (self.options.paging) { 765 | left = Math.round(left / self.__clientWidth) * self.__clientWidth; 766 | } else if (self.options.snapping) { 767 | left = Math.round(left / self.__snapWidth) * self.__snapWidth; 768 | } 769 | 770 | } 771 | 772 | if (!self.options.scrollingY) { 773 | 774 | top = self.__scrollTop; 775 | 776 | } else { 777 | 778 | if (self.options.paging) { 779 | top = Math.round(top / self.__clientHeight) * self.__clientHeight; 780 | } else if (self.options.snapping) { 781 | top = Math.round(top / self.__snapHeight) * self.__snapHeight; 782 | } 783 | 784 | } 785 | 786 | // Limit for allowed ranges 787 | left = Math.max(Math.min(self.__maxScrollLeft, left), 0); 788 | top = Math.max(Math.min(self.__maxScrollTop, top), 0); 789 | 790 | // Don't animate when no change detected, still call publish to make sure 791 | // that rendered position is really in-sync with internal data 792 | if (left === self.__scrollLeft && top === self.__scrollTop) { 793 | animate = false; 794 | } 795 | 796 | // Publish new values 797 | if (!self.__isTracking) { 798 | self.__publish(left, top, zoom, animate); 799 | } 800 | 801 | }, 802 | 803 | 804 | /** 805 | * Scroll by the given offset 806 | * 807 | * @param left {Number ? 0} Scroll x-axis by given offset 808 | * @param top {Number ? 0} Scroll x-axis by given offset 809 | * @param animate {Boolean ? false} Whether to animate the given change 810 | */ 811 | scrollBy: function(left, top, animate) { 812 | 813 | var self = this; 814 | 815 | var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft; 816 | var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop; 817 | 818 | self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate); 819 | 820 | }, 821 | 822 | 823 | 824 | /* 825 | --------------------------------------------------------------------------- 826 | EVENT CALLBACKS 827 | --------------------------------------------------------------------------- 828 | */ 829 | 830 | /** 831 | * Mouse wheel handler for zooming support 832 | */ 833 | doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) { 834 | 835 | var self = this; 836 | var change = wheelDelta > 0 ? 0.97 : 1.03; 837 | 838 | return self.zoomTo(self.__zoomLevel * change, false, pageX - self.__clientLeft, pageY - self.__clientTop); 839 | 840 | }, 841 | 842 | 843 | /** 844 | * Touch start handler for scrolling support 845 | */ 846 | doTouchStart: function(touches, timeStamp) { 847 | // Array-like check is enough here 848 | if (touches.length == null) { 849 | throw new Error("Invalid touch list: " + touches); 850 | } 851 | 852 | if (timeStamp instanceof Date) { 853 | timeStamp = timeStamp.valueOf(); 854 | } 855 | if (typeof timeStamp !== "number") { 856 | throw new Error("Invalid timestamp value: " + timeStamp); 857 | } 858 | 859 | var self = this; 860 | 861 | // Reset interruptedAnimation flag 862 | self.__interruptedAnimation = true; 863 | 864 | // Stop deceleration 865 | if (self.__isDecelerating) { 866 | core.effect.Animate.stop(self.__isDecelerating); 867 | self.__isDecelerating = false; 868 | self.__interruptedAnimation = true; 869 | } 870 | 871 | // Stop animation 872 | if (self.__isAnimating) { 873 | core.effect.Animate.stop(self.__isAnimating); 874 | self.__isAnimating = false; 875 | self.__interruptedAnimation = true; 876 | } 877 | 878 | // Use center point when dealing with two fingers 879 | var currentTouchLeft, currentTouchTop; 880 | var isSingleTouch = touches.length === 1; 881 | if (isSingleTouch) { 882 | currentTouchLeft = touches[0].pageX; 883 | currentTouchTop = touches[0].pageY; 884 | } else { 885 | currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2; 886 | currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2; 887 | } 888 | 889 | // Store initial positions 890 | self.__initialTouchLeft = currentTouchLeft; 891 | self.__initialTouchTop = currentTouchTop; 892 | 893 | // Store current zoom level 894 | self.__zoomLevelStart = self.__zoomLevel; 895 | 896 | // Store initial touch positions 897 | self.__lastTouchLeft = currentTouchLeft; 898 | self.__lastTouchTop = currentTouchTop; 899 | 900 | // Store initial move time stamp 901 | self.__lastTouchMove = timeStamp; 902 | 903 | // Reset initial scale 904 | self.__lastScale = 1; 905 | 906 | // Reset locking flags 907 | self.__enableScrollX = !isSingleTouch && self.options.scrollingX; 908 | self.__enableScrollY = !isSingleTouch && self.options.scrollingY; 909 | 910 | // Reset tracking flag 911 | self.__isTracking = true; 912 | 913 | // Reset deceleration complete flag 914 | self.__didDecelerationComplete = false; 915 | 916 | // Dragging starts directly with two fingers, otherwise lazy with an offset 917 | self.__isDragging = !isSingleTouch; 918 | 919 | // Some features are disabled in multi touch scenarios 920 | self.__isSingleTouch = isSingleTouch; 921 | 922 | // Clearing data structure 923 | self.__positions = []; 924 | 925 | }, 926 | 927 | 928 | /** 929 | * Touch move handler for scrolling support 930 | */ 931 | doTouchMove: function(touches, timeStamp, scale) { 932 | 933 | // Array-like check is enough here 934 | if (touches.length == null) { 935 | throw new Error("Invalid touch list: " + touches); 936 | } 937 | 938 | if (timeStamp instanceof Date) { 939 | timeStamp = timeStamp.valueOf(); 940 | } 941 | if (typeof timeStamp !== "number") { 942 | throw new Error("Invalid timestamp value: " + timeStamp); 943 | } 944 | 945 | var self = this; 946 | 947 | // Ignore event when tracking is not enabled (event might be outside of element) 948 | if (!self.__isTracking) { 949 | return; 950 | } 951 | 952 | 953 | var currentTouchLeft, currentTouchTop; 954 | 955 | // Compute move based around of center of fingers 956 | if (touches.length === 2) { 957 | currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2; 958 | currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2; 959 | } else { 960 | currentTouchLeft = touches[0].pageX; 961 | currentTouchTop = touches[0].pageY; 962 | } 963 | 964 | var positions = self.__positions; 965 | 966 | // Are we already is dragging mode? 967 | if (self.__isDragging) { 968 | 969 | // Compute move distance 970 | var moveX = currentTouchLeft - self.__lastTouchLeft; 971 | var moveY = currentTouchTop - self.__lastTouchTop; 972 | 973 | // Read previous scroll position and zooming 974 | var scrollLeft = self.__scrollLeft; 975 | var scrollTop = self.__scrollTop; 976 | var level = self.__zoomLevel; 977 | 978 | // Work with scaling 979 | if (scale != null && self.options.zooming) { 980 | 981 | var oldLevel = level; 982 | 983 | // Recompute level based on previous scale and new scale 984 | level = level / self.__lastScale * scale; 985 | 986 | // Limit level according to configuration 987 | level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom); 988 | 989 | // Only do further compution when change happened 990 | if (oldLevel !== level) { 991 | 992 | // Compute relative event position to container 993 | var currentTouchLeftRel = currentTouchLeft - self.__clientLeft; 994 | var currentTouchTopRel = currentTouchTop - self.__clientTop; 995 | 996 | // Recompute left and top coordinates based on new zoom level 997 | scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel; 998 | scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel; 999 | 1000 | // Recompute max scroll values 1001 | self.__computeScrollMax(level); 1002 | 1003 | } 1004 | } 1005 | 1006 | if (self.__enableScrollX) { 1007 | 1008 | scrollLeft -= moveX * this.options.speedMultiplier; 1009 | var maxScrollLeft = self.__maxScrollLeft; 1010 | 1011 | if (scrollLeft > maxScrollLeft || scrollLeft < 0) { 1012 | 1013 | // Slow down on the edges 1014 | if (self.options.bouncing) { 1015 | 1016 | scrollLeft += (moveX / 2 * this.options.speedMultiplier); 1017 | 1018 | } else if (scrollLeft > maxScrollLeft) { 1019 | 1020 | scrollLeft = maxScrollLeft; 1021 | 1022 | } else { 1023 | 1024 | scrollLeft = 0; 1025 | 1026 | } 1027 | } 1028 | } 1029 | 1030 | // Compute new vertical scroll position 1031 | if (self.__enableScrollY) { 1032 | 1033 | scrollTop -= moveY * this.options.speedMultiplier; 1034 | var maxScrollTop = self.__maxScrollTop; 1035 | 1036 | if (scrollTop > maxScrollTop || scrollTop < 0) { 1037 | 1038 | // Slow down on the edges 1039 | if (self.options.bouncing) { 1040 | 1041 | scrollTop += (moveY / 2 * this.options.speedMultiplier); 1042 | 1043 | // Support pull-to-refresh (only when only y is scrollable) 1044 | if (!self.__enableScrollX && self.__refreshHeight != null) { 1045 | 1046 | if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) { 1047 | 1048 | self.__refreshActive = true; 1049 | if (self.__refreshActivate) { 1050 | self.__refreshActivate(); 1051 | } 1052 | 1053 | } else if (self.__refreshActive && scrollTop > -self.__refreshHeight) { 1054 | 1055 | self.__refreshActive = false; 1056 | if (self.__refreshDeactivate) { 1057 | self.__refreshDeactivate(); 1058 | } 1059 | 1060 | } 1061 | } 1062 | 1063 | } else if (scrollTop > maxScrollTop) { 1064 | 1065 | scrollTop = maxScrollTop; 1066 | 1067 | } else { 1068 | 1069 | scrollTop = 0; 1070 | 1071 | } 1072 | } 1073 | } 1074 | 1075 | // Keep list from growing infinitely (holding min 10, max 20 measure points) 1076 | if (positions.length > 60) { 1077 | positions.splice(0, 30); 1078 | } 1079 | 1080 | // Track scroll movement for decleration 1081 | positions.push(scrollLeft, scrollTop, timeStamp); 1082 | 1083 | // Sync scroll position 1084 | self.__publish(scrollLeft, scrollTop, level); 1085 | 1086 | // Otherwise figure out whether we are switching into dragging mode now. 1087 | } else { 1088 | 1089 | var minimumTrackingForScroll = self.options.locking ? 3 : 0; 1090 | var minimumTrackingForDrag = 5; 1091 | 1092 | var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft); 1093 | var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop); 1094 | 1095 | self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll; 1096 | self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll; 1097 | 1098 | positions.push(self.__scrollLeft, self.__scrollTop, timeStamp); 1099 | 1100 | self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag); 1101 | if (self.__isDragging) { 1102 | self.__interruptedAnimation = false; 1103 | } 1104 | 1105 | } 1106 | 1107 | // Update last touch positions and time stamp for next event 1108 | self.__lastTouchLeft = currentTouchLeft; 1109 | self.__lastTouchTop = currentTouchTop; 1110 | self.__lastTouchMove = timeStamp; 1111 | self.__lastScale = scale; 1112 | 1113 | }, 1114 | 1115 | 1116 | /** 1117 | * Touch end handler for scrolling support 1118 | */ 1119 | doTouchEnd: function(timeStamp) { 1120 | 1121 | if (timeStamp instanceof Date) { 1122 | timeStamp = timeStamp.valueOf(); 1123 | } 1124 | if (typeof timeStamp !== "number") { 1125 | throw new Error("Invalid timestamp value: " + timeStamp); 1126 | } 1127 | 1128 | var self = this; 1129 | 1130 | // Ignore event when tracking is not enabled (no touchstart event on element) 1131 | // This is required as this listener ('touchmove') sits on the document and not on the element itself. 1132 | if (!self.__isTracking) { 1133 | return; 1134 | } 1135 | 1136 | // Not touching anymore (when two finger hit the screen there are two touch end events) 1137 | self.__isTracking = false; 1138 | 1139 | // Be sure to reset the dragging flag now. Here we also detect whether 1140 | // the finger has moved fast enough to switch into a deceleration animation. 1141 | if (self.__isDragging) { 1142 | 1143 | // Reset dragging flag 1144 | self.__isDragging = false; 1145 | 1146 | // Start deceleration 1147 | // Verify that the last move detected was in some relevant time frame 1148 | if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) { 1149 | 1150 | // Then figure out what the scroll position was about 100ms ago 1151 | var positions = self.__positions; 1152 | var endPos = positions.length - 1; 1153 | var startPos = endPos; 1154 | 1155 | // Move pointer to position measured 100ms ago 1156 | for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) { 1157 | startPos = i; 1158 | } 1159 | 1160 | // If start and stop position is identical in a 100ms timeframe, 1161 | // we cannot compute any useful deceleration. 1162 | if (startPos !== endPos) { 1163 | 1164 | // Compute relative movement between these two points 1165 | var timeOffset = positions[endPos] - positions[startPos]; 1166 | var movedLeft = self.__scrollLeft - positions[startPos - 2]; 1167 | var movedTop = self.__scrollTop - positions[startPos - 1]; 1168 | 1169 | // Based on 50ms compute the movement to apply for each render step 1170 | self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60); 1171 | self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60); 1172 | 1173 | // How much velocity is required to start the deceleration 1174 | var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? 4 : 1; 1175 | 1176 | // Verify that we have enough velocity to start deceleration 1177 | if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) { 1178 | 1179 | // Deactivate pull-to-refresh when decelerating 1180 | if (!self.__refreshActive) { 1181 | self.__startDeceleration(timeStamp); 1182 | } 1183 | } else { 1184 | self.options.scrollingComplete(); 1185 | } 1186 | } else { 1187 | self.options.scrollingComplete(); 1188 | } 1189 | } else if ((timeStamp - self.__lastTouchMove) > 100) { 1190 | self.options.scrollingComplete(); 1191 | } 1192 | } 1193 | 1194 | // If this was a slower move it is per default non decelerated, but this 1195 | // still means that we want snap back to the bounds which is done here. 1196 | // This is placed outside the condition above to improve edge case stability 1197 | // e.g. touchend fired without enabled dragging. This should normally do not 1198 | // have modified the scroll positions or even showed the scrollbars though. 1199 | if (!self.__isDecelerating) { 1200 | 1201 | if (self.__refreshActive && self.__refreshStart) { 1202 | 1203 | // Use publish instead of scrollTo to allow scrolling to out of boundary position 1204 | // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled 1205 | self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true); 1206 | 1207 | if (self.__refreshStart) { 1208 | self.__refreshStart(); 1209 | } 1210 | 1211 | } else { 1212 | 1213 | if (self.__interruptedAnimation || self.__isDragging) { 1214 | self.options.scrollingComplete(); 1215 | } 1216 | self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel); 1217 | 1218 | // Directly signalize deactivation (nothing todo on refresh?) 1219 | if (self.__refreshActive) { 1220 | 1221 | self.__refreshActive = false; 1222 | if (self.__refreshDeactivate) { 1223 | self.__refreshDeactivate(); 1224 | } 1225 | 1226 | } 1227 | } 1228 | } 1229 | 1230 | // Fully cleanup list 1231 | self.__positions.length = 0; 1232 | 1233 | }, 1234 | 1235 | 1236 | 1237 | /* 1238 | --------------------------------------------------------------------------- 1239 | PRIVATE API 1240 | --------------------------------------------------------------------------- 1241 | */ 1242 | 1243 | /** 1244 | * Applies the scroll position to the content element 1245 | * 1246 | * @param left {Number} Left scroll position 1247 | * @param top {Number} Top scroll position 1248 | * @param animate {Boolean?false} Whether animation should be used to move to the new coordinates 1249 | */ 1250 | __publish: function(left, top, zoom, animate) { 1251 | 1252 | var self = this; 1253 | 1254 | // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation 1255 | var wasAnimating = self.__isAnimating; 1256 | if (wasAnimating) { 1257 | core.effect.Animate.stop(wasAnimating); 1258 | self.__isAnimating = false; 1259 | } 1260 | 1261 | if (animate && self.options.animating) { 1262 | 1263 | // Keep scheduled positions for scrollBy/zoomBy functionality 1264 | self.__scheduledLeft = left; 1265 | self.__scheduledTop = top; 1266 | self.__scheduledZoom = zoom; 1267 | 1268 | var oldLeft = self.__scrollLeft; 1269 | var oldTop = self.__scrollTop; 1270 | var oldZoom = self.__zoomLevel; 1271 | 1272 | var diffLeft = left - oldLeft; 1273 | var diffTop = top - oldTop; 1274 | var diffZoom = zoom - oldZoom; 1275 | 1276 | var step = function(percent, now, render) { 1277 | 1278 | if (render) { 1279 | 1280 | self.__scrollLeft = oldLeft + (diffLeft * percent); 1281 | self.__scrollTop = oldTop + (diffTop * percent); 1282 | self.__zoomLevel = oldZoom + (diffZoom * percent); 1283 | 1284 | // Push values out 1285 | if (self.__callback) { 1286 | self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel); 1287 | } 1288 | 1289 | } 1290 | }; 1291 | 1292 | var verify = function(id) { 1293 | return self.__isAnimating === id; 1294 | }; 1295 | 1296 | var completed = function(renderedFramesPerSecond, animationId, wasFinished) { 1297 | if (animationId === self.__isAnimating) { 1298 | self.__isAnimating = false; 1299 | } 1300 | if (self.__didDecelerationComplete || wasFinished) { 1301 | self.options.scrollingComplete(); 1302 | } 1303 | 1304 | if (self.options.zooming) { 1305 | self.__computeScrollMax(); 1306 | if(self.__zoomComplete) { 1307 | self.__zoomComplete(); 1308 | self.__zoomComplete = null; 1309 | } 1310 | } 1311 | }; 1312 | 1313 | // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out 1314 | self.__isAnimating = core.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic); 1315 | 1316 | } else { 1317 | 1318 | self.__scheduledLeft = self.__scrollLeft = left; 1319 | self.__scheduledTop = self.__scrollTop = top; 1320 | self.__scheduledZoom = self.__zoomLevel = zoom; 1321 | 1322 | // Push values out 1323 | if (self.__callback) { 1324 | self.__callback(left, top, zoom); 1325 | } 1326 | 1327 | // Fix max scroll ranges 1328 | if (self.options.zooming) { 1329 | self.__computeScrollMax(); 1330 | if(self.__zoomComplete) { 1331 | self.__zoomComplete(); 1332 | self.__zoomComplete = null; 1333 | } 1334 | } 1335 | } 1336 | }, 1337 | 1338 | 1339 | /** 1340 | * Recomputes scroll minimum values based on client dimensions and content dimensions. 1341 | */ 1342 | __computeScrollMax: function(zoomLevel) { 1343 | 1344 | var self = this; 1345 | 1346 | if (zoomLevel == null) { 1347 | zoomLevel = self.__zoomLevel; 1348 | } 1349 | 1350 | self.__maxScrollLeft = Math.max((self.__contentWidth * zoomLevel) - self.__clientWidth, 0); 1351 | self.__maxScrollTop = Math.max((self.__contentHeight * zoomLevel) - self.__clientHeight, 0); 1352 | 1353 | }, 1354 | 1355 | 1356 | 1357 | /* 1358 | --------------------------------------------------------------------------- 1359 | ANIMATION (DECELERATION) SUPPORT 1360 | --------------------------------------------------------------------------- 1361 | */ 1362 | 1363 | /** 1364 | * Called when a touch sequence end and the speed of the finger was high enough 1365 | * to switch into deceleration mode. 1366 | */ 1367 | __startDeceleration: function(timeStamp) { 1368 | 1369 | var self = this; 1370 | 1371 | if (self.options.paging) { 1372 | 1373 | var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft), 0); 1374 | var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop), 0); 1375 | var clientWidth = self.__clientWidth; 1376 | var clientHeight = self.__clientHeight; 1377 | 1378 | // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area. 1379 | // Each page should have exactly the size of the client area. 1380 | self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth; 1381 | self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight; 1382 | self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth; 1383 | self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight; 1384 | 1385 | } else { 1386 | 1387 | self.__minDecelerationScrollLeft = 0; 1388 | self.__minDecelerationScrollTop = 0; 1389 | self.__maxDecelerationScrollLeft = self.__maxScrollLeft; 1390 | self.__maxDecelerationScrollTop = self.__maxScrollTop; 1391 | 1392 | } 1393 | 1394 | // Wrap class method 1395 | var step = function(percent, now, render) { 1396 | self.__stepThroughDeceleration(render); 1397 | }; 1398 | 1399 | // How much velocity is required to keep the deceleration running 1400 | var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.001; 1401 | 1402 | // Detect whether it's still worth to continue animating steps 1403 | // If we are already slow enough to not being user perceivable anymore, we stop the whole process here. 1404 | var verify = function() { 1405 | var shouldContinue = Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating; 1406 | if (!shouldContinue) { 1407 | self.__didDecelerationComplete = true; 1408 | } 1409 | return shouldContinue; 1410 | }; 1411 | 1412 | var completed = function(renderedFramesPerSecond, animationId, wasFinished) { 1413 | self.__isDecelerating = false; 1414 | if (self.__didDecelerationComplete) { 1415 | self.options.scrollingComplete(); 1416 | } 1417 | 1418 | // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions 1419 | self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping); 1420 | }; 1421 | 1422 | // Start animation and switch on flag 1423 | self.__isDecelerating = core.effect.Animate.start(step, verify, completed); 1424 | 1425 | }, 1426 | 1427 | 1428 | /** 1429 | * Called on every step of the animation 1430 | * 1431 | * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only! 1432 | */ 1433 | __stepThroughDeceleration: function(render) { 1434 | 1435 | var self = this; 1436 | 1437 | 1438 | // 1439 | // COMPUTE NEXT SCROLL POSITION 1440 | // 1441 | 1442 | // Add deceleration to scroll position 1443 | var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX; 1444 | var scrollTop = self.__scrollTop + self.__decelerationVelocityY; 1445 | 1446 | 1447 | // 1448 | // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE 1449 | // 1450 | 1451 | if (!self.options.bouncing) { 1452 | 1453 | var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft); 1454 | if (scrollLeftFixed !== scrollLeft) { 1455 | scrollLeft = scrollLeftFixed; 1456 | self.__decelerationVelocityX = 0; 1457 | } 1458 | 1459 | var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop); 1460 | if (scrollTopFixed !== scrollTop) { 1461 | scrollTop = scrollTopFixed; 1462 | self.__decelerationVelocityY = 0; 1463 | } 1464 | 1465 | } 1466 | 1467 | 1468 | // 1469 | // UPDATE SCROLL POSITION 1470 | // 1471 | 1472 | if (render) { 1473 | 1474 | self.__publish(scrollLeft, scrollTop, self.__zoomLevel); 1475 | 1476 | } else { 1477 | 1478 | self.__scrollLeft = scrollLeft; 1479 | self.__scrollTop = scrollTop; 1480 | 1481 | } 1482 | 1483 | 1484 | // 1485 | // SLOW DOWN 1486 | // 1487 | 1488 | // Slow down velocity on every iteration 1489 | if (!self.options.paging) { 1490 | 1491 | // This is the factor applied to every iteration of the animation 1492 | // to slow down the process. This should emulate natural behavior where 1493 | // objects slow down when the initiator of the movement is removed 1494 | var frictionFactor = 0.95; 1495 | 1496 | self.__decelerationVelocityX *= frictionFactor; 1497 | self.__decelerationVelocityY *= frictionFactor; 1498 | 1499 | } 1500 | 1501 | 1502 | // 1503 | // BOUNCING SUPPORT 1504 | // 1505 | 1506 | if (self.options.bouncing) { 1507 | 1508 | var scrollOutsideX = 0; 1509 | var scrollOutsideY = 0; 1510 | 1511 | // This configures the amount of change applied to deceleration/acceleration when reaching boundaries 1512 | var penetrationDeceleration = self.options.penetrationDeceleration; 1513 | var penetrationAcceleration = self.options.penetrationAcceleration; 1514 | 1515 | // Check limits 1516 | if (scrollLeft < self.__minDecelerationScrollLeft) { 1517 | scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft; 1518 | } else if (scrollLeft > self.__maxDecelerationScrollLeft) { 1519 | scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft; 1520 | } 1521 | 1522 | if (scrollTop < self.__minDecelerationScrollTop) { 1523 | scrollOutsideY = self.__minDecelerationScrollTop - scrollTop; 1524 | } else if (scrollTop > self.__maxDecelerationScrollTop) { 1525 | scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop; 1526 | } 1527 | 1528 | // Slow down until slow enough, then flip back to snap position 1529 | if (scrollOutsideX !== 0) { 1530 | if (scrollOutsideX * self.__decelerationVelocityX <= 0) { 1531 | self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration; 1532 | } else { 1533 | self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration; 1534 | } 1535 | } 1536 | 1537 | if (scrollOutsideY !== 0) { 1538 | if (scrollOutsideY * self.__decelerationVelocityY <= 0) { 1539 | self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration; 1540 | } else { 1541 | self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration; 1542 | } 1543 | } 1544 | } 1545 | } 1546 | }; 1547 | 1548 | // Copy over members to prototype 1549 | for (var key in members) { 1550 | Scroller.prototype[key] = members[key]; 1551 | } 1552 | 1553 | if (typeof module != 'undefined' && module.exports) { 1554 | module.exports = Scroller; 1555 | } else if (typeof define == 'function' && define.amd) { 1556 | define( function () { return Scroller; } ); 1557 | } else { 1558 | window.Scroller = Scroller; 1559 | } 1560 | 1561 | })(window); 1562 | --------------------------------------------------------------------------------