scroller.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. // [z-paging]scroll相关模块
  2. import u from '.././z-paging-utils'
  3. import Enum from '.././z-paging-enum'
  4. // #ifdef APP-NVUE
  5. const weexDom = weex.requireModule('dom');
  6. // #endif
  7. export default {
  8. props: {
  9. //使用页面滚动,默认为否,当设置为是时则使用页面的滚动而非此组件内部的scroll-view的滚动,使用页面滚动时z-paging无需设置确定的高度且对于长列表展示性能更高,但配置会略微繁琐
  10. usePageScroll: {
  11. type: Boolean,
  12. default: u.gc('usePageScroll', false)
  13. },
  14. //是否可以滚动,使用内置scroll-view和nvue时有效,默认为是
  15. scrollable: {
  16. type: Boolean,
  17. default: u.gc('scrollable', true)
  18. },
  19. //控制是否出现滚动条,默认为是
  20. showScrollbar: {
  21. type: Boolean,
  22. default: u.gc('showScrollbar', true)
  23. },
  24. //是否允许横向滚动,默认为否
  25. scrollX: {
  26. type: Boolean,
  27. default: u.gc('scrollX', false)
  28. },
  29. //iOS设备上滚动到顶部时是否允许回弹效果,默认为否。关闭回弹效果后可使滚动到顶部与下拉刷新更连贯,但是有吸顶view时滚动到顶部时可能出现抖动。
  30. scrollToTopBounceEnabled: {
  31. type: Boolean,
  32. default: u.gc('scrollToTopBounceEnabled', false)
  33. },
  34. //iOS设备上滚动到底部时是否允许回弹效果,默认为是。
  35. scrollToBottomBounceEnabled: {
  36. type: Boolean,
  37. default: u.gc('scrollToBottomBounceEnabled', true)
  38. },
  39. //在设置滚动条位置时使用动画过渡,默认为否
  40. scrollWithAnimation: {
  41. type: Boolean,
  42. default: u.gc('scrollWithAnimation', false)
  43. },
  44. //值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素
  45. scrollIntoView: {
  46. type: String,
  47. default: u.gc('scrollIntoView', '')
  48. },
  49. },
  50. data() {
  51. return {
  52. scrollTop: 0,
  53. oldScrollTop: 0,
  54. scrollViewStyle: {},
  55. scrollViewContainerStyle: {},
  56. scrollViewInStyle: {},
  57. pageScrollTop: -1,
  58. scrollEnable: true,
  59. privateScrollWithAnimation: -1,
  60. cacheScrollNodeHeight: -1
  61. }
  62. },
  63. watch: {
  64. oldScrollTop(newVal) {
  65. !this.usePageScroll && this._scrollTopChange(newVal,false);
  66. },
  67. pageScrollTop(newVal) {
  68. this.usePageScroll && this._scrollTopChange(newVal,true);
  69. },
  70. usePageScroll: {
  71. handler(newVal) {
  72. this.loaded && this.autoHeight && this._setAutoHeight(!newVal)
  73. // #ifdef H5
  74. if (newVal) {
  75. this.$nextTick(()=>{
  76. const mainScrollRef = this.$refs['zp-scroll-view'].$refs.main;
  77. if (mainScrollRef) {
  78. mainScrollRef.style = {};
  79. }
  80. })
  81. }
  82. // #endif
  83. },
  84. immediate: true
  85. },
  86. finalScrollTop(newVal) {
  87. if (!this.useChatRecordMode) {
  88. this.renderPropScrollTop = newVal < 6 ? 0 : 10;
  89. }
  90. },
  91. },
  92. computed: {
  93. finalScrollWithAnimation() {
  94. if (this.privateScrollWithAnimation !== -1) {
  95. const scrollWithAnimation = this.privateScrollWithAnimation === 1;
  96. this.privateScrollWithAnimation = -1;
  97. return scrollWithAnimation;
  98. }
  99. return this.scrollWithAnimation;
  100. },
  101. finalScrollViewStyle() {
  102. if (this.superContentZIndex != 1) {
  103. this.scrollViewStyle['z-index'] = this.superContentZIndex;
  104. this.scrollViewStyle['position'] = 'relative';
  105. }
  106. return this.scrollViewStyle;
  107. },
  108. finalScrollTop() {
  109. return this.usePageScroll ? this.pageScrollTop : this.oldScrollTop;
  110. },
  111. finalIsOldWebView() {
  112. return this.isOldWebView && !this.usePageScroll;
  113. }
  114. },
  115. methods: {
  116. //滚动到顶部,animate为是否展示滚动动画,默认为是
  117. scrollToTop(animate, checkReverse = true) {
  118. // #ifdef APP-NVUE
  119. if (checkReverse && this.useChatRecordMode) {
  120. if(!this.nIsFirstPageAndNoMore){
  121. this.scrollToBottom(animate, false);
  122. return;
  123. }
  124. }
  125. // #endif
  126. this.$nextTick(() => {
  127. this._scrollToTop(animate, false);
  128. // #ifdef APP-NVUE
  129. if (this.nvueFastScroll && animate) {
  130. setTimeout(() => {
  131. this._scrollToTop(false, false);
  132. }, 150);
  133. }
  134. // #endif
  135. })
  136. },
  137. //滚动到底部,animate为是否展示滚动动画,默认为是
  138. scrollToBottom(animate, checkReverse = true) {
  139. // #ifdef APP-NVUE
  140. if (checkReverse && this.useChatRecordMode) {
  141. if(!this.nIsFirstPageAndNoMore){
  142. this.scrollToTop(animate, false);
  143. return;
  144. }
  145. }
  146. // #endif
  147. this.$nextTick(() => {
  148. this._scrollToBottom(animate);
  149. // #ifdef APP-NVUE
  150. if (this.nvueFastScroll && animate) {
  151. setTimeout(() => {
  152. this._scrollToBottom(false);
  153. }, 150);
  154. }
  155. // #endif
  156. })
  157. },
  158. //滚动到指定view(vue中有效)。sel为需要滚动的view的id值,不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
  159. scrollIntoViewById(sel, offset, animate) {
  160. this._scrollIntoView(sel, offset, animate);
  161. },
  162. //滚动到指定view(vue中有效)。nodeTop为需要滚动的view的top值(通过uni.createSelectorQuery()获取);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
  163. scrollIntoViewByNodeTop(nodeTop, offset, animate) {
  164. this.scrollTop = this.oldScrollTop;
  165. this.$nextTick(() => {
  166. this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
  167. })
  168. },
  169. //滚动到指定位置(vue中有效)。y为与顶部的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
  170. scrollToY(y, offset, animate) {
  171. this.scrollTop = this.oldScrollTop;
  172. this.$nextTick(() => {
  173. this._scrollToY(y, offset, animate);
  174. })
  175. },
  176. //滚动到指定view(nvue中有效)。index为需要滚动的view的index(第几个);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
  177. scrollIntoViewByIndex(index, offset, animate) {
  178. this._scrollIntoView(index, offset, animate);
  179. },
  180. //滚动到指定view(nvue中有效)。view为需要滚动的view(通过`this.$refs.xxx`获取),不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否
  181. scrollIntoViewByView(view, offset, animate) {
  182. this._scrollIntoView(view, offset, animate);
  183. },
  184. //当使用页面滚动并且自定义下拉刷新时,请在页面的onPageScroll中调用此方法,告知z-paging当前的pageScrollTop,否则会导致在任意位置都可以下拉刷新
  185. updatePageScrollTop(value) {
  186. this.pageScrollTop = value;
  187. },
  188. //当使用页面滚动并且设置了slot="top"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="top"的view高度动态改变时,在其高度需要更新时调用此方法
  189. updatePageScrollTopHeight() {
  190. this._updatePageScrollTopOrBottomHeight('top');
  191. },
  192. //当使用页面滚动并且设置了slot="bottom"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="bottom"的view高度动态改变时,在其高度需要更新时调用此方法
  193. updatePageScrollBottomHeight() {
  194. this._updatePageScrollTopOrBottomHeight('bottom');
  195. },
  196. //更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用
  197. updateLeftAndRightWidth() {
  198. this.$nextTick(() => {
  199. this._updateLeftAndRightWidth();
  200. })
  201. },
  202. //更新z-paging内置scroll-view的scrollTop
  203. updateScrollViewScrollTop(scrollTop, animate = true) {
  204. this.privateScrollWithAnimation = animate ? 1 : 0;
  205. this.scrollTop = this.oldScrollTop;
  206. this.$nextTick(() => {
  207. this.scrollTop = scrollTop;
  208. this.oldScrollTop = this.scrollTop;
  209. });
  210. },
  211. //当滚动到顶部时
  212. _scrollToUpper() {
  213. this.$emit('scrolltoupper');
  214. this.$emit('scrollTopChange', 0);
  215. this.$nextTick(() => {
  216. this.oldScrollTop = 0;
  217. })
  218. if (!this.useChatRecordMode) return;
  219. if (this.loadingStatus === Enum.More.NoMore) return;
  220. this._onLoadingMore('click');
  221. },
  222. //滚动到顶部
  223. _scrollToTop(animate = true, isPrivate = true) {
  224. // #ifdef APP-NVUE
  225. const el = this.$refs['zp-n-list-top-tag'];
  226. if (this.usePageScroll) {
  227. this._getNodeClientRect('zp-page-scroll-top', false).then((node) => {
  228. const nodeHeight = node ? node[0].height : 0;
  229. weexDom.scrollToElement(el, {
  230. offset: -nodeHeight,
  231. animated: animate
  232. });
  233. });
  234. } else {
  235. if (!this.isIos && this.nvueListIs === 'scroller') {
  236. this._getNodeClientRect('zp-n-refresh-container', false).then((node) => {
  237. const nodeHeight = node ? node[0].height : 0;
  238. weexDom.scrollToElement(el, {
  239. offset: -nodeHeight,
  240. animated: animate
  241. });
  242. });
  243. } else {
  244. weexDom.scrollToElement(el, {
  245. offset: 0,
  246. animated: animate
  247. });
  248. }
  249. }
  250. return;
  251. // #endif
  252. if (this.usePageScroll) {
  253. this.$nextTick(() => {
  254. uni.pageScrollTo({
  255. scrollTop: 0,
  256. duration: animate ? 100 : 0,
  257. });
  258. });
  259. return;
  260. }
  261. this.privateScrollWithAnimation = animate ? 1 : 0;
  262. this.scrollTop = this.oldScrollTop;
  263. this.$nextTick(() => {
  264. this.scrollTop = 0;
  265. this.oldScrollTop = this.scrollTop;
  266. });
  267. },
  268. //滚动到底部
  269. async _scrollToBottom(animate = true) {
  270. // #ifdef APP-NVUE
  271. const el = this.$refs['zp-n-list-bottom-tag'];
  272. if (el) {
  273. weexDom.scrollToElement(el, {
  274. offset: 0,
  275. animated: animate
  276. });
  277. } else {
  278. u.consoleErr('滚动到底部失败,因为您设置了hideNvueBottomTag为true');
  279. }
  280. return;
  281. // #endif
  282. if (this.usePageScroll) {
  283. this.$nextTick(() => {
  284. uni.pageScrollTo({
  285. scrollTop: Number.MAX_VALUE,
  286. duration: animate ? 100 : 0,
  287. });
  288. });
  289. return;
  290. }
  291. try {
  292. this.privateScrollWithAnimation = animate ? 1 : 0;
  293. const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container');
  294. const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
  295. const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
  296. const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
  297. if (pagingContainerH > scrollViewH) {
  298. this.scrollTop = this.oldScrollTop;
  299. this.$nextTick(() => {
  300. this.scrollTop = pagingContainerH - scrollViewH + this.virtualPlaceholderTopHeight;
  301. this.oldScrollTop = this.scrollTop;
  302. });
  303. }
  304. } catch (e) {}
  305. },
  306. //滚动到指定view
  307. _scrollIntoView(sel, offset = 0, animate = false, finishCallback) {
  308. try {
  309. this.scrollTop = this.oldScrollTop;
  310. this.$nextTick(() => {
  311. // #ifdef APP-NVUE
  312. const refs = this.$parent.$refs;
  313. if (!refs) return;
  314. const dataType = Object.prototype.toString.call(sel);
  315. let el = null;
  316. if (dataType === '[object Number]') {
  317. const els = refs[`z-paging-${sel}`];
  318. el = els ? els[0] : null;
  319. } else if (dataType === '[object Array]') {
  320. el = sel[0];
  321. } else {
  322. el = sel;
  323. }
  324. if (el) {
  325. weexDom.scrollToElement(el, {
  326. offset: -offset,
  327. animated: animate
  328. });
  329. } else {
  330. u.consoleErr('在nvue中滚动到指定位置,cell必须设置 :ref="`z-paging-${index}`"');
  331. }
  332. return;
  333. // #endif
  334. if (sel.indexOf('#') != -1) {
  335. sel = sel.replace('#', '');
  336. }
  337. this._getNodeClientRect('#' + sel, false).then((node) => {
  338. if (node) {
  339. let nodeTop = node[0].top;
  340. this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
  341. if (finishCallback) {
  342. finishCallback();
  343. }
  344. }
  345. });
  346. });
  347. } catch (e) {}
  348. },
  349. //通过nodeTop滚动到指定view
  350. _scrollIntoViewByNodeTop(nodeTop, offset = 0, animate = false) {
  351. this._scrollToY(nodeTop, offset, animate, true);
  352. },
  353. //滚动到指定位置
  354. _scrollToY(y, offset = 0, animate = false, addScrollTop = false) {
  355. this.privateScrollWithAnimation = animate ? 1 : 0;
  356. if (this.usePageScroll) {
  357. uni.pageScrollTo({
  358. scrollTop: y - offset,
  359. duration: animate ? 100 : 0
  360. });
  361. } else {
  362. if(addScrollTop){
  363. y += this.oldScrollTop;
  364. }
  365. this.scrollTop = y - offset;
  366. this.oldScrollTop = this.scrollTop;
  367. }
  368. },
  369. //scroll-view滚动中
  370. _scroll(e) {
  371. this.$emit('scroll', e);
  372. const scrollTop = e.detail.scrollTop;
  373. // #ifndef APP-NVUE
  374. this.finalUseVirtualList && this._updateVirtualScroll(scrollTop, this.oldScrollTop - scrollTop);
  375. // #endif
  376. this.oldScrollTop = scrollTop;
  377. const scrollDiff = e.detail.scrollHeight - this.oldScrollTop;
  378. !this.isIos && this._checkScrolledToBottom(scrollDiff);
  379. },
  380. //scrollTop改变时触发
  381. _scrollTopChange(newVal, isPageScrollTop){
  382. this.$emit('scrollTopChange', newVal);
  383. this.$emit('update:scrollTop', newVal);
  384. this._checkShouldShowBackToTop(newVal);
  385. const scrollTop = this.isIos ? (newVal > 5 ? 6 : 0) : newVal;
  386. if (isPageScrollTop) {
  387. this.wxsPageScrollTop = scrollTop;
  388. } else {
  389. this.wxsScrollTop = scrollTop;
  390. }
  391. },
  392. //更新使用页面滚动时slot="top"或"bottom"插入view的高度
  393. _updatePageScrollTopOrBottomHeight(type) {
  394. // #ifndef APP-NVUE
  395. if (!this.usePageScroll) return;
  396. // #endif
  397. this._doCheckScrollViewShouldFullHeight(this.realTotalData);
  398. const node = `.zp-page-${type}`;
  399. const marginText = `margin${type.slice(0,1).toUpperCase() + type.slice(1)}`;
  400. let safeAreaInsetBottomAdd = this.safeAreaInsetBottom;
  401. this.$nextTick(() => {
  402. let delayTime = 0;
  403. // #ifdef MP-BAIDU || APP-NVUE
  404. delayTime = 50;
  405. // #endif
  406. setTimeout(() => {
  407. this._getNodeClientRect(node).then((res) => {
  408. if (res) {
  409. let pageScrollNodeHeight = res[0].height;
  410. if (type === 'bottom') {
  411. if (safeAreaInsetBottomAdd) {
  412. pageScrollNodeHeight += this.safeAreaBottom;
  413. }
  414. } else {
  415. this.cacheTopHeight = pageScrollNodeHeight;
  416. }
  417. this.$set(this.scrollViewStyle, marginText, `${pageScrollNodeHeight}px`);
  418. } else if (safeAreaInsetBottomAdd) {
  419. this.$set(this.scrollViewStyle, marginText, `${this.safeAreaBottom}px`);
  420. }
  421. });
  422. }, delayTime)
  423. })
  424. },
  425. //获取slot="left"和slot="right"宽度并且更新布局
  426. _updateLeftAndRightWidth(){
  427. if (!this.finalIsOldWebView) return;
  428. this.$nextTick(() => {
  429. let delayTime = 0;
  430. // #ifdef MP-BAIDU
  431. delayTime = 10;
  432. // #endif
  433. setTimeout(() => {
  434. ['left','right'].map(position => {
  435. this._getNodeClientRect(`.zp-page-${position}`).then((res) => {
  436. this.$set(this.scrollViewContainerStyle, position, res ? res[0].width + 'px' : '0px');
  437. });
  438. })
  439. }, delayTime)
  440. })
  441. }
  442. }
  443. }