├── .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 |
11 | 12 |
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 | 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 |
13 | 14 |
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 |
11 | 12 |
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 |
31 |

32 | 33 |
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 |
11 | 12 | 15 |
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 | 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 | 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 |
13 | 14 |
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 |
11 | 12 | 15 |
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 | 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 |