mescroll.js 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. /* mescroll
  2. * version 1.4.2
  3. * 2019-08-01 wenju
  4. * http://www.mescroll.com
  5. */
  6. (function (name, definition) {
  7. if (typeof define === 'function') {
  8. // AMD环境或CMD环境
  9. define(definition);
  10. } else if (typeof module !== 'undefined' && module.exports) {
  11. // 定义为普通Node模块
  12. module.exports = definition();
  13. } else {
  14. // 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
  15. this[name] = definition();
  16. }
  17. })('MeScroll', function () {
  18. var MeScroll = function (mescrollId, options) {
  19. var me = this;
  20. me.version = '1.4.2'; // mescroll版本号
  21. me.isScrollBody = (!mescrollId || mescrollId === 'body'); // 滑动区域是否为body
  22. me.scrollDom = me.isScrollBody ? document.body : me.getDomById(mescrollId); // MeScroll的滑动区域
  23. if (!me.scrollDom) return;
  24. me.options = options || {}; // 配置
  25. var u = navigator.userAgent;
  26. var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); // 是否为ios设备
  27. var isPC = typeof window.orientation === 'undefined'; // 是否为PC端
  28. var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1;// 是否为android端
  29. var isWx = u.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'; // 是否为微信端,此处不能为===,因为match的结果可能是null或数组
  30. me.os = {
  31. ios: isIOS,
  32. pc: isPC,
  33. android: isAndroid,
  34. wx: isWx
  35. }
  36. me.isDownScrolling = false; // 是否在执行下拉刷新的回调
  37. me.isUpScrolling = false; // 是否在执行上拉加载的回调
  38. var hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
  39. // 初始化下拉刷新
  40. me.initDownScroll();
  41. // 初始化上拉加载,则初始化
  42. me.initUpScroll();
  43. // 自动加载
  44. setTimeout(function () { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
  45. // 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
  46. if (me.optDown.use && me.optDown.auto && hasDownCallback) {
  47. if (me.optDown.autoShowLoading) {
  48. me.triggerDownScroll(); // 显示下拉进度,执行下拉回调
  49. } else {
  50. me.optDown.callback && me.optDown.callback(me); // 不显示下拉进度,直接执行下拉回调
  51. }
  52. }
  53. // 自动触发上拉加载
  54. me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll();
  55. }, 30); // 需让me.optDown.inited和me.optUp.inited先执行
  56. }
  57. /* 配置参数:下拉刷新 */
  58. MeScroll.prototype.extendDownScroll = function (optDown) {
  59. // 下拉刷新的配置
  60. MeScroll.extend(optDown, {
  61. use: true, // 是否启用下拉刷新; 默认true
  62. auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
  63. autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
  64. isLock: false, // 是否锁定下拉刷新,默认false;
  65. isBoth: false, // 下拉刷新时,如果滑动到列表底部是否可以同时触发上拉加载;默认false,两者不可同时触发;
  66. offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
  67. inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
  68. outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
  69. bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
  70. minAngle: 45, // 向下滑动最少偏移的角度,取值区间 [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
  71. hardwareClass: 'mescroll-hardware', // 硬件加速样式,解决部分手机闪屏或动画不流畅的问题
  72. mustToTop: false, // 是否滚动条必须在顶部,才可以下拉刷新.默认false. 当您发现下拉刷新会闪白屏时,设置true即可修复.
  73. warpId: null, // 可配置下拉刷新的布局添加到指定id的div;默认不配置,默认添加到mescrollId
  74. warpClass: 'mescroll-downwarp', // 下拉刷新的布局容器样式,参见mescroll.css
  75. resetClass: 'mescroll-downwarp-reset', // 下拉刷新高度重置的动画,参见mescroll.css
  76. textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
  77. textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
  78. textLoading: '加载中 ...', // 加载中的提示文本
  79. htmlContent: '<p class="downwarp-progress"></p><p class="downwarp-tip"></p>', // 布局内容
  80. inited: function (mescroll, downwarp) {
  81. // 下拉刷新初始化完毕的回调
  82. mescroll.downTipDom = downwarp.getElementsByClassName('downwarp-tip')[0];
  83. mescroll.downProgressDom = downwarp.getElementsByClassName('downwarp-progress')[0];
  84. },
  85. inOffset: function (mescroll) {
  86. // 下拉的距离进入offset范围内那一刻的回调
  87. if (mescroll.downTipDom) mescroll.downTipDom.innerHTML = mescroll.optDown.textInOffset;
  88. if (mescroll.downProgressDom) mescroll.downProgressDom.classList.remove('mescroll-rotate');
  89. },
  90. outOffset: function (mescroll) {
  91. // 下拉的距离大于offset那一刻的回调
  92. if (mescroll.downTipDom) mescroll.downTipDom.innerHTML = mescroll.optDown.textOutOffset;
  93. },
  94. onMoving: function (mescroll, rate, downHight) {
  95. // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
  96. if (mescroll.downProgressDom) {
  97. var progress = 360 * rate;
  98. mescroll.downProgressDom.style.webkitTransform = 'rotate(' + progress + 'deg)';
  99. mescroll.downProgressDom.style.transform = 'rotate(' + progress + 'deg)';
  100. }
  101. },
  102. beforeLoading: function (mescroll, downwarp) {
  103. // 准备触发下拉刷新的回调
  104. return false; // 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
  105. },
  106. showLoading: function (mescroll) {
  107. // 显示下拉刷新进度的回调
  108. if (mescroll.downTipDom) mescroll.downTipDom.innerHTML = mescroll.optDown.textLoading;
  109. if (mescroll.downProgressDom) mescroll.downProgressDom.classList.add('mescroll-rotate');
  110. },
  111. afterLoading: function (mescroll) {
  112. // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
  113. return 0
  114. },
  115. callback: function (mescroll) {
  116. // 下拉刷新的回调;默认重置上拉加载列表为第一页
  117. mescroll.resetUpScroll();
  118. }
  119. })
  120. }
  121. /* 配置参数:上拉加载 */
  122. MeScroll.prototype.extendUpScroll = function (optUp) {
  123. // 是否为PC端,如果是scrollbar端,默认自定义滚动条
  124. var isPC = this.os.pc;
  125. // 上拉加载的配置
  126. MeScroll.extend(optUp, {
  127. use: true, // 是否启用上拉加载; 默认true
  128. auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
  129. isLock: false, // 是否锁定上拉加载,默认false;
  130. isBoth: false, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认false,两者不可同时触发;
  131. isBounce: true, // 是否允许ios的bounce回弹;默认true,允许; 如果设置为false,则除了mescroll, mescroll-touch, mescroll-touch-x, mescroll-touch-y能够接收touchmove事件,其他部分均无法滑动,能够有效禁止bounce
  132. callback: null, // 上拉加载的回调;function(page,mescroll){ }
  133. page: {
  134. num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
  135. size: 10, // 每页数据的数量
  136. time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
  137. },
  138. noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
  139. offset: 100, // 列表滚动到距离底部小于100px,即可触发上拉加载的回调
  140. toTop: {
  141. // 回到顶部按钮,需配置src才显示
  142. warpId: null, // 父布局的id; 默认添加在body中
  143. src: null, // 图片路径,默认null;
  144. html: null, // html标签内容,默认null; 如果同时设置了src,则优先取src
  145. offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
  146. warpClass: 'mescroll-totop', // 按钮样式,参见mescroll.css
  147. showClass: 'mescroll-fade-in', // 显示样式,参见mescroll.css
  148. hideClass: 'mescroll-fade-out', // 隐藏样式,参见mescroll.css
  149. fadeDuration: 0.5, // 回到顶部按钮的显示隐藏动画时长,默认0.5秒. (注意:showClass和hideClass设置的动画时长不会生效.)
  150. duration: 300, // 回到顶部的动画时长,默认300ms
  151. supportTap: false, // 如果您的运行环境支持tap,则可配置true;
  152. btnClick: null // 点击按钮的回调; 小提示:如果在回调里return true,将不执行回到顶部的操作.
  153. },
  154. loadFull: {
  155. use: false, // 列表数据过少,不足以滑动触发上拉加载,是否自动加载下一页,直到满屏或者无更多数据为止;默认false,因为可通过调高page.size避免这个情况
  156. delay: 500 // 延时执行的毫秒数; 延时是为了保证列表数据或占位的图片都已初始化完成,且下拉刷新上拉加载中区域动画已执行完毕;
  157. },
  158. empty: {
  159. // 列表第一页无任何数据时,显示的空提示布局; 需配置warpId或clearEmptyId才生效;
  160. warpId: null, // 父布局的id; 如果此项有值,将不使用clearEmptyId的值;
  161. customId: null, // 自定义空布局的id; 如果此项有值,将不使用empty所有配置;空布局的显示隐藏只控制customId元素的显示隐藏;用于完全自定义empty的场景
  162. icon: null, // 图标路径
  163. tip: '~ 空空如也 ~', // 提示
  164. btntext: '', // 按钮
  165. btnClick: null, // 点击按钮的回调
  166. supportTap: false // 如果您的运行环境支持tap,则可配置true;
  167. },
  168. clearId: null, // 加载第一页时需清空数据的列表id; 如果此项有值,将不使用clearEmptyId的值;
  169. clearEmptyId: null, // 相当于同时设置了clearId和empty.warpId; 简化写法;默认null; 注意vue中不能配置此项
  170. hardwareClass: 'mescroll-hardware', // 硬件加速样式,使上拉动画流畅
  171. warpId: null, // 可配置上拉加载的布局添加到指定id的div;默认不配置,默认添加到mescrollId
  172. warpClass: 'mescroll-upwarp', // 上拉加载的布局容器样式
  173. htmlLoading: '<p class="upwarp-progress mescroll-rotate"></p><p class="upwarp-tip">加载中..</p>', // 上拉加载中的布局
  174. htmlNodata: '<p class="upwarp-nodata">-- END --</p>', // 无数据的布局
  175. inited: function (mescroll, upwarp) {
  176. // 初始化完毕的回调,可缓存dom 比如 mescroll.upProgressDom = upwarp.getElementsByClassName("upwarp-progress")[0];
  177. },
  178. showLoading: function (mescroll, upwarp) {
  179. // 上拉加载中.. mescroll.upProgressDom.style.display = "block" 不通过此方式显示,因为ios快速滑动到底部,进度条会无法及时渲染
  180. upwarp.innerHTML = mescroll.optUp.htmlLoading;
  181. },
  182. showNoMore: function (mescroll, upwarp) {
  183. // 无更多数据
  184. upwarp.innerHTML = mescroll.optUp.htmlNodata;
  185. },
  186. onScroll: null, // 列表滑动监听,默认null; 例如 onScroll: function(mescroll, y, isUp){ }; //y为列表当前滚动条的位置; isUp=true向上滑,isUp=false向下滑
  187. scrollbar: {
  188. use: isPC, // 默认只在PC端自定义滚动条样式
  189. barClass: 'mescroll-bar'
  190. },
  191. lazyLoad: {
  192. use: false, // 是否开启懒加载,默认false
  193. attr: 'imgurl', // 网络图片地址的属性名 (图片加载成功会自动移除改属性): <img imgurl='网络图 src='占位图''/>
  194. showClass: 'mescroll-lazy-in', // 显示样式,参见mescroll.css
  195. delay: 500, // 列表滚动的过程中每500ms检查一次图片是否在可视区域,如果在可视区域则加载图片
  196. offset: 200 // 超出可视区域200px的图片仍可触发懒加载,目的是提前加载部分图片
  197. }
  198. })
  199. }
  200. /* 配置参数 */
  201. MeScroll.extend = function (userOption, defaultOption) {
  202. if (!userOption) return defaultOption;
  203. for (var key in defaultOption) {
  204. if (userOption[key] == null) {
  205. userOption[key] = defaultOption[key];
  206. } else if (typeof userOption[key] === 'object') {
  207. MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
  208. }
  209. }
  210. return userOption;
  211. }
  212. /* -------初始化下拉刷新------- */
  213. MeScroll.prototype.initDownScroll = function () {
  214. var me = this;
  215. // 配置参数
  216. me.optDown = me.options.down || {};
  217. // 具体参数配置
  218. me.extendDownScroll(me.optDown);
  219. // 鼠标或手指的按下事件
  220. me.touchstartEvent = function (e) {
  221. if (me.isScrollTo) me.preventDefault(e); // 如果列表执行滑动事件,则阻止事件,优先执行scrollTo方法
  222. me.startPoint = me.getPoint(e); // 记录起点
  223. me.lastPoint = me.startPoint; // 重置上次move的点
  224. me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
  225. me.inTouchend = false; // 标记不是touchend
  226. var scrollTop = me.getScrollTop();// 滚动条的位置
  227. me.isKeepTop = scrollTop === 0; // 标记滚动条起点为0
  228. if (me.os.pc && scrollTop <= 0) {
  229. // 在顶部给PC端添加move事件
  230. me.scrollDom.addEventListener('mousemove', me.touchmoveEvent, {
  231. passive: false
  232. });
  233. document.ondragstart = function () { // 在顶部禁止PC端拖拽图片,避免与下拉刷新冲突
  234. return false;
  235. }
  236. }
  237. }
  238. me.scrollDom.addEventListener('mousedown', me.touchstartEvent); // PC端鼠标事件
  239. me.scrollDom.addEventListener('touchstart', me.touchstartEvent); // 移动端手指事件
  240. // 鼠标或手指的滑动事件
  241. me.touchmoveEvent = function (e) {
  242. if (!me.startPoint) return;
  243. var scrollTop = me.getScrollTop(); // 当前滚动条的距离
  244. if (scrollTop > 0) me.isKeepTop = false; // 在移动过程中,只要滚动条有一次大于0,则标记false
  245. var curPoint = me.getPoint(e); // 当前点
  246. var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
  247. // 向下拉
  248. if (moveY > 0) {
  249. // 在顶部
  250. if (scrollTop <= 0) {
  251. me.preventDefault(e); // 阻止浏览器默认的滚动,避免触发bounce
  252. // 可下拉的条件
  253. if (me.optDown.use && !me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.optUp.isBoth))) {
  254. if (me.optDown.mustToTop && !me.isKeepTop) return; // 是否配置了必须在顶部才可以下拉
  255. // 下拉的角度是否在配置的范围内
  256. var x = Math.abs(me.lastPoint.x - curPoint.x);
  257. var y = Math.abs(me.lastPoint.y - curPoint.y);
  258. var z = Math.sqrt(x * x + y * y);
  259. if (z !== 0) {
  260. var angle = Math.asin(y / z) / Math.PI * 180; // 两点之间的角度,区间 [0,90]
  261. if (angle < me.optDown.minAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
  262. }
  263. // 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
  264. if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
  265. me.inTouchend = true; // 标记执行touchend
  266. me.touchendEvent(); // 提前触发touchend
  267. return;
  268. }
  269. var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
  270. if (!me.downHight) me.downHight = 0; // 下拉区域的高度
  271. // 下拉距离 < 指定距离
  272. if (me.downHight < me.optDown.offset) {
  273. if (me.movetype !== 1) {
  274. me.movetype = 1; // 加入标记,保证只执行一次
  275. me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
  276. me.downwarp.classList.remove(me.optDown.resetClass); // 移除高度重置的动画
  277. me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
  278. if (me.os.ios && !me.isKeepTop) { // 下拉过程中,滚动条一直在顶部的,则不必取消回弹,否则会闪白屏
  279. me.scrollDom.classList.add(me.optDown.hardwareClass); // 开启硬件加速,解决iOS下拉因隐藏进度条而闪屏的问题
  280. me.scrollDom.style.webkitOverflowScrolling = 'auto'; // 取消列表回弹效果,避免与下面me.downwarp.style.height混合,而导致界面抖动闪屏
  281. me.isSetScrollAuto = true; // 标记设置了webkitOverflowScrolling为auto
  282. }
  283. }
  284. me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
  285. // 指定距离 <= 下拉距离
  286. } else {
  287. if (me.movetype !== 2) {
  288. me.movetype = 2; // 加入标记,保证只执行一次
  289. me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
  290. me.downwarp.classList.remove(me.optDown.resetClass); // 移除高度重置的动画
  291. me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
  292. if (me.os.ios && !me.isKeepTop) { // 下拉过程中,滚动条一直在顶部的,则不必取消回弹,否则会闪白屏
  293. me.scrollDom.classList.add(me.optDown.hardwareClass); // 开启硬件加速,解决iOS下拉因隐藏进度条而闪屏的问题
  294. me.scrollDom.style.webkitOverflowScrolling = 'auto'; // 取消列表回弹效果,避免与下面me.downwarp.style.height混合,而导致界面抖动闪屏
  295. me.isSetScrollAuto = true; // 标记设置了webkitOverflowScrolling为auto
  296. }
  297. }
  298. if (diff > 0) { // 向下拉
  299. me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
  300. } else { // 向上收
  301. me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
  302. }
  303. }
  304. me.downwarp.style.height = me.downHight + 'px'; // 实时更新下拉区域高度
  305. var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
  306. me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
  307. }
  308. }
  309. // 向上拉
  310. } else if (moveY < 0) {
  311. var scrollHeight = me.getScrollHeight(); // 滚动内容的高度
  312. var clientHeight = me.getClientHeight(); // 滚动容器的高度
  313. var toBottom = scrollHeight - clientHeight - scrollTop; // 滚动条距离底部的距离
  314. // 如果在底部,则阻止浏览器默认事件
  315. if (!me.optUp.isBounce && toBottom <= 0) me.preventDefault(e);
  316. // 如果不满屏或者已经在底部,无法触发scroll事件,此时需主动触发上拉回调
  317. if (me.optUp.use && !me.optUp.isLock && me.optUp.hasNext && !me.isUpScrolling && (!me.isDownScrolling || (me.isDownScrolling && me.optDown.isBoth)) && (clientHeight + me.optUp.offset >= scrollHeight || toBottom <= 0)) {
  318. me.triggerUpScroll();
  319. }
  320. }
  321. me.lastPoint = curPoint; // 记录本次移动的点
  322. }
  323. // 移动端手指的滑动事件
  324. me.scrollDom.addEventListener('touchmove', me.touchmoveEvent, {
  325. passive: false
  326. });
  327. // 鼠标或手指的离开事件
  328. me.touchendEvent = function () {
  329. // 如果下拉区域高度已改变,则需重置回来
  330. if (me.optDown.use && me.isMoveDown) {
  331. if (me.downHight >= me.optDown.offset) {
  332. // 符合触发刷新的条件
  333. me.triggerDownScroll();
  334. } else {
  335. // 不符合的话 则重置
  336. me.downwarp.classList.add(me.optDown.resetClass); // 加入高度重置的动画,过渡平滑
  337. me.downHight = 0;
  338. me.downwarp.style.height = 0;
  339. }
  340. if (me.isSetScrollAuto) {
  341. me.scrollDom.style.webkitOverflowScrolling = 'touch';
  342. me.scrollDom.classList.remove(me.optDown.hardwareClass);
  343. me.isSetScrollAuto = false;
  344. }
  345. me.movetype = 0;
  346. me.isMoveDown = false;
  347. }
  348. if (me.os.pc) {
  349. me.scrollDom.removeEventListener('mousemove', me.touchmoveEvent); // 移除pc端的move事件
  350. document.ondragstart = function () { // 解除PC端禁止拖拽图片
  351. return true;
  352. }
  353. }
  354. }
  355. me.scrollDom.addEventListener('mouseup', me.touchendEvent); // PC端鼠标抬起事件
  356. me.scrollDom.addEventListener('mouseleave', me.touchendEvent); // PC端鼠标离开事件
  357. me.scrollDom.addEventListener('touchend', me.touchendEvent); // 移动端手指事件
  358. me.scrollDom.addEventListener('touchcancel', me.touchendEvent); // 移动端系统停止跟踪触摸
  359. // 在页面中加入下拉布局
  360. if (me.optDown.use) {
  361. me.downwarp = document.createElement('div');
  362. me.downwarp.className = me.optDown.warpClass;
  363. me.downwarp.innerHTML = '<div class="downwarp-content">' + me.optDown.htmlContent + '</div>';
  364. var downparent = me.optDown.warpId ? me.getDomById(me.optDown.warpId) : me.scrollDom;
  365. if (me.optDown.warpId && downparent) {
  366. downparent.appendChild(me.downwarp);
  367. } else {
  368. if (!downparent) downparent = me.scrollDom;
  369. downparent.insertBefore(me.downwarp, me.scrollDom.firstChild);
  370. }
  371. // 初始化完毕的回调
  372. setTimeout(function () { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
  373. me.optDown.inited(me, me.downwarp);
  374. }, 0)
  375. }
  376. }
  377. /* 阻止浏览器默认滚动事件 */
  378. MeScroll.prototype.preventDefault = function (e) {
  379. // cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
  380. if (e && e.cancelable && !e.defaultPrevented) e.preventDefault()
  381. }
  382. /* 根据点击滑动事件获取第一个手指的坐标 */
  383. MeScroll.prototype.getPoint = function (e) {
  384. return {
  385. x: e.touches ? e.touches[0].pageX : e.clientX,
  386. y: e.touches ? e.touches[0].pageY : e.clientY
  387. }
  388. }
  389. /* 触发下拉刷新 */
  390. MeScroll.prototype.triggerDownScroll = function () {
  391. if (!this.optDown.beforeLoading(this, this.downwarp)) { // 准备触发下拉的回调,return true则处于完全自定义状态;默认return false;
  392. this.showDownScroll(); // 下拉刷新中...
  393. this.optDown.callback && this.optDown.callback(this); // 执行回调,联网加载数据
  394. }
  395. }
  396. /* 显示下拉进度布局 */
  397. MeScroll.prototype.showDownScroll = function () {
  398. this.isDownScrolling = true; // 标记下拉中
  399. this.optDown.showLoading(this); // 下拉刷新中...
  400. this.downHight = this.optDown.offset; // 更新下拉区域高度
  401. this.downwarp.classList.add(this.optDown.resetClass); // 加入高度重置的动画,过渡平滑
  402. this.downwarp.style.height = this.optDown.offset + 'px'; // 调整下拉区域高度
  403. }
  404. /* 结束下拉刷新 */
  405. MeScroll.prototype.endDownScroll = function () {
  406. var me = this;
  407. // 结束下拉刷新的方法
  408. var endScroll = function () {
  409. me.downHight = 0;
  410. me.downwarp.style.height = 0;
  411. me.isDownScrolling = false;
  412. if (me.downProgressDom) me.downProgressDom.classList.remove('mescroll-rotate');
  413. }
  414. // 结束下拉刷新时的回调
  415. var delay = me.optDown.afterLoading(me); // 结束下拉刷新的延时,单位ms
  416. if (typeof delay === 'number' && delay > 0) {
  417. setTimeout(endScroll, delay);
  418. } else {
  419. endScroll();
  420. }
  421. }
  422. /* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
  423. MeScroll.prototype.lockDownScroll = function (isLock) {
  424. if (isLock == null) isLock = true;
  425. this.optDown.isLock = isLock;
  426. }
  427. /* -------初始化上拉加载------- */
  428. MeScroll.prototype.initUpScroll = function () {
  429. var me = this;
  430. // 配置参数
  431. me.optUp = me.options.up || {
  432. use: false
  433. };
  434. // 具体参数配置
  435. me.extendUpScroll(me.optUp);
  436. // 自定义滚动条 (默认只在PC端设置)
  437. if (me.optUp.scrollbar.use) me.scrollDom.classList.add(me.optUp.scrollbar.barClass);
  438. // 不允许ios的bounce时,需禁止webview的touchmove事件
  439. if (!me.optUp.isBounce) me.setBounce(false);
  440. if (me.optUp.use === false) return; // 配置不使用上拉加载时,则不初始化上拉布局
  441. me.optUp.hasNext = true; // 如果使用上拉,则默认有下一页
  442. // 在页面中加入上拉布局
  443. me.upwarp = document.createElement('div');
  444. me.upwarp.className = me.optUp.warpClass;
  445. var upparent;
  446. if (me.optUp.warpId) upparent = me.getDomById(me.optUp.warpId);
  447. if (!upparent) upparent = me.scrollDom;
  448. upparent.appendChild(me.upwarp);
  449. // 滚动监听
  450. me.preScrollY = 0;
  451. me.lazyStartTime = new Date().getTime();// 懒加载的初始间隔时间
  452. me.lazyTag = 'mescroll-lazying';// 懒加载时,图片正在加载的标记
  453. me.scrollEvent = function () {
  454. // 列表内容顶部卷去的高度(含列表边框)
  455. var scrollTop = me.getScrollTop();
  456. // 向上滑还是向下滑动
  457. var isUp = scrollTop - me.preScrollY > 0;
  458. me.preScrollY = scrollTop;
  459. // 如果没有在加载中
  460. if (!me.isUpScrolling && (!me.isDownScrolling || (me.isDownScrolling && me.optDown.isBoth))) {
  461. // offsetHeight 列表高度(内容+内边距+边框),滚动条在边框之内,所以使用clientHeight即可
  462. // clientHeight 列表高度(内容+内边距),不含列表边框
  463. // scrollHeight 列表内容撑开的高度
  464. if (!me.optUp.isLock && me.optUp.hasNext) {
  465. var toBottom = me.getScrollHeight() - me.getClientHeight() - scrollTop; // 滚动条距离底部的距离
  466. if (toBottom <= me.optUp.offset && isUp) {
  467. // 如果滚动条距离底部指定范围内且向上滑,则执行上拉加载回调
  468. me.triggerUpScroll();
  469. }
  470. }
  471. }
  472. // 顶部按钮的显示隐藏
  473. var optTop = me.optUp.toTop;
  474. if (optTop.src || optTop.html) {
  475. if (scrollTop >= optTop.offset) {
  476. me.showTopBtn();
  477. } else {
  478. me.hideTopBtn();
  479. }
  480. }
  481. // 懒加载
  482. if (me.optUp.lazyLoad.use) {
  483. // 节流:限制触发时间间隔
  484. var curTime = new Date().getTime();
  485. me.lazyTimer && clearTimeout(me.lazyTimer);
  486. if (curTime - me.lazyStartTime >= me.optUp.lazyLoad.delay) {
  487. me.lazyStartTime = curTime;
  488. me.lazyLoad(0);// 列表刚滚动的时候,懒加载一次
  489. } else {
  490. me.lazyTimer = me.lazyLoad();
  491. }
  492. }
  493. // 滑动监听
  494. me.optUp.onScroll && me.optUp.onScroll(me, scrollTop, isUp);
  495. }
  496. if (me.isScrollBody) {
  497. window.addEventListener('scroll', me.scrollEvent);
  498. } else {
  499. me.scrollDom.addEventListener('scroll', me.scrollEvent);
  500. }
  501. // 初始化完毕的回调
  502. setTimeout(function () { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
  503. me.optUp.inited(me, me.upwarp);
  504. }, 0)
  505. }
  506. /* 是否允许ios的Bounce; true或null为允许; false禁止bounce */
  507. MeScroll.prototype.setBounce = function (isBounce) {
  508. if (this.isScrollBody || !this.os.ios) return; // 不支持body为滚动区域和非ios设备
  509. if (isBounce === false) {
  510. this.optUp.isBounce = false; // 禁止
  511. window.addEventListener('touchmove', this.bounceTouchmove, {
  512. passive: false
  513. });
  514. } else {
  515. this.optUp.isBounce = true; // 允许
  516. window.removeEventListener('touchmove', this.bounceTouchmove);
  517. }
  518. }
  519. /* 处理bounce的touchmove事件 */
  520. MeScroll.prototype.bounceTouchmove = function (e) {
  521. var me = this;
  522. var el = e.target;
  523. // 当前touch的元素及父元素是否要拦截touchmove事件
  524. var isPrevent = true;
  525. while (el !== document.body && el !== document) {
  526. var cls = el.classList;
  527. if (cls) {
  528. if (cls.contains('mescroll') || cls.contains('mescroll-touch')) {
  529. isPrevent = false; // 如果是指定条件的元素,则无需拦截touchmove事件
  530. break;
  531. } else if (cls.contains('mescroll-touch-x') || cls.contains('mescroll-touch-y')) {
  532. // 如果配置了水平或者垂直滑动
  533. var curX = e.touches ? e.touches[0].pageX : e.clientX; // 当前第一个手指距离列表顶部的距离x
  534. var curY = e.touches ? e.touches[0].pageY : e.clientY; // 当前第一个手指距离列表顶部的距离y
  535. if (!me.preWinX) me.preWinX = curX; // 设置上次移动的距离x
  536. if (!me.preWinY) me.preWinY = curY; // 设置上次移动的距离y
  537. // 计算两点之间的角度
  538. var x = Math.abs(me.preWinX - curX);
  539. var y = Math.abs(me.preWinY - curY);
  540. var z = Math.sqrt(x * x + y * y);
  541. me.preWinX = curX; // 记录本次curX的值
  542. me.preWinY = curY; // 记录本次curY的值
  543. if (z !== 0) {
  544. var angle = Math.asin(y / z) / Math.PI * 180; // 角度区间 [0,90]
  545. if ((angle <= 45 && cls.contains('mescroll-touch-x')) || (angle > 45 && cls.contains('mescroll-touch-y'))) {
  546. isPrevent = false; // 水平滑动或者垂直滑动,不拦截touchmove事件
  547. break;
  548. }
  549. }
  550. }
  551. }
  552. el = el.parentNode; // 继续检查其父元素
  553. }
  554. // 拦截touchmove事件:是否可以被禁用&&是否已经被禁用 (这里不使用me.preventDefault(e)的方法,因为某些情况下会报找不到方法的异常)
  555. if (isPrevent && e.cancelable && !e.defaultPrevented && typeof e.preventDefault === "function") e.preventDefault();
  556. }
  557. /* 触发上拉加载 */
  558. MeScroll.prototype.triggerUpScroll = function () {
  559. if (this.optUp.callback && !this.isUpScrolling) {
  560. this.showUpScroll(); // 上拉加载中...
  561. this.optUp.page.num++; // 预先加一页,如果失败则减回
  562. this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
  563. this.optUp.callback(this.optUp.page, this); // 执行回调,联网加载数据
  564. }
  565. }
  566. /* 显示上拉加载中 */
  567. MeScroll.prototype.showUpScroll = function () {
  568. this.isUpScrolling = true; // 标记上拉加载中
  569. this.upwarp.classList.add(this.optUp.hardwareClass); // 添加硬件加速样式,使动画更流畅
  570. this.upwarp.style.visibility = 'visible'; // 显示上拉加载区域
  571. this.upwarp.style.display = 'block'; // 显示上拉加载区域
  572. this.optUp.showLoading(this, this.upwarp); // 加载中...
  573. }
  574. /* 显示上拉无更多数据 */
  575. MeScroll.prototype.showNoMore = function () {
  576. this.upwarp.style.visibility = 'visible'; // 显示上拉加载区域
  577. this.upwarp.style.display = 'block'; // 显示上拉加载区域
  578. this.optUp.hasNext = false; // 无更多数据
  579. this.optUp.showNoMore(this, this.upwarp); // 无更多数据
  580. }
  581. /* 隐藏上拉区域. displayAble: 是否通过display:none隐藏, 默认false通过visibility:hidden的方式隐藏**/
  582. MeScroll.prototype.hideUpScroll = function (displayAble) {
  583. if (displayAble) {
  584. this.upwarp.style.display = 'none'; // 通过display:none隐藏: 优点隐藏后不占位,缺点列表快速滑动到底部不能及时显示加载中
  585. } else {
  586. this.upwarp.style.visibility = 'hidden'; // 通过visibility:hidden的方式隐藏,优点当列表快速滑动到底部能及时显示加载中,缺点隐藏后会占位
  587. }
  588. this.upwarp.classList.remove(this.optUp.hardwareClass); // 移除硬件加速样式
  589. var upProgressDom = this.upwarp.getElementsByClassName('upwarp-progress')[0];
  590. if (upProgressDom) upProgressDom.classList.remove('mescroll-rotate');
  591. }
  592. /* 结束上拉加载 */
  593. MeScroll.prototype.endUpScroll = function (isShowNoMore, displayAble) {
  594. if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
  595. if (isShowNoMore) {
  596. this.showNoMore(); // isShowNoMore=true,显示无更多数据
  597. } else {
  598. this.hideUpScroll(displayAble); // isShowNoMore=false,隐藏上拉加载
  599. }
  600. }
  601. this.isUpScrolling = false; // 标记结束上拉加载
  602. }
  603. /* 重置上拉加载列表为第一页
  604. *isShowLoading 是否显示进度布局;
  605. * 1.默认null,不传参,则显示上拉加载的进度布局
  606. * 2.传参true, 则显示下拉刷新的进度布局
  607. * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
  608. */
  609. MeScroll.prototype.resetUpScroll = function (isShowLoading) {
  610. if (this.optUp && this.optUp.use) {
  611. var page = this.optUp.page;
  612. this.prePageNum = page.num; // 缓存重置前的页码,加载失败可退回
  613. this.prePageTime = page.time; // 缓存重置前的时间,加载失败可退回
  614. page.num = 1; // 重置为第一页
  615. page.time = null; // 重置时间为空
  616. if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
  617. if (isShowLoading == null) {
  618. this.removeEmpty(); // 移除空布局
  619. this.clearDataList(); // 先清空列表数据,才能显示到上拉加载的布局
  620. this.showUpScroll(); // 不传参,默认显示上拉加载的进度布局
  621. } else {
  622. this.showDownScroll(); // 传true,显示下拉刷新的进度布局,不清空列表
  623. }
  624. }
  625. this.isUpAutoLoad = true; // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
  626. this.optUp.callback && this.optUp.callback(page, this); // 执行上拉回调
  627. }
  628. }
  629. /* 设置page.num的值 */
  630. MeScroll.prototype.setPageNum = function (num) {
  631. this.optUp.page.num = num - 1;
  632. }
  633. /* 设置page.size的值 */
  634. MeScroll.prototype.setPageSize = function (size) {
  635. this.optUp.page.size = size;
  636. }
  637. /* 清空上拉加载的数据列表 */
  638. MeScroll.prototype.clearDataList = function () {
  639. var listId = this.optUp.clearId || this.optUp.clearEmptyId; // 优先使用clearId
  640. if (listId) {
  641. var listDom = this.getDomById(listId);
  642. if (listDom) listDom.innerHTML = '';
  643. }
  644. }
  645. /* 联网回调成功,结束下拉刷新和上拉加载
  646. * dataSize: 当前页的数据量(必传)
  647. * totalPage: 总页数(必传)
  648. * systime: 服务器时间 (可空)
  649. */
  650. MeScroll.prototype.endByPage = function (dataSize, totalPage, systime) {
  651. var hasNext;
  652. if (this.optUp.use && totalPage != null) hasNext = this.optUp.page.num < totalPage; // 是否还有下一页
  653. this.endSuccess(dataSize, hasNext, systime);
  654. }
  655. /* 联网回调成功,结束下拉刷新和上拉加载
  656. * dataSize: 当前页的数据量(必传)
  657. * totalSize: 列表所有数据总数量(必传)
  658. * systime: 服务器时间 (可空)
  659. */
  660. MeScroll.prototype.endBySize = function (dataSize, totalSize, systime) {
  661. var hasNext;
  662. if (this.optUp.use && totalSize != null) {
  663. var loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize; // 已加载的数据总数
  664. hasNext = loadSize < totalSize; // 是否还有下一页
  665. }
  666. this.endSuccess(dataSize, hasNext, systime);
  667. }
  668. /* 联网回调成功,结束下拉刷新和上拉加载
  669. * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
  670. * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
  671. * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
  672. */
  673. MeScroll.prototype.endSuccess = function (dataSize, hasNext, systime) {
  674. var me = this;
  675. // 结束下拉刷新
  676. if (me.isDownScrolling) me.endDownScroll();
  677. // 结束上拉加载
  678. if (me.optUp.use) {
  679. var isShowNoMore; // 是否已无更多数据
  680. if (dataSize != null) {
  681. var pageNum = me.optUp.page.num; // 当前页码
  682. var pageSize = me.optUp.page.size; // 每页长度
  683. // 如果是第一页
  684. if (pageNum === 1) {
  685. me.clearDataList(); // 自动清空第一页列表数据
  686. if (systime) me.optUp.page.time = systime; // 设置加载列表数据第一页的时间
  687. }
  688. if (dataSize < pageSize || hasNext === false) {
  689. // 返回的数据不满一页时,则说明已无更多数据
  690. me.optUp.hasNext = false;
  691. if (dataSize === 0 && pageNum === 1) {
  692. // 如果第一页无任何数据且配置了空布局
  693. isShowNoMore = false;
  694. me.showEmpty();
  695. } else {
  696. // 总列表数少于配置的数量,则不显示无更多数据
  697. var allDataSize = (pageNum - 1) * pageSize + dataSize;
  698. if (allDataSize < me.optUp.noMoreSize) {
  699. isShowNoMore = false;
  700. } else {
  701. isShowNoMore = true;
  702. }
  703. me.removeEmpty(); // 移除空布局
  704. }
  705. } else {
  706. // 还有下一页
  707. isShowNoMore = false;
  708. me.optUp.hasNext = true;
  709. me.removeEmpty(); // 移除空布局
  710. }
  711. }
  712. // 隐藏上拉
  713. var displayAble = !me.optUp.hasNext; // 没有下一页且少于noMoreSize,则以display:none的方式隐藏上拉布局
  714. me.endUpScroll(isShowNoMore, displayAble);
  715. // 检查是否满屏自动加载下一页
  716. me.loadFull();
  717. // 懒加载
  718. me.optUp.lazyLoad.use && me.lazyLoad(20);
  719. }
  720. }
  721. /* 回调失败,结束下拉刷新和上拉加载 */
  722. MeScroll.prototype.endErr = function () {
  723. // 结束下拉,回调失败重置回原来的页码和时间
  724. if (this.isDownScrolling) {
  725. var page = this.optUp.page;
  726. if (page && this.prePageNum) {
  727. page.num = this.prePageNum;
  728. page.time = this.prePageTime;
  729. }
  730. this.endDownScroll();
  731. }
  732. // 结束上拉,回调失败重置回原来的页码
  733. if (this.isUpScrolling) {
  734. this.optUp.page.num--;
  735. this.endUpScroll(false);
  736. }
  737. }
  738. /* 检查如果加载的数据过少,无法触发上拉加载时,则自动加载下一页,直到满屏或者没有更多数据
  739. 此方法最好在列表的数据加载完成之后调用,以便计算列表内容高度的准确性 */
  740. MeScroll.prototype.loadFull = function () {
  741. var me = this;
  742. if (me.optUp.loadFull.use && !me.optUp.isLock && me.optUp.hasNext && me.optUp.callback && me.getScrollHeight() <= me.getClientHeight()) {
  743. setTimeout(function () {
  744. // 延时之后,还需再判断一下高度,因为可能有些图片在延时期间加载完毕撑开高度
  745. if (me.getScrollHeight() <= me.getClientHeight()) me.triggerUpScroll();
  746. }, me.optUp.loadFull.delay)
  747. }
  748. }
  749. /* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
  750. MeScroll.prototype.lockUpScroll = function (isLock) {
  751. if (isLock == null) isLock = true;
  752. this.optUp.isLock = isLock;
  753. }
  754. /* --------无任何数据的空布局-------- */
  755. MeScroll.prototype.showEmpty = function () {
  756. var me = this;
  757. var optEmpty = me.optUp.empty; // 空布局的配置
  758. if (optEmpty.customId) {
  759. // 如果是显示自定义内容 && 该customId元素存在
  760. me.emptyDom = me.getDomById(optEmpty.customId);
  761. if(me.emptyDom){
  762. me.emptyDom.style.display = 'block';
  763. return;
  764. }
  765. }
  766. var warpId = optEmpty.warpId || me.optUp.clearEmptyId; // 优先使用warpId
  767. if (warpId == null) return;
  768. var emptyWarp = me.getDomById(warpId) // 要显示空布局的位置
  769. if (emptyWarp) {
  770. me.removeEmpty(); // 先移除,避免重复加入
  771. // 初始化无任何数据的空布局
  772. var str = '';
  773. if (optEmpty.icon) str += '<img class="empty-icon" src="' + optEmpty.icon + '"/>'; // 图标
  774. if (optEmpty.tip) str += '<p class="empty-tip">' + optEmpty.tip + '</p>'; // 提示
  775. if (optEmpty.btntext) str += '<p class="empty-btn">' + optEmpty.btntext + '</p>'; // 按钮
  776. me.emptyDom = document.createElement('div');
  777. me.emptyDom.className = 'mescroll-empty';
  778. me.emptyDom.innerHTML = str;
  779. emptyWarp.appendChild(me.emptyDom);
  780. if (optEmpty.btnClick) { // 点击按钮的回调
  781. var emptyBtn = me.emptyDom.getElementsByClassName('empty-btn')[0];
  782. if (optEmpty.supportTap) {
  783. emptyBtn.addEventListener('tap', function (e) {
  784. e.stopPropagation();
  785. me.preventDefault(e)
  786. optEmpty.btnClick();
  787. })
  788. } else {
  789. emptyBtn.onclick = function () {
  790. optEmpty.btnClick();
  791. }
  792. }
  793. }
  794. }
  795. }
  796. /* 移除空布局 */
  797. MeScroll.prototype.removeEmpty = function () {
  798. if (this.optUp.empty.customId && this.emptyDom) {
  799. this.emptyDom.style.display = 'none' // 隐藏自定义空布局
  800. } else {
  801. this.removeChild(this.emptyDom);
  802. }
  803. }
  804. /* --------回到顶部的按钮-------- */
  805. MeScroll.prototype.showTopBtn = function (fadeDuration) {
  806. if (!this.topBtnShow) {
  807. this.topBtnShow = true; // 标记显示
  808. var me = this;
  809. var optTop = me.optUp.toTop; // 回到顶部的配置
  810. if (me.toTopBtn == null) {
  811. // 未加入按钮,则加入
  812. if (optTop.html) {
  813. me.toTopBtn = document.createElement('div');
  814. me.toTopBtn.innerHTML = optTop.html;
  815. } else {
  816. me.toTopBtn = document.createElement('img');
  817. me.toTopBtn.src = optTop.src;
  818. }
  819. me.toTopBtn.className = optTop.warpClass;
  820. if (optTop.supportTap) {
  821. me.toTopBtn.addEventListener('tap', function (e) {
  822. e.stopPropagation();
  823. me.preventDefault(e);
  824. var disToTop = optTop.btnClick && optTop.btnClick(); // 执行回调
  825. if (disToTop !== true) { // 如果回调里return true,将不执行回到顶部操作
  826. me.scrollTo(0, me.optUp.toTop.duration); // 置顶
  827. }
  828. })
  829. } else {
  830. me.toTopBtn.onclick = function () {
  831. var disToTop = optTop.btnClick && optTop.btnClick(); // 执行回调
  832. if (disToTop !== true) { // 如果回调里return true,将不执行回到顶部操作
  833. me.scrollTo(0, me.optUp.toTop.duration); // 置顶
  834. }
  835. }
  836. }
  837. var warpDom; // 是否配置父布局
  838. if (optTop.warpId) warpDom = me.getDomById(optTop.warpId);
  839. if (!warpDom) warpDom = document.body;
  840. warpDom.appendChild(me.toTopBtn);
  841. }
  842. // 显示--淡入动画
  843. me.toTopBtn.classList.remove(optTop.hideClass);
  844. me.toTopBtn.classList.add(optTop.showClass);
  845. me.setTopBtnFadeDuration(fadeDuration);
  846. }
  847. }
  848. /* 隐藏回到顶部的按钮 */
  849. MeScroll.prototype.hideTopBtn = function (fadeDuration) {
  850. if (this.topBtnShow && this.toTopBtn) {
  851. this.topBtnShow = false;
  852. this.toTopBtn.classList.remove(this.optUp.toTop.showClass);
  853. this.toTopBtn.classList.add(this.optUp.toTop.hideClass);
  854. this.setTopBtnFadeDuration(fadeDuration);
  855. }
  856. }
  857. /* 设置回到顶部按钮的显示隐藏动画时长,默认0.5秒 */
  858. MeScroll.prototype.setTopBtnFadeDuration = function (fadeDuration) {
  859. if (this.toTopBtn) {
  860. var duration = (fadeDuration != null ? fadeDuration : this.optUp.toTop.fadeDuration) + 's';
  861. this.toTopBtn.style.animationDuration = duration;
  862. this.toTopBtn.style.webkitAnimationDuration = duration;
  863. }
  864. }
  865. /* 滑动列表到指定位置--带缓冲效果 (y=0回到顶部;如果要滚动到底部可以传一个较大的值,比如99999);t时长,单位ms,默认300 */
  866. MeScroll.prototype.scrollTo = function (y, t) {
  867. var me = this;
  868. var star = me.getScrollTop();
  869. var end = y;
  870. if (end > 0) {
  871. var maxY = me.getScrollHeight() - me.getClientHeight(); // y的最大值
  872. if (end > maxY) end = maxY; // 不可超过最大值
  873. } else {
  874. end = 0; // 不可小于0
  875. }
  876. me.isScrollTo = true; // 标记在滑动中,阻止列表的触摸事件
  877. me.scrollDom.style.webkitOverflowScrolling = 'auto';
  878. me.scrollDom.style.overflow = "hidden"; // 避免iOS惯性滚动的影响
  879. me.getStep(star, end, function (step) {
  880. me.setScrollTop(step);
  881. if (step === end) {
  882. me.scrollDom.style.webkitOverflowScrolling = 'touch';
  883. me.scrollDom.style.overflow = "auto";
  884. me.isScrollTo = false;
  885. }
  886. }, t)
  887. }
  888. /* 计步器
  889. star: 开始值
  890. end: 结束值
  891. callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
  892. t: 计步时长,传0则直接回调end值;不传则默认300ms
  893. rate: 周期;不传则默认30ms计步一次
  894. * */
  895. MeScroll.prototype.getStep = function (star, end, callback, t, rate) {
  896. var diff = end - star; // 差值
  897. if (t === 0 || diff === 0) {
  898. callback && callback(end);
  899. return;
  900. }
  901. t = t || 300; // 时长 300ms
  902. rate = rate || 30; // 周期 30ms
  903. var count = t / rate; // 次数
  904. var step = diff / count; // 步长
  905. var i = 0; // 计数
  906. var timer = window.setInterval(function () {
  907. if (i < count - 1) {
  908. star += step;
  909. callback && callback(star, timer);
  910. i++;
  911. } else {
  912. callback && callback(end, timer); // 最后一次直接设置end,避免计算误差
  913. window.clearInterval(timer);
  914. }
  915. }, rate);
  916. }
  917. /* 加载可视区域的图片 */
  918. MeScroll.prototype.lazyLoad = function (delay) {
  919. var me = this;
  920. var t = delay != null ? delay : me.optUp.lazyLoad.delay; // delay需支持传0,不使用短路求值
  921. var timer = setTimeout(function () {
  922. var domArr = me.scrollDom.querySelectorAll('[' + me.optUp.lazyLoad.attr + ']');
  923. var len = domArr.length;
  924. for (var i = 0; i < len; i++) {
  925. var dom = domArr[i];
  926. if (dom.getAttribute(me.lazyTag) !== 'true' && me.isInSee(dom, me.optUp.lazyLoad.offset)) {
  927. var imgurl = dom.getAttribute(me.optUp.lazyLoad.attr);
  928. // 采用临时img标签加载网络图: 1.不影响占位图; 2.成功可设置渐变动画; 3.失败后可重新触发加载;
  929. var temp = new Image();
  930. temp.onload = function () {
  931. var imgurl = this.src;// 需通过this取值
  932. var dom = this.dom;
  933. var showClass = me.optUp.lazyLoad.showClass;// 渐变动画
  934. showClass && dom.classList.add(showClass);
  935. if (dom.tagName === 'IMG') {
  936. dom.src = imgurl;// 如果是img标签,则直接设置src
  937. } else {
  938. dom.style.backgroundImage = 'url(' + imgurl + ')';// 如果其他标签,则直接设置背景
  939. }
  940. dom.removeAttribute(me.optUp.lazyLoad.attr);
  941. dom.removeAttribute(me.lazyTag);
  942. }
  943. temp.onerror = function () {
  944. this.dom.removeAttribute(me.lazyTag);// 失败的时候取消加载中的标记
  945. }
  946. temp.onabort = function () {
  947. this.dom.removeAttribute(me.lazyTag);// 失败的时候取消加载中的标记
  948. }
  949. temp.src = imgurl;
  950. // 标记在加载中..
  951. dom.setAttribute(me.lazyTag, 'true');
  952. // 把dom挂载到temp,确保在for循环的temp.onload能够正确取到对应的dom
  953. temp.dom = dom;
  954. }
  955. }
  956. }, t)
  957. return timer;
  958. }
  959. /* 判断元素是否在列表垂直区域可视 (不考虑scrollLeft的情况,因为没法监听每个div的水平滚动事件,也不考虑translateY的情况,因为情况比较少,且会增加计算的复杂度) */
  960. MeScroll.prototype.isInSee = function (dom, offset) {
  961. offset = offset || 0; // 可视区域上下偏移的距离
  962. var topDom = this.getOffsetTop(dom);// 元素顶部到容器顶部的距离
  963. var topSee = this.getScrollTop() - offset;// 滚动条的位置(可视范围的顶部)
  964. var bottomDom = topDom + dom.offsetHeight;// 元素底部到容器顶部的距离
  965. var bottomSee = topSee + offset + this.getClientHeight() + offset;// 滚动条的位置+容器高度(可视范围的底部)
  966. // 图片顶部在可视范围内 || 图片底部在可视范围 ; 不考虑scrollLeft和translateY的情况
  967. return (topDom < bottomSee && topDom >= topSee) || (bottomDom <= bottomSee && bottomDom > topSee);
  968. }
  969. /* 获取元素到mescroll滚动列表顶部的距离 */
  970. MeScroll.prototype.getOffsetTop = function (dom) {
  971. var top = dom.offsetTop;
  972. var parent = dom.offsetParent;
  973. while (parent != null && parent !== this.scrollDom) {
  974. top += parent.offsetTop + parent.clientTop;
  975. parent = parent.offsetParent;
  976. }
  977. return top;
  978. }
  979. /* 滚动内容的高度 */
  980. MeScroll.prototype.getScrollHeight = function () {
  981. return this.scrollDom.scrollHeight;
  982. }
  983. /* 滚动容器的高度 */
  984. MeScroll.prototype.getClientHeight = function () {
  985. if (this.isScrollBody && document.compatMode === 'CSS1Compat') {
  986. return document.documentElement.clientHeight;
  987. } else {
  988. return this.scrollDom.clientHeight;
  989. }
  990. }
  991. /* body的高度 */
  992. MeScroll.prototype.getBodyHeight = function () {
  993. return document.body.clientHeight || document.documentElement.clientHeight;
  994. }
  995. /* 滚动条的位置 */
  996. MeScroll.prototype.getScrollTop = function () {
  997. if (this.isScrollBody) {
  998. return document.documentElement.scrollTop || document.body.scrollTop;
  999. } else {
  1000. return this.scrollDom.scrollTop;
  1001. }
  1002. }
  1003. /* 滚动条到底部的距离 */
  1004. MeScroll.prototype.getToBottom = function () {
  1005. return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop();
  1006. }
  1007. /* 设置滚动条的位置 */
  1008. MeScroll.prototype.setScrollTop = function (y) {
  1009. if (typeof y === 'number') {
  1010. if (this.isScrollBody) {
  1011. document.documentElement.scrollTop = y;
  1012. document.body.scrollTop = y;
  1013. } else {
  1014. this.scrollDom.scrollTop = y;
  1015. }
  1016. }
  1017. }
  1018. /* 查找dom元素 */
  1019. MeScroll.prototype.getDomById = function (id) {
  1020. var dom;
  1021. if (id) {
  1022. if (typeof id === 'string') {
  1023. dom = document.getElementById(id); // 如果是String,则根据id查找
  1024. } else if (id.nodeType) {
  1025. dom = id; // 如果是dom对象,则直接赋值
  1026. }
  1027. }
  1028. if (!dom) console.error('the element with id as "' + id + '" can not be found: document.getElementById("' + id + '")==null');
  1029. return dom;
  1030. }
  1031. /* 删除dom元素 */
  1032. MeScroll.prototype.removeChild = function (dom) {
  1033. if (dom) {
  1034. var parent = dom.parentNode;
  1035. parent && parent.removeChild(dom);
  1036. dom = null;
  1037. }
  1038. }
  1039. /* 销毁mescroll */
  1040. MeScroll.prototype.destroy = function () {
  1041. var me = this;
  1042. // 移除下拉布局,移除事件
  1043. me.scrollDom.removeEventListener('touchstart', me.touchstartEvent); // 移动端手指事件
  1044. me.scrollDom.removeEventListener('touchmove', me.touchmoveEvent); // 移动端手指事件
  1045. me.scrollDom.removeEventListener('touchend', me.touchendEvent); // 移动端手指事件
  1046. me.scrollDom.removeEventListener('touchcancel', me.touchendEvent); // 移动端手指事件
  1047. me.scrollDom.removeEventListener('mousedown', me.touchstartEvent); // PC端鼠标事件
  1048. me.scrollDom.removeEventListener('mousemove', me.touchmoveEvent); // PC端鼠标事件
  1049. me.scrollDom.removeEventListener('mouseup', me.touchendEvent); // PC端鼠标抬起事件
  1050. me.scrollDom.removeEventListener('mouseleave', me.touchendEvent); // PC端鼠标离开事件
  1051. me.removeChild(me.downwarp); // 下拉布局
  1052. // 移除上拉布局,回到顶部按钮,移除事件
  1053. if (me.isScrollBody) {
  1054. window.removeEventListener('scroll', me.scrollEvent); // window的滚动事件
  1055. } else {
  1056. me.scrollDom.removeEventListener('scroll', me.scrollEvent); // div的滚动事件
  1057. }
  1058. me.removeChild(me.upwarp); // 上拉布局
  1059. me.removeChild(me.toTopBtn); // 回到顶部按钮
  1060. // 允许回弹,解除禁止webview的touchmove事件
  1061. me.setBounce(true);
  1062. }
  1063. return MeScroll;
  1064. });