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