New component: Scroll
This commit is contained in:
parent
d9e0bcc987
commit
be01f0b4bb
9 changed files with 399 additions and 9 deletions
|
@ -8,7 +8,8 @@
|
|||
"sourceType": "module"
|
||||
},
|
||||
"env": {
|
||||
"browser": true
|
||||
"browser": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"plugins": ["vue"],
|
||||
|
|
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "iview",
|
||||
"version": "2.3.2",
|
||||
"version": "2.4.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -12493,6 +12493,11 @@
|
|||
"lodash.escape": "3.2.0"
|
||||
}
|
||||
},
|
||||
"lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ="
|
||||
},
|
||||
"lodash.uniq": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"core-js": "^2.5.0",
|
||||
"deepmerge": "^1.5.1",
|
||||
"element-resize-detector": "^1.1.12",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"popper.js": "^0.6.4",
|
||||
"tinycolor2": "^1.4.1"
|
||||
},
|
||||
|
|
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>
|
|
@ -23,6 +23,7 @@ import Form from './components/form';
|
|||
import Icon from './components/icon';
|
||||
import Input from './components/input';
|
||||
import InputNumber from './components/input-number';
|
||||
import Scroll from './components/scroll';
|
||||
import LoadingBar from './components/loading-bar';
|
||||
import Menu from './components/menu';
|
||||
import Message from './components/message';
|
||||
|
@ -84,6 +85,7 @@ const iview = {
|
|||
Input,
|
||||
iInput: Input,
|
||||
InputNumber,
|
||||
Scroll,
|
||||
LoadingBar,
|
||||
Menu,
|
||||
iMenu: Menu,
|
||||
|
@ -131,7 +133,7 @@ const install = function (Vue, opts = {}) {
|
|||
locale.use(opts.locale);
|
||||
locale.i18n(opts.i18n);
|
||||
|
||||
Object.keys(iview).forEach((key) => {
|
||||
Object.keys(iview).forEach(key => {
|
||||
Vue.component(key, iview[key]);
|
||||
});
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
@import "checkbox";
|
||||
@import "switch";
|
||||
@import "input-number";
|
||||
@import "scroll";
|
||||
@import "tag";
|
||||
@import "loading-bar";
|
||||
@import "progress";
|
||||
|
|
67
src/styles/components/scroll.less
Normal file
67
src/styles/components/scroll.less
Normal file
|
@ -0,0 +1,67 @@
|
|||
@scroll-prefix-cls: ~"@{css-prefix}scroll";
|
||||
|
||||
.@{scroll-prefix-cls} {
|
||||
&-wrapper {
|
||||
width: auto;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&-container {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
&-content {
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
&-content-loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&-loader {
|
||||
text-align: center;
|
||||
padding: 0px;
|
||||
transition: padding 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
.@{scroll-prefix-cls}-loader-wrapper {
|
||||
padding: 5px 0;
|
||||
height: 0;
|
||||
background-color: inherit;
|
||||
transform: scale(0);
|
||||
transition: opacity .3s, transform .5s, height .5s;
|
||||
|
||||
&-active {
|
||||
height: 40px;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@keyframes ani-demo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.@{scroll-prefix-cls}-spinner {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.@{scroll-prefix-cls}-spinner-icon {
|
||||
animation: ani-demo-spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.@{scroll-prefix-cls} {
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue