New component: Scroll

This commit is contained in:
Sergio Crisostomo 2017-09-27 14:46:42 +02:00
parent d9e0bcc987
commit be01f0b4bb
9 changed files with 399 additions and 9 deletions

View file

@ -8,7 +8,8 @@
"sourceType": "module"
},
"env": {
"browser": true
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"plugins": ["vue"],

7
package-lock.json generated
View file

@ -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",

View file

@ -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"
},

View file

@ -0,0 +1,3 @@
import Scroll from './scroll.vue';
export default Scroll;

View 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>

View 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>

View file

@ -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';
@ -46,8 +47,8 @@ import Tooltip from './components/tooltip';
import Transfer from './components/transfer';
import Tree from './components/tree';
import Upload from './components/upload';
import { Row, Col } from './components/grid';
import { Select, Option, OptionGroup } from './components/select';
import {Row, Col} from './components/grid';
import {Select, Option, OptionGroup} from './components/select';
import locale from './locale';
const iview = {
@ -84,6 +85,7 @@ const iview = {
Input,
iInput: Input,
InputNumber,
Scroll,
LoadingBar,
Menu,
iMenu: Menu,
@ -111,7 +113,7 @@ const iview = {
Spin,
Step: Steps.Step,
Steps,
// Switch,
// Switch,
iSwitch: Switch,
iTable: Table,
Table,
@ -127,11 +129,11 @@ const iview = {
Upload
};
const install = function (Vue, opts = {}) {
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]);
});
@ -147,4 +149,4 @@ if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
module.exports = Object.assign(iview, {install}); // eslint-disable-line no-undef
module.exports = Object.assign(iview, {install}); // eslint-disable-line no-undef

View file

@ -13,6 +13,7 @@
@import "checkbox";
@import "switch";
@import "input-number";
@import "scroll";
@import "tag";
@import "loading-bar";
@import "progress";
@ -41,4 +42,4 @@
@import "tree";
@import "avatar";
@import "color-picker";
@import "auto-complete";
@import "auto-complete";

View 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} {
}
}