组件代码如下
<template>
<div :class="wrapClasses" style="touch-action: none;">
<div
:class="scrollContainerClasses"
:style="{height: height + 'px'}"
@scroll="handleScroll"
@wheel="onWheel"
@touchstart="onPointerDown"
ref="scrollContainer"
>
<div :class="loaderClasses" :style="{paddingTop: wrapperPadding.paddingTop}" ref="toploader"
v-loading.body="showTopLoader"
:element-loading-text="loadingText"
:element-loading-spinner="loadingSpinner">
</div>
<div :class="slotContainerClasses" ref="scrollContent">
<slot></slot>
</div>
<div :class="loaderClasses" :style="{paddingBottom: wrapperPadding.paddingBottom}" ref="bottomLoader"
v-loading.body="showBottomLoader"
:element-loading-text="loadingText"
:element-loading-spinner="loadingSpinner">
</div>
</div>
</div>
</template>
<script>
import throttle from 'lodash.throttle'
import { on, off } from 'element-ui/lib/utils/dom'
const prefixCls = 'xdh-scroll'
const dragConfig = {
sensitivity: 10,
minimumStartDragOffset: 5 // minimum start drag offset
}
const noop = () => Promise.resolve()
export default {
name: 'XdhScroll',
props: {
height: {
type: [Number, String],
default: 300
},
onReachTop: {
type: Function
},
onReachBottom: {
type: Function
},
onReachEdge: {
type: Function
},
loadingText: {
type: String,
default: '加载中...'
},
loadingSpinner: {
type: String,
default: 'el-icon-loading'
},
distanceToEdge: [Number, Array]
},
data () {
const distanceToEdge = this.calculateProximityThreshold()
return {
showTopLoader: false,
showBottomLoader: false,
showBodyLoader: false,
lastScroll: 0,
reachedTopScrollLimit: true,
reachedBottomScrollLimit: false,
topRubberPadding: 0,
bottomRubberPadding: 0,
rubberRollBackTimeout: false,
isLoading: false,
pointerTouchDown: null,
touchScroll: false,
handleScroll: () => {},
pointerUpHandler: () => {},
pointerMoveHandler: () => {},
// near to edge detectors
topProximityThreshold: distanceToEdge[0],
bottomProximityThreshold: distanceToEdge[1]
}
},
computed: {
wrapClasses () {
return `${prefixCls}-wrapper`
},
scrollContainerClasses () {
return `${prefixCls}-container`
},
slotContainerClasses () {
return [
`${prefixCls}-content`,
{
[`${prefixCls}-content-loading`]: this.showBodyLoader
}
]
},
loaderClasses () {
return `${prefixCls}-loader`
},
wrapperPadding () {
return {
paddingTop: this.topRubberPadding + 'px',
paddingBottom: this.bottomRubberPadding + 'px'
}
}
},
methods: {
// just to improve feeling of loading and avoid scroll trailing events fired by the browser
waitOneSecond () {
return new Promise(resolve => {
setTimeout(resolve, 1000)
})
},
calculateProximityThreshold () {
const dte = this.distanceToEdge
if (typeof dte === 'undefined') return [20, 20]
return Array.isArray(dte) ? dte : [dte, dte]
},
onCallback (dir) {
this.isLoading = true
this.showBodyLoader = true
if (dir > 0) {
this.showTopLoader = true
this.topRubberPadding = 20
} else {
this.showBottomLoader = true
this.bottomRubberPadding = 20
// to force the scroll to the bottom while height is animating
let bottomLoaderHeight = 0
const container = this.$refs.scrollContainer
const initialScrollTop = container.scrollTop
for (let i = 0; i < 20; i++) {
setTimeout(() => {
bottomLoaderHeight = Math.max(
bottomLoaderHeight,
this.$refs.bottomLoader.getBoundingClientRect().height
)
container.scrollTop = initialScrollTop + bottomLoaderHeight
}, i * 50)
}
}
const callbacks = [this.waitOneSecond(), this.onReachEdge ? this.onReachEdge(dir) : noop()]
callbacks.push(dir > 0 ? this.onReachTop ? this.onReachTop() : noop() : this.onReachBottom ? this.onReachBottom() : noop())
let tooSlow = setTimeout(() => {
this.reset()
}, 5000)
Promise.all(callbacks).then(() => {
clearTimeout(tooSlow)
this.reset()
})
},
reset () {
[
'showTopLoader',
'showBottomLoader',
'showBodyLoader',
'isLoading',
'reachedTopScrollLimit',
'reachedBottomScrollLimit'
].forEach(prop => (this[prop] = false))
this.lastScroll = 0
this.topRubberPadding = 0
this.bottomRubberPadding = 0
clearInterval(this.rubberRollBackTimeout)
// if we remove the handler too soon the screen will bump
if (this.touchScroll) {
setTimeout(() => {
off(window, 'touchend', this.pointerUpHandler)
this.$refs.scrollContainer.removeEventListener('touchmove', this.pointerMoveHandler)
this.touchScroll = false
}, 500)
}
},
onWheel (event) {
if (this.isLoading) return
// get the wheel direction
const wheelDelta = event.wheelDelta ? event.wheelDelta : -(event.detail || event.deltaY)
this.stretchEdge(wheelDelta)
},
stretchEdge (direction) {
clearTimeout(this.rubberRollBackTimeout)
// check if set these props
if (!this.onReachEdge) {
if (direction > 0) {
if (!this.onReachTop) return
} else {
if (!this.onReachBottom) return
}
}
// if the scroll is not strong enough, lets reset it
this.rubberRollBackTimeout = setTimeout(() => {
if (!this.isLoading) this.reset()
}, 250)
// to give the feeling its ruberish and can be puled more to start loading
if (direction > 0 && this.reachedTopScrollLimit) {
this.topRubberPadding += 5 - this.topRubberPadding / 5
if (this.topRubberPadding > this.topProximityThreshold) this.onCallback(1)
} else if (direction < 0 && this.reachedBottomScrollLimit) {
this.bottomRubberPadding += 6 - this.bottomRubberPadding / 4
if (this.bottomRubberPadding > this.bottomProximityThreshold) this.onCallback(-1)
} else {
this.onScroll()
}
},
onScroll () {
if (this.isLoading) return
const el = this.$refs.scrollContainer
const scrollDirection = Math.sign(this.lastScroll - el.scrollTop) // IE has no Math.sign, check that webpack polyfills this
const displacement = el.scrollHeight - el.clientHeight - el.scrollTop
const topNegativeProximity = this.topProximityThreshold < 0 ? this.topProximityThreshold : 0
const bottomNegativeProximity = this.bottomProximityThreshold < 0 ? this.bottomProximityThreshold : 0
if (scrollDirection === -1 && displacement + bottomNegativeProximity <= dragConfig.sensitivity) {
this.reachedBottomScrollLimit = true
} else if (scrollDirection >= 0 && el.scrollTop + topNegativeProximity <= 0) {
this.reachedTopScrollLimit = true
} else {
this.reachedTopScrollLimit = false
this.reachedBottomScrollLimit = false
this.lastScroll = el.scrollTop
}
},
getTouchCoordinates (e) {
return {
x: e.touches[0].pageX,
y: e.touches[0].pageY
}
},
onPointerDown (e) {
// we just use scroll and wheel in desktop, no mousedown
if (this.isLoading) return
if (e.type === 'touchstart') {
// if we start do touchmove on the scroll edger the browser will scroll the body
// by adding 5px margin on pointer down we avoid this behaviour and the scroll/touchmove
// in the component will not be exported outside of the component
const container = this.$refs.scrollContainer
if (this.reachedTopScrollLimit) container.scrollTop = 5
else if (this.reachedBottomScrollLimit) container.scrollTop -= 5
}
if (e.type === 'touchstart' && this.$refs.scrollContainer.scrollTop === 0) {
this.$refs.scrollContainer.scrollTop = 5
}
this.pointerTouchDown = this.getTouchCoordinates(e)
on(window, 'touchend', this.pointerUpHandler)
this.$refs.scrollContainer.parentElement.addEventListener('touchmove', e => {
e.stopPropagation()
this.pointerMoveHandler(e)
}, {passive: false, useCapture: true})
},
onPointerMove (e) {
if (!this.pointerTouchDown) return
if (this.isLoading) return
const pointerPosition = this.getTouchCoordinates(e)
const yDiff = pointerPosition.y - this.pointerTouchDown.y
this.stretchEdge(yDiff)
if (!this.touchScroll) {
const wasDragged = Math.abs(yDiff) > dragConfig.minimumStartDragOffset
if (wasDragged) this.touchScroll = true
}
},
onPointerUp () {
this.pointerTouchDown = null
}
},
created () {
this.handleScroll = throttle(this.onScroll, 150, {leading: false})
this.pointerUpHandler = this.onPointerUp.bind(this) // because we need the same function to add and remove event handlers
this.pointerMoveHandler = throttle(this.onPointerMove, 50, {leading: false})
}
}
</script>
使用方法
<template>
<xdh-scroll :on-reach-bottom="handleReachBottom">
<el-card v-for="(item, index) in list1" :key="index" style="margin: 32px 0">
Content {{ item }}
</el-card>
</xdh-scroll>
</template>
<script>
export default {
data () {
return {
list1: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
},
methods: {
handleReachBottom () {
return new Promise(resolve => {
setTimeout(() => {
const last = this.list1[this.list1.length - 1];
for (let i = 1; i < 11; i++) {
this.list1.push(last + i);
}
resolve();
}, 2000);
});
}
}
}
</script>
属性
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
height | 滚动区域的高度,单位像素 | String/Number | - | 300 |
loading-text | 加载中的文案 | String | - | 加载中... |
loading-spinner | 自定义加载图标类名 | String | - | el-icon-loading |
on-reach-top | 滚动至顶部时触发,需返回 Promise | Function | - | - |
on-reach-bottom | 滚动至底部时触发,需返回 Promise | Function | - | - |
on-reach-edge | 滚动至顶部或底部时触发,需返回 Promise | Function | - | - |
distance-to-edge | 从边缘到触发回调的距离。如果是负的,回调将在到达边缘之前触发。值最好在 24 以下。 | Number/Array | - | [20, 20] |
网友评论