Pārlūkot izejas kodu

infiniteScroll: add infiniteScroll component (#15567)

iamkun 6 gadi atpakaļ
vecāks
revīzija
5fea8b46f2

+ 2 - 1
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

+ 2 - 1
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"
 }

+ 1 - 0
examples/demo-styles/index.scss

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

+ 48 - 0
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
+    }
+  }
+}
+   
+
+ 

+ 87 - 0
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
+<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 |

+ 87 - 0
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
+<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 |

+ 87 - 0
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
+<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 |

+ 87 - 0
examples/docs/zh-CN/infiniteScroll.md

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

+ 16 - 0
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"
             }
           ]
         }

+ 8 - 0
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;

+ 147 - 0
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);
+    }
+  }
+};
+

+ 4 - 1
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
 };

+ 13 - 0
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;
+};

+ 32 - 0
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: `
+      <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;
+  });
+});
+

+ 4 - 0
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 {}

+ 6 - 0
types/infiniteScroll.d.ts

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