diff --git a/build/bin/build-entry.js b/build/bin/build-entry.js
index b68e5a8728c..8639eb551f2 100644
--- a/build/bin/build-entry.js
+++ b/build/bin/build-entry.js
@@ -27,6 +27,7 @@ const install = function(Vue, opts = {}) {
Vue.component(component.name, component);
});
+ Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
Vue.prototype.$ELEMENT = {
@@ -76,7 +77,7 @@ ComponentNames.forEach(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, {
name: componentName,
component: name
diff --git a/components.json b/components.json
index 8d6a6b8005b..c610146d424 100644
--- a/components.json
+++ b/components.json
@@ -73,5 +73,6 @@
"link": "./packages/link/index.js",
"divider": "./packages/divider/index.js",
"image": "./packages/image/index.js",
- "calendar": "./packages/calendar/index.js"
+ "calendar": "./packages/calendar/index.js",
+ "infiniteScroll": "./packages/infiniteScroll/index.js"
}
diff --git a/examples/demo-styles/index.scss b/examples/demo-styles/index.scss
index ae0f0d8b312..f8d200f6ece 100644
--- a/examples/demo-styles/index.scss
+++ b/examples/demo-styles/index.scss
@@ -41,3 +41,4 @@
@import "./upload.scss";
@import "./divider.scss";
@import "./image.scss";
+@import "./infiniteScroll.scss";
diff --git a/examples/demo-styles/infiniteScroll.scss b/examples/demo-styles/infiniteScroll.scss
new file mode 100644
index 00000000000..6db0900b857
--- /dev/null
+++ b/examples/demo-styles/infiniteScroll.scss
@@ -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
+ }
+ }
+}
+
+
+
diff --git a/examples/docs/en-US/infiniteScroll.md b/examples/docs/en-US/infiniteScroll.md
new file mode 100644
index 00000000000..f337a0942d3
--- /dev/null
+++ b/examples/docs/en-US/infiniteScroll.md
@@ -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
+
+
+
+
+
+```
+:::
+
+### Disable Loading
+
+:::demo
+```html
+
+
+
+
loading...
+
Mo more
+
+
+
+
+```
+:::
+
+
+### 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 |
diff --git a/examples/docs/es/infiniteScroll.md b/examples/docs/es/infiniteScroll.md
new file mode 100644
index 00000000000..f337a0942d3
--- /dev/null
+++ b/examples/docs/es/infiniteScroll.md
@@ -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
+
+
+
+
+
+```
+:::
+
+### Disable Loading
+
+:::demo
+```html
+
+
+
+
loading...
+
Mo more
+
+
+
+
+```
+:::
+
+
+### 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 |
diff --git a/examples/docs/fr-FR/infiniteScroll.md b/examples/docs/fr-FR/infiniteScroll.md
new file mode 100644
index 00000000000..f337a0942d3
--- /dev/null
+++ b/examples/docs/fr-FR/infiniteScroll.md
@@ -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
+
+
+
+
+
+```
+:::
+
+### Disable Loading
+
+:::demo
+```html
+
+
+
+
loading...
+
Mo more
+
+
+
+
+```
+:::
+
+
+### 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 |
diff --git a/examples/docs/zh-CN/infiniteScroll.md b/examples/docs/zh-CN/infiniteScroll.md
new file mode 100644
index 00000000000..faf119c6496
--- /dev/null
+++ b/examples/docs/zh-CN/infiniteScroll.md
@@ -0,0 +1,87 @@
+## InfiniteScroll 无限滚动
+
+滚动至底部时,加载更多数据。
+
+### 基础用法
+在要实现滚动加载的列表上上添加`v-infinite-scroll`,并赋值相应的加载方法,可实现滚动到底部时自动执行加载方法。
+:::demo
+```html
+
+
+
+
+
+```
+:::
+
+### 禁用加载
+
+:::demo
+```html
+
+
+
+
+
+```
+:::
+
+
+### Attributes
+
+| 参数 | 说明 | 类型 | 可选值 | 默认值 |
+| -------------- | ------------------------------ | --------- | ------------------------------------ | ------- |
+| infinite-scroll-disabled | 是否禁用 | boolean | - |false |
+| infinite-scroll-delay | 节流时延,单位为ms | number | - |200 |
+| infinite-scroll-distance| 触发加载的距离阈值,单位为px | number |- |0 |
+| infinite-scroll-immediate | 是否立即执行加载方法,以防初始状态下内容无法撑满容器。| boolean | - |true |
diff --git a/examples/nav.config.json b/examples/nav.config.json
index bfd297fc004..fbce249109d 100644
--- a/examples/nav.config.json
+++ b/examples/nav.config.json
@@ -267,6 +267,10 @@
{
"path": "/image",
"title": "Image 图片"
+ },
+ {
+ "path": "/infiniteScroll",
+ "title": "InfiniteScroll 无限滚动"
}
]
}
@@ -541,6 +545,10 @@
{
"path": "/image",
"title": "Image"
+ },
+ {
+ "path": "/infiniteScroll",
+ "title": "InfiniteScroll"
}
]
}
@@ -815,6 +823,10 @@
{
"path": "/image",
"title": "Image"
+ },
+ {
+ "path": "/infiniteScroll",
+ "title": "InfiniteScroll"
}
]
}
@@ -1089,6 +1101,10 @@
{
"path": "/image",
"title": "Image"
+ },
+ {
+ "path": "/infiniteScroll",
+ "title": "InfiniteScroll"
}
]
}
diff --git a/packages/infiniteScroll/index.js b/packages/infiniteScroll/index.js
new file mode 100644
index 00000000000..228bbba43fe
--- /dev/null
+++ b/packages/infiniteScroll/index.js
@@ -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;
diff --git a/packages/infiniteScroll/src/main.js b/packages/infiniteScroll/src/main.js
new file mode 100644
index 00000000000..202f26cdbc1
--- /dev/null
+++ b/packages/infiniteScroll/src/main.js
@@ -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);
+ }
+ }
+};
+
diff --git a/src/index.js b/src/index.js
index d1d4f69602f..cf398f1976b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -75,6 +75,7 @@ import Link from '../packages/link/index.js';
import Divider from '../packages/divider/index.js';
import Image from '../packages/image/index.js';
import Calendar from '../packages/calendar/index.js';
+import InfiniteScroll from '../packages/infiniteScroll/index.js';
import locale from 'element-ui/src/locale';
import CollapseTransition from 'element-ui/src/transitions/collapse-transition';
@@ -161,6 +162,7 @@ const install = function(Vue, opts = {}) {
Vue.component(component.name, component);
});
+ Vue.use(InfiniteScroll);
Vue.use(Loading.directive);
Vue.prototype.$ELEMENT = {
@@ -263,5 +265,6 @@ export default {
Link,
Divider,
Image,
- Calendar
+ Calendar,
+ InfiniteScroll
};
diff --git a/src/utils/types.js b/src/utils/types.js
index a5d44923132..bfcceed5f0f 100644
--- a/src/utils/types.js
+++ b/src/utils/types.js
@@ -9,3 +9,16 @@ export function isObject(obj) {
export function isHtmlElement(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;
+};
diff --git a/test/unit/specs/infiniteScroll.spec.js b/test/unit/specs/infiniteScroll.spec.js
new file mode 100644
index 00000000000..fc89748eabb
--- /dev/null
+++ b/test/unit/specs/infiniteScroll.spec.js
@@ -0,0 +1,32 @@
+import { createVue, wait, destroyVM } from '../util';
+
+describe('InfiniteScroll', () => {
+ let vm;
+ afterEach(() => {
+ destroyVM(vm);
+ });
+
+ it('create', async() => {
+ vm = createVue({
+ template: `
+
+ `,
+ 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;
+ });
+});
+
diff --git a/types/element-ui.d.ts b/types/element-ui.d.ts
index eb15a7d41fb..a352f6a0dae 100644
--- a/types/element-ui.d.ts
+++ b/types/element-ui.d.ts
@@ -74,6 +74,7 @@ import { ElDivider } from './divider'
import { ElIcon } from './icon'
import { ElCalendar } from './calendar'
import { ElImage } from './image'
+import { ElInfiniteScroll } from './infiniteScroll'
export interface InstallationOptions {
locale: any,
@@ -320,3 +321,6 @@ export class Icon extends ElIcon {}
/** Calendar Component */
export class Calendar extends ElCalendar {}
+
+/** InfiniteScroll Component */
+export class InfiniteScroll extends ElInfiniteScroll {}
diff --git a/types/infiniteScroll.d.ts b/types/infiniteScroll.d.ts
new file mode 100644
index 00000000000..a672195e123
--- /dev/null
+++ b/types/infiniteScroll.d.ts
@@ -0,0 +1,6 @@
+import { VNodeDirective } from 'vue'
+
+export interface ElLoadingDirective extends VNodeDirective {
+ name: 'infinite-scroll',
+ value: Function
+}
\ No newline at end of file