You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
610 lines
18 KiB
610 lines
18 KiB
<template>
|
|
<uni-scroll-view v-on="$listeners">
|
|
<div
|
|
ref="wrap"
|
|
class="uni-scroll-view"
|
|
>
|
|
<div
|
|
ref="main"
|
|
:style="{'overflow-x': scrollX?'auto':'hidden','overflow-y': scrollY?'auto':'hidden'}"
|
|
class="uni-scroll-view"
|
|
>
|
|
<div
|
|
ref="content"
|
|
class="uni-scroll-view-content"
|
|
>
|
|
<div
|
|
v-if="refresherEnabled"
|
|
ref="refresherinner"
|
|
:style="{'background-color': refresherBackground, 'height': refresherHeight + 'px'}"
|
|
class="uni-scroll-view-refresher"
|
|
>
|
|
<div
|
|
v-if="refresherDefaultStyle !== 'none'"
|
|
class="uni-scroll-view-refresh"
|
|
>
|
|
<div class="uni-scroll-view-refresh-inner">
|
|
<svg
|
|
v-if="refreshState=='pulling'"
|
|
:style="{'transform': 'rotate('+ refreshRotate +'deg)'}"
|
|
fill="#2BD009"
|
|
class="uni-scroll-view-refresh__icon"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
|
|
<path
|
|
d="M0 0h24v24H0z"
|
|
fill="none"
|
|
/>
|
|
</svg>
|
|
<svg
|
|
v-if="refreshState=='refreshing'"
|
|
class="uni-scroll-view-refresh__spinner"
|
|
width="24"
|
|
height="24"
|
|
viewBox="25 25 50 50"
|
|
>
|
|
<circle
|
|
cx="50"
|
|
cy="50"
|
|
r="20"
|
|
fill="none"
|
|
style="color: #2BD009;"
|
|
stroke-width="3"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<slot
|
|
v-if="refresherDefaultStyle=='none'"
|
|
name="refresher"
|
|
/>
|
|
</div>
|
|
<slot />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</uni-scroll-view>
|
|
</template>
|
|
<script>
|
|
import scroller from 'uni-mixins/scroller/index'
|
|
import {
|
|
supportsPassive
|
|
} from 'uni-shared'
|
|
import {
|
|
initScrollBounce,
|
|
disableScrollBounce
|
|
} from 'uni-platform/helpers/scroll'
|
|
|
|
const passiveOptions = supportsPassive ? {
|
|
passive: true
|
|
} : false
|
|
|
|
// const PULLING = 'pulling'
|
|
// const REFRESHING = 'refreshing'
|
|
|
|
export default {
|
|
name: 'ScrollView',
|
|
mixins: [scroller],
|
|
props: {
|
|
scrollX: {
|
|
type: [Boolean, String],
|
|
default: false
|
|
},
|
|
scrollY: {
|
|
type: [Boolean, String],
|
|
default: false
|
|
},
|
|
upperThreshold: {
|
|
type: [Number, String],
|
|
default: 50
|
|
},
|
|
lowerThreshold: {
|
|
type: [Number, String],
|
|
default: 50
|
|
},
|
|
scrollTop: {
|
|
type: [Number, String],
|
|
default: 0
|
|
},
|
|
scrollLeft: {
|
|
type: [Number, String],
|
|
default: 0
|
|
},
|
|
scrollIntoView: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
scrollWithAnimation: {
|
|
type: [Boolean, String],
|
|
default: false
|
|
},
|
|
enableBackToTop: {
|
|
type: [Boolean, String],
|
|
default: false
|
|
},
|
|
refresherEnabled: {
|
|
type: [Boolean, String],
|
|
default: false
|
|
},
|
|
refresherThreshold: {
|
|
type: Number,
|
|
default: 45
|
|
},
|
|
refresherDefaultStyle: {
|
|
type: String,
|
|
default: 'back'
|
|
},
|
|
refresherBackground: {
|
|
type: String,
|
|
default: '#fff'
|
|
},
|
|
refresherTriggered: {
|
|
type: [Boolean, String],
|
|
default: false
|
|
}
|
|
},
|
|
data () {
|
|
return {
|
|
lastScrollTop: this.scrollTopNumber,
|
|
lastScrollLeft: this.scrollLeftNumber,
|
|
lastScrollToUpperTime: 0,
|
|
lastScrollToLowerTime: 0,
|
|
refresherHeight: 0,
|
|
refreshRotate: 0,
|
|
refreshState: ''
|
|
}
|
|
},
|
|
computed: {
|
|
upperThresholdNumber () {
|
|
var val = Number(this.upperThreshold)
|
|
return isNaN(val) ? 50 : val
|
|
},
|
|
lowerThresholdNumber () {
|
|
var val = Number(this.lowerThreshold)
|
|
return isNaN(val) ? 50 : val
|
|
},
|
|
scrollTopNumber () {
|
|
return Number(this.scrollTop) || 0
|
|
},
|
|
scrollLeftNumber () {
|
|
return Number(this.scrollLeft) || 0
|
|
}
|
|
},
|
|
watch: {
|
|
scrollTopNumber (val) {
|
|
this._scrollTopChanged(val)
|
|
},
|
|
scrollLeftNumber (val) {
|
|
this._scrollLeftChanged(val)
|
|
},
|
|
scrollIntoView (val) {
|
|
this._scrollIntoViewChanged(val)
|
|
},
|
|
refresherTriggered (val) {
|
|
// TODO
|
|
if (val === true) {
|
|
this._setRefreshState('refreshing')
|
|
} else if (val === false) {
|
|
this._setRefreshState('restore')
|
|
}
|
|
}
|
|
},
|
|
mounted () {
|
|
var self = this
|
|
this._attached = true
|
|
this._scrollTopChanged(this.scrollTopNumber)
|
|
this._scrollLeftChanged(this.scrollLeftNumber)
|
|
this._scrollIntoViewChanged(this.scrollIntoView)
|
|
this.__handleScroll = function (e) {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
self._handleScroll.bind(self, event)()
|
|
}
|
|
var touchStart = null
|
|
var needStop = null
|
|
this.__handleTouchMove = function (event) {
|
|
var x = event.touches[0].pageX
|
|
var y = event.touches[0].pageY
|
|
var main = self.$refs.main
|
|
if (needStop === null) {
|
|
if (Math.abs(x - touchStart.x) > Math.abs(y - touchStart.y)) {
|
|
// 横向滑动
|
|
if (self.scrollX) {
|
|
if (main.scrollLeft === 0 && x > touchStart.x) {
|
|
needStop = false
|
|
return
|
|
} else if (main.scrollWidth === main.offsetWidth + main.scrollLeft && x < touchStart.x) {
|
|
needStop = false
|
|
return
|
|
}
|
|
needStop = true
|
|
} else {
|
|
needStop = false
|
|
}
|
|
} else {
|
|
// 纵向滑动
|
|
if (self.scrollY) {
|
|
if (main.scrollTop === 0 && y > touchStart.y) {
|
|
needStop = false
|
|
return
|
|
} else if (main.scrollHeight === main.offsetHeight + main.scrollTop && y < touchStart.y) {
|
|
needStop = false
|
|
return
|
|
}
|
|
needStop = true
|
|
} else {
|
|
needStop = false
|
|
}
|
|
}
|
|
}
|
|
if (needStop) {
|
|
event.stopPropagation()
|
|
}
|
|
|
|
if (self.refresherEnabled && self.refreshState === 'pulling') {
|
|
const dy = y - touchStart.y
|
|
self.refresherHeight = dy
|
|
|
|
let rotate = dy / self.refresherThreshold
|
|
if (rotate > 1) {
|
|
rotate = 1
|
|
} else {
|
|
rotate = rotate * 360
|
|
}
|
|
self.refreshRotate = rotate
|
|
|
|
self.$trigger('refresherpulling', event, {
|
|
deltaY: dy
|
|
})
|
|
}
|
|
}
|
|
|
|
this.__handleTouchStart = function (event) {
|
|
if (event.touches.length === 1) {
|
|
disableScrollBounce({
|
|
disable: true
|
|
})
|
|
needStop = null
|
|
touchStart = {
|
|
x: event.touches[0].pageX,
|
|
y: event.touches[0].pageY
|
|
}
|
|
if (self.refresherEnabled && self.refreshState !== 'refreshing' && self.$refs.main.scrollTop === 0) {
|
|
self.refreshState = 'pulling'
|
|
}
|
|
}
|
|
}
|
|
this.__handleTouchEnd = function (event) {
|
|
touchStart = null
|
|
disableScrollBounce({
|
|
disable: false
|
|
})
|
|
if (self.refresherHeight >= self.refresherThreshold) {
|
|
self._setRefreshState('refreshing')
|
|
} else {
|
|
self.refresherHeight = 0
|
|
self.$trigger('refresherabort', event, {})
|
|
}
|
|
}
|
|
this.$refs.main.addEventListener('touchstart', this.__handleTouchStart, passiveOptions)
|
|
this.$refs.main.addEventListener('touchmove', this.__handleTouchMove, passiveOptions)
|
|
this.$refs.main.addEventListener('scroll', this.__handleScroll, supportsPassive ? {
|
|
passive: false
|
|
} : false)
|
|
this.$refs.main.addEventListener('touchend', this.__handleTouchEnd, passiveOptions)
|
|
initScrollBounce()
|
|
},
|
|
activated () {
|
|
// 还原 scroll-view 滚动位置
|
|
this.scrollY && (this.$refs.main.scrollTop = this.lastScrollTop)
|
|
this.scrollX && (this.$refs.main.scrollLeft = this.lastScrollLeft)
|
|
},
|
|
beforeDestroy () {
|
|
this.$refs.main.removeEventListener('touchstart', this.__handleTouchStart, passiveOptions)
|
|
this.$refs.main.removeEventListener('touchmove', this.__handleTouchMove, passiveOptions)
|
|
this.$refs.main.removeEventListener('scroll', this.__handleScroll, supportsPassive ? {
|
|
passive: false
|
|
} : false)
|
|
this.$refs.main.removeEventListener('touchend', this.__handleTouchEnd, passiveOptions)
|
|
},
|
|
methods: {
|
|
scrollTo: function (t, n) {
|
|
var i = this.$refs.main
|
|
t < 0 ? t = 0 : n === 'x' && t > i.scrollWidth - i.offsetWidth ? t = i.scrollWidth - i.offsetWidth
|
|
: n === 'y' && t > i.scrollHeight - i.offsetHeight && (t = i.scrollHeight - i.offsetHeight)
|
|
var r = 0
|
|
var o = ''
|
|
n === 'x' ? r = i.scrollLeft - t : n === 'y' && (r = i.scrollTop - t)
|
|
if (r !== 0) {
|
|
this.$refs.content.style.transition = 'transform .3s ease-out'
|
|
this.$refs.content.style.webkitTransition = '-webkit-transform .3s ease-out'
|
|
if (n === 'x') {
|
|
o = 'translateX(' + r + 'px) translateZ(0)'
|
|
} else {
|
|
n === 'y' && (o = 'translateY(' + r + 'px) translateZ(0)')
|
|
}
|
|
this.$refs.content.removeEventListener('transitionend', this.__transitionEnd)
|
|
this.$refs.content.removeEventListener('webkitTransitionEnd', this.__transitionEnd)
|
|
this.__transitionEnd = this._transitionEnd.bind(this, t, n)
|
|
this.$refs.content.addEventListener('transitionend', this.__transitionEnd)
|
|
this.$refs.content.addEventListener('webkitTransitionEnd', this.__transitionEnd)
|
|
if (n === 'x') {
|
|
// if (e !== 'ios') {
|
|
i.style.overflowX = 'hidden'
|
|
// }
|
|
} else if (n === 'y') {
|
|
i.style.overflowY = 'hidden'
|
|
}
|
|
|
|
this.$refs.content.style.transform = o
|
|
this.$refs.content.style.webkitTransform = o
|
|
}
|
|
},
|
|
_handleTrack: function ($event) {
|
|
if ($event.detail.state === 'start') {
|
|
this._x = $event.detail.x
|
|
this._y = $event.detail.y
|
|
this._noBubble = null
|
|
return
|
|
}
|
|
if ($event.detail.state === 'end') {
|
|
this._noBubble = false
|
|
}
|
|
if (this._noBubble === null && this.scrollY) {
|
|
if (Math.abs(this._y - $event.detail.y) / Math.abs(this._x - $event.detail.x) > 1) {
|
|
this._noBubble = true
|
|
} else {
|
|
this._noBubble = false
|
|
}
|
|
}
|
|
if (this._noBubble === null && this.scrollX) {
|
|
if (Math.abs(this._x - $event.detail.x) / Math.abs(this._y - $event.detail.y) > 1) {
|
|
this._noBubble = true
|
|
} else {
|
|
this._noBubble = false
|
|
}
|
|
}
|
|
this._x = $event.detail.x
|
|
this._y = $event.detail.y
|
|
if (this._noBubble) {
|
|
$event.stopPropagation()
|
|
}
|
|
},
|
|
_handleScroll: function ($event) {
|
|
if (!($event.timeStamp - this._lastScrollTime < 20)) {
|
|
this._lastScrollTime = $event.timeStamp
|
|
const target = $event.target
|
|
this.$trigger('scroll', $event, {
|
|
scrollLeft: target.scrollLeft,
|
|
scrollTop: target.scrollTop,
|
|
scrollHeight: target.scrollHeight,
|
|
scrollWidth: target.scrollWidth,
|
|
deltaX: this.lastScrollLeft - target.scrollLeft,
|
|
deltaY: this.lastScrollTop - target.scrollTop
|
|
})
|
|
if (this.scrollY) {
|
|
if (target.scrollTop <= this.upperThresholdNumber && this.lastScrollTop - target.scrollTop > 0 && $event.timeStamp - this.lastScrollToUpperTime > 200) {
|
|
this.$trigger('scrolltoupper', $event, {
|
|
direction: 'top'
|
|
})
|
|
this.lastScrollToUpperTime = $event.timeStamp
|
|
}
|
|
if (target.scrollTop + target.offsetHeight + this.lowerThresholdNumber >= target.scrollHeight && this.lastScrollTop - target.scrollTop < 0 && $event.timeStamp - this.lastScrollToLowerTime > 200) {
|
|
this.$trigger('scrolltolower', $event, {
|
|
direction: 'bottom'
|
|
})
|
|
this.lastScrollToLowerTime = $event.timeStamp
|
|
}
|
|
}
|
|
if (this.scrollX) {
|
|
if (target.scrollLeft <= this.upperThresholdNumber && this.lastScrollLeft - target.scrollLeft > 0 && $event.timeStamp - this.lastScrollToUpperTime > 200) {
|
|
this.$trigger('scrolltoupper', $event, {
|
|
direction: 'left'
|
|
})
|
|
this.lastScrollToUpperTime = $event.timeStamp
|
|
}
|
|
if (target.scrollLeft + target.offsetWidth + this.lowerThresholdNumber >= target.scrollWidth && this.lastScrollLeft - target.scrollLeft < 0 && $event.timeStamp - this.lastScrollToLowerTime > 200) {
|
|
this.$trigger('scrolltolower', $event, {
|
|
direction: 'right'
|
|
})
|
|
this.lastScrollToLowerTime = $event.timeStamp
|
|
}
|
|
}
|
|
this.lastScrollTop = target.scrollTop
|
|
this.lastScrollLeft = target.scrollLeft
|
|
}
|
|
},
|
|
_scrollTopChanged: function (val) {
|
|
if (this.scrollY) {
|
|
if (this._innerSetScrollTop) {
|
|
this._innerSetScrollTop = false
|
|
} else {
|
|
if (this.scrollWithAnimation) {
|
|
this.scrollTo(val, 'y')
|
|
} else {
|
|
this.$refs.main.scrollTop = val
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_scrollLeftChanged: function (val) {
|
|
if (this.scrollX) {
|
|
if (this._innerSetScrollLeft) {
|
|
this._innerSetScrollLeft = false
|
|
} else {
|
|
if (this.scrollWithAnimation) {
|
|
this.scrollTo(val, 'x')
|
|
} else {
|
|
this.$refs.main.scrollLeft = val
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_scrollIntoViewChanged: function (val) {
|
|
if (val) {
|
|
if (!/^[_a-zA-Z][-_a-zA-Z0-9:]*$/.test(val)) {
|
|
console.group('scroll-into-view="' + val + '" 有误')
|
|
console.error('id 属性值格式错误。如不能以数字开头。')
|
|
console.groupEnd()
|
|
return
|
|
}
|
|
var element = this.$el.querySelector('#' + val)
|
|
if (element) {
|
|
var mainRect = this.$refs.main.getBoundingClientRect()
|
|
var elRect = element.getBoundingClientRect()
|
|
if (this.scrollX) {
|
|
var left = elRect.left - mainRect.left
|
|
var scrollLeft = this.$refs.main.scrollLeft
|
|
var x = scrollLeft + left
|
|
if (this.scrollWithAnimation) {
|
|
this.scrollTo(x, 'x')
|
|
} else {
|
|
this.$refs.main.scrollLeft = x
|
|
}
|
|
}
|
|
if (this.scrollY) {
|
|
var top = elRect.top - mainRect.top
|
|
var scrollTop = this.$refs.main.scrollTop
|
|
var y = scrollTop + top
|
|
if (this.scrollWithAnimation) {
|
|
this.scrollTo(y, 'y')
|
|
} else {
|
|
this.$refs.main.scrollTop = y
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_transitionEnd: function (val, type) {
|
|
this.$refs.content.style.transition = ''
|
|
this.$refs.content.style.webkitTransition = ''
|
|
this.$refs.content.style.transform = ''
|
|
this.$refs.content.style.webkitTransform = ''
|
|
var main = this.$refs.main
|
|
if (type === 'x') {
|
|
main.style.overflowX = this.scrollX ? 'auto' : 'hidden'
|
|
main.scrollLeft = val
|
|
} else if (type === 'y') {
|
|
main.style.overflowY = this.scrollY ? 'auto' : 'hidden'
|
|
main.scrollTop = val
|
|
}
|
|
this.$refs.content.removeEventListener('transitionend', this.__transitionEnd)
|
|
this.$refs.content.removeEventListener('webkitTransitionEnd', this.__transitionEnd)
|
|
},
|
|
_setRefreshState (state) {
|
|
switch (state) {
|
|
case 'refreshing':
|
|
this.refresherHeight = this.refresherThreshold
|
|
this.$trigger('refresherrefresh', event, {})
|
|
break
|
|
case 'restore':
|
|
this.refresherHeight = 0
|
|
this.$trigger('refresherrestore', {}, {})
|
|
break
|
|
}
|
|
this.refreshState = state
|
|
},
|
|
getScrollPosition () {
|
|
const main = this.$refs.main
|
|
return {
|
|
scrollLeft: main.scrollLeft,
|
|
scrollTop: main.scrollTop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<style>
|
|
uni-scroll-view {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
uni-scroll-view[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.uni-scroll-view {
|
|
position: relative;
|
|
-webkit-overflow-scrolling: touch;
|
|
width: 100%;
|
|
/* display: flex; 时在安卓下会导致scrollWidth和offsetWidth一样 */
|
|
height: 100%;
|
|
max-height: inherit;
|
|
}
|
|
|
|
.uni-scroll-view-content {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.uni-scroll-view-refresher {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.uni-scroll-view-refresh {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.uni-scroll-view-refresh-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
line-height: 0;
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background-color: #fff;
|
|
box-shadow: 0 1px 6px rgba(0, 0, 0, .117647), 0 1px 4px rgba(0, 0, 0, .117647);
|
|
}
|
|
|
|
.uni-scroll-view-refresh__spinner {
|
|
transform-origin: center center;
|
|
animation: uni-scroll-view-refresh-rotate 2s linear infinite;
|
|
}
|
|
|
|
.uni-scroll-view-refresh__spinner > circle {
|
|
stroke: currentColor;
|
|
stroke-linecap: round;
|
|
animation: uni-scroll-view-refresh-dash 2s linear infinite;
|
|
}
|
|
|
|
@keyframes uni-scroll-view-refresh-rotate {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
@keyframes uni-scroll-view-refresh-dash {
|
|
0% {
|
|
stroke-dasharray: 1, 200;
|
|
stroke-dashoffset: 0;
|
|
}
|
|
|
|
50% {
|
|
stroke-dasharray: 89, 200;
|
|
stroke-dashoffset: -35px;
|
|
}
|
|
|
|
100% {
|
|
stroke-dasharray: 89, 200;
|
|
stroke-dashoffset: -124px;
|
|
}
|
|
}
|
|
</style>
|
|
|