├── .gitignore
├── docs
├── assets
│ └── schema.png
├── .vuepress
│ ├── public
│ │ └── favicon.ico
│ ├── styles
│ │ └── palette.styl
│ ├── theme
│ │ ├── assets
│ │ │ └── images
│ │ │ │ ├── logo.png
│ │ │ │ ├── icon-box.png
│ │ │ │ ├── icon-dir.png
│ │ │ │ ├── icon-msg.png
│ │ │ │ └── 16th-mockup.png
│ │ ├── styles
│ │ │ ├── config.styl
│ │ │ └── button.styl
│ │ ├── components
│ │ │ ├── Previewer.vue
│ │ │ └── Intro.vue
│ │ └── layouts
│ │ │ └── Layout.vue
│ └── config.js
├── zh
│ ├── README.md
│ ├── guide
│ │ ├── configure-plugin-opts.md
│ │ ├── use-with-el-table.md
│ │ ├── top-dir-scroll.md
│ │ ├── README.md
│ │ ├── start-with-hn.md
│ │ ├── use-with-filter-or-tabs.md
│ │ └── configure-load-msg.md
│ └── api
│ │ └── README.md
├── README.md
├── guide
│ ├── configure-plugin-opts.md
│ ├── top-dir-scroll.md
│ ├── use-with-el-table.md
│ ├── README.md
│ ├── start-with-hn.md
│ ├── use-with-filter-or-tabs.md
│ └── configure-load-msg.md
└── api
│ └── README.md
├── .travis.yml
├── test
└── unit
│ ├── .eslintrc.yml
│ ├── index.js
│ ├── utils.js
│ └── specs
│ ├── index.spec.js
│ └── InfiniteLoading.spec.js
├── .github
├── PULL_REQUEST_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── FEATURE_REQUEST.md
│ └── BUG_REPORT.md
├── COMMIT_CONVENTION.md
├── CODE_OF_CONDUCT.md
└── CONTRIBUTING.md
├── .editorconfig
├── scripts
├── deploy_docs.sh
├── release.sh
├── karma.conf.js
├── ssr_vue_loader.js
├── dev_template.js
└── webpack.config.js
├── src
├── index.js
├── styles
│ ├── wave-dots.less
│ ├── circles.less
│ ├── bubbles.less
│ └── spinner.less
├── components
│ ├── Spinner.vue
│ └── InfiniteLoading.vue
├── utils.js
└── config.js
├── LICENSE
├── types
└── index.d.ts
├── README.md
├── package.json
└── dist
└── vue-infinite-loading.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | npm-debug.log
4 | test/unit/coverage
5 | docs/.vuepress/dist
6 |
--------------------------------------------------------------------------------
/docs/assets/schema.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeachScript/vue-infinite-loading/HEAD/docs/assets/schema.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | after_success:
5 | - bash <(curl -s https://codecov.io/bash)
6 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeachScript/vue-infinite-loading/HEAD/docs/.vuepress/public/favicon.ico
--------------------------------------------------------------------------------
/docs/.vuepress/styles/palette.styl:
--------------------------------------------------------------------------------
1 | @require '../theme/styles/config'
2 |
3 | $accentColor = saturation(lighten($c-basic, 25%), 40%)
4 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeachScript/vue-infinite-loading/HEAD/docs/.vuepress/theme/assets/images/logo.png
--------------------------------------------------------------------------------
/docs/.vuepress/theme/assets/images/icon-box.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeachScript/vue-infinite-loading/HEAD/docs/.vuepress/theme/assets/images/icon-box.png
--------------------------------------------------------------------------------
/docs/.vuepress/theme/assets/images/icon-dir.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeachScript/vue-infinite-loading/HEAD/docs/.vuepress/theme/assets/images/icon-dir.png
--------------------------------------------------------------------------------
/docs/.vuepress/theme/assets/images/icon-msg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeachScript/vue-infinite-loading/HEAD/docs/.vuepress/theme/assets/images/icon-msg.png
--------------------------------------------------------------------------------
/docs/.vuepress/theme/assets/images/16th-mockup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeachScript/vue-infinite-loading/HEAD/docs/.vuepress/theme/assets/images/16th-mockup.png
--------------------------------------------------------------------------------
/test/unit/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | ---
2 | env:
3 | mocha: true
4 | globals:
5 | expect: true
6 | sinon: true
7 | rules:
8 | no-unused-expressions: 0
9 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_size = 2
6 | indent_style = space
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/docs/zh/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | home: true
3 | actionText: 开始使用
4 | actionLink: /zh/guide/
5 | GitHubText: 前往 GitHub
6 | features:
7 | - title: 开箱即用
8 | details: 简洁至上的 API、内置加载动画以及良好的兼容性,可立即投入生产
9 | - title: 双向支持
10 | details: 目前支持向上和向下两种加载方式,可适应于更多的应用场景
11 | - title: 结果展示
12 | details: 可配置的加载结果展示,比如没有更多数据、没有任何数据等等
13 | previewLink: //jsfiddle.net/PeachScript/a4Lxbf9w/embedded/result/
14 | ---
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | ---
5 |
6 |
9 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | home: true
3 | actionText: Get Started
4 | actionLink: /guide/
5 | GitHubText: View GitHub
6 | features:
7 | - title: Out of the box
8 | details: Clear API, internal spinners and better compatibility, ready for production immediately
9 | - title: 2-directions support
10 | details: Support top and bottom directions currently, adapt to more different scenes
11 | - title: Result display
12 | details: Configurable load result display, for example no more data, no results and etc
13 | previewLink: //jsfiddle.net/PeachScript/a4Lxbf9w/embedded/result/
14 | ---
15 |
--------------------------------------------------------------------------------
/scripts/deploy_docs.sh:
--------------------------------------------------------------------------------
1 | set -e
2 | shopt -s extglob
3 |
4 | TEMP_PATH="docs/.vuepress/.temp"
5 |
6 | # build docs
7 | npm run docs:build
8 |
9 | # prepare deploy
10 | mkdir $TEMP_PATH
11 | cd $TEMP_PATH
12 | git init
13 | git pull git@github.com:PeachScript/vue-infinite-loading.git gh-pages
14 | rm -rf ./!(old) # keep old version docs
15 | cp -r ../dist/* .
16 |
17 | # commit and push changes
18 | git add -A
19 | git commit --am -m "deploy documentation"
20 | git push -f git@github.com:PeachScript/vue-infinite-loading.git master:gh-pages
21 |
22 | # clean
23 | cd -
24 | rm -rf $TEMP_PATH
25 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/styles/config.styl:
--------------------------------------------------------------------------------
1 | /**
2 | * color config
3 | */
4 |
5 | $c-basic = #32495f
6 | $c-basic-light = desaturate(lighten($c-basic, 35%), 10%)
7 | $c-bg = #fbfcff
8 |
9 | /**
10 | * size config
11 | */
12 |
13 | $s-home-divide-ratio = 36.5%
14 | $s-home-middle-gap = 40px
15 | $s-edge-gap = 24px
16 | $s-header-height = 62px
17 | $s-preview-width = 334px
18 | $s-sidebar-width = 16.4rem
19 |
20 | /**
21 | * responsive breakpoints
22 | * @note keep inline with https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/core/lib/app/style/config.styl
23 | */
24 |
25 | $mq-narrow = 959px
26 | $mq-mobile = 719px
27 | $mq-mobile-narrow = 419px
28 |
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | set -e
2 | echo "Enter release version: "
3 | read VERSION
4 |
5 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r
6 | echo # (optional) move to a new line
7 | if [[ $REPLY =~ ^[Yy]$ ]]; then
8 | echo "Releasing $VERSION ..."
9 |
10 | # lint and test
11 | npm run lint 2>/dev/null
12 | npm test 2>/dev/null
13 |
14 | # build
15 | VERSION=$VERSION npm run build
16 |
17 | # commit
18 | git add -A
19 | git commit -m "build: build $VERSION"
20 | npm version $VERSION -m "build: release $VERSION"
21 |
22 | # publish
23 | npm publish
24 | echo "Publish $VERSION successfully!"
25 |
26 | # push
27 | git push origin refs/tags/v$VERSION
28 | git push
29 | echo "Done!"
30 | fi
31 |
--------------------------------------------------------------------------------
/scripts/karma.conf.js:
--------------------------------------------------------------------------------
1 | const webpackConfig = require('./webpack.config');
2 |
3 | delete webpackConfig.entry;
4 |
5 | // Karma configuration
6 | module.exports = function(config) {
7 | config.set({
8 | frameworks: ['mocha', 'sinon-chai'],
9 | files: [
10 | '../test/unit/index.js'
11 | ],
12 | preprocessors: {
13 | '../test/unit/index.js': 'webpack'
14 | },
15 | browsers: ['PhantomJS'],
16 | reporters: ['spec', 'coverage'],
17 | coverageReporter: {
18 | dir: '../test/unit/coverage',
19 | reporters: [
20 | { type: 'lcov', subdir: '.' },
21 | { type: 'text-summary' }
22 | ]
23 | },
24 | webpack: webpackConfig,
25 | webpackMiddleware: {
26 | stats: 'errors-only'
27 | },
28 | singleRun: true
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue.common';
2 |
3 | // mock passive event listener support and not support
4 | (() => {
5 | const originAddListener = window.addEventListener;
6 | let flag;
7 |
8 | window.addEventListener = (...args) => {
9 | // try to read passive property and only read once time if it is accessible
10 | if (!flag && args[2] && args[2].passive) {
11 | flag = false;
12 | }
13 | originAddListener.apply(window, args);
14 | };
15 | })();
16 |
17 | // disable Vue production tip
18 | Vue.config.productionTip = false;
19 |
20 | // setup global Vue manually
21 | window.Vue = Vue;
22 |
23 | const testsContext = require.context('./specs', true, /\.spec$/);
24 | testsContext.keys().forEach(testsContext);
25 |
26 | const srcContext = require.context('../../src', true, /\.js$/);
27 | srcContext.keys().forEach(srcContext);
28 |
--------------------------------------------------------------------------------
/docs/zh/guide/configure-plugin-opts.md:
--------------------------------------------------------------------------------
1 | # 配置插件选项
2 |
3 | 我们可以通过插件 API 配置该插件的默认属性、默认插槽以及默认系统配置,它们将会作为你项目中的所有 `InfiniteLoading` 组件的默认值,你仍然可以通过每个组件的属性及插槽对它们进行覆盖。
4 |
5 | ## 属性/设置
6 |
7 | 只需要传递一个包含 `props`/`settings` 字段的对象就可以配置它们了,点击[这里](../api/#选项)查看所有可用的选项。
8 |
9 | ``` js
10 | import Vue from 'vue';
11 | import InfiniteLoading from 'vue-infinite-loading';
12 |
13 | Vue.use(InfiniteLoading, {
14 | props: {
15 | spinner: 'default',
16 | /* other props need to configure */
17 | },
18 | system: {
19 | throttleLimit: 50,
20 | /* other settings need to configure */
21 | },
22 | });
23 | ```
24 |
25 | ## 插槽
26 |
27 | 和属性及配置不一样,插槽选项可以是一个字符串,也可以是一个 `Vue Component`:
28 |
29 | ``` js
30 | import Vue from 'vue';
31 | import InfiniteLoading from 'vue-infinite-loading';
32 | import InfiniteError from 'path/to/your/components/InfiniteError';
33 |
34 | Vue.use(InfiniteLoading, {
35 | slots: {
36 | noMore: 'No more message', // you can pass a string value
37 | error: InfiniteError, // you also can pass a Vue component as a slot
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/docs/zh/guide/use-with-el-table.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/uyjb6z34/embedded/result/
3 | ---
4 |
5 | # 和 Element UI 一起使用
6 |
7 | 此前有一些关于如何将此插件与 Element UI 的表格组件一起使用的问题,所以这里提供一个示例以作参考。
8 |
9 | 和标准的表格组件一起使用很简单,将 `InfiniteLoading` 组件放在表格组件下方即可;但与带滚动条的表格组件一起使用,在创建模板时就需要注意以下两点:
10 |
11 | 1. 需要使用 Element UI 表格组件提供的名为 `append` 的[插槽](http://element-cn.eleme.io/#/zh-CN/component/table#table-slot),将 `InfiniteLoading` 组件放入表格末尾;
12 | 2. 由于 Element UI 表格组件的滚动条是根据内容高度动态触发的,该插件无法自动找到正确的滚动容器,需要将 `forceUseInfiniteWrapper` 属性设置为真实滚动容器的 CSS 选择器进行强制指定。
13 |
14 | ::: warning 注意
15 | 如果在同一个页面中有多个 Element UI 表格组件,我们需要用更加详细的 CSS 选择器来替代 `.el-table__body-wrapper`,否则组件可能会把错误的表格组件当做滚动容器
16 | :::
17 |
18 | 最后模板应该大致如此:
19 |
20 | ``` html {6,8}
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 | ```
33 |
34 | 逻辑中无需做任何特殊处理,组件便可以像右边的预览一样正常工作了。
35 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import config from './config';
2 | import InfiniteLoading from './components/InfiniteLoading.vue';
3 |
4 | function syncModeFromVue(Vue) {
5 | config.mode = Vue.config.productionTip ? 'development' : 'production';
6 | }
7 |
8 | Object.defineProperty(InfiniteLoading, 'install', {
9 | configurable: false,
10 | enumerable: false,
11 | value(Vue, options) {
12 | // override default props
13 | Object.assign(config.props, options && options.props);
14 |
15 | // override default slots
16 | Object.assign(config.slots, options && options.slots);
17 |
18 | // override default system settings
19 | Object.assign(config.system, options && options.system);
20 |
21 | // register component
22 | Vue.component('infinite-loading', InfiniteLoading);
23 |
24 | syncModeFromVue(Vue);
25 | },
26 | });
27 |
28 | // register component automatically if there has global Vue
29 | /* istanbul ignore else */
30 | if (typeof window !== 'undefined' && window.Vue) {
31 | window.Vue.component('infinite-loading', InfiniteLoading);
32 | syncModeFromVue(window.Vue);
33 | }
34 |
35 | export default InfiniteLoading;
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-present PeachScript
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/unit/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * check display status for a specific element
3 | * @param {DOM} elm
4 | * @return {Boolean}
5 | */
6 | export function isShow(elm) {
7 | return window.getComputedStyle(elm).display !== 'none';
8 | }
9 |
10 | /**
11 | * continues call the specified number of times for a function
12 | * @param {Function} fn target function
13 | * @param {Number} times calls
14 | * @param {Function} cb [description]
15 | */
16 | export function continuesCall(fn, times, cb) {
17 | if (times) {
18 | fn();
19 | setTimeout(() => {
20 | continuesCall(fn, times - 1, cb);
21 | }, 1);
22 | } else {
23 | cb();
24 | }
25 | }
26 |
27 | let fakeCache;
28 | /**
29 | * fake function or restore function
30 | * @param {Function} fn target function
31 | * @param {Function} cb fake function
32 | */
33 | export function fakeBox(fn, cb) {
34 | let result;
35 |
36 | if (fakeCache) {
37 | result = fakeCache;
38 | fakeCache = null;
39 | } else {
40 | fakeCache = fn;
41 | result = (...args) => {
42 | cb(...args);
43 | };
44 | }
45 |
46 | return result;
47 | }
48 |
49 | export default {
50 | isShow,
51 | continuesCall,
52 | fakeBox,
53 | };
54 |
--------------------------------------------------------------------------------
/test/unit/specs/index.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue.common';
2 | import InfiniteLoading from '../../../src';
3 | import config from '../../../src/config';
4 |
5 | describe('vue-infinite-loading:index', () => {
6 | const options = {
7 | props: { distance: 200 },
8 | };
9 |
10 | function getRunningModeFromVue() {
11 | return Vue.config.productionTip ? 'development' : 'production';
12 | }
13 |
14 | afterEach(() => {
15 | // clear component
16 | delete Vue.options.components['infinite-loading'];
17 | });
18 |
19 | it('should register component automatically if there has global Vue', () => {
20 | expect(Vue.options.components['infinite-loading']).to.not.be.undefined;
21 | });
22 |
23 | it('should register component, sync app running mode from Vue, and override config when using the plugin install API', () => {
24 | // assert before install
25 | expect(config.mode).to.equal(getRunningModeFromVue());
26 | expect(config.props.distance).to.not.equal(options.props.distance);
27 |
28 | // change running mode for Vue
29 | Vue.config.productionTip = true;
30 |
31 | // install plugin
32 | Vue.use(InfiniteLoading, options);
33 |
34 | // assert after install
35 | expect(config.props.distance).to.equal(options.props.distance);
36 | expect(config.mode).to.equal(getRunningModeFromVue());
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/styles/wave-dots.less:
--------------------------------------------------------------------------------
1 | .loading-wave-dots {
2 | @size: 8px;
3 | @wave: -6px;
4 | @delay: .14s;
5 | position: relative;
6 | @{deep} .wave-item {
7 | position: absolute;
8 | top: 50%;
9 | left: 50%;
10 | display: inline-block;
11 | margin-top: -@size/2;
12 | width: @size;
13 | height: @size;
14 | border-radius: 50%;
15 | animation: loading-wave-dots linear 2.8s infinite;
16 | &:first-child {
17 | margin-left: -@size/2 + -@size * 4;
18 | }
19 | &:nth-child(2) {
20 | margin-left: -@size/2 + -@size * 2;
21 | animation-delay: @delay;
22 | }
23 | &:nth-child(3) {
24 | margin-left: -@size/2;
25 | animation-delay: @delay * 2;
26 | }
27 | &:nth-child(4) {
28 | margin-left: -@size/2 + @size * 2;
29 | animation-delay: @delay * 3;
30 | }
31 | &:last-child {
32 | margin-left: -@size/2 + @size * 4;
33 | animation-delay: @delay * 4;
34 | }
35 | }
36 | @keyframes loading-wave-dots {
37 | 0% {
38 | transform: translateY(0);
39 | background: #bbb;
40 | }
41 | 10% {
42 | transform: translateY(@wave);
43 | background: #999;
44 | }
45 | 20% {
46 | transform: translateY(0);
47 | background: #bbb;
48 | }
49 | 100% {
50 | transform: translateY(0);
51 | background: #bbb;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/docs/guide/configure-plugin-opts.md:
--------------------------------------------------------------------------------
1 | # Configure Plugin Options
2 |
3 | We can configure default properties, default slots and default system settings for this plugin via the plugin API, which will then be the default values for all of the `InfiniteLoading` components in your project. You can still override them through properties or slots in every component.
4 |
5 | ## Props/Settings
6 |
7 | Simply pass an object containing the `props`/`settings` field to configure them. To check out all available options, click [here](../api/#options).
8 |
9 | ``` js
10 | import Vue from 'vue';
11 | import InfiniteLoading from 'vue-infinite-loading';
12 |
13 | Vue.use(InfiniteLoading, {
14 | props: {
15 | spinner: 'default',
16 | /* other props need to configure */
17 | },
18 | system: {
19 | throttleLimit: 50,
20 | /* other settings need to configure */
21 | },
22 | });
23 | ```
24 |
25 | ## Slots
26 |
27 | Unlike properties and settings, slot options can be either a string or a `Vue Component`:
28 |
29 | ``` js
30 | import Vue from 'vue';
31 | import InfiniteLoading from 'vue-infinite-loading';
32 | import InfiniteError from 'path/to/your/components/InfiniteError';
33 |
34 | Vue.use(InfiniteLoading, {
35 | slots: {
36 | noMore: 'No more message', // you can pass a string value
37 | error: InfiniteError, // you also can pass a Vue component as a slot
38 | },
39 | });
40 |
--------------------------------------------------------------------------------
/docs/zh/guide/top-dir-scroll.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/qac2h5v1/embedded/result/
3 | ---
4 |
5 | # 向上进行无限滚动
6 |
7 | 现在是时候尝试完成一个方向朝上的无限滚动列表了,从 `v2.4.0` 版本开始,此插件将会自动保存和复原滚动条的高度,这意味着向上无限滚动的功能现在可以开箱即用!
8 |
9 | ``` html {5}
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
21 | 在模板中,我们将 `InfiniteLoading` 组件移动到了新闻列表的上方,并且设置 `direction` 属性值为 `top`。
22 |
23 | ``` js {21}
24 | import axios from 'axios';
25 |
26 | const api = '//hn.algolia.com/api/v1/search_by_date?tags=story';
27 |
28 | export default {
29 | data() {
30 | return {
31 | page: 1,
32 | list: [],
33 | };
34 | },
35 | methods: {
36 | infiniteHandler($state) {
37 | axios.get(api, {
38 | params: {
39 | page: this.page,
40 | },
41 | }).then(({ data }) => {
42 | if (data.hits.length) {
43 | this.page += 1;
44 | this.list.unshift(...data.hits.reverse());
45 | $state.loaded();
46 | } else {
47 | $state.complete();
48 | }
49 | });
50 | },
51 | },
52 | };
53 | ```
54 |
55 | 逻辑部分跟 [基础 Hacker News](./start-with-hn.md) 几乎一致,不同的地方在于,我们将拿到的新闻数据进行了反转然后插入到了列表的最前面。这样就可以了,剩下的事插件将会替我们完成,是不是很简单?
56 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/styles/button.styl:
--------------------------------------------------------------------------------
1 | @require './config';
2 |
3 | button-shadow-generator($color, $offsetx = 0) {
4 | box-shadow: 0 (12px + $offsetx) 30px -10px $color
5 | 0 (8px + $offsetx) 20px -20px fadein($color, 5%)
6 | }
7 |
8 | .button
9 | padding 12px 24px
10 | color $c-basic
11 | font-size 16px
12 | background #fff
13 | border 0
14 | border-radius 2px
15 | cursor pointer
16 | transition opacity 0.2s, transform 0.2s
17 |
18 | &,
19 | &:active
20 | button-shadow-generator(rgba(0, 0, 0, 0.1))
21 |
22 | // action status
23 | &:hover
24 | opacity 0.95
25 | transform translateY(-1px)
26 | button-shadow-generator(rgba(0, 0, 0, 0.1), 1px)
27 |
28 | &:focus:not(.focus-visible)
29 | outline none
30 |
31 | &:active
32 | opacity 1
33 | transform none
34 |
35 | // size control
36 | &.button-large
37 | padding 16px 48px
38 | font-size 20px
39 |
40 | &.button-small
41 | padding 8px 16px
42 | font-size 14px
43 |
44 | // color control
45 | &.button-basic
46 | color #fff
47 | background linear-gradient(30deg, lighten($c-basic, 10%), lighten($c-basic, 25%))
48 |
49 | &,
50 | &:active
51 | button-shadow-generator(rgba(28, 90, 160, 0.5))
52 |
53 | &:hover
54 | button-shadow-generator(rgba(28, 90, 160, 0.5), 1px)
55 |
56 | a.button
57 | display inline-block
58 |
59 | &:hover
60 | text-decoration none !important
61 |
--------------------------------------------------------------------------------
/docs/zh/guide/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/a4Lxbf9w/embedded/result/
3 | ---
4 | # 指南
5 |
6 | ## 安装插件
7 |
8 | ### NPM
9 |
10 | 推荐在构建大型应用的时候使用这种方式进行安装。
11 |
12 | ``` bash
13 | npm install vue-infinite-loading -S
14 | ```
15 |
16 | ### 直接使用 `
26 | ```
27 |
28 | #### 手动引入
29 |
30 | 你也可以下载插件文件后手动引入:
31 |
32 | 下载插件
33 |
34 | ## 引用插件
35 |
36 | ### 组件形式
37 |
38 | 你可以直接将它当做一个自定义组件进行引用:
39 |
40 | ``` vue
41 |
42 |
43 |
44 |
45 |
54 | ```
55 |
56 | ### 插件 API
57 |
58 | 如果你需要改变插件的默认配置,那么可以采用 Vue.js 提供的 `use` API 对此插件进行注册:
59 |
60 | ``` js
61 | // main.js or index.js
62 | import InfiniteLoading from 'vue-infinite-loading';
63 |
64 | Vue.use(InfiniteLoading, { /* 配置 */ });
65 | ```
66 |
67 | 和 `script` 引入方式一样,使用插件 API 也会将 `InfiniteLoading` 组件注册为全局组件,在你自己的组件中就无需再使用 `components` 属性重复注册了。
68 |
--------------------------------------------------------------------------------
/src/styles/circles.less:
--------------------------------------------------------------------------------
1 | .loading-circles {
2 | @size: 5px;
3 | @radius: 12px;
4 | @shallow: 56%;
5 | @c-basic: #505050;
6 | @{deep} .circle-item {
7 | width: @size;
8 | height: @size;
9 | animation: loading-circles linear .75s infinite;
10 | &:first-child {
11 | margin-top: -@size/2 + -@radius;
12 | margin-left: -@size/2;
13 | }
14 | &:nth-child(2) {
15 | margin-top: -@size/2 + -@radius * .73;
16 | margin-left: -@size/2 + @radius * .73;
17 | }
18 | &:nth-child(3) {
19 | margin-top: -@size/2;
20 | margin-left: -@size/2 + @radius;
21 | }
22 | &:nth-child(4) {
23 | margin-top: -@size/2 + @radius * .73;
24 | margin-left: -@size/2 + @radius * .73;
25 | }
26 | &:nth-child(5) {
27 | margin-top: -@size/2 + @radius;
28 | margin-left: -@size/2;
29 | }
30 | &:nth-child(6) {
31 | margin-top: -@size/2 + @radius * .73;
32 | margin-left: -@size/2 + -@radius * .73;
33 | }
34 | &:nth-child(7) {
35 | margin-top: -@size/2;
36 | margin-left: -@size/2 + -@radius;
37 | }
38 | &:last-child {
39 | margin-top: -@size/2 + -@radius * .73;
40 | margin-left: -@size/2 + -@radius * .73;
41 | }
42 | }
43 | @keyframes loading-circles {
44 | 0% {
45 | background: lighten(@c-basic, @shallow);
46 | }
47 | 90% {
48 | background: @c-basic;
49 | }
50 | 100% {
51 | background: lighten(@c-basic, @shallow);
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_REPORT.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | ---
5 |
6 |
17 |
18 | ### Version
19 |
20 | ### Vue.js version
21 |
22 | ### Reproduction Link
23 |
24 |
25 |
26 | ### Steps to reproduce
27 |
28 | ### What is Expected?
29 |
30 | ### What is actually happening?
31 |
--------------------------------------------------------------------------------
/scripts/ssr_vue_loader.js:
--------------------------------------------------------------------------------
1 | const importReg = /import (style\d+) from (".+")\n/g;
2 | const injectReg = /\nfunction injectStyles[^]+?\n}/;
3 |
4 | function replaceImportWithRequire(source) {
5 | let result;
6 | let count = source.match(importReg).length;
7 | const fragments = [];
8 | const hasInjection = injectReg.test(source);
9 |
10 | result = source.replace(importReg, (_match, name, request) => {
11 | fragments.push([
12 | `var ${name} = require(${request})\n`,
13 | `if (${name}.__inject__) ${name}.__inject__(context)\n`
14 | ].join(''));
15 |
16 | // replace import with require through append way if there has no injection
17 | return (!hasInjection && !(--count))
18 | ? `
19 | function injectStyles (context) {
20 | ${fragments.join('')}
21 | }`
22 | : '';
23 | });
24 |
25 | // append require statements into inject function if there already has injection
26 | if (hasInjection) {
27 | result = result.replace(injectReg, (func) => {
28 | return func.replace(/}$/, `${fragments.join('')}}`);
29 | });
30 | }
31 |
32 | // replace argument for normalizer function
33 | result = result.replace(/(normalizer\((?:[^,]+,){4})([^,]+)/, '$1\n injectStyles');
34 |
35 | return result;
36 | }
37 |
38 | module.exports = function(source) {
39 | let result = source;
40 |
41 | // only enable if there has import statement
42 | if (importReg.test(source)) {
43 | result = replaceImportWithRequire(source);
44 | }
45 |
46 | return result;
47 | };
48 |
--------------------------------------------------------------------------------
/src/styles/bubbles.less:
--------------------------------------------------------------------------------
1 | .loading-bubbles {
2 | @size: 1px;
3 | @radius: 12px;
4 | @shallow: 3px;
5 | @c-basic: #666;
6 | @{deep} .bubble-item {
7 | background: @c-basic;
8 | animation: loading-bubbles linear .75s infinite;
9 | &:first-child {
10 | margin-top: -@size/2 + -@radius;
11 | margin-left: -@size/2;
12 | }
13 | &:nth-child(2) {
14 | margin-top: -@size/2 + -@radius * .73;
15 | margin-left: -@size/2 + @radius * .73;
16 | }
17 | &:nth-child(3) {
18 | margin-top: -@size/2;
19 | margin-left: -@size/2 + @radius;
20 | }
21 | &:nth-child(4) {
22 | margin-top: -@size/2 + @radius * .73;
23 | margin-left: -@size/2 + @radius * .73;
24 | }
25 | &:nth-child(5) {
26 | margin-top: -@size/2 + @radius;
27 | margin-left: -@size/2;
28 | }
29 | &:nth-child(6) {
30 | margin-top: -@size/2 + @radius * .73;
31 | margin-left: -@size/2 + -@radius * .73;
32 | }
33 | &:nth-child(7) {
34 | margin-top: -@size/2;
35 | margin-left: -@size/2 + -@radius;
36 | }
37 | &:last-child {
38 | margin-top: -@size/2 + -@radius * .73;
39 | margin-left: -@size/2 + -@radius * .73;
40 | }
41 | }
42 | @keyframes loading-bubbles {
43 | 0% {
44 | width: @size;
45 | height: @size;
46 | box-shadow: 0 0 0 @shallow @c-basic;
47 | }
48 | 90% {
49 | width: @size;
50 | height: @size;
51 | box-shadow: 0 0 0 0 @c-basic;
52 | }
53 | 100% {
54 | width: @size;
55 | height: @size;
56 | box-shadow: 0 0 0 @shallow @c-basic;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/docs/zh/guide/start-with-hn.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/a4Lxbf9w/embedded/result/
3 | ---
4 |
5 | # 从 Hacker News 开始
6 |
7 | 作为了解这款插件的第一步,我们将会创建一个无限滚动版的 [Hacker News](https://news.ycombinator.com/)。
8 |
9 | 首先,我们需要编写模板,内容大概如下(已省略不重要的代码):
10 |
11 | ``` html {9}
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ```
22 |
23 | 在模板中,我们将 `InfiniteLoading` 组件放在了新闻列表的最下方,并且使用一个叫做 `infiniteHandler` 的方法监听组件的 `infinite` 事件。
24 |
25 | 接下来编写核心逻辑—— `infiniteHandler` 方法:
26 |
27 | ``` js
28 | import axios from 'axios';
29 |
30 | const api = '//hn.algolia.com/api/v1/search_by_date?tags=story';
31 |
32 | export default {
33 | data() {
34 | return {
35 | page: 1,
36 | list: [],
37 | };
38 | },
39 | methods: {
40 | infiniteHandler($state) {
41 | axios.get(api, {
42 | params: {
43 | page: this.page,
44 | },
45 | }).then(({ data }) => {
46 | if (data.hits.length) {
47 | this.page += 1;
48 | this.list.push(...data.hits);
49 | $state.loaded();
50 | } else {
51 | $state.complete();
52 | }
53 | });
54 | },
55 | },
56 | };
57 | ```
58 |
59 | 在这段脚本中,我们使用 [HN Search API](https://hn.algolia.com/api) 和 [axios](https://github.com/mzabriskie/axios) 来获取新闻数据。如果服务端 API 返回了带新闻数据的数组,我们会将数据放入 `list`、会记录当前页码,并且通过 `$state.loaded` 方法通知插件我们已经拿到数据了;如果服务端 API 返回的是空数据,我们将会通过 `$state.complete` 方法通知插件所有数据都加载完了。
60 |
61 | 现在,你已经完成了一个无限滚动版本的 Hacker News, 就像右边的预览一样。
62 |
--------------------------------------------------------------------------------
/docs/guide/top-dir-scroll.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/qac2h5v1/embedded/result/
3 | ---
4 |
5 | # Top Direction Scroll
6 |
7 | Okay, it's time to try the top direction scroll list. This plugin will save and restore the scroll height automatically since `v2.4.0`, which means that the top direction feature can be used out of the box now!
8 |
9 | ``` html {5}
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ```
20 |
21 | In the template, we moved the `InfiniteLoading` component to the top of the news list, and set the `direction` property to `top`.
22 |
23 | ``` js {21}
24 | import axios from 'axios';
25 |
26 | const api = '//hn.algolia.com/api/v1/search_by_date?tags=story';
27 |
28 | export default {
29 | data() {
30 | return {
31 | page: 1,
32 | list: [],
33 | };
34 | },
35 | methods: {
36 | infiniteHandler($state) {
37 | axios.get(api, {
38 | params: {
39 | page: this.page,
40 | },
41 | }).then(({ data }) => {
42 | if (data.hits.length) {
43 | this.page += 1;
44 | this.list.unshift(...data.hits.reverse());
45 | $state.loaded();
46 | } else {
47 | $state.complete();
48 | }
49 | });
50 | },
51 | },
52 | };
53 | ```
54 |
55 | The script part is almost the same as the [basic Hacker News](./start-with-hn.md). The only difference is that we reverse the news data from the server and unshift it into the `list`. That's it! This plugin will do the remaining work, isn't it very easy?
56 |
--------------------------------------------------------------------------------
/scripts/dev_template.js:
--------------------------------------------------------------------------------
1 | module.exports = `
2 |
3 |
4 |
5 |
6 |
7 | Vue-infinite-loading Testing
8 |
9 |
10 |
28 |
29 |
30 |
34 |
59 |
60 |
61 | `;
62 |
--------------------------------------------------------------------------------
/docs/guide/use-with-el-table.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/uyjb6z34/embedded/result/
3 | ---
4 |
5 | # Use With Element UI
6 |
7 | There were some issues before regarding how to use this plugin with the table component of the Element UI, so here is a case for reference.
8 |
9 | It is easy to use this plugin with the standard table component! Just place the `InfiniteLoading` component under the table component, but we need to pay attention to the following 2 points when writing a template if we use this plugin with the scrollable table component:
10 |
11 | 1. Place the `InfiniteLoading` component at the end of the table component via a [slot](https://element.eleme.io/#/en-US/component/table#table-slot) named `append` in the Element UI table component;
12 | 2. Set the `forceUseInfiniteWrapper` property to the CSS selector of the real scroll container. Because the scroll bar of the Element UI table component is enabled dynamically according to the content height, this plugin cannot find the correct scroll container automatically.
13 |
14 | ::: warning
15 | If there are multiple Element UI table components on the same page, we need a more detailed CSS selector instead of the `.el-table__body-wrapper`. If not, this plugin may find an error table component as the scroll container
16 | :::
17 |
18 | The final template should be similar to:
19 |
20 | ``` html {6,8}
21 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 | ```
33 |
34 | No special handling is required in the logic. This plugin should alread work, just like the preview on the right!
35 |
--------------------------------------------------------------------------------
/src/styles/spinner.less:
--------------------------------------------------------------------------------
1 | @deep: ~'>>>';
2 |
3 | @import './wave-dots';
4 | @import './circles';
5 | @import './bubbles';
6 |
7 | // default spinner
8 | .loading-default {
9 | position: relative;
10 | border: 1px solid #999;
11 | animation: loading-rotating ease 1.5s infinite;
12 | &:before {
13 | @size: 6px;
14 | content: '';
15 | position: absolute;
16 | display: block;
17 | top: 0;
18 | left: 50%;
19 | margin-top: -@size/2;
20 | margin-left: -@size/2;
21 | width: @size;
22 | height: @size;
23 | background-color: #999;
24 | border-radius: 50%;
25 | }
26 | }
27 |
28 | // spiral spinner
29 | .loading-spiral {
30 | border: 2px solid #777;
31 | border-right-color: transparent;
32 | animation: loading-rotating linear .85s infinite;
33 | }
34 |
35 | // rotate animation
36 | @keyframes loading-rotating {
37 | 0%{
38 | transform: rotate(0);
39 | }
40 | 100%{
41 | transform: rotate(360deg);
42 | }
43 | }
44 |
45 | // common styles for the bubble spinner and circle spinner
46 | .loading-bubbles,
47 | .loading-circles {
48 | position: relative;
49 | }
50 | .loading-circles @{deep} .circle-item,
51 | .loading-bubbles @{deep} .bubble-item {
52 | @delay: .093s;
53 | position: absolute;
54 | top: 50%;
55 | left: 50%;
56 | display: inline-block;
57 | border-radius: 50%;
58 | &:nth-child(2) {
59 | animation-delay: @delay;
60 | }
61 | &:nth-child(3) {
62 | animation-delay: @delay * 2;
63 | }
64 | &:nth-child(4) {
65 | animation-delay: @delay * 3;
66 | }
67 | &:nth-child(5) {
68 | animation-delay: @delay * 4;
69 | }
70 | &:nth-child(6) {
71 | animation-delay: @delay * 5;
72 | }
73 | &:nth-child(7) {
74 | animation-delay: @delay * 6;
75 | }
76 | &:last-child {
77 | animation-delay: @delay * 7;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/docs/zh/guide/use-with-filter-or-tabs.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/w197rfy0/embedded/result/
3 | ---
4 |
5 | # 与过滤器和选项卡一起使用
6 |
7 | 加载数据的过程与上一个案例完全一致,关键在于当我们改变过滤器选项或者切换选项卡的时候应该如何重设组件。实际上,每当 `identifier` 属性发生变化的时候,该组件就会自行重设,所以一切看起来都很容易,我们开始吧!
8 |
9 | ``` html {12}
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ```
23 |
24 | 在模板中,我们添加了一个 `select` 元素并监听它的 `change` 事件,我们还为 `InfiniteLoading` 组件添加了一个 `identifier` 属性。
25 |
26 | ``` js {10,11,19,31,32,33,34,35}
27 | import axios from 'axios';
28 |
29 | const api = '//hn.algolia.com/api/v1/search_by_date';
30 |
31 | export default {
32 | data() {
33 | return {
34 | page: 1,
35 | list: [],
36 | newsType: 'story',
37 | infiniteId: +new Date(),
38 | };
39 | },
40 | methods: {
41 | infiniteHandler($state) {
42 | axios.get(api, {
43 | params: {
44 | page: this.page,
45 | tags: this.newsType,
46 | },
47 | }).then(({ data }) => {
48 | if (data.hits.length) {
49 | this.page += 1;
50 | this.list.push(...data.hits);
51 | $state.loaded();
52 | } else {
53 | $state.complete();
54 | }
55 | });
56 | },
57 | changeType() {
58 | this.page = 1;
59 | this.list = [];
60 | this.infiniteId += 1;
61 | },
62 | },
63 | };
64 | ```
65 |
66 | 在这段脚本中,我们为 `select` 和 `identifier` 属性设定了默认值,然后在 API 请求逻辑中添加了类型参数。我们还创建了一个 `changeType` 方法用于重设列表数据和加载组件,请注意,我们必须**在清空列表之后**再改变 `identifier` 属性,否则组件将可能无法在重设之后立即触发 `infinite` 事件。
67 |
68 | 恭喜,你已经搞定了!
69 |
--------------------------------------------------------------------------------
/docs/guide/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/a4Lxbf9w/embedded/result/
3 | ---
4 | # Guide
5 |
6 | ## Installation
7 |
8 | ### NPM
9 |
10 | If you are building a large application, we recommend you use the following method:
11 |
12 | ``` bash
13 | npm install vue-infinite-loading -S
14 | ```
15 |
16 | ### Direct `
26 | ```
27 |
28 | #### Manual
29 |
30 | You can also download and import it manually:
31 |
32 | Download
33 |
34 | ## Import
35 |
36 | ### Component
37 |
38 | You can import it as a custom component:
39 |
40 | ``` vue
41 |
42 |
43 |
44 |
45 |
54 | ```
55 |
56 | ### Plugin API
57 |
58 | If you want to configure default options, you can register this plugin through the `use` API of Vue.js:
59 |
60 | ``` js
61 | // main.js or index.js
62 | import InfiniteLoading from 'vue-infinite-loading';
63 |
64 | Vue.use(InfiniteLoading, { /* options */ });
65 | ```
66 |
67 | If you use the plugin API, the `InfiniteLoading` component will be registered as a global component just like when including it with the `script` tag, but you won't need to re-register it through the `components` property in your own components.
68 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | // Type definitions for vue-infinite-loading v2.3.0
2 | // Project: https://github.com/PeachScript/vue-infinite-loading
3 | // Definitions by: Phil Scott
4 | // PeachScript
5 |
6 | import Vue, { VNode, Component, PluginFunction } from 'vue';
7 |
8 | export type SpinnerType = 'default' | 'bubbles' | 'circles' | 'spiral' | 'waveDots';
9 | export type DirectionType = 'top' | 'bottom';
10 |
11 | export interface Slots {
12 | spinner: VNode[];
13 | 'no-result': VNode[];
14 | 'no-more': VNode[];
15 | 'error': VNode[];
16 | [key: string]: VNode[];
17 | }
18 |
19 | export interface StateChanger {
20 | loaded(): void;
21 | complete(): void;
22 | reset(): void;
23 | error(): void;
24 | }
25 |
26 | export interface InfiniteOptions {
27 | props?: {
28 | spinner?: SpinnerType;
29 | distance?: number;
30 | forceUseInfiniteWrapper?: boolean | string;
31 | };
32 |
33 | system?: {
34 | throttleLimit?: number;
35 | };
36 |
37 | slots?: {
38 | noResults?: string | Component;
39 | noMore?: string | Component;
40 | error?: string | Component;
41 | errorBtnText?: string;
42 | spinner?: string | Component;
43 | };
44 | }
45 |
46 | export default class InfiniteLoading extends Vue {
47 | // The trigger distance
48 | distance: number;
49 |
50 | // The load spinner type
51 | spinner: SpinnerType;
52 |
53 | // The scroll direction
54 | direction: DirectionType;
55 |
56 | // Whether find the element which has `infinite-wrapper` attribute as the scroll wrapper
57 | forceUseInfiniteWrapper: boolean | string;
58 |
59 | // Infinite event handler
60 | onInfinite: ($state: StateChanger) => void;
61 |
62 | // The method collection used to change infinite state
63 | stateChanger: StateChanger;
64 |
65 | // Slots
66 | $slots: Slots;
67 |
68 | static install: PluginFunction;
69 | }
70 |
71 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/Previewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
29 |
30 |
78 |
--------------------------------------------------------------------------------
/docs/guide/start-with-hn.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/a4Lxbf9w/embedded/result/
3 | ---
4 |
5 | # Start With Hacker News
6 |
7 | As the first step in learning how to use this plugin, we will create an infinite scrolling version of [Hacker News](https://news.ycombinator.com/).
8 |
9 | Firstly, we need to write a template, which is similar to this (ommited unimportant code):
10 |
11 | ``` html {9}
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ```
22 |
23 | In the template, we put the `InfiniteLoading` component at the bottom of the news list, and listen for it's `infinite` event with a method called `infiniteHandler`.
24 |
25 | Then, we write the core logic for the `infiniteHandler` method:
26 |
27 | ``` js
28 | import axios from 'axios';
29 |
30 | const api = '//hn.algolia.com/api/v1/search_by_date?tags=story';
31 |
32 | export default {
33 | data() {
34 | return {
35 | page: 1,
36 | list: [],
37 | };
38 | },
39 | methods: {
40 | infiniteHandler($state) {
41 | axios.get(api, {
42 | params: {
43 | page: this.page,
44 | },
45 | }).then(({ data }) => {
46 | if (data.hits.length) {
47 | this.page += 1;
48 | this.list.push(...data.hits);
49 | $state.loaded();
50 | } else {
51 | $state.complete();
52 | }
53 | });
54 | },
55 | },
56 | };
57 | ```
58 |
59 | In the script, we use [HN Search API](https://hn.algolia.com/api) and [axios](https://github.com/mzabriskie/axios) to fetch the news data. If the server API returns an array with the news data, we will push it into `list`, record the current page, and tell this plugin through the `$state.loaded` method that we got some data. If the server API returns an empty array, we will tell this plugin through `$state.complete` method that all data has been loaded.
60 |
61 | Now, you can get an infinite scroll version of Hacker News, just like the preview on the right.
62 |
--------------------------------------------------------------------------------
/docs/guide/use-with-filter-or-tabs.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/w197rfy0/embedded/result/
3 | ---
4 |
5 | # Use With Filter/Tabs
6 |
7 | The loading process is exactly the same as in the previous example. The key point here is how to reset the component when we change the filter or tabs. The infinite loading component will reset itself whenever the `identifier` property has changed. It sounds easy, so let's do it!
8 |
9 | ``` html {12}
10 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ```
23 |
24 | In the template, we add a `select` element and listen for its `change` event. For the `InfiniteLoading` component, we add an `identifier` property.
25 |
26 | ``` js {10,11,19,31,32,33,34,35}
27 | import axios from 'axios';
28 |
29 | const api = '//hn.algolia.com/api/v1/search_by_date';
30 |
31 | export default {
32 | data() {
33 | return {
34 | page: 1,
35 | list: [],
36 | newsType: 'story',
37 | infiniteId: +new Date(),
38 | };
39 | },
40 | methods: {
41 | infiniteHandler($state) {
42 | axios.get(api, {
43 | params: {
44 | page: this.page,
45 | tags: this.newsType,
46 | },
47 | }).then(({ data }) => {
48 | if (data.hits.length) {
49 | this.page += 1;
50 | this.list.push(...data.hits);
51 | $state.loaded();
52 | } else {
53 | $state.complete();
54 | }
55 | });
56 | },
57 | changeType() {
58 | this.page = 1;
59 | this.list = [];
60 | this.infiniteId += 1;
61 | },
62 | },
63 | };
64 | ```
65 |
66 | In the script, we set default values for the `select` and `identifier` properties, then add the type parameter in the API request logic, and we create the `changeType` method to reset the list data and infinite loading component. Please note, we must change the `identifier` property *after* we empty the `list`. Otherwise, the component may not trigger the `infinite` event immediately after reset.
67 |
68 | That's all, you're done!
69 |
--------------------------------------------------------------------------------
/src/components/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
101 |
102 |
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## Intro
12 | An infinite scroll plugin for Vue.js, to help you implement an infinite scroll list more easily.
13 |
14 | ### Features
15 | - Mobile friendly
16 | - Internal spinners
17 | - 2-directional support
18 | - Load result message display
19 |
20 | ## Usage & Guide
21 | To check out live examples and docs, visit [Vue-infinite-loading GitHub Pages](https://peachscript.github.io/vue-infinite-loading/).
22 |
23 | ## Changelog
24 | Detailed changes for each release are documented in the [release notes](https://github.com/PeachScript/vue-infinite-loading/releases).
25 |
26 | ## Contribution
27 | Please make sure to read the [Contributing Guide](https://github.com/PeachScript/vue-infinite-loading/blob/master/.github/CONTRIBUTING.md) before making a pull request.
28 |
29 | ## Licence
30 | The MIT License (MIT)
31 |
32 | Copyright (c) 2016-present PeachScript
33 |
34 | Permission is hereby granted, free of charge, to any person obtaining a copy
35 | of this software and associated documentation files (the "Software"), to deal
36 | in the Software without restriction, including without limitation the rights
37 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
38 | copies of the Software, and to permit persons to whom the Software is
39 | furnished to do so, subject to the following conditions:
40 |
41 | The above copyright notice and this permission notice shall be included in all
42 | copies or substantial portions of the Software.
43 |
44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
45 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
46 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
47 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
48 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
49 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
50 | SOFTWARE.
51 |
--------------------------------------------------------------------------------
/scripts/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const VueLoaderPlugin = require('vue-loader/lib/plugin');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | module.exports = {
7 | entry: {
8 | 'vue-infinite-loading': './src/index.js'
9 | },
10 | output: {
11 | path: path.join(__dirname, '../dist'),
12 | filename: '[name].js',
13 | library: 'VueInfiniteLoading',
14 | libraryTarget: 'umd',
15 | globalObject: 'this'
16 | },
17 | resolve: {
18 | extensions: ['.js', '.vue'],
19 | alias: {
20 | vue$: 'vue/dist/vue.min.js'
21 | }
22 | },
23 | module: {
24 | rules: [
25 | {
26 | test: /\.(vue|js)$/,
27 | enforce: 'pre',
28 | include: [path.join(__dirname, '../src'), path.join(__dirname, '../test')],
29 | loader: 'eslint-loader',
30 | options: {
31 | formatter: require('eslint-formatter-friendly')
32 | }
33 | },
34 | {
35 | test: /\.js$/,
36 | include: [path.join(__dirname, '../src'), path.join(__dirname, '../test')],
37 | loader: 'babel-loader',
38 | options: {
39 | presets: ['@babel/preset-env'],
40 | plugins: ['@babel/plugin-transform-runtime'],
41 | env: {
42 | test: {
43 | presets: [['@babel/preset-env', { useBuiltIns: 'usage', corejs: 2 }]],
44 | plugins: ['babel-plugin-istanbul']
45 | }
46 | }
47 | }
48 | },
49 | {
50 | test: /\.vue$/,
51 | use: [
52 | path.join(__dirname, './ssr_vue_loader'),
53 | 'vue-loader'
54 | ]
55 | },
56 | {
57 | test: /\.less$/,
58 | use: [
59 | 'vue-style-loader',
60 | {
61 | loader: 'css-loader',
62 | options: {
63 | importLoaders: 2
64 | }
65 | },
66 | {
67 | loader: 'postcss-loader',
68 | options: {
69 | ident: 'postcss',
70 | plugins: [
71 | require('autoprefixer')
72 | ]
73 | }
74 | },
75 | 'less-loader'
76 | ]
77 | }
78 | ]
79 | },
80 | plugins: [
81 | new VueLoaderPlugin()
82 | ],
83 | mode: process.env.NODE_ENV || 'development'
84 | };
85 |
86 | if (process.env.NODE_ENV === 'production') {
87 | // production configurations
88 | const pkg = require('../package');
89 | const banner = [
90 | `${pkg.name} v${process.env.VERSION || pkg.version}`,
91 | `(c) 2016-${new Date().getFullYear()} ${pkg.author.name}`,
92 | `${pkg.license} License`
93 | ].join('\n');
94 |
95 | module.exports.plugins.push(new webpack.BannerPlugin(banner));
96 | } else {
97 | // development configurations
98 | module.exports.plugins.push(new HtmlWebpackPlugin({
99 | filename: 'index.html',
100 | template: './scripts/dev_template.js',
101 | inject: false
102 | }));
103 | }
104 |
--------------------------------------------------------------------------------
/.github/COMMIT_CONVENTION.md:
--------------------------------------------------------------------------------
1 | ## Git Commit Message Convention
2 |
3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular).
4 |
5 | #### TL;DR:
6 |
7 | Messages must be matched by the following regex:
8 |
9 | ``` js
10 | /^(revert: )?(build|chore|ci|docs|feat|fix|perf|refactor|style|test)(\((core|config|spinner|deps)\))?: .{1,72}$/
11 | ```
12 |
13 | #### Examples
14 |
15 | Appears under "Features" header, `core` subheader:
16 |
17 | ```
18 | feat(core): support top direction
19 | ```
20 |
21 | Appears under "Bug Fixes" header, `spinner` subheader, with a link to issue #28:
22 |
23 | ```
24 | fix(spinner): animation compatibility
25 |
26 | close #28
27 | ```
28 |
29 | Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
30 |
31 | ```
32 | perf(core): use @infinite event instead of on-infinite property
33 |
34 | BREAKING CHANGE: The 'on-infinite' property has been removed.
35 | ```
36 |
37 | The following commit and commit `a88ffb7` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
38 |
39 | ```
40 | revert: feat(core): support top direction
41 |
42 | This reverts commit a88ffb776878a07cb2f349bc3dd8cce59932b7e1.
43 | ```
44 |
45 | ### Full Message Format
46 |
47 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
48 |
49 | ```
50 | ():
51 |
52 |
53 |
54 |
55 | ```
56 |
57 | The **header** is mandatory and the **scope** of the header is optional.
58 |
59 | ### Revert
60 |
61 | If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted.
62 |
63 | ### Type
64 |
65 | If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
66 |
67 | Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`, `style`, `refactor`, and `test` for non-changelog related tasks.
68 |
69 | ### Scope
70 |
71 | The scope is optional, there are the supported scopes: `core`, `spinner`, `config` and `deps`.
72 |
73 | ### Subject
74 |
75 | The subject contains succinct description of the change:
76 |
77 | * use the imperative, present tense: "change" not "changed" nor "changes"
78 | * don't capitalize first letter
79 | * no dot (.) at the end
80 |
81 | ### Body
82 |
83 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
84 | The body should include the motivation for the change and contrast this with previous behavior.
85 |
86 | ### Footer
87 |
88 | The footer should contain any information about **Breaking Changes** and is also the place to
89 | reference GitHub issues that this commit **Closes**.
90 |
91 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
92 |
--------------------------------------------------------------------------------
/docs/zh/guide/configure-load-msg.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/94kL0bvs/embedded/result/
3 | ---
4 |
5 | # 配置加载提示
6 |
7 | 此组件提供了四种不同的插槽用来显示不同的加载提示:`spinner`、`no-more`、`no-results`、`error`,所有插槽的默认值都在右边的预览中列出来了,你还可以通过[这里](../api/#插槽)了解更多。
8 |
9 | ## 通过组件 Prop
10 |
11 | 只有 `spinner` 插槽可以通过 prop 进行配置,并且此时的设定值只能是内置的动画类型:
12 |
13 | ``` html
14 |
15 | ```
16 |
17 | 你可以在右边预览所有内置加载动画,如果你希望创建自己的加载动画,请使用其他方式。
18 |
19 | ## 通过 `v-slot` 指令
20 |
21 | ::: warning
22 | Vue.js 官方于 v2.6.0 后[废弃 slot 特殊特性](https://cn.vuejs.org/v2/api/#slot-废弃),推荐使用[v-slot 指令](https://cn.vuejs.org/v2/api/#v-slot)。
23 | :::
24 |
25 | 我们可以通过[`v-slot` 指令](https://cn.vuejs.org/v2/api/#v-slot)来配置它们:
26 |
27 | ``` html
28 |
29 | Loading...
30 | No more message
31 | No results message
32 |
33 | ```
34 |
35 | 与其他插槽不同的是,`error` 插槽的默认值除了会提供文字信息之外,还会提供一个重试按钮供用户重新尝试加载;在自定义 `error` 插槽时,如果你也希望提供一个重试按钮给用户,可以接收 prop 中的重試的方法 `trigger` 並注入到按鈕,就像下面这样:
36 |
37 | ``` html
38 |
39 |
40 | Error message, click
here to retry
41 |
42 |
43 | ```
44 |
45 | ## 通过插件 API
46 |
47 | 在我们构建大型应用时,为了保证所有加载提示的行为一致,此插件支持通过插件 API 统一配置所有的插槽内容,我们只需要传递一个字符串或者 Vue 组件给它就可以了,点击[这里](./configure-plugin-opts.md#插槽)了解更多。
48 |
49 | 在这里 `error` 插槽仍然是最特殊的哪一个,和使用 `v-slot` 指令一样,如果你希望提供一个重试按钮给用户,你可以使用 [`vm.$attrs`](https://cn.vuejs.org/v2/api/#vm-attrs) 属性,就像这样:
50 |
51 | ``` html
52 |
53 |
54 | Error message, click
55 |
here
56 | to retry
57 |
58 | ```
59 |
60 | 如果你想保持变量名的整洁,你也可以定义一个叫做 `trigger` 的函数属性,并且把它绑定到重试按钮上即可:
61 |
62 | ``` js
63 | // your own error component
64 | export default {
65 | /* ... */
66 | props: {
67 | trigger: Function,
68 | },
69 | /* ... */
70 | };
71 | ```
72 |
73 | ## 关于隐藏与默认样式
74 |
75 | 为了便于使用,该组件为插槽内容提供了一些默认样式(`font-size`、`color` 和 `padding`),如果你希望在通过 `v-slot` 指令配置插槽时保持默认样式的存在,你需要将插槽内容用 `template` 标签包裹:
76 |
77 | ``` html
78 |
79 |
80 | No more message
81 |
82 |
83 | ```
84 |
85 | 如果你希望隐藏某个插槽,你可以创建一个不是 `template` 标签的空元素,因为 Vue.js 会忽略空的 `template` 元素:
86 |
87 | ``` html
88 |
89 |
90 |
91 |
92 | ```
93 |
94 | 如果你希望移除默认样式以避免影响自己的样式,你可以将插槽内容用不是 `template` 标签的元素包裹:
95 |
96 | ``` html
97 |
98 |
99 | No more message
100 |
101 | ```
102 |
103 | 差点忘了,如果你想通过插件 API 全局配置插槽内容,可以像这样进行控制:
104 |
105 | ``` js
106 | import Vue from 'vue';
107 | import InfiniteLoading from 'vue-infinite-loading';
108 | import InfiniteError from 'path/to/your/components/InfiniteError',
109 |
110 | Vue.use(InfiniteLoading, {
111 | slots: {
112 | // keep default styles
113 | noResults: 'No results message',
114 |
115 | // remove default styles
116 | noMore: InfiniteError,
117 |
118 | // hide slot
119 | error: {
120 | render: h => h('div'),
121 | },
122 | },
123 | });
124 | ```
125 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import config, { ERRORS } from './config';
4 |
5 | /**
6 | * console warning in production
7 | * @param {String} msg console content
8 | */
9 | export function warn(msg) {
10 | /* istanbul ignore else */
11 | if (config.mode !== 'production') {
12 | console.warn(`[Vue-infinite-loading warn]: ${msg}`);
13 | }
14 | }
15 |
16 | /**
17 | * console error
18 | * @param {String} msg console content
19 | */
20 | export function error(msg) {
21 | console.error(`[Vue-infinite-loading error]: ${msg}`);
22 | }
23 |
24 | export const throttleer = {
25 | timers: [],
26 | caches: [],
27 | throttle(fn) {
28 | if (this.caches.indexOf(fn) === -1) {
29 | // cache current handler
30 | this.caches.push(fn);
31 |
32 | // save timer for current handler
33 | this.timers.push(setTimeout(() => {
34 | fn();
35 |
36 | // empty cache and timer
37 | this.caches.splice(this.caches.indexOf(fn), 1);
38 | this.timers.shift();
39 | }, config.system.throttleLimit));
40 | }
41 | },
42 | reset() {
43 | // reset all timers
44 | this.timers.forEach((timer) => {
45 | clearTimeout(timer);
46 | });
47 | this.timers.length = 0;
48 |
49 | // empty caches
50 | this.caches = [];
51 | },
52 | };
53 |
54 | export const loopTracker = {
55 | isChecked: false,
56 | timer: null,
57 | times: 0,
58 | track() {
59 | // record track times
60 | this.times += 1;
61 |
62 | // try to mark check status
63 | clearTimeout(this.timer);
64 | this.timer = setTimeout(() => {
65 | this.isChecked = true;
66 | }, config.system.loopCheckTimeout);
67 |
68 | // throw warning if the times of continuous calls large than the maximum times
69 | if (this.times > config.system.loopCheckMaxCalls) {
70 | error(ERRORS.INFINITE_LOOP);
71 | this.isChecked = true;
72 | }
73 | },
74 | };
75 |
76 | export const scrollBarStorage = {
77 | key: '_infiniteScrollHeight',
78 | getScrollElm(elm) {
79 | return elm === window ? document.documentElement : elm;
80 | },
81 | save(elm) {
82 | const target = this.getScrollElm(elm);
83 |
84 | // save scroll height on the scroll parent
85 | target[this.key] = target.scrollHeight;
86 | },
87 | restore(elm) {
88 | const target = this.getScrollElm(elm);
89 |
90 | /* istanbul ignore else */
91 | if (typeof target[this.key] === 'number') {
92 | target.scrollTop = target.scrollHeight - target[this.key] + target.scrollTop;
93 | }
94 |
95 | this.remove(target);
96 | },
97 | remove(elm) {
98 | if (elm[this.key] !== undefined) {
99 | // remove scroll height
100 | delete elm[this.key]; // eslint-disable-line no-param-reassign
101 | }
102 | },
103 | };
104 |
105 | /**
106 | * kebab-case a camel-case string
107 | * @param {String} str source string
108 | * @return {String}
109 | */
110 | export function kebabCase(str) {
111 | return str.replace(/[A-Z]/g, s => `-${s.toLowerCase()}`);
112 | }
113 |
114 | /**
115 | * get visibility for element
116 | * @param {DOM} elm
117 | * @return {Boolean}
118 | */
119 | export function isVisible(elm) {
120 | return (elm.offsetWidth + elm.offsetHeight) > 0;
121 | }
122 |
123 | export default {
124 | warn,
125 | error,
126 | throttleer,
127 | loopTracker,
128 | kebabCase,
129 | scrollBarStorage,
130 | isVisible,
131 | };
132 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [scdzwyxst@gmail.com](mailto:scdzwyxst@gmail.com). All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | title: 'Vue-infinite-loading',
3 | head: [
4 | ['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
5 | ],
6 | base: '/vue-infinite-loading/',
7 | ga: 'UA-128069695-1',
8 | locales: {
9 | '/': {
10 | description: 'An infinite scroll plugin for Vue.js',
11 | },
12 | '/zh/': {
13 | description: '一款用于 Vue.js 的无限滚动插件',
14 | },
15 | },
16 | themeConfig: {
17 | sidebar: 'auto',
18 | repo: '/PeachScript/vue-infinite-loading',
19 | editLinks: true,
20 | docsDir: 'docs',
21 | locales: {
22 | '/': {
23 | lang: 'en-US',
24 | selectText: 'Languages',
25 | label: 'English',
26 | lastUpdated: true,
27 | nav: [
28 | {
29 | text: 'Guide',
30 | link: '/guide/',
31 | },
32 | {
33 | text: 'API',
34 | link: '/api/',
35 | },
36 | {
37 | text: 'Old Version',
38 | link: 'https://peachscript.github.io/vue-infinite-loading/old/',
39 | },
40 | {
41 | text: 'Changelog',
42 | link: 'https://github.com/PeachScript/vue-infinite-loading/releases',
43 | },
44 | ],
45 | sidebar: {
46 | '/guide/': [
47 | '',
48 | 'start-with-hn',
49 | 'use-with-filter-or-tabs',
50 | 'top-dir-scroll',
51 | 'use-with-el-table',
52 | 'configure-load-msg',
53 | 'configure-plugin-opts',
54 | ],
55 | },
56 | footer: `
57 |
58 | Released under the
59 | MIT License
60 | |
61 | Powered by
62 | VuePress
63 |
64 |
65 | ©2016-present Made with ♥ by
66 | PeachScript
67 |
68 | `,
69 | },
70 | '/zh/': {
71 | lang: 'zh-CN',
72 | selectText: '选择语言',
73 | label: '简体中文',
74 | lastUpdated: '上次更新',
75 | editLinkText: '在 GitHub 上编辑此页',
76 | nav: [
77 | {
78 | text: '指南',
79 | link: '/zh/guide/',
80 | },
81 | {
82 | text: 'API',
83 | link: '/zh/api/',
84 | },
85 | {
86 | text: '旧版文档',
87 | link: 'https://peachscript.github.io/vue-infinite-loading/old/',
88 | },
89 | {
90 | text: '更新日志',
91 | link: 'https://github.com/PeachScript/vue-infinite-loading/releases',
92 | },
93 | ],
94 | sidebar: {
95 | '/zh/guide/': [
96 | '',
97 | 'start-with-hn',
98 | 'use-with-filter-or-tabs',
99 | 'top-dir-scroll',
100 | 'use-with-el-table',
101 | 'configure-load-msg',
102 | 'configure-plugin-opts',
103 | ],
104 | },
105 | footer: `
106 |
107 | 遵循
108 | MIT 开源协议
109 | |
110 | 由
111 | VuePress
112 | 强力驱动
113 |
114 |
115 | ©2016-present Made with ♥ by
116 | PeachScript
117 |
118 | `,
119 | },
120 | },
121 | },
122 | plugins: [
123 | require('@vuepress/theme-default/index'),
124 | '@vuepress/google-analytics'
125 | ],
126 | };
127 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-infinite-loading",
3 | "version": "2.4.5",
4 | "description": "An infinite scroll plugin for Vue.js",
5 | "main": "dist/vue-infinite-loading.js",
6 | "typings": "types/index.d.ts",
7 | "files": [
8 | "dist/vue-infinite-loading.js",
9 | "types/*.d.ts",
10 | "src"
11 | ],
12 | "author": {
13 | "name": "PeachScript",
14 | "email": "scdzwyxst@gmail.com"
15 | },
16 | "scripts": {
17 | "dev": "webpack-dev-server --config scripts/webpack.config.js --hot --info=false --port 8000",
18 | "build": "NODE_ENV=production webpack --config scripts/webpack.config.js -p --progress --hide-modules",
19 | "docs:dev": "vuepress dev docs",
20 | "docs:build": "vuepress build docs",
21 | "docs:deploy": "bash ./scripts/deploy_docs.sh",
22 | "lint": "eslint -f friendly --ext .js,.vue ./",
23 | "test": "BABEL_ENV=test karma start scripts/karma.conf.js",
24 | "precommit": "npm run lint",
25 | "commitmsg": "commitlint -E GIT_PARAMS",
26 | "release": "bash ./scripts/release.sh"
27 | },
28 | "keywords": [
29 | "vue",
30 | "vue components",
31 | "infinite loading",
32 | "infinite scroll",
33 | "vue infinite"
34 | ],
35 | "repository": {
36 | "type": "git",
37 | "url": "git+https://github.com/PeachScript/vue-infinite-loading.git"
38 | },
39 | "bugs": {
40 | "url": "https://github.com/PeachScript/vue-infinite-loading/issues"
41 | },
42 | "homepage": "https://github.com/PeachScript/vue-infinite-loading",
43 | "devDependencies": {
44 | "@babel/core": "^7.0.0",
45 | "@babel/plugin-transform-runtime": "^7.0.0",
46 | "@babel/preset-env": "^7.0.0",
47 | "@babel/runtime": "^7.0.0",
48 | "@commitlint/cli": "^7.0.0",
49 | "@commitlint/config-conventional": "^7.0.1",
50 | "@vuepress/plugin-google-analytics": "^1.0.0-alpha.16",
51 | "autoprefixer": "^9.1.5",
52 | "babel-loader": "^8.0.2",
53 | "babel-plugin-istanbul": "^5.0.1",
54 | "chai": "^3.5.0",
55 | "css-loader": "^0.28.4",
56 | "eslint": "^4.19.1",
57 | "eslint-config-airbnb-base": "^13.1.0",
58 | "eslint-formatter-friendly": "^6.0.0",
59 | "eslint-loader": "^2.1.0",
60 | "eslint-plugin-import": "^2.14.0",
61 | "eslint-plugin-vue": "^4.7.1",
62 | "focus-visible": "^4.1.5",
63 | "html-webpack-plugin": "^3.2.0",
64 | "husky": "^0.14.3",
65 | "karma": "^3.0.0",
66 | "karma-chai": "^0.1.0",
67 | "karma-coverage": "^1.1.1",
68 | "karma-mocha": "^1.3.0",
69 | "karma-phantomjs-launcher": "^1.0.4",
70 | "karma-sinon-chai": "^1.3.1",
71 | "karma-spec-reporter": "0.0.31",
72 | "karma-webpack": "^4.0.0-rc.2",
73 | "less": "^3.8.1",
74 | "less-loader": "^4.1.0",
75 | "mocha": "^5.2.0",
76 | "phantomjs-prebuilt": "^2.1.15",
77 | "postcss-loader": "^3.0.0",
78 | "sinon": "^2.4.1",
79 | "sinon-chai": "^2.13.0",
80 | "vue": "^2.6.10",
81 | "vue-loader": "^15.7.0",
82 | "vue-template-compiler": "^2.6.10",
83 | "vuepress": "^1.0.0-alpha.23",
84 | "webpack": "^4.17.2",
85 | "webpack-cli": "^3.1.0",
86 | "webpack-dev-server": "^3.1.8"
87 | },
88 | "peerDependencies": {
89 | "vue": "^2.6.10"
90 | },
91 | "license": "MIT",
92 | "browserslist": [
93 | "> 1%",
94 | "last 1 versions",
95 | "last 4 Android versions",
96 | "last 3 iOS versions"
97 | ],
98 | "commitlint": {
99 | "extends": [
100 | "@commitlint/config-conventional"
101 | ],
102 | "rules": {
103 | "scope-enum": [
104 | 2,
105 | "always",
106 | [
107 | "core",
108 | "config",
109 | "spinner",
110 | "deps"
111 | ]
112 | ]
113 | }
114 | },
115 | "eslintConfig": {
116 | "root": true,
117 | "extends": [
118 | "airbnb-base",
119 | "plugin:vue/essential"
120 | ]
121 | },
122 | "eslintIgnore": [
123 | "dist",
124 | "test/unit/coverage",
125 | "scripts"
126 | ]
127 | }
128 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | /*
2 | * default property values
3 | */
4 |
5 | const props = {
6 | // the default spinner type
7 | spinner: 'default',
8 |
9 | // the default critical distance
10 | distance: 100,
11 |
12 | // the default force use infinite wrapper flag
13 | forceUseInfiniteWrapper: false,
14 | };
15 |
16 | /**
17 | * default system settings
18 | */
19 |
20 | const system = {
21 | // the default throttle space of time
22 | throttleLimit: 50,
23 |
24 | // the timeout for check infinite loop, unit: ms
25 | loopCheckTimeout: 1000,
26 |
27 | // the max allowed number of continuous calls, unit: ms
28 | loopCheckMaxCalls: 10,
29 | };
30 |
31 | /**
32 | * default slot messages
33 | */
34 | const slots = {
35 | noResults: 'No results :(',
36 | noMore: 'No more data :)',
37 | error: 'Opps, something went wrong :(',
38 | errorBtnText: 'Retry',
39 | spinner: '',
40 | };
41 |
42 | /**
43 | * the 3rd argument for event bundler
44 | * @see https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
45 | */
46 |
47 | export const evt3rdArg = (() => {
48 | let result = false;
49 |
50 | try {
51 | const arg = Object.defineProperty({}, 'passive', {
52 | get() {
53 | result = { passive: true };
54 | return true;
55 | },
56 | });
57 |
58 | window.addEventListener('testpassive', arg, arg);
59 | window.remove('testpassive', arg, arg);
60 | } catch (e) { /* */ }
61 |
62 | return result;
63 | })();
64 |
65 | /**
66 | * warning messages
67 | */
68 |
69 | export const WARNINGS = {
70 | STATE_CHANGER: [
71 | 'emit `loaded` and `complete` event through component instance of `$refs` may cause error, so it will be deprecated soon, please use the `$state` argument instead (`$state` just the special `$event` variable):',
72 | '\ntemplate:',
73 | ' ',
74 | `
75 | script:
76 | ...
77 | infiniteHandler($state) {
78 | ajax('https://www.example.com/api/news')
79 | .then((res) => {
80 | if (res.data.length) {
81 | $state.loaded();
82 | } else {
83 | $state.complete();
84 | }
85 | });
86 | }
87 | ...`,
88 | '',
89 | 'more details: https://github.com/PeachScript/vue-infinite-loading/issues/57#issuecomment-324370549',
90 | ].join('\n'),
91 | INFINITE_EVENT: '`:on-infinite` property will be deprecated soon, please use `@infinite` event instead.',
92 | IDENTIFIER: 'the `reset` event will be deprecated soon, please reset this component by change the `identifier` property.',
93 | };
94 |
95 | /**
96 | * error messages
97 | */
98 |
99 | export const ERRORS = {
100 | INFINITE_LOOP: [
101 | `executed the callback function more than ${system.loopCheckMaxCalls} times for a short time, it looks like searched a wrong scroll wrapper that doest not has fixed height or maximum height, please check it. If you want to force to set a element as scroll wrapper ranther than automatic searching, you can do this:`,
102 | `
103 |
104 |
105 | ...
106 |
107 |
108 |
109 | or
110 |
111 | ...
112 |
113 |
114 |
115 | `,
116 | 'more details: https://github.com/PeachScript/vue-infinite-loading/issues/55#issuecomment-316934169',
117 | ].join('\n'),
118 | };
119 |
120 | /**
121 | * plugin status constants
122 | */
123 | export const STATUS = {
124 | READY: 0,
125 | LOADING: 1,
126 | COMPLETE: 2,
127 | ERROR: 3,
128 | };
129 |
130 | /**
131 | * default slot styles
132 | */
133 | export const SLOT_STYLES = {
134 | color: '#666',
135 | fontSize: '14px',
136 | padding: '10px 0',
137 | };
138 |
139 | export default {
140 | mode: 'development',
141 | props,
142 | system,
143 | slots,
144 | WARNINGS,
145 | ERRORS,
146 | STATUS,
147 | };
148 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/Intro.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | |
7 |
8 |
9 |
15 |
16 |
19 |
20 |
22 |
23 |
24 |
25 | {{ feature.details }}
26 |
27 |
28 |
29 |
30 |
31 |
74 |
75 |
76 |
171 |
--------------------------------------------------------------------------------
/docs/guide/configure-load-msg.md:
--------------------------------------------------------------------------------
1 | ---
2 | previewLink: //jsfiddle.net/PeachScript/94kL0bvs/embedded/result/
3 | ---
4 |
5 | # Configure Load Messages
6 |
7 | This component provides four different slots that you can use to display different load messages: `spinner`, `no-more`, `no-results`, `error`. All of the default values are listed in the preview container on the right, and you can read more about them [here](../api/#slots).
8 |
9 | ## Via Component Prop
10 |
11 | Only the `spinner` slot can be configured via the prop, and the set value can only be the built-in spinner type:
12 |
13 | ``` html
14 |
15 | ```
16 |
17 | You can preview all built-in spinner types on the right. Please use other ways if you want to create your own spinner.
18 |
19 | ## Via `v-slot` Directive
20 |
21 | ::: warning
22 | Vue.js [deprecated slot special attributes](https://vuejs.org/v2/api/#slot-deprecated) after v2.6.0, it is recommended to use the [v-slot directive](https://vuejs.org/v2/api/#v-slot).
23 | :::
24 |
25 | We can use the [`v-slot` directive] (https:// Vuejs.org/v2/api/#v-slot) to configure them:
26 |
27 | ``` html
28 |
29 | Loading...
30 | No more message
31 | No results message
32 |
33 | ```
34 |
35 | Unlike other slots, the default value for the `error` slot will provide a retry button for users to load the data again. If you want to implement a retry button for users when you customize the `error` slot, you can receive the retry method `trigger` in prop and inject it into the retry button. like this:
36 |
37 | ``` html
38 |
39 |
40 | Error message, click
here to retry
41 |
42 |
43 | ```
44 |
45 | ## Via Plugin API
46 |
47 | In order to maintain consistent behavior for all load messages when we are building a large application, this plugin supports configuration on all slots using the plugin API. We just need to pass a string or Vue component to it, click [here](./configure-plugin-opts.md#slots) to read more about that.
48 |
49 | The `error` slot is still special in this way. Just as with the `v-slot` directive, if you want to implement a retry button for users in your own error component, you can use the [`vm.$attrs`](https://cn.vuejs.org/v2/api/#vm-attrs) property, like this:
50 |
51 | ``` html
52 |
53 |
54 | Error message, click
55 |
here
56 | to retry
57 |
58 | ```
59 |
60 | If you want to keep variables clear, you can also define a function property named `trigger`, and bind it to your retry button:
61 |
62 | ``` js
63 | // your own error component
64 | export default {
65 | /* ... */
66 | props: {
67 | trigger: Function,
68 | },
69 | /* ... */
70 | };
71 | ```
72 |
73 | ## About Hide & Default Styles
74 |
75 | For easy use, this component provides some default styles (`font-size`, `color` and `padding`) for slot content. If you want to keep all default styles when you configure via the `v-slot` directive, you have to wrap the content with a `template` tag:
76 |
77 | ``` html
78 |
79 |
80 | No more message
81 |
82 |
83 | ```
84 |
85 | If you want to hide a slot, you can create an empty element that is not a `template` element, because the empty `template` element will be ignored by Vue.js:
86 |
87 | ``` html
88 |
89 |
90 |
91 |
92 | ```
93 |
94 | If you want to remove all default styles to avoid affecting your own styles, you can wrap the content with an element that is not a `template` element:
95 |
96 | ``` html
97 |
98 |
99 | No more message
100 |
101 | ```
102 |
103 | I almost forgot, if you want to configure the slot content globally via the plugin API, you can control it like this:
104 |
105 | ``` js
106 | import Vue from 'vue';
107 | import InfiniteLoading from 'vue-infinite-loading';
108 | import InfiniteError from 'path/to/your/components/InfiniteError',
109 |
110 | Vue.use(InfiniteLoading, {
111 | slots: {
112 | // keep default styles
113 | noResults: 'No results message',
114 |
115 | // remove default styles
116 | noMore: InfiniteError,
117 |
118 | // hide slot
119 | error: {
120 | render: h => h('div'),
121 | },
122 | },
123 | });
124 | ```
125 |
--------------------------------------------------------------------------------
/docs/zh/api/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar: auto
3 | ---
4 |
5 | # API
6 |
7 | ## 属性
8 |
9 | 大多数属性都可以通过插件 API 改写全局默认值。
10 |
11 | - 参考:[配置插件选项 - 属性/设置](../guide/configure-plugin-opts.md#属性-设置)
12 |
13 | ### distance
14 |
15 | - 类型:`Number`
16 | - 默认值:`100`
17 | - 详细:
18 |
19 | 当滚动条的距离小于此值时,`infinite` 事件将会被触发。如果 `direction` 的值是 `top`,则计算滚动条距离顶部的距离;如果 `direction` 的值是 `bottom`,则计算滚动条距离底部的距离。
20 |
21 | ### spinner
22 |
23 | - 类型:`String`
24 | - 默认值:`default`
25 | - 有效值:`default` | `spiral` | `circles` | `bubbles` | `waveDots`
26 | - 详细:
27 |
28 | 此选项用于设置加载动画,你可以从内置选项中选择一个你喜欢的,你也可以通过名为 `spinner` 的[具名插槽](#spinner-2)来进行自定义。
29 |
30 | - 参考:[配置加载提示](../guide/configure-load-msg.md)
31 |
32 | ### direction
33 |
34 | - 类型:`String`
35 | - 默认值:`bottom`
36 | - 有效值:`top` | `bottom`
37 | - 详细:
38 |
39 | 此选项用于设置滚动加载的方向。
40 |
41 | ### forceUseInfiniteWrapper
42 |
43 | - 类型:`Boolean` | `String`
44 | - 默认值:`false`
45 | - 详细:
46 |
47 | 默认情况下,该组件会寻找最近的具备 `overflow-y: auto | scroll` CSS 样式的父元素,作为监听滚动事件的目标元素,此选项用于强制指定滚动元素,通常用于有复杂布局或第三方滚动组件(例如 [`perfect-scrollbar`](https://github.com/noraesae/perfect-scrollbar))的场景。
48 |
49 | 如果该值为 `true`,则会向上查找最近的具备 `infinite-wrapper` 属性的父元素作为滚动容器;如果该值为一个字符串,则会将该值当作 CSS 选择器并使用 `querySelector` 查找该元素,将其作为滚动容器;如果以上两种情况都找不到目标元素,则会使用 `window` 作为滚动容器。
50 |
51 | ### identifier
52 |
53 | - 类型:`any`
54 | - 默认值:`+new Date()`
55 | - 详细:
56 |
57 | 该组件会在此值改变时对组件进行重设,就像重新创建了一个新的组件一样,通常用于过滤器或者选项卡的场景。
58 |
59 | ## 插槽
60 |
61 | ::: warning
62 | Vue.js 官方于 v2.6.0 后[废弃 slot 特殊特性](https://cn.vuejs.org/v2/api/#slot-废弃),推荐使用[v-slot 指令](https://cn.vuejs.org/v2/api/#v-slot)。
63 | :::
64 |
65 | 插槽的内容可以通过 `Vue.js` 官方提供的[`v-slot` 指令](https://cn.vuejs.org/v2/api/#v-slot)进行设置,也可以通过插件 API 进行全局设置。
66 |
67 | - 参考:
68 | - [配置加载提示](../guide/configure-load-msg.md)
69 | - [配置插件选项 - 插槽](../guide/configure-plugin-opts.md#插槽)
70 |
71 | ### no-results
72 |
73 | - 默认值:`No results :(`
74 | - 详细:
75 |
76 | 该信息将会在没有加载到任何数据时呈现给用户,即没有调用过 `$state.loaded` 方法就调用了 `$state.complete` 方法。
77 |
78 | ### no-more
79 |
80 | - 默认值:`No more data :)`
81 | - 详细:
82 |
83 | 该信息将会在所有数据都已经加载完时呈现给用户,即调用过 `$state.loaded` 方法之后调用了 `$state.complete` 方法。
84 |
85 | ### error
86 |
87 | - 默认值:`Opps, something went wrong :( Retry `
88 | - 详细:
89 |
90 | 该信息将会在加载出现错误时呈现给用户,即调用 `$state.error` 方法时。
91 |
92 | ### spinner
93 |
94 | - 默认值:`Internal Spinner`
95 | - 详细:
96 |
97 | 此插槽将会在加载数据时展示,你可以通过它自定义加载动画。
98 |
99 | ## 事件
100 |
101 | ### infinite
102 |
103 | - 参数:`event`
104 | - 详细:
105 |
106 | 该事件将会在滚动距离小于 `distance` 属性时被触发,组件会传递一个特殊的事件参数给事件监听器,用于改变组件的加载状态,通常我们将其命名为 `$state`,它包含以下几个方法:
107 |
108 | #### $state.loaded
109 |
110 | 通知组件此次加载已经顺利完成,如果此时数据仍然没有填满首屏,`infinite` 事件将会被再次触发;反之,组件将会关闭加载动画并继续监听滚动事件。
111 |
112 | #### $state.complete
113 |
114 | 通知组件所有的数据已经加载完成,如果调用此方法前没有调用过 `$state.loaded`,那么插槽 `no-results` 的内容将会被展示;如果调用此方法前调用过 `$state.loaded`,那么插槽 `no-more` 的内容将会被展示。
115 |
116 | #### $state.error
117 |
118 | 通知组件此次加载失败了,插槽 `error` 的内容将会被展示。
119 |
120 | #### $state.reset
121 |
122 | 重设组件状态,等同于改变 `identifier` 属性。
123 |
124 | ## 选项
125 |
126 | 你可以通过插件 API 配置所有的插件选项。
127 |
128 | - 参考:[配置插件选项](../guide/configure-plugin-opts.md)
129 |
130 | ### props.spinner
131 |
132 | - 类型:`String`
133 | - 详细:
134 |
135 | 配置 `spinner` 属性的默认值。
136 |
137 | ::: warning 注意
138 | 该配置被读取的优先级低于 [选项 - slots.spinner](#slots-spinner),这意味着如果你正确配置了 `slots.spinner`,该配置就永远不会生效
139 | :::
140 |
141 | - 参考:[属性 - spinner](#spinner)
142 |
143 | ### props.distance
144 |
145 | - 类型:`Number`
146 | - 详细:
147 |
148 | 配置 `distance` 属性的默认值。
149 |
150 | - 参考:[属性 - distance](#distance)
151 |
152 | ### props.forceUseInfiniteWrapper
153 |
154 | - 类型:`Boolean` | `String`
155 | - 详细:
156 |
157 | 配置 `forceUseInfiniteWrapper` 属性的默认值。
158 |
159 | - 参考:[属性 - forceUseInfiniteWrapper](#forceuseinfinitewrapper)
160 |
161 | ### slots.noResults
162 |
163 | - 类型:`String` | `Vue Component`
164 | - 详细:
165 |
166 | 配置 `no-results` 插槽的默认内容。
167 |
168 | - 参考:[插槽 - no-results](#no-results)
169 |
170 | ### slots.noMore
171 |
172 | - 类型:`String` | `Vue Component`
173 | - 详细:
174 |
175 | 配置 `no-more` 插槽的默认内容。
176 |
177 | - 参考:[插槽 - no-more](#no-more)
178 |
179 | ### slots.error
180 |
181 | - 类型:`String` | `Vue Component`
182 | - 详细:
183 |
184 | 配置 `error` 插槽的默认内容。
185 |
186 | - 参考:[插槽 - error](#error)
187 |
188 | ### slots.errorBtnText
189 |
190 | - 类型:`String`
191 | - 默认值:`Retry`
192 | - 详细:
193 |
194 | 配置默认 `error` 插槽中重试按钮的显示文案。请注意,如果你自定义了 `error` 插槽的内容,此配置将没有任何作用,你需要自行创建重试按钮。
195 |
196 | - 参考:
197 | - [插槽 - error](#error)
198 | - [配置加载提示](../guide/configure-load-msg.md)
199 |
200 | ### slots.spinner
201 |
202 | - 类型:`String` | `Vue Component`
203 | - 详细:
204 |
205 | 配置 `spinner` 插槽的默认内容。
206 |
207 | - 参考:[插槽 - spinner](#spinner-2)
208 |
209 | ### system.throttleLimit
210 |
211 | - 类型:`Number`
212 | - 默认值:`50`
213 | - 详细:
214 |
215 | 配置 `scroll` 事件节流的间隔时间(单位:毫秒)。
216 |
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guide
2 |
3 | > This is adapted from [Vue.js's contributing guide](https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md).
4 |
5 | Hi! I’m really excited that you are interested in contributing to Vue-infinite-loading. Before submitting your contribution though, please make sure to take a moment and read through the following guidelines.
6 |
7 | - [Code of Conduct](https://github.com/PeachScript/vue-infinite-loading/blob/master/.github/CODE_OF_CONDUCT.md)
8 | - [Issue Reporting Guidelines](#issue-reporting-guidelines)
9 | - [Pull Request Guidelines](#pull-request-guidelines)
10 | - [Development Setup](#development-setup)
11 | - [Project Structure](#project-structure)
12 |
13 | ## Issue Reporting Guidelines
14 |
15 | - The issue list of this repo is **exclusively** for bug reports and feature requests.
16 |
17 | - Try to search for your issue, it may have already been answered or even fixed in the development version.
18 |
19 | - Check if the issue is reproducible with the latest stable version of Vue-infinite-loading. If you are using a pre-release, please indicate the specific version you are using.
20 |
21 | - It is **required** that you clearly describe the steps necessary to reproduce the issue you are running into. Although we would love to help our users as much as possible, diagnosing issues without clear reproduction steps is extremely time-consuming and simply not sustainable.
22 |
23 | - Use only the minimum amount of code necessary to reproduce the unexpected behavior. A good bug report should isolate specific methods that exhibit unexpected behavior and precisely define how expectations were violated. What did you expect the method or methods to do, and how did the observed behavior differ? The more precisely you isolate the issue, the faster we can investigate.
24 |
25 | - Issues with no clear repro steps will not be triaged. If an issue labeled "need repro" receives no further input from the issue author for more than 5 days, it will be closed.
26 |
27 | - It is recommended that you make a JSFiddle/JSBin/Codepen to demonstrate your issue. You could start with [this template](https://jsfiddle.net/PeachScript/qax0dbnc/) that already includes the latest version of Vue-infinite-loading.
28 |
29 | - If your issue is resolved but still open, don’t hesitate to close it. In case you found a solution by yourself, it could be helpful to explain how you fixed it.
30 |
31 | ## Pull Request Guidelines
32 |
33 | - Checkout a topic branch from the `master` branch, and merge back against it.
34 |
35 | - Work in the `src` folder and **DO NOT** checkin `dist` in the commits.
36 |
37 | - It's OK to have multiple small commits as you work on the PR - we will let GitHub automatically squash it before merging.
38 |
39 | - Make sure `npm test` passes. (see [development setup](#development-setup))
40 |
41 | - If adding new feature:
42 | - Add accompanying test case.
43 | - Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it.
44 |
45 | - If fixing a bug:
46 | - If you are resolving a special issue, add `(fix #xxxx[,#xxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `update entities encoding/decoding (fix #3899)`.
47 | - Provide detailed description of the bug in the PR. Live demo preferred.
48 | - Add appropriate test coverage if applicable.
49 |
50 | ## Development Setup
51 |
52 | You will need [Node.js](http://nodejs.org) **version 6+** installed.
53 |
54 | After cloning the repo, run:
55 |
56 | ``` bash
57 | $ npm install # or yarn
58 | ```
59 |
60 | ### Committing Changes
61 |
62 | Commit messages should follow the [commit message convention](./COMMIT_CONVENTION.md) so that changelogs can be automatically generated. Commit messages will be automatically validated upon commit.
63 |
64 | ### Commonly used NPM scripts
65 |
66 | ``` bash
67 | # run dev-server
68 | $ npm run dev
69 |
70 | # lint code
71 | $ npm run lint
72 |
73 | # run the test suite
74 | $ npm test
75 | ```
76 |
77 | There are some other scripts available in the `scripts` section of the `package.json` file.
78 |
79 | **Please make sure to have test pass successfully before submitting a PR.** Although the same tests will be run against your PR on the CI server, it is better to have it working locally beforehand.
80 |
81 | ## Project Structure
82 |
83 | - **`dist`**: contains built file for distribution. Note this directory is only updated when a release happens; they do not reflect the latest changes in development.
84 |
85 | - **`docs`**: contains documentation. Powered by [Vuepress](https://github.com/vuejs/vuepress).
86 |
87 | - **`src`**: contains the source code, obviously. The codebase is written in ES2015.
88 |
89 | - **`components`**: contains code for the core single-file component (`InfiniteLoading.vue`) and spinner single-file component (`Spinner.vue`).
90 |
91 | - **`styles`**: contains code for styles of different spinners, they are written with [Less](http://lesscss.org/).
92 |
93 | - **`config.js`**: contains all the runtime configurations for this plugin.
94 |
95 | - **`utils.js`**: contains all the tool functions for this plugin.
96 |
97 | - **`index.js`**: entry file, contains plugin `install` API definition and the logic to register `InfiniteLoading` component.
98 |
99 | - **`scripts`**: contains all the scripts use to developing, testing, building and deploying documentation.
100 |
101 | - **`test`**: contains unit tests. They are written with [Mocha](https://mochajs.org/) and run with [Karma](https://karma-runner.github.io/2.0/index.html).
102 |
103 | - **`types`**: contains TypeScript type definitions.
104 |
--------------------------------------------------------------------------------
/docs/api/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar: auto
3 | ---
4 |
5 | # API
6 |
7 | ## Props
8 |
9 | The default value for most of properties can be overridden through the Plugin API.
10 |
11 | - See also: [Configure Plugin Options - Props/Settings](../guide/configure-plugin-opts.md#props-settings)
12 |
13 | ### distance
14 |
15 | - Type: `Number`
16 | - Default: `100`
17 | - Details:
18 |
19 | The `infinite` event will be fired if the scroll distance is less than this value. If `direction` is set to `top`, it will calculate the distance between the scroll bar and the top, if `direction` is set to `bottom`, it will calculate the distance between the scroll bar and the bottom.
20 |
21 | ### spinner
22 |
23 | - Type: `String`
24 | - Default: `default`
25 | - Available: `default` | `spiral` | `circles` | `bubbles` | `waveDots`
26 | - Details:
27 |
28 | This property is used to set the loading animation, you can choose one from the internal spinners that you like, you can also customize it with a [named slot](#spinner-2) that called `spinner`.
29 |
30 | - See also: [Configure Load Messages](../guide/configure-load-msg.md)
31 |
32 | ### direction
33 |
34 | - Type: `String`
35 | - Default: `bottom`
36 | - Available: `top` | `bottom`
37 | - Details:
38 |
39 | This property is used to set the load direction.
40 |
41 | ### forceUseInfiniteWrapper
42 |
43 | - Type: `Boolean` | `String`
44 | - Default: `false`
45 | - Details:
46 |
47 | By default, the component will search for the closest parent element which has `overflow-y: auto | scroll` CSS style as the scroll container, this property is used to force to specify the scroll container, usually used when the case has complex layout or 3rd-party scroll plugin (eg: [`perfect-scrollbar`](https://github.com/noraesae/perfect-scrollbar)).
48 |
49 | If this value set be `true`, the component will search the closest parent element which has `infinite-wrapper` attribute as the scroll container, if this value is a string, the component will use it as a CSS selector, and search the element as the scroll container via the `querySelector` API, if all failed, the component will use `window` as the scroll container.
50 |
51 | ### identifier
52 |
53 | - Type: `any`
54 | - Default: `+new Date()`
55 | - Details:
56 |
57 | The component will be reset if this property has changed, just like recreating a new component, usually used when the case has filter or tabs.
58 |
59 | ## Slots
60 |
61 | ::: warning
62 | Vue.js [deprecated slot special attributes](https://vuejs.org/v2/api/#slot-deprecated) after v2.6.0, it is recommended to use the [v-slot directive](https://vuejs.org/v2/api/#v-slot).
63 | :::
64 |
65 | The contents for these slots can be configured via the [`v-slot` directives](https://vuejs.org/v2/api/#v-slot), also can be configure via the plugin API.
66 |
67 | - See also:
68 | - [Configure Load Messages](../guide/configure-load-msg.md)
69 | - [Configure Plugin Options - Slots](../guide/configure-plugin-opts.md#slots)
70 |
71 | ### no-results
72 |
73 | - Default: `No results :(`
74 | - Details:
75 |
76 | This message will be displayed if there is no data, it means we did not call the `$state.loaded` method before calling the `$stat.complete` method.
77 |
78 | ### no-more
79 |
80 | - Default: `No more data :)`
81 | - Details:
82 |
83 | This message will be displayed if there is no more data, it means we called the `$state.loaded` method before calling the `$state.complete` method.
84 |
85 | ### error
86 |
87 | - Default: `Opps, something went wrong :( Retry `
88 | - Details:
89 |
90 | This message will be displayed if loading failed, it means we called the `$state.error` method.
91 |
92 | ### spinner
93 |
94 | - Default: `Internal Spinner`
95 | - Details:
96 |
97 | This slot will be displayed when loading data, you can customize your own spinner through it.
98 |
99 | ## Events
100 |
101 | ### infinite
102 |
103 | - Argument: `event`
104 | - Details:
105 |
106 | This event will be fired if the scroll distance is less than the `distance` property, the component will pass a special event argument for the event handler to change loading status, usually we name it `$state`, include these methods:
107 |
108 | #### $state.loaded
109 |
110 | Inform the component that this loading has been successful, and the `infinite` event will be fired again if the first screen was not be filled up, otherwise, the component will hide the loading animation and continue to listen scroll event.
111 |
112 | #### $state.complete
113 |
114 | Inform the component that all the data has been loaded successfully, if the `$state.loaded` method has not been called before this, the content of `no-results` slot will be displayed, otherwise, the content of `no-more` slot will be displayed.
115 |
116 | #### $state.error
117 |
118 | Inform the component that this loading failed, the content of `error` slot will be displayed.
119 |
120 | #### $state.reset
121 |
122 | Reset the component, same as changing the `identifier` property.
123 |
124 | ## Options
125 |
126 | You can configure all these plugin options via the plugin API.
127 |
128 | - See also: [Configure Plugin Options](../guide/configure-plugin-opts.md)
129 |
130 | ### props.spinner
131 |
132 | - Type: `String`
133 | - Details:
134 |
135 | Configure the default value for `spinner` property.
136 |
137 | - See also: [Properties - spinner](#spinner)
138 |
139 | ### props.distance
140 |
141 | - Type: `Number`
142 | - Details:
143 |
144 | Configure the default value for `distance` property.
145 |
146 | ::: warning
147 | This option is read with a lower priority than [Options - slots.spinner](#slots-spinner), it means if you configure `slots.spinner` correctly, this option will never take effect
148 | :::
149 |
150 | - See also: [Properties - distance](#distance)
151 |
152 | ### props.forceUseInfiniteWrapper
153 |
154 | - Type: `Boolean` | `String`
155 | - Details:
156 |
157 | Configure the default value for `forceUseInfiniteWrapper` property.
158 |
159 | - See also: [Properties - forceUseInfiniteWrapper](#forceuseinfinitewrapper)
160 |
161 | ### slots.noResults
162 |
163 | - Type: `String` | `Vue Component`
164 | - Details:
165 |
166 | Configure the default content for `no-results` slot.
167 |
168 | - See also: [Slots - no-results](#no-results)
169 |
170 | ### slots.noMore
171 |
172 | - Type: `String` | `Vue Component`
173 | - Details:
174 |
175 | Configure the default content for `no-more` slot.
176 |
177 | - See also: [Slots - no-more](#no-more)
178 |
179 | ### slots.error
180 |
181 | - Type: `String` | `Vue Component`
182 | - Details:
183 |
184 | Configure the default content for `error` slot.
185 |
186 | - See also: [Slots - error](#error)
187 |
188 | ### slots.errorBtnText
189 |
190 | - Type: `String`
191 | - Default: `Retry`
192 | - Details:
193 |
194 | Configure the default text for retry button in the default `error` slot. Please note, it won't work if you customize the `error` slot, you need to configure retry button yourself.
195 |
196 | - See also:
197 | - [Slots - error](#error)
198 | - [Configure Load Messages](../guide/configure-load-msg.md)
199 |
200 | ### slots.spinner
201 |
202 | - Type: `String` | `Vue Component`
203 | - Details:
204 |
205 | Configure the default content for `spinner` slot.
206 |
207 | - See also: [Slots - spinner](#spinner-2)
208 |
209 | ### system.throttleLimit
210 |
211 | - Type: `Number`
212 | - Default: `50`
213 | - Details:
214 |
215 | Configure the default throttle space of time for `scroll` event (unit: ms).
216 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
20 |
21 |
25 |
26 |
30 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
48 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
62 |
63 |
64 |
65 |
94 |
95 |
369 |
--------------------------------------------------------------------------------
/src/components/InfiniteLoading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
17 | {{ slots.noResults }}
18 |
19 |
20 |
24 |
25 |
26 | {{ slots.noMore }}
27 |
28 |
29 |
33 |
34 |
38 |
39 |
40 | {{ slots.error }}
41 |
42 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
345 |
381 |
--------------------------------------------------------------------------------
/dist/vue-infinite-loading.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * vue-infinite-loading v2.4.5
3 | * (c) 2016-2020 PeachScript
4 | * MIT License
5 | */
6 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.VueInfiniteLoading=e():t.VueInfiniteLoading=e()}(this,(function(){return function(t){var e={};function n(i){if(e[i])return e[i].exports;var a=e[i]={i:i,l:!1,exports:{}};return t[i].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var a in t)n.d(i,a,function(e){return t[e]}.bind(null,a));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=9)}([function(t,e,n){var i=n(6);"string"==typeof i&&(i=[[t.i,i,""]]),i.locals&&(t.exports=i.locals);(0,n(3).default)("6223ff68",i,!0,{})},function(t,e,n){var i=n(8);"string"==typeof i&&(i=[[t.i,i,""]]),i.locals&&(t.exports=i.locals);(0,n(3).default)("27f0e51f",i,!0,{})},function(t,e){t.exports=function(t){var e=[];return e.toString=function(){return this.map((function(e){var n=function(t,e){var n=t[1]||"",i=t[3];if(!i)return n;if(e&&"function"==typeof btoa){var a=(o=i,"/*# sourceMappingURL=data:application/json;charset=utf-8;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(o))))+" */"),r=i.sources.map((function(t){return"/*# sourceURL="+i.sourceRoot+t+" */"}));return[n].concat(r).concat([a]).join("\n")}var o;return[n].join("\n")}(e,t);return e[2]?"@media "+e[2]+"{"+n+"}":n})).join("")},e.i=function(t,n){"string"==typeof t&&(t=[[null,t,""]]);for(var i={},a=0;an.parts.length&&(i.parts.length=n.parts.length)}else{var o=[];for(a=0;a',"\nscript:\n...\ninfiniteHandler($state) {\n ajax('https://www.example.com/api/news')\n .then((res) => {\n if (res.data.length) {\n $state.loaded();\n } else {\n $state.complete();\n }\n });\n}\n...","","more details: https://github.com/PeachScript/vue-infinite-loading/issues/57#issuecomment-324370549"].join("\n"),INFINITE_EVENT:"`:on-infinite` property will be deprecated soon, please use `@infinite` event instead.",IDENTIFIER:"the `reset` event will be deprecated soon, please reset this component by change the `identifier` property."},o={INFINITE_LOOP:["executed the callback function more than ".concat(i.loopCheckMaxCalls," times for a short time, it looks like searched a wrong scroll wrapper that doest not has fixed height or maximum height, please check it. If you want to force to set a element as scroll wrapper ranther than automatic searching, you can do this:"),'\n\x3c!-- add a special attribute for the real scroll wrapper --\x3e\n\n ...\n \x3c!-- set force-use-infinite-wrapper --\x3e\n \n
\nor\n\n ...\n \x3c!-- set force-use-infinite-wrapper as css selector of the real scroll wrapper --\x3e\n \n
\n ',"more details: https://github.com/PeachScript/vue-infinite-loading/issues/55#issuecomment-316934169"].join("\n")},s={READY:0,LOADING:1,COMPLETE:2,ERROR:3},l={color:"#666",fontSize:"14px",padding:"10px 0"},d={mode:"development",props:{spinner:"default",distance:100,forceUseInfiniteWrapper:!1},system:i,slots:{noResults:"No results :(",noMore:"No more data :)",error:"Opps, something went wrong :(",errorBtnText:"Retry",spinner:""},WARNINGS:r,ERRORS:o,STATUS:s},c=n(4),u=n.n(c),p={BUBBLES:{render:function(t){return t("span",{attrs:{class:"loading-bubbles"}},Array.apply(Array,Array(8)).map((function(){return t("span",{attrs:{class:"bubble-item"}})})))}},CIRCLES:{render:function(t){return t("span",{attrs:{class:"loading-circles"}},Array.apply(Array,Array(8)).map((function(){return t("span",{attrs:{class:"circle-item"}})})))}},DEFAULT:{render:function(t){return t("i",{attrs:{class:"loading-default"}})}},SPIRAL:{render:function(t){return t("i",{attrs:{class:"loading-spiral"}})}},WAVEDOTS:{render:function(t){return t("span",{attrs:{class:"loading-wave-dots"}},Array.apply(Array,Array(5)).map((function(){return t("span",{attrs:{class:"wave-item"}})})))}}};function f(t,e,n,i,a,r,o,s){var l,d="function"==typeof t?t.options:t;if(e&&(d.render=e,d.staticRenderFns=n,d._compiled=!0),i&&(d.functional=!0),r&&(d._scopeId="data-v-"+r),o?(l=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),a&&a.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(o)},d._ssrRegister=l):a&&(l=s?function(){a.call(this,this.$root.$options.shadowRoot)}:a),l)if(d.functional){d._injectStyles=l;var c=d.render;d.render=function(t,e){return l.call(e),c(t,e)}}else{var u=d.beforeCreate;d.beforeCreate=u?[].concat(u,l):[l]}return{exports:t,options:d}}var b=f({name:"Spinner",computed:{spinnerView:function(){return p[(this.$attrs.spinner||"").toUpperCase()]||this.spinnerInConfig},spinnerInConfig:function(){return d.slots.spinner&&"string"==typeof d.slots.spinner?{render:function(){return this._v(d.slots.spinner)}}:"object"===u()(d.slots.spinner)?d.slots.spinner:p[d.props.spinner.toUpperCase()]||p.DEFAULT}}},(function(){var t=this.$createElement;return(this._self._c||t)(this.spinnerView,{tag:"component"})}),[],!1,(function(t){var e=n(5);e.__inject__&&e.__inject__(t)}),"46b20d22",null).exports;function h(t){"production"!==d.mode&&console.warn("[Vue-infinite-loading warn]: ".concat(t))}function m(t){console.error("[Vue-infinite-loading error]: ".concat(t))}var g={timers:[],caches:[],throttle:function(t){var e=this;-1===this.caches.indexOf(t)&&(this.caches.push(t),this.timers.push(setTimeout((function(){t(),e.caches.splice(e.caches.indexOf(t),1),e.timers.shift()}),d.system.throttleLimit)))},reset:function(){this.timers.forEach((function(t){clearTimeout(t)})),this.timers.length=0,this.caches=[]}},v={isChecked:!1,timer:null,times:0,track:function(){var t=this;this.times+=1,clearTimeout(this.timer),this.timer=setTimeout((function(){t.isChecked=!0}),d.system.loopCheckTimeout),this.times>d.system.loopCheckMaxCalls&&(m(o.INFINITE_LOOP),this.isChecked=!0)}},w={key:"_infiniteScrollHeight",getScrollElm:function(t){return t===window?document.documentElement:t},save:function(t){var e=this.getScrollElm(t);e[this.key]=e.scrollHeight},restore:function(t){var e=this.getScrollElm(t);"number"==typeof e[this.key]&&(e.scrollTop=e.scrollHeight-e[this.key]+e.scrollTop),this.remove(e)},remove:function(t){void 0!==t[this.key]&&delete t[this.key]}};function y(t){return t.replace(/[A-Z]/g,(function(t){return"-".concat(t.toLowerCase())}))}function x(t){return t.offsetWidth+t.offsetHeight>0}var k=f({name:"InfiniteLoading",data:function(){return{scrollParent:null,scrollHandler:null,isFirstLoad:!0,status:s.READY,slots:d.slots}},components:{Spinner:b},computed:{isShowSpinner:function(){return this.status===s.LOADING},isShowError:function(){return this.status===s.ERROR},isShowNoResults:function(){return this.status===s.COMPLETE&&this.isFirstLoad},isShowNoMore:function(){return this.status===s.COMPLETE&&!this.isFirstLoad},slotStyles:function(){var t=this,e={};return Object.keys(d.slots).forEach((function(n){var i=y(n);(!t.$slots[i]&&!d.slots[n].render||t.$slots[i]&&!t.$slots[i][0].tag)&&(e[n]=l)})),e}},props:{distance:{type:Number,default:d.props.distance},spinner:String,direction:{type:String,default:"bottom"},forceUseInfiniteWrapper:{type:[Boolean,String],default:d.props.forceUseInfiniteWrapper},identifier:{default:+new Date},onInfinite:Function},watch:{identifier:function(){this.stateChanger.reset()}},mounted:function(){var t=this;this.$watch("forceUseInfiniteWrapper",(function(){t.scrollParent=t.getScrollParent()}),{immediate:!0}),this.scrollHandler=function(e){t.status===s.READY&&(e&&e.constructor===Event&&x(t.$el)?g.throttle(t.attemptLoad):t.attemptLoad())},setTimeout((function(){t.scrollHandler(),t.scrollParent.addEventListener("scroll",t.scrollHandler,a)}),1),this.$on("$InfiniteLoading:loaded",(function(e){t.isFirstLoad=!1,"top"===t.direction&&t.$nextTick((function(){w.restore(t.scrollParent)})),t.status===s.LOADING&&t.$nextTick(t.attemptLoad.bind(null,!0)),e&&e.target===t||h(r.STATE_CHANGER)})),this.$on("$InfiniteLoading:complete",(function(e){t.status=s.COMPLETE,t.$nextTick((function(){t.$forceUpdate()})),t.scrollParent.removeEventListener("scroll",t.scrollHandler,a),e&&e.target===t||h(r.STATE_CHANGER)})),this.$on("$InfiniteLoading:reset",(function(e){t.status=s.READY,t.isFirstLoad=!0,w.remove(t.scrollParent),t.scrollParent.addEventListener("scroll",t.scrollHandler,a),setTimeout((function(){g.reset(),t.scrollHandler()}),1),e&&e.target===t||h(r.IDENTIFIER)})),this.stateChanger={loaded:function(){t.$emit("$InfiniteLoading:loaded",{target:t})},complete:function(){t.$emit("$InfiniteLoading:complete",{target:t})},reset:function(){t.$emit("$InfiniteLoading:reset",{target:t})},error:function(){t.status=s.ERROR,g.reset()}},this.onInfinite&&h(r.INFINITE_EVENT)},deactivated:function(){this.status===s.LOADING&&(this.status=s.READY),this.scrollParent.removeEventListener("scroll",this.scrollHandler,a)},activated:function(){this.scrollParent.addEventListener("scroll",this.scrollHandler,a)},methods:{attemptLoad:function(t){var e=this;this.status!==s.COMPLETE&&x(this.$el)&&this.getCurrentDistance()<=this.distance?(this.status=s.LOADING,"top"===this.direction&&this.$nextTick((function(){w.save(e.scrollParent)})),"function"==typeof this.onInfinite?this.onInfinite.call(null,this.stateChanger):this.$emit("infinite",this.stateChanger),!t||this.forceUseInfiniteWrapper||v.isChecked||v.track()):this.status===s.LOADING&&(this.status=s.READY)},getCurrentDistance:function(){var t;"top"===this.direction?t="number"==typeof this.scrollParent.scrollTop?this.scrollParent.scrollTop:this.scrollParent.pageYOffset:t=this.$el.getBoundingClientRect().top-(this.scrollParent===window?window.innerHeight:this.scrollParent.getBoundingClientRect().bottom);return t},getScrollParent:function(){var t,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.$el;return"string"==typeof this.forceUseInfiniteWrapper&&(t=document.querySelector(this.forceUseInfiniteWrapper)),t||("BODY"===e.tagName?t=window:!this.forceUseInfiniteWrapper&&["scroll","auto"].indexOf(getComputedStyle(e).overflowY)>-1?t=e:(e.hasAttribute("infinite-wrapper")||e.hasAttribute("data-infinite-wrapper"))&&(t=e)),t||this.getScrollParent(e.parentNode)}},destroyed:function(){!this.status!==s.COMPLETE&&(g.reset(),w.remove(this.scrollParent),this.scrollParent.removeEventListener("scroll",this.scrollHandler,a))}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"infinite-loading-container"},[n("div",{directives:[{name:"show",rawName:"v-show",value:t.isShowSpinner,expression:"isShowSpinner"}],staticClass:"infinite-status-prompt",style:t.slotStyles.spinner},[t._t("spinner",[n("spinner",{attrs:{spinner:t.spinner}})])],2),t._v(" "),n("div",{directives:[{name:"show",rawName:"v-show",value:t.isShowNoResults,expression:"isShowNoResults"}],staticClass:"infinite-status-prompt",style:t.slotStyles.noResults},[t._t("no-results",[t.slots.noResults.render?n(t.slots.noResults,{tag:"component"}):[t._v(t._s(t.slots.noResults))]])],2),t._v(" "),n("div",{directives:[{name:"show",rawName:"v-show",value:t.isShowNoMore,expression:"isShowNoMore"}],staticClass:"infinite-status-prompt",style:t.slotStyles.noMore},[t._t("no-more",[t.slots.noMore.render?n(t.slots.noMore,{tag:"component"}):[t._v(t._s(t.slots.noMore))]])],2),t._v(" "),n("div",{directives:[{name:"show",rawName:"v-show",value:t.isShowError,expression:"isShowError"}],staticClass:"infinite-status-prompt",style:t.slotStyles.error},[t._t("error",[t.slots.error.render?n(t.slots.error,{tag:"component",attrs:{trigger:t.attemptLoad}}):[t._v("\n "+t._s(t.slots.error)+"\n "),n("br"),t._v(" "),n("button",{staticClass:"btn-try-infinite",domProps:{textContent:t._s(t.slots.errorBtnText)},on:{click:t.attemptLoad}})]],{trigger:t.attemptLoad})],2)])}),[],!1,(function(t){var e=n(7);e.__inject__&&e.__inject__(t)}),"644ea9c9",null).exports;function E(t){d.mode=t.config.productionTip?"development":"production"}Object.defineProperty(k,"install",{configurable:!1,enumerable:!1,value:function(t,e){Object.assign(d.props,e&&e.props),Object.assign(d.slots,e&&e.slots),Object.assign(d.system,e&&e.system),t.component("infinite-loading",k),E(t)}}),"undefined"!=typeof window&&window.Vue&&(window.Vue.component("infinite-loading",k),E(window.Vue));e.default=k}])}));
--------------------------------------------------------------------------------
/test/unit/specs/InfiniteLoading.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import Vue from 'vue/dist/vue.common';
4 | import { isShow, continuesCall, fakeBox } from '../utils';
5 | import config from '../../../src/config';
6 | import { loopTracker } from '../../../src/utils';
7 | import InfiniteLoading from '../../../src/components/InfiniteLoading.vue';
8 |
9 | describe('vue-infinite-loading:component', () => {
10 | let vm; // save Vue model
11 | const basicConfig = {
12 | data: {
13 | list: [],
14 | isDivScroll: false,
15 | direction: 'bottom',
16 | spinner: 'default',
17 | },
18 | template: `
19 |
31 | `,
32 | components: { InfiniteLoading },
33 | methods: {
34 | infiniteHandler() {},
35 | },
36 | };
37 |
38 | before(() => {
39 | // insert global CSS
40 | const styles = document.createElement('style');
41 |
42 | styles.id = 'testing-style';
43 | styles.innerHTML = `
44 | body {
45 | margin: 0;
46 | }
47 | div {
48 | height: 200px;
49 | }
50 | ul {
51 | list-style: none;
52 | margin: 0;
53 | padding: 0;
54 | }
55 | ul li {
56 | height: 40px;
57 | }
58 | `;
59 |
60 | document.body.appendChild(styles);
61 |
62 | // use development mode
63 | config.mode = 'development';
64 | });
65 |
66 | after(() => {
67 | // remove global CSS
68 | document.getElementById('testing-style').remove();
69 |
70 | // restore app mode
71 | config.mode = Vue.config.productionTip ? 'development' : 'production';
72 | });
73 |
74 | beforeEach(() => {
75 | // create Vue model before each case start
76 | const container = document.createElement('div');
77 |
78 | container.id = 'app';
79 | document.body.appendChild(container);
80 | });
81 |
82 | afterEach(() => {
83 | basicConfig.data.list = [];
84 | // destroy Vue model after each case complete
85 | vm.$el.parentNode.removeChild(vm.$el);
86 |
87 | if (vm.$refs.infiniteLoading) {
88 | vm.$refs.infiniteLoading.$destroy();
89 | }
90 |
91 | if (document.getElementById('app')) {
92 | document.getElementById('app').remove();
93 | }
94 | });
95 |
96 | it('should complete a standard life circle\n (init -> loading -> loaded -> complete)', (done) => {
97 | vm = new Vue(Object.assign({}, basicConfig, {
98 | methods: {
99 | infiniteHandler: function infiniteHandler($state) {
100 | for (let i = 0, j = this.list.length; i < 3; i += 1) {
101 | this.list.push(j + i);
102 | }
103 |
104 | const expectedLength = this.list.length;
105 | const isComplete = expectedLength > 6;
106 |
107 | // check spinner
108 | expect(isShow(this.$el.querySelector('.loading-default'))).to.be.true;
109 |
110 | if (isComplete) {
111 | $state.complete();
112 | } else {
113 | $state.loaded();
114 | }
115 |
116 | this.$nextTick(() => {
117 | // check list items
118 | expect(this.$el.querySelectorAll('ul li')).to.have.lengthOf(expectedLength);
119 |
120 | if (isComplete) {
121 | // check no more text
122 | expect(isShow(this.$el.querySelector('.infinite-status-prompt:nth-child(3)'))).to.be.true;
123 | done();
124 | }
125 | });
126 | },
127 | },
128 | }));
129 |
130 | vm.$mount('#app');
131 | });
132 |
133 | it('should not trigger load again before the last load is complete\n (use div as the container and spiral spinner)', (done) => {
134 | vm = new Vue(Object.assign({}, basicConfig, {
135 | data: {
136 | list: [],
137 | isDivScroll: true,
138 | direction: 'bottom',
139 | spinner: 'spiral',
140 | },
141 | methods: {
142 | infiniteHandler: function infiniteHandler() {
143 | const { scrollParent } = this.$refs.infiniteLoading;
144 | const loadCountEachTime = 10;
145 |
146 | this.list.push(...new Array(loadCountEachTime + 1).join('1').split(''));
147 | this.$nextTick(() => {
148 | // trigger scroll event manually
149 | scrollParent.scrollTop = scrollParent.scrollHeight;
150 |
151 | // wait for the scroll event handler process scroll event
152 | setTimeout(() => {
153 | expect(this.list).to.have.lengthOf(loadCountEachTime);
154 | done();
155 | }, 10);
156 | });
157 | },
158 | },
159 | }));
160 |
161 | vm.$mount('#app');
162 | });
163 |
164 | it('should works again when reset it after a completion\n (use top direction and bubbles spinner)', (done) => {
165 | let calledTimes = 0;
166 |
167 | vm = new Vue(Object.assign({}, basicConfig, {
168 | data: {
169 | list: [],
170 | isDivScroll: false,
171 | direction: 'top',
172 | spinner: 'bubbles',
173 | },
174 | methods: {
175 | infiniteHandler: function infiniteHandler($state) {
176 | calledTimes += 1;
177 |
178 | if (calledTimes === 1) {
179 | $state.complete();
180 | this.$nextTick(() => {
181 | // check no results text
182 | expect(isShow(this.$el.querySelector('.infinite-status-prompt:nth-child(2)'))).to.be.true;
183 |
184 | // reset component
185 | $state.reset();
186 | });
187 | } else if (calledTimes === 2) {
188 | // check spinner
189 | expect(isShow(this.$el.querySelector('.loading-bubbles'))).to.be.true;
190 | done();
191 | }
192 | },
193 | },
194 | }));
195 |
196 | vm.$mount('#app');
197 | });
198 |
199 | it('should always load data until fill up the container\n (use div as the container and circles spinner)', (done) => {
200 | let timer;
201 |
202 | vm = new Vue(Object.assign({}, basicConfig, {
203 | data: {
204 | list: [],
205 | isDivScroll: true,
206 | direction: 'bottom',
207 | spinner: 'circles',
208 | },
209 | methods: {
210 | infiniteHandler: function infiniteHandler($state) {
211 | this.list.push(this.list.length + 1);
212 | this.$nextTick(() => {
213 | $state.loaded();
214 | });
215 |
216 | // wait for the container be filled up
217 | clearTimeout(timer);
218 | timer = setTimeout(() => {
219 | const listHeight = parseInt(getComputedStyle(this.$el.querySelector('ul')).height, 10);
220 | const itemHeight = parseInt(getComputedStyle(this.$el.querySelector('li')).height, 10);
221 | const expectedCount = listHeight / itemHeight;
222 |
223 | expect(this.list.length).to.be.at.least(expectedCount);
224 | done();
225 | }, 100);
226 | },
227 | },
228 | }));
229 |
230 | vm.$mount('#app');
231 | });
232 |
233 | it('should works properly with scroll plugins through the `infinite-wrapper` property', (done) => {
234 | const app = document.getElementById('app');
235 | const wrapper = document.createElement('div');
236 |
237 | app.appendChild(wrapper);
238 | app.setAttribute('infinite-wrapper', ''); // add `infinite-wrapper` property for app container
239 | vm = new Vue(Object.assign({}, basicConfig, {
240 | methods: {
241 | infiniteHandler: function infiniteHandler() {
242 | expect(this.$refs.infiniteLoading.scrollParent).to.equal(app);
243 | done();
244 | },
245 | },
246 | }));
247 |
248 | vm.$mount(wrapper);
249 | });
250 |
251 | it('should not works when deactivated by the `keep-alive` feature\n (use top direction and use div as the container)', (done) => {
252 | let calledTimes = 0;
253 | const InfiniteView = Object.assign({}, basicConfig, {
254 | data() {
255 | return {
256 | list: [],
257 | isDivScroll: true,
258 | direction: 'top',
259 | spinner: 'unknown',
260 | };
261 | },
262 | methods: {
263 | infiniteHandler: function infiniteHandler($state) {
264 | calledTimes += 1;
265 |
266 | if (calledTimes === 1) {
267 | // change view to deactivate the component
268 | this.$parent.currentView = null;
269 | this.$nextTick(() => {
270 | // trigger loaded event
271 | $state.loaded();
272 | this.$nextTick(() => {
273 | // doesnot care the loaded event when it be deactivated
274 | expect(calledTimes).to.equal(1);
275 | done();
276 | });
277 | });
278 | }
279 | },
280 | },
281 | });
282 |
283 | vm = new Vue({
284 | data: {
285 | currentView: 'InfiniteView',
286 | },
287 | template: `
288 |
289 |
290 |
291 | `,
292 | components: {
293 | InfiniteView,
294 | },
295 | });
296 |
297 | vm.$mount('#app');
298 | });
299 |
300 | it('should still works properly with the deprecated property `:on-infinite` but throw warning', (done) => {
301 | let isThrowWarn;
302 |
303 | console.warn = fakeBox(console.warn, (text) => {
304 | if (text.indexOf('@infinite') > -1) {
305 | isThrowWarn = true;
306 | }
307 | });
308 |
309 | vm = new Vue(Object.assign({}, basicConfig, {
310 | methods: {
311 | onInfinite: function onInfinite() {
312 | expect(isThrowWarn).to.be.true;
313 | console.warn = fakeBox();
314 | done();
315 | },
316 | },
317 | template: `
318 |
321 |
322 | `,
323 | }));
324 |
325 | vm.$mount('#app');
326 | });
327 |
328 | it('should interpolate custom spinner slot', (done) => {
329 | vm = new Vue(Object.assign({}, basicConfig, {
330 | mounted: function mounted() {
331 | expect(this.$el.querySelector('.icon-spinner')).to.be.not.null;
332 | done();
333 | },
334 | template: `
335 |
336 |
337 |
338 |
339 |
340 | `,
341 | }));
342 |
343 | vm.$mount('#app');
344 | });
345 |
346 | it('should remove slot styles if does not configure with `template` tag', (done) => {
347 | vm = new Vue(Object.assign({}, basicConfig, {
348 | methods: {
349 | infiniteHandler: function infiniteHandler($state) {
350 | // expect empty styles for no results slot
351 | expect(this.$el.querySelector('.infinite-status-prompt:nth-child(2)').style.color).to.be.empty;
352 |
353 | // expect valid styles for no more slot
354 | expect(this.$el.querySelector('.infinite-status-prompt:nth-child(3)').style.color).to.not.be.empty;
355 | $state.complete();
356 | done();
357 | },
358 | },
359 | template: `
360 |
361 |
362 |
363 | `,
364 | }));
365 |
366 | vm.$mount('#app');
367 | });
368 |
369 | it('should throttle properly for the scroll event handler\n (use div as the container and wave dots spinner)', (done) => {
370 | vm = new Vue(Object.assign({}, basicConfig, {
371 | data: {
372 | list: [...new Array(20).join('1').split('')],
373 | isDivScroll: true,
374 | direction: 'bottom',
375 | spinner: 'waveDots',
376 | },
377 | mounted: function mounted() {
378 | const { scrollParent } = this.$refs.infiniteLoading;
379 | const spyFn = sinon.spy(this.$refs.infiniteLoading, 'attemptLoad');
380 | const alreadyCalledTimes = 1; // it will be called immediately after mount
381 |
382 | continuesCall(() => {
383 | scrollParent.scrollTop += 10;
384 | }, 10, () => {
385 | expect(spyFn).to.have.been.callCount(0 + alreadyCalledTimes);
386 | setTimeout(() => {
387 | expect(spyFn).to.have.been.callCount(1 + alreadyCalledTimes);
388 | done();
389 | }, config.system.throttleLimit + 10);
390 | });
391 | },
392 | }));
393 |
394 | vm.$mount('#app');
395 | });
396 |
397 | it('should still works properly with the $refs.component.$emit but throw warning', (done) => {
398 | let throwWarnTimes = 0;
399 |
400 | console.warn = fakeBox(console.warn, (text) => {
401 | if (text.indexOf('$state') > -1) {
402 | throwWarnTimes += 1;
403 | }
404 | });
405 |
406 | vm = new Vue(Object.assign({}, basicConfig, {
407 | methods: {
408 | infiniteHandler: function infiniteHandler() {
409 | if (!throwWarnTimes) {
410 | this.$refs.infiniteLoading.$emit('$InfiniteLoading:loaded');
411 | } else {
412 | this.$refs.infiniteLoading.$emit('$InfiniteLoading:complete');
413 | expect(throwWarnTimes).to.equal(2);
414 | console.warn = fakeBox();
415 | done();
416 | }
417 | },
418 | },
419 | }));
420 |
421 | vm.$mount('#app');
422 | });
423 |
424 | it('should find my forcible element as scroll wrapper when using `force-use-infinite-wrapper` property', (done) => {
425 | vm = new Vue(Object.assign({}, basicConfig, {
426 | template: `
427 |
443 | `,
444 | methods: {
445 | infiniteHandler: function infiniteHandler() {
446 | expect(this.$refs.infiniteLoading.scrollParent).to.equal(this.$el);
447 | done();
448 | },
449 | },
450 | }));
451 |
452 | vm.$mount('#app');
453 | });
454 |
455 | it('should throw error when the component be in a infinite loop caused by a wrapper with unfixed height', (done) => {
456 | let isThrowError;
457 |
458 | console.error = fakeBox(console.error, (text) => {
459 | if (text.indexOf('issues/55') > -1) {
460 | isThrowError = true;
461 | }
462 | });
463 |
464 | vm = new Vue(Object.assign({}, basicConfig, {
465 | template: `
466 |
477 | `,
478 | methods: {
479 | infiniteHandler: function infiniteHandler($state) {
480 | if (!isThrowError) {
481 | this.list.push(this.list.length + 1);
482 | $state.loaded();
483 | } else {
484 | $state.complete();
485 | expect(isThrowError).to.be.true;
486 | console.error = fakeBox();
487 | // wait for the loop check flag be marked as true
488 | setTimeout(() => {
489 | expect(loopTracker.isChecked).to.be.true;
490 | done();
491 | }, config.system.loopCheckTimeout);
492 | }
493 | },
494 | },
495 | }));
496 |
497 | vm.$mount('#app');
498 | });
499 |
500 | it('should search scroll wrapper again when change the `force-use-infinite-wrapper` property', (done) => {
501 | vm = new Vue(Object.assign({}, basicConfig, {
502 | data: {
503 | forceUseInfiniteWrapper: true,
504 | },
505 | template: `
506 |
507 |
508 |
513 |
514 |
515 |
516 | `,
517 | mounted: function mounted() {
518 | expect(this.$refs.infiniteLoading.scrollParent).to.equal(this.$el);
519 | this.forceUseInfiniteWrapper = false;
520 | this.$nextTick(() => {
521 | expect(this.$refs.infiniteLoading.scrollParent).to.equal(this.$el.querySelector('div'));
522 | done();
523 | });
524 | },
525 | }));
526 |
527 | vm.$mount('#app');
528 | });
529 |
530 | it('should find my forcible element as scroll wrapper when using `force-use-infinite-wrapper` as seletor', (done) => {
531 | vm = new Vue(Object.assign({}, basicConfig, {
532 | template: `
533 |
549 | `,
550 | methods: {
551 | infiniteHandler: function infiniteHandler() {
552 | expect(this.$refs.infiniteLoading.scrollParent).to.equal(this.$el);
553 | done();
554 | },
555 | },
556 | }));
557 |
558 | vm.$mount('#app');
559 | });
560 |
561 | it('should be reset if received `reset` event or change `identifier`, and throw warning if use the event way', (done) => {
562 | let triggerTimes = 0;
563 | let isThrowWarn;
564 |
565 | vm = new Vue(Object.assign({}, basicConfig, {
566 | data: {
567 | list: [],
568 | identifier: '',
569 | },
570 | template: `
571 |
572 |
balabala
573 |
577 |
578 |
579 | `,
580 | methods: {
581 | infiniteHandler: function infiniteHandler() {
582 | switch (triggerTimes += 1) {
583 | case 1:
584 | console.warn = fakeBox(console.warn, (text) => {
585 | if (text.indexOf('identifier') > -1) {
586 | isThrowWarn = true;
587 | }
588 | });
589 |
590 | // emit reset event to reset component
591 | this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset');
592 | console.warn = fakeBox();
593 | break;
594 | case 2:
595 | // expect get warning if use reset event
596 | expect(isThrowWarn).to.be.true;
597 |
598 | // trigger scroll manually to test throttle
599 | this.$refs.infiniteLoading.scrollParent.scrollTop += 1;
600 |
601 | // change identifier to reset component (also clear the throttleer)
602 | this.identifier = +new Date();
603 |
604 | break;
605 | case 3:
606 | done();
607 | break;
608 | default:
609 | }
610 | },
611 | },
612 | }));
613 |
614 | vm.$mount('#app');
615 | });
616 |
617 | it('should display error message and support retry when load failed', (done) => {
618 | let triggerTimes = 0;
619 |
620 | vm = new Vue(Object.assign({}, basicConfig, {
621 | methods: {
622 | infiniteHandler: function infiniteHandler($state) {
623 | switch (triggerTimes += 1) {
624 | case 1:
625 | $state.error();
626 | this.$nextTick(() => {
627 | const btnRetry = this.$el.querySelector('.btn-try-infinite');
628 |
629 | expect(btnRetry).to.be.not.null;
630 |
631 | // trigger load again
632 | btnRetry.click();
633 | });
634 | break;
635 | case 2:
636 | done();
637 | break;
638 | default:
639 | }
640 | },
641 | },
642 | }));
643 |
644 | vm.$mount('#app');
645 | });
646 |
647 | it('should support use a component as the default spinner', (done) => {
648 | const spinnerId = 'custom-spinner';
649 |
650 | // override default slot spinner
651 | config.slots.spinner = { template: `Loading...
` };
652 |
653 | vm = new Vue(Object.assign({}, basicConfig, {
654 | template: `
655 |
657 |
658 | `,
659 | methods: {
660 | infiniteHandler: function infiniteHandler() {
661 | // assert custom spinner
662 | expect(this.$el.querySelector(`#${spinnerId}`)).to.be.not.null;
663 |
664 | // restore config
665 | config.slots.spinner = '';
666 | done();
667 | },
668 | },
669 | }));
670 |
671 | vm.$mount('#app');
672 | });
673 |
674 | it('should support use a string as the default spinner', (done) => {
675 | config.slots.spinner = 'custom-spinner';
676 |
677 | vm = new Vue(Object.assign({}, basicConfig, {
678 | template: `
679 |
681 |
682 | `,
683 | methods: {
684 | infiniteHandler: function infiniteHandler() {
685 | // assert custom spinner
686 | expect(this.$el.innerHTML).to.contain(config.slots.spinner);
687 |
688 | // restore config
689 | config.slots.spinner = '';
690 | done();
691 | },
692 | },
693 | }));
694 |
695 | vm.$mount('#app');
696 | });
697 |
698 | it('should save and restore scroll bar when using top direction', (done) => {
699 | vm = new Vue(Object.assign({}, basicConfig, {
700 | data: {
701 | list: [1, 2, 3, 4, 5],
702 | },
703 | template: `
704 |
713 | `,
714 | methods: {
715 | infiniteHandler($state) {
716 | expect(this.$el.scrollTop).to.equal(0);
717 | this.list.push(6, 7, 8, 9, 10);
718 | $state.loaded();
719 |
720 | // wait for scroll bar restore
721 | setTimeout(() => {
722 | expect(this.$el.scrollTop).to.not.equal(0);
723 | done();
724 | }, 100);
725 | },
726 | },
727 | }));
728 |
729 | vm.$mount('#app');
730 | });
731 | });
732 |
--------------------------------------------------------------------------------