mall-uniapp/uni_modules/z-paging/components/z-paging/js/modules/scroller.js

519 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// [z-paging]scroll相关模块
import u from '.././z-paging-utils'
import Enum from '.././z-paging-enum'
// #ifdef APP-NVUE
const weexDom = weex.requireModule('dom');
// #endif
export default {
props: {
// 使用页面滚动默认为否当设置为是时则使用页面的滚动而非此组件内部的scroll-view的滚动使用页面滚动时z-paging无需设置确定的高度且对于长列表展示性能更高但配置会略微繁琐
usePageScroll: {
type: Boolean,
default: u.gc('usePageScroll', false)
},
// 是否可以滚动使用内置scroll-view和nvue时有效默认为是
scrollable: {
type: Boolean,
default: u.gc('scrollable', true)
},
// 控制是否出现滚动条,默认为是
showScrollbar: {
type: Boolean,
default: u.gc('showScrollbar', true)
},
// 是否允许横向滚动,默认为否
scrollX: {
type: Boolean,
default: u.gc('scrollX', false)
},
// iOS设备上滚动到顶部时是否允许回弹效果默认为否。关闭回弹效果后可使滚动到顶部与下拉刷新更连贯但是有吸顶view时滚动到顶部时可能出现抖动。
scrollToTopBounceEnabled: {
type: Boolean,
default: u.gc('scrollToTopBounceEnabled', false)
},
// iOS设备上滚动到底部时是否允许回弹效果默认为是。
scrollToBottomBounceEnabled: {
type: Boolean,
default: u.gc('scrollToBottomBounceEnabled', true)
},
// 在设置滚动条位置时使用动画过渡,默认为否
scrollWithAnimation: {
type: Boolean,
default: u.gc('scrollWithAnimation', false)
},
// 值应为某子元素idid不能以数字开头。设置哪个方向可滚动则在哪个方向滚动到该元素
scrollIntoView: {
type: String,
default: u.gc('scrollIntoView', '')
},
},
data() {
return {
scrollTop: 0,
oldScrollTop: 0,
scrollViewStyle: {},
scrollViewContainerStyle: {},
scrollViewInStyle: {},
pageScrollTop: -1,
scrollEnable: true,
privateScrollWithAnimation: -1,
cacheScrollNodeHeight: -1,
superContentHeight: 0,
}
},
watch: {
oldScrollTop(newVal) {
!this.usePageScroll && this._scrollTopChange(newVal,false);
},
pageScrollTop(newVal) {
this.usePageScroll && this._scrollTopChange(newVal,true);
},
usePageScroll: {
handler(newVal) {
this.loaded && this.autoHeight && this._setAutoHeight(!newVal);
// #ifdef H5
if (newVal) {
this.$nextTick(() => {
const mainScrollRef = this.$refs['zp-scroll-view'].$refs.main;
if (mainScrollRef) {
mainScrollRef.style = {};
}
})
}
// #endif
},
immediate: true
},
finalScrollTop(newVal) {
this.renderPropScrollTop = newVal < 6 ? 0 : 10;
}
},
computed: {
finalScrollWithAnimation() {
if (this.privateScrollWithAnimation !== -1) {
return this.privateScrollWithAnimation === 1;
}
return this.scrollWithAnimation;
},
finalScrollViewStyle() {
if (this.superContentZIndex != 1) {
this.scrollViewStyle['z-index'] = this.superContentZIndex;
this.scrollViewStyle['position'] = 'relative';
}
return this.scrollViewStyle;
},
finalScrollTop() {
return this.usePageScroll ? this.pageScrollTop : this.oldScrollTop;
},
// 当前是否是旧版webview
finalIsOldWebView() {
return this.isOldWebView && !this.usePageScroll;
},
// 当前scroll-view/list-view是否允许滚动
finalScrollable() {
return this.scrollable && !this.usePageScroll && this.scrollEnable
&& (this.refresherCompleteScrollable ? true : this.refresherStatus !== Enum.Refresher.Complete)
&& (this.refresherRefreshingScrollable ? true : this.refresherStatus !== Enum.Refresher.Loading);
}
},
methods: {
// 滚动到顶部animate为是否展示滚动动画默认为是
scrollToTop(animate, checkReverse = true) {
// 如果是聊天记录模式并且列表倒置了,则滚动到顶部实际上是滚动到底部
if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) {
this.scrollToBottom(animate, false);
return;
}
this.$nextTick(() => {
this._scrollToTop(animate, false);
// #ifdef APP-NVUE
if (this.nvueFastScroll && animate) {
u.delay(() => {
this._scrollToTop(false, false);
});
}
// #endif
})
},
// 滚动到底部animate为是否展示滚动动画默认为是
scrollToBottom(animate, checkReverse = true) {
// 如果是聊天记录模式并且列表倒置了,则滚动到底部实际上是滚动到顶部
if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) {
this.scrollToTop(animate, false);
return;
}
this.$nextTick(() => {
this._scrollToBottom(animate);
// #ifdef APP-NVUE
if (this.nvueFastScroll && animate) {
u.delay(() => {
this._scrollToBottom(false);
});
}
// #endif
})
},
// 滚动到指定view(vue中有效)。sel为需要滚动的view的id值不包含"#"offset为偏移量单位为pxanimate为是否展示滚动动画默认为否
scrollIntoViewById(sel, offset, animate) {
this._scrollIntoView(sel, offset, animate);
},
// 滚动到指定view(vue中有效)。nodeTop为需要滚动的view的top值(通过uni.createSelectorQuery()获取)offset为偏移量单位为pxanimate为是否展示滚动动画默认为否
scrollIntoViewByNodeTop(nodeTop, offset, animate) {
this.scrollTop = this.oldScrollTop;
this.$nextTick(() => {
this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
})
},
// 滚动到指定位置(vue中有效)。y为与顶部的距离单位为pxoffset为偏移量单位为pxanimate为是否展示滚动动画默认为否
scrollToY(y, offset, animate) {
this.scrollTop = this.oldScrollTop;
this.$nextTick(() => {
this._scrollToY(y, offset, animate);
})
},
// 滚动到指定view(nvue中和虚拟列表中有效)。index为需要滚动的view的index(第几个从0开始)offset为偏移量单位为pxanimate为是否展示滚动动画默认为否
scrollIntoViewByIndex(index, offset, animate) {
if (index >= this.realTotalData.length) {
u.consoleErr('当前滚动的index超出已渲染列表长度请先通过refreshToPage加载到对应index页并等待渲染成功后再调用此方法')
return;
}
this.$nextTick(() => {
// #ifdef APP-NVUE
// 在nvue中根据index获取对应节点信息并滚动到此节点位置
this._scrollIntoView(index, offset, animate);
// #endif
// #ifndef APP-NVUE
if (this.finalUseVirtualList) {
const isCellFixed = this.cellHeightMode === Enum.CellHeightMode.Fixed;
u.delay(() => {
if (this.finalUseVirtualList) {
// 虚拟列表 + 每个cell高度完全相同模式下此时滚动到对应index的cell就是滚动到scrollTop = cellHeight * index的位置
// 虚拟列表 + 高度是动态非固定的模式下此时滚动到对应index的cell就是滚动到scrollTop = 缓存的cell高度数组中第index个的lastTotalHeight的位置
const scrollTop = isCellFixed ? this.virtualCellHeight * index : this.virtualHeightCacheList[index].lastTotalHeight;
this.scrollToY(scrollTop, offset, animate);
}
}, isCellFixed ? 0 : 100)
}
// #endif
})
},
// 滚动到指定view(nvue中有效)。view为需要滚动的view(通过`this.$refs.xxx`获取),不包含"#"offset为偏移量单位为pxanimate为是否展示滚动动画默认为否
scrollIntoViewByView(view, offset, animate) {
this._scrollIntoView(view, offset, animate);
},
// 当使用页面滚动并且自定义下拉刷新时请在页面的onPageScroll中调用此方法告知z-paging当前的pageScrollTop否则会导致在任意位置都可以下拉刷新
updatePageScrollTop(value) {
this.pageScrollTop = value;
},
// 当使用页面滚动并且设置了slot="top"时默认初次加载会自动获取其高度并使内部容器下移当slot="top"的view高度动态改变时在其高度需要更新时调用此方法
updatePageScrollTopHeight() {
this._updatePageScrollTopOrBottomHeight('top');
},
// 当使用页面滚动并且设置了slot="bottom"时默认初次加载会自动获取其高度并使内部容器下移当slot="bottom"的view高度动态改变时在其高度需要更新时调用此方法
updatePageScrollBottomHeight() {
this._updatePageScrollTopOrBottomHeight('bottom');
},
// 更新slot="left"和slot="right"宽度当slot="left"或slot="right"宽度动态改变时调用
updateLeftAndRightWidth() {
if (!this.finalIsOldWebView) return;
this.$nextTick(() => this._updateLeftAndRightWidth(this.scrollViewContainerStyle, 'zp-page'));
},
// 更新z-paging内置scroll-view的scrollTop
updateScrollViewScrollTop(scrollTop, animate = true) {
this._updatePrivateScrollWithAnimation(animate);
this.scrollTop = this.oldScrollTop;
this.$nextTick(() => {
this.scrollTop = scrollTop;
this.oldScrollTop = this.scrollTop;
});
},
// 当滚动到顶部时
_onScrollToUpper() {
this.$emit('scrolltoupper');
this.$emit('scrollTopChange', 0);
this.$nextTick(() => {
this.oldScrollTop = 0;
})
},
// 当滚动到底部时
_onScrollToLower(e) {
(!e.detail || !e.detail.direction || e.detail.direction === 'bottom') && this._onLoadingMore(this.useChatRecordMode ? 'click' : 'toBottom')
},
// 滚动到顶部
_scrollToTop(animate = true, isPrivate = true) {
// #ifdef APP-NVUE
// 在nvue中需要通过weex.scrollToElement滚动到顶部此时在顶部插入了一个view使得滚动到这个view位置
const el = this.$refs['zp-n-list-top-tag'];
if (this.usePageScroll) {
this._getNodeClientRect('zp-page-scroll-top', false).then(node => {
const nodeHeight = node ? node[0].height : 0;
weexDom.scrollToElement(el, {
offset: -nodeHeight,
animated: animate
});
});
} else {
if (!this.isIos && this.nvueListIs === 'scroller') {
this._getNodeClientRect('zp-n-refresh-container', false).then(node => {
const nodeHeight = node ? node[0].height : 0;
weexDom.scrollToElement(el, {
offset: -nodeHeight,
animated: animate
});
});
} else {
weexDom.scrollToElement(el, {
offset: 0,
animated: animate
});
}
}
return;
// #endif
if (this.usePageScroll) {
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: 0,
duration: animate ? 100 : 0,
});
});
return;
}
this._updatePrivateScrollWithAnimation(animate);
this.scrollTop = this.oldScrollTop;
this.$nextTick(() => {
this.scrollTop = 0;
this.oldScrollTop = this.scrollTop;
});
},
// 滚动到底部
async _scrollToBottom(animate = true) {
// #ifdef APP-NVUE
// 在nvue中需要通过weex.scrollToElement滚动到顶部此时在底部插入了一个view使得滚动到这个view位置
const el = this.$refs['zp-n-list-bottom-tag'];
if (el) {
weexDom.scrollToElement(el, {
offset: 0,
animated: animate
});
} else {
u.consoleErr('滚动到底部失败因为您设置了hideNvueBottomTag为true');
}
return;
// #endif
if (this.usePageScroll) {
this.$nextTick(() => {
uni.pageScrollTo({
scrollTop: Number.MAX_VALUE,
duration: animate ? 100 : 0,
});
});
return;
}
try {
this._updatePrivateScrollWithAnimation(animate);
const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container');
const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0;
const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0;
if (pagingContainerH > scrollViewH) {
this.scrollTop = this.oldScrollTop;
this.$nextTick(() => {
this.scrollTop = pagingContainerH - scrollViewH + this.virtualPlaceholderTopHeight;
this.oldScrollTop = this.scrollTop;
});
}
} catch (e) {}
},
// 滚动到指定view
_scrollIntoView(sel, offset = 0, animate = false, finishCallback) {
try {
this.scrollTop = this.oldScrollTop;
this.$nextTick(() => {
// #ifdef APP-NVUE
const refs = this.$parent.$refs;
if (!refs) return;
const dataType = Object.prototype.toString.call(sel);
let el = null;
if (dataType === '[object Number]') {
const els = refs[`z-paging-${sel}`];
el = els ? els[0] : null;
} else if (dataType === '[object Array]') {
el = sel[0];
} else {
el = sel;
}
if (el) {
weexDom.scrollToElement(el, {
offset: -offset,
animated: animate
});
} else {
u.consoleErr('在nvue中滚动到指定位置cell必须设置 :ref="`z-paging-${index}`"');
}
return;
// #endif
this._getNodeClientRect('#' + sel.replace('#', ''), this.$parent).then((node) => {
if (node) {
let nodeTop = node[0].top;
this._scrollIntoViewByNodeTop(nodeTop, offset, animate);
finishCallback && finishCallback();
}
});
});
} catch (e) {}
},
// 通过nodeTop滚动到指定view
_scrollIntoViewByNodeTop(nodeTop, offset = 0, animate = false) {
// 如果是聊天记录模式并且列表倒置了此时nodeTop需要等于scroll-view高度 - nodeTop
if (this.isChatRecordModeAndInversion) {
this._getNodeClientRect('.zp-scroll-view').then(sNode => {
if (sNode) {
this._scrollToY(sNode[0].height - nodeTop, offset, animate, true);
}
})
} else {
this._scrollToY(nodeTop, offset, animate, true);
}
},
// 滚动到指定位置
_scrollToY(y, offset = 0, animate = false, addScrollTop = false) {
this._updatePrivateScrollWithAnimation(animate);
u.delay(() => {
if (this.usePageScroll) {
if (addScrollTop && this.pageScrollTop !== -1) {
y += this.pageScrollTop;
}
const scrollTop = y - offset;
uni.pageScrollTo({
scrollTop,
duration: animate ? 100 : 0
});
} else {
if (addScrollTop) {
y += this.oldScrollTop;
}
this.scrollTop = y - offset;
}
}, 10)
},
// scroll-view滚动中
_scroll(e) {
this.$emit('scroll', e);
const scrollTop = e.detail.scrollTop;
// #ifndef APP-NVUE
this.finalUseVirtualList && this._updateVirtualScroll(scrollTop, this.oldScrollTop - scrollTop);
// #endif
this.oldScrollTop = scrollTop;
// 滚动区域内容的总高度 - 当前滚动的scrollTop = 当前滚动区域的顶部与内容底部的距离
const scrollDiff = e.detail.scrollHeight - this.oldScrollTop;
// 在非ios平台滚动中再次验证一下是否滚动到了底部。因为在一些安卓设备中有概率滚动到底部不触发@scrolltolower事件因此添加双重检测逻辑
!this.isIos && this._checkScrolledToBottom(scrollDiff);
},
// 更新内置的scroll-view是否启用滚动动画
_updatePrivateScrollWithAnimation(animate) {
this.privateScrollWithAnimation = animate ? 1 : 0;
u.delay(() => this.$nextTick(() => {
// 在滚动结束后将滚动动画状态设置回初始状态
this.privateScrollWithAnimation = -1;
}), 100, 'updateScrollWithAnimationDelay')
},
// 检测scrollView是否要铺满屏幕
_doCheckScrollViewShouldFullHeight(totalData) {
if (this.autoFullHeight && this.usePageScroll && this.isTotalChangeFromAddData) {
// #ifndef APP-NVUE
this.$nextTick(() => {
this._checkScrollViewShouldFullHeight((scrollViewNode, pagingContainerNode) => {
this._preCheckShowNoMoreInside(totalData, scrollViewNode, pagingContainerNode)
});
})
// #endif
// #ifdef APP-NVUE
this._preCheckShowNoMoreInside(totalData)
// #endif
} else {
this._preCheckShowNoMoreInside(totalData)
}
},
// 检测z-paging是否要全屏覆盖(当使用页面滚动并且不满全屏时默认z-paging需要铺满全屏避免数据过少时内部的empty-view无法正确展示)
async _checkScrollViewShouldFullHeight(callback) {
try {
const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view');
const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container-content');
if (!scrollViewNode || !pagingContainerNode) return;
const scrollViewHeight = pagingContainerNode[0].height;
const scrollViewTop = scrollViewNode[0].top;
if (this.isAddedData && scrollViewHeight + scrollViewTop <= this.windowHeight) {
this._setAutoHeight(true, scrollViewNode);
callback(scrollViewNode, pagingContainerNode);
} else {
this._setAutoHeight(false);
callback(null, null);
}
} catch (e) {
callback(null, null);
}
},
// 更新缓存中z-paging整个内容容器高度
async _updateCachedSuperContentHeight() {
const superContentNode = await this._getNodeClientRect('.z-paging-content');
if (superContentNode) {
this.superContentHeight = superContentNode[0].height;
}
},
// scrollTop改变时触发
_scrollTopChange(newVal, isPageScrollTop){
this.$emit('scrollTopChange', newVal);
this.$emit('update:scrollTop', newVal);
this._checkShouldShowBackToTop(newVal);
// 之前在安卓中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常因此判断scrollTop在105之内都允许下拉刷新但此方案会导致某些情况例如滚动到距离顶部10px处下拉抖动因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案
// const scrollTop = this.isIos ? (newVal > 5 ? 6 : 0) : (newVal > 105 ? 106 : (newVal > 5 ? 6 : 0));
const scrollTop = newVal > 5 ? 6 : 0;
if (isPageScrollTop && this.wxsPageScrollTop !== scrollTop) {
this.wxsPageScrollTop = scrollTop;
} else if (!isPageScrollTop && this.wxsScrollTop !== scrollTop) {
this.wxsScrollTop = scrollTop;
if (scrollTop > 6) {
this.scrollEnable = true;
}
}
},
// 更新使用页面滚动时slot="top"或"bottom"插入view的高度
_updatePageScrollTopOrBottomHeight(type) {
// #ifndef APP-NVUE
if (!this.usePageScroll) return;
// #endif
this._doCheckScrollViewShouldFullHeight(this.realTotalData);
const node = `.zp-page-${type}`;
const marginText = `margin${type.slice(0,1).toUpperCase() + type.slice(1)}`;
let safeAreaInsetBottomAdd = this.safeAreaInsetBottom;
this.$nextTick(() => {
let delayTime = 0;
// #ifdef MP-BAIDU || APP-NVUE
delayTime = 50;
// #endif
u.delay(() => {
this._getNodeClientRect(node).then((res) => {
if (res) {
let pageScrollNodeHeight = res[0].height;
if (type === 'bottom') {
if (safeAreaInsetBottomAdd) {
pageScrollNodeHeight += this.safeAreaBottom;
}
} else {
this.cacheTopHeight = pageScrollNodeHeight;
}
this.$set(this.scrollViewStyle, marginText, `${pageScrollNodeHeight}px`);
} else if (safeAreaInsetBottomAdd) {
this.$set(this.scrollViewStyle, marginText, `${this.safeAreaBottom}px`);
}
});
}, delayTime)
})
},
}
}