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 + + + +``` +::: + + +### 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 + + + +``` +::: + + +### 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 + + + +``` +::: + + +### 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