infiniteScroll: add infiniteScroll component (#15567)

pull/15541/head^2
iamkun 2019-05-27 16:28:13 +08:00 committed by luckyCao
parent c42716000c
commit 5fea8b46f2
16 changed files with 631 additions and 3 deletions

View File

@ -27,6 +27,7 @@ const install = function(Vue, opts = {}) {
Vue.component(component.name, component); Vue.component(component.name, component);
}); });
Vue.use(InfiniteScroll);
Vue.use(Loading.directive); Vue.use(Loading.directive);
Vue.prototype.$ELEMENT = { Vue.prototype.$ELEMENT = {
@ -76,7 +77,7 @@ ComponentNames.forEach(name => {
package: name package: name
})); }));
if (['Loading', 'MessageBox', 'Notification', 'Message'].indexOf(componentName) === -1) { if (['Loading', 'MessageBox', 'Notification', 'Message', 'InfiniteScroll'].indexOf(componentName) === -1) {
installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, { installTemplate.push(render(INSTALL_COMPONENT_TEMPLATE, {
name: componentName, name: componentName,
component: name component: name

View File

@ -73,5 +73,6 @@
"link": "./packages/link/index.js", "link": "./packages/link/index.js",
"divider": "./packages/divider/index.js", "divider": "./packages/divider/index.js",
"image": "./packages/image/index.js", "image": "./packages/image/index.js",
"calendar": "./packages/calendar/index.js" "calendar": "./packages/calendar/index.js",
"infiniteScroll": "./packages/infiniteScroll/index.js"
} }

View File

@ -41,3 +41,4 @@
@import "./upload.scss"; @import "./upload.scss";
@import "./divider.scss"; @import "./divider.scss";
@import "./image.scss"; @import "./image.scss";
@import "./infiniteScroll.scss";

View File

@ -0,0 +1,48 @@
.infinite-list {
height: 300px;
padding: 0;
margin: 0;
list-style: none;
overflow: auto;
.infinite-list-item {
display: flex;
align-items: center;
justify-content: center;
height: 50px;
background: #e8f3fe;
margin: 10px;
color: lighten(#1989fa, 20%);
& + .list-item {
margin-top: 10px
}
}
}
.infinite-list-wrapper {
height: 300px;
overflow: auto;
text-align: center;
.list{
padding: 0;
margin: 0;
list-style: none;
}
.list-item{
display: flex;
align-items: center;
justify-content: center;
height: 50px;
background: #fff6f6;
color: #ff8484;
& + .list-item {
margin-top: 10px
}
}
}

View File

@ -0,0 +1,87 @@
## InfiniteScroll
Load more data while reach bottom of the page
### Basic usage
Add `v-infinite-scroll` to the list to automatically execute loading method when scrolling to the bottom.
:::demo
```html
<template>
<ul class="infinite-list" v-infinite-scroll="load">
<li v-for="i in count" class="infinite-list-item">{{ i }}</li>
</ul>
</template>
<script>
export default {
data () {
return {
count: 0
}
},
methods: {
load () {
this.count += 2
}
}
}
</script>
```
:::
### Disable Loading
:::demo
```html
<template>
<div class="infinite-list-wrapper">
<ul
class="list"
v-infinite-scroll="load"
infinite-scroll-disabled="disabled">
<li v-for="i in count" class="list-item">{{ i }}</li>
</ul>
<p v-if="loading">loading...</p>
<p v-if="noMore">Mo more</p>
</div>
</template>
<script>
export default {
data () {
return {
count: 10,
loading: false
}
},
computed: {
noMore () {
return this.count >= 20
},
disabled () {
return this.loading || this.noMore
}
},
methods: {
load () {
this.loading = true
setTimeout(() => {
this.count += 2
this.loading = false
}, 2000)
}
}
}
</script>
```
:::
### Attributes
| Attribute | Description | Type | Accepted values | Default |
| -------------- | ------------------------------ | --------- | ------------------------------------ | ------- |
| infinite-scroll-disabled | is disabled | boolean | - |false |
| infinite-scroll-delay | throttle delay (ms) | number | - |200 |
| infinite-scroll-distance| trigger distance (px) | number |- |0 |
| infinite-scroll-immediate |Whether to execute the loading method immediately, in case the content cannot be filled up in the initial state. | boolean | - |true |

View File

@ -0,0 +1,87 @@
## InfiniteScroll
Load more data while reach bottom of the page
### Basic usage
Add `v-infinite-scroll` to the list to automatically execute loading method when scrolling to the bottom.
:::demo
```html
<template>
<ul class="infinite-list" v-infinite-scroll="load">
<li v-for="i in count" class="infinite-list-item">{{ i }}</li>
</ul>
</template>
<script>
export default {
data () {
return {
count: 0
}
},
methods: {
load () {
this.count += 2
}
}
}
</script>
```
:::
### Disable Loading
:::demo
```html
<template>
<div class="infinite-list-wrapper">
<ul
class="list"
v-infinite-scroll="load"
infinite-scroll-disabled="disabled">
<li v-for="i in count" class="list-item">{{ i }}</li>
</ul>
<p v-if="loading">loading...</p>
<p v-if="noMore">Mo more</p>
</div>
</template>
<script>
export default {
data () {
return {
count: 10,
loading: false
}
},
computed: {
noMore () {
return this.count >= 20
},
disabled () {
return this.loading || this.noMore
}
},
methods: {
load () {
this.loading = true
setTimeout(() => {
this.count += 2
this.loading = false
}, 2000)
}
}
}
</script>
```
:::
### Attributes
| Attribute | Description | Type | Accepted values | Default |
| -------------- | ------------------------------ | --------- | ------------------------------------ | ------- |
| infinite-scroll-disabled | is disabled | boolean | - |false |
| infinite-scroll-delay | throttle delay (ms) | number | - |200 |
| infinite-scroll-distance| trigger distance (px) | number |- |0 |
| infinite-scroll-immediate |Whether to execute the loading method immediately, in case the content cannot be filled up in the initial state. | boolean | - |true |

View File

@ -0,0 +1,87 @@
## InfiniteScroll
Load more data while reach bottom of the page
### Basic usage
Add `v-infinite-scroll` to the list to automatically execute loading method when scrolling to the bottom.
:::demo
```html
<template>
<ul class="infinite-list" v-infinite-scroll="load">
<li v-for="i in count" class="infinite-list-item">{{ i }}</li>
</ul>
</template>
<script>
export default {
data () {
return {
count: 0
}
},
methods: {
load () {
this.count += 2
}
}
}
</script>
```
:::
### Disable Loading
:::demo
```html
<template>
<div class="infinite-list-wrapper">
<ul
class="list"
v-infinite-scroll="load"
infinite-scroll-disabled="disabled">
<li v-for="i in count" class="list-item">{{ i }}</li>
</ul>
<p v-if="loading">loading...</p>
<p v-if="noMore">Mo more</p>
</div>
</template>
<script>
export default {
data () {
return {
count: 10,
loading: false
}
},
computed: {
noMore () {
return this.count >= 20
},
disabled () {
return this.loading || this.noMore
}
},
methods: {
load () {
this.loading = true
setTimeout(() => {
this.count += 2
this.loading = false
}, 2000)
}
}
}
</script>
```
:::
### Attributes
| Attribute | Description | Type | Accepted values | Default |
| -------------- | ------------------------------ | --------- | ------------------------------------ | ------- |
| infinite-scroll-disabled | is disabled | boolean | - |false |
| infinite-scroll-delay | throttle delay (ms) | number | - |200 |
| infinite-scroll-distance| trigger distance (px) | number |- |0 |
| infinite-scroll-immediate |Whether to execute the loading method immediately, in case the content cannot be filled up in the initial state. | boolean | - |true |

View File

@ -0,0 +1,87 @@
## InfiniteScroll 无限滚动
滚动至底部时,加载更多数据。
### 基础用法
在要实现滚动加载的列表上上添加`v-infinite-scroll`,并赋值相应的加载方法,可实现滚动到底部时自动执行加载方法。
:::demo
```html
<template>
<ul class="infinite-list" v-infinite-scroll="load">
<li v-for="i in count" class="infinite-list-item">{{ i }}</li>
</ul>
</template>
<script>
export default {
data () {
return {
count: 0
}
},
methods: {
load () {
this.count += 2
}
}
}
</script>
```
:::
### 禁用加载
:::demo
```html
<template>
<div class="infinite-list-wrapper">
<ul
class="list"
v-infinite-scroll="load"
infinite-scroll-disabled="disabled">
<li v-for="i in count" class="list-item">{{ i }}</li>
</ul>
<p v-if="loading">加载中...</p>
<p v-if="noMore">没有更多了</p>
</div>
</template>
<script>
export default {
data () {
return {
count: 10,
loading: false
}
},
computed: {
noMore () {
return this.count >= 20
},
disabled () {
return this.loading || this.noMore
}
},
methods: {
load () {
this.loading = true
setTimeout(() => {
this.count += 2
this.loading = false
}, 2000)
}
}
}
</script>
```
:::
### Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| -------------- | ------------------------------ | --------- | ------------------------------------ | ------- |
| infinite-scroll-disabled | 是否禁用 | boolean | - |false |
| infinite-scroll-delay | 节流时延单位为ms | number | - |200 |
| infinite-scroll-distance| 触发加载的距离阈值单位为px | number |- |0 |
| infinite-scroll-immediate | 是否立即执行加载方法,以防初始状态下内容无法撑满容器。| boolean | - |true |

View File

@ -267,6 +267,10 @@
{ {
"path": "/image", "path": "/image",
"title": "Image 图片" "title": "Image 图片"
},
{
"path": "/infiniteScroll",
"title": "InfiniteScroll 无限滚动"
} }
] ]
} }
@ -541,6 +545,10 @@
{ {
"path": "/image", "path": "/image",
"title": "Image" "title": "Image"
},
{
"path": "/infiniteScroll",
"title": "InfiniteScroll"
} }
] ]
} }
@ -815,6 +823,10 @@
{ {
"path": "/image", "path": "/image",
"title": "Image" "title": "Image"
},
{
"path": "/infiniteScroll",
"title": "InfiniteScroll"
} }
] ]
} }
@ -1089,6 +1101,10 @@
{ {
"path": "/image", "path": "/image",
"title": "Image" "title": "Image"
},
{
"path": "/infiniteScroll",
"title": "InfiniteScroll"
} }
] ]
} }

View File

@ -0,0 +1,8 @@
import InfiniteScroll from './src/main.js';
/* istanbul ignore next */
InfiniteScroll.install = function(Vue) {
Vue.directive(InfiniteScroll.name, InfiniteScroll);
};
export default InfiniteScroll;

View File

@ -0,0 +1,147 @@
import throttle from 'throttle-debounce/debounce';
import {
isHtmlElement,
isFunction,
isUndefined,
isDefined
} from 'element-ui/src/utils/types';
import {
getScrollContainer
} from 'element-ui/src/utils/dom';
const getStyleComputedProperty = (element, property) => {
if (element === window) {
element = document.documentElement;
}
if (element.nodeType !== 1) {
return [];
}
// NOTE: 1 DOM access here
const css = window.getComputedStyle(element, null);
return property ? css[property] : css;
};
const entries = (obj) => {
return Object.keys(obj || {})
.map(key => ([key, obj[key]]));
};
const getPositionSize = (el, prop) => {
return el === window || el === document
? document.documentElement[prop]
: el[prop];
};
const getOffsetHeight = el => {
return getPositionSize(el, 'offsetHeight');
};
const getClientHeight = el => {
return getPositionSize(el, 'clientHeight');
};
const scope = 'ElInfiniteScroll';
const attributes = {
delay: {
type: Number,
default: 200
},
distance: {
type: Number,
default: 0
},
disabled: {
type: Boolean,
default: false
},
immediate: {
type: Boolean,
default: true
}
};
const getScrollOptions = (el, vm) => {
if (!isHtmlElement(el)) return {};
return entries(attributes).reduce((map, [key, option]) => {
const { type, default: defaultValue } = option;
let value = el.getAttribute(`infinite-scroll-${key}`);
value = isUndefined(vm[value]) ? value : vm[value];
switch (type) {
case Number:
value = Number(value);
value = Number.isNaN(value) ? defaultValue : value;
break;
case Boolean:
value = isDefined(value) ? value === 'false' ? false : Boolean(value) : defaultValue;
break;
default:
value = type(value);
}
map[key] = value;
return map;
}, {});
};
const getElementTop = el => el.getBoundingClientRect().top;
const handleScroll = function(cb) {
const { el, vm, container, observer } = this[scope];
const { distance, disabled } = getScrollOptions(el, vm);
if (disabled) return;
let shouldTrigger = false;
if (container === el) {
// be aware of difference between clientHeight & offsetHeight & window.getComputedStyle().height
const scrollBottom = container.scrollTop + getClientHeight(container);
shouldTrigger = container.scrollHeight - scrollBottom <= distance;
} else {
const heightBelowTop = getOffsetHeight(el) + getElementTop(el) - getElementTop(container);
const offsetHeight = getOffsetHeight(container);
const borderBottom = Number.parseFloat(getStyleComputedProperty(container, 'borderBottomWidth'));
shouldTrigger = heightBelowTop - offsetHeight + borderBottom <= distance;
}
if (shouldTrigger && isFunction(cb)) {
cb.call(vm);
} else if (observer) {
observer.disconnect();
this[scope].observer = null;
}
};
export default {
name: 'InfiniteScroll',
inserted(el, binding, vnode) {
const cb = binding.value;
const vm = vnode.context;
// only include vertical scroll
const container = getScrollContainer(el, true);
const { delay, immediate } = getScrollOptions(el, vm);
const onScroll = throttle(delay, handleScroll.bind(el, cb));
el[scope] = { el, vm, container, onScroll };
if (container) {
container.addEventListener('scroll', onScroll);
if (immediate) {
const observer = el[scope].observer = new MutationObserver(onScroll);
observer.observe(container, { childList: true, subtree: true });
onScroll();
}
}
},
unbind(el) {
const { container, onScroll } = el[scope];
if (container) {
container.removeEventListener('scroll', onScroll);
}
}
};

View File

@ -75,6 +75,7 @@ import Link from '../packages/link/index.js';
import Divider from '../packages/divider/index.js'; import Divider from '../packages/divider/index.js';
import Image from '../packages/image/index.js'; import Image from '../packages/image/index.js';
import Calendar from '../packages/calendar/index.js'; import Calendar from '../packages/calendar/index.js';
import InfiniteScroll from '../packages/infiniteScroll/index.js';
import locale from 'element-ui/src/locale'; import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition'; import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
@ -161,6 +162,7 @@ const install = function(Vue, opts = {}) {
Vue.component(component.name, component); Vue.component(component.name, component);
}); });
Vue.use(InfiniteScroll);
Vue.use(Loading.directive); Vue.use(Loading.directive);
Vue.prototype.$ELEMENT = { Vue.prototype.$ELEMENT = {
@ -263,5 +265,6 @@ export default {
Link, Link,
Divider, Divider,
Image, Image,
Calendar Calendar,
InfiniteScroll
}; };

View File

@ -9,3 +9,16 @@ export function isObject(obj) {
export function isHtmlElement(node) { export function isHtmlElement(node) {
return node && node.nodeType === Node.ELEMENT_NODE; return node && node.nodeType === Node.ELEMENT_NODE;
} }
export const isFunction = (functionToCheck) => {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
};
export const isUndefined = (val)=> {
return val === void 0;
};
export const isDefined = (val) => {
return val !== undefined && val !== null;
};

View File

@ -0,0 +1,32 @@
import { createVue, wait, destroyVM } from '../util';
describe('InfiniteScroll', () => {
let vm;
afterEach(() => {
destroyVM(vm);
});
it('create', async() => {
vm = createVue({
template: `
<ul ref="scrollTarget" v-infinite-scroll="load" style="height: 300px;overflow: auto;">
<li v-for="i in count" style="display: flex;height: 50px;">{{ i }}</li>
</ul>
`,
data() {
return {
count: 0
};
},
methods: {
load() {
this.count += 2;
}
}
}, true);
vm.$refs.scrollTarget.scrollTop = 2000;
await wait();
expect(vm.$el.innerText.indexOf('2') > -1).to.be.true;
});
});

View File

@ -74,6 +74,7 @@ import { ElDivider } from './divider'
import { ElIcon } from './icon' import { ElIcon } from './icon'
import { ElCalendar } from './calendar' import { ElCalendar } from './calendar'
import { ElImage } from './image' import { ElImage } from './image'
import { ElInfiniteScroll } from './infiniteScroll'
export interface InstallationOptions { export interface InstallationOptions {
locale: any, locale: any,
@ -320,3 +321,6 @@ export class Icon extends ElIcon {}
/** Calendar Component */ /** Calendar Component */
export class Calendar extends ElCalendar {} export class Calendar extends ElCalendar {}
/** InfiniteScroll Component */
export class InfiniteScroll extends ElInfiniteScroll {}

6
types/infiniteScroll.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
import { VNodeDirective } from 'vue'
export interface ElLoadingDirective extends VNodeDirective {
name: 'infinite-scroll',
value: Function
}