304 lines
12 KiB
Vue
304 lines
12 KiB
Vue
<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">
|
|
<loader :text="localeLoadingText" :active="showTopLoader"></loader>
|
|
</div>
|
|
<div :class="slotContainerClasses" ref="scrollContent">
|
|
<slot></slot>
|
|
</div>
|
|
<div :class="loaderClasses" :style="{paddingBottom: wrapperPadding.paddingBottom}" ref="bottomLoader">
|
|
<loader :text="localeLoadingText" :active="showBottomLoader"></loader>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import throttle from 'lodash.throttle';
|
|
import loader from './loading-component.vue';
|
|
import { on, off } from '../../utils/dom';
|
|
import Locale from '../../mixins/locale';
|
|
|
|
const prefixCls = 'ivu-scroll';
|
|
const dragConfig = {
|
|
sensitivity: 10,
|
|
minimumStartDragOffset: 5, // minimum start drag offset
|
|
};
|
|
|
|
const noop = () => Promise.resolve();
|
|
|
|
export default {
|
|
name: 'Scroll',
|
|
mixins: [ Locale ],
|
|
components: {loader},
|
|
props: {
|
|
height: {
|
|
type: [Number, String],
|
|
default: 300
|
|
},
|
|
onReachTop: {
|
|
type: Function
|
|
},
|
|
onReachBottom: {
|
|
type: Function
|
|
},
|
|
onReachEdge: {
|
|
type: Function
|
|
},
|
|
loadingText: {
|
|
type: String
|
|
},
|
|
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'
|
|
};
|
|
},
|
|
localeLoadingText () {
|
|
if (this.loadingText === undefined) {
|
|
return this.t('i.select.loading');
|
|
} else {
|
|
return this.loadingText;
|
|
}
|
|
},
|
|
},
|
|
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() {
|
|
const el = this.$refs.scrollContainer;
|
|
if (this.isLoading || !el) return;
|
|
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>
|