New component: Scroll
This commit is contained in:
parent
d9e0bcc987
commit
be01f0b4bb
9 changed files with 399 additions and 9 deletions
3
src/components/scroll/index.js
Normal file
3
src/components/scroll/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Scroll from './scroll.vue';
|
||||
|
||||
export default Scroll;
|
39
src/components/scroll/loading-component.vue
Normal file
39
src/components/scroll/loading-component.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
|
||||
<template lang="html">
|
||||
<div :class="wrapperClasses">
|
||||
<div :class="spinnerClasses">
|
||||
<Spin fix>
|
||||
<Icon type="load-c" size="18" :class="iconClasses"></Icon>
|
||||
<div v-if="text" :class="textClasses">{{text}}</div>
|
||||
</Spin>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const prefixCls = 'ivu-scroll';
|
||||
|
||||
export default {
|
||||
props: ['text', 'active', 'spinnerHeight'],
|
||||
computed: {
|
||||
wrapperClasses() {
|
||||
return [
|
||||
`${prefixCls}-loader-wrapper`,
|
||||
{
|
||||
[`${prefixCls}-loader-wrapper-active`]: this.active
|
||||
}
|
||||
];
|
||||
},
|
||||
spinnerClasses() {
|
||||
return `${prefixCls}-spinner`;
|
||||
},
|
||||
iconClasses() {
|
||||
return `${prefixCls}-spinner-icon`;
|
||||
},
|
||||
textClasses() {
|
||||
return `${prefixCls}-loader-text`;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
271
src/components/scroll/scroll.vue
Normal file
271
src/components/scroll/scroll.vue
Normal file
|
@ -0,0 +1,271 @@
|
|||
|
||||
<template>
|
||||
<div :class="wrapClasses" style="touch-action: none;">
|
||||
<div
|
||||
:class="scrollContainerClasses"
|
||||
@scroll="handleScroll"
|
||||
@wheel="onWheel"
|
||||
@touchstart="onPointerDown"
|
||||
ref="scrollContainer"
|
||||
>
|
||||
<div :class="loaderClasses" :style="{paddingTop: wrapperPadding.paddingTop}" ref="toploader">
|
||||
<loader :text="loadingText" :active="showTopLoader"></loader>
|
||||
</div>
|
||||
<div :class="slotContainerClasses" ref="scrollContent">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div :class="loaderClasses" :style="{paddingBottom: wrapperPadding.paddingBottom}" ref="bottomLoader">
|
||||
<loader :text="loadingText" :active="showBottomLoader"></loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import throttle from 'lodash.throttle';
|
||||
import loader from './loading-component.vue';
|
||||
|
||||
const prefixCls = 'ivu-scroll';
|
||||
const dragConfig = {
|
||||
sensitivity: 10,
|
||||
minimumStartDragOffset: 5, // minimum start drag offset
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'Scroll',
|
||||
mixins: [],
|
||||
components: {loader},
|
||||
props: {
|
||||
onReachTop: {
|
||||
type: Function,
|
||||
default: () => Promise.resolve()
|
||||
},
|
||||
onReachBottom: {
|
||||
type: Function,
|
||||
default: () => Promise.resolve()
|
||||
},
|
||||
onReachEdge: {
|
||||
type: Function,
|
||||
default: () => Promise.resolve()
|
||||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
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: () => {},
|
||||
};
|
||||
},
|
||||
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, 2000);
|
||||
});
|
||||
},
|
||||
|
||||
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 (var 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(dir)];
|
||||
callbacks.push(dir > 0 ? this.onReachTop() : this.onReachBottom());
|
||||
|
||||
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(() => {
|
||||
window.removeEventListener('touchend', this.pointerUpHandler);
|
||||
this.$refs.scrollContainer.removeEventListener('touchmove', this.pointerMoveHandler);
|
||||
this.touchScroll = false;
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
onWheel() {
|
||||
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);
|
||||
|
||||
// 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 > 20) this.onCallback(1);
|
||||
} else if (direction < 0 && this.reachedBottomScrollLimit) {
|
||||
this.bottomRubberPadding += 6 - this.bottomRubberPadding / 4;
|
||||
if (this.bottomRubberPadding > 20) 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;
|
||||
|
||||
if (scrollDirection == -1 && displacement <= dragConfig.sensitivity) {
|
||||
this.reachedBottomScrollLimit = true;
|
||||
} else if (scrollDirection >= 0 && el.scrollTop == 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);
|
||||
window.addEventListener('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;
|
||||
else return;
|
||||
}
|
||||
},
|
||||
|
||||
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>
|
Loading…
Add table
Add a link
Reference in a new issue