├── .npmignore
├── .gitignore
├── .editorconfig
├── rollup.config.js
├── src
├── index.js
└── directive.js
├── example
└── index.html
├── bower.json
├── package.json
├── karma.conf.js
├── README.MD
└── test
└── unit
└── vue-infinite-scroll.test.js
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | coverage
4 | .idea
5 | .jshintrc
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | coverage
4 | .idea
5 | .jshintrc
6 | vue-infinite-scroll.js
7 | *.log
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 |
3 | export default {
4 | entry: './src/index.js',
5 | dest: 'vue-infinite-scroll.js',
6 | plugins: [
7 | babel({
8 | exclude: 'node_modules/**',
9 | presets: ['es2015-rollup']
10 | })
11 | ],
12 | format: 'umd',
13 | moduleName: 'infiniteScroll'
14 | };
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import InfiniteScroll from './directive';
2 |
3 | const install = function(Vue) {
4 | Vue.directive('InfiniteScroll', InfiniteScroll);
5 | };
6 |
7 | if (window.Vue) {
8 | window.infiniteScroll = InfiniteScroll;
9 | Vue.use(install); // eslint-disable-line
10 | }
11 |
12 | InfiniteScroll.install = install;
13 | export default InfiniteScroll;
14 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | vue-infinite-scroll
6 |
7 |
8 |
13 |
14 |
15 |
16 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-infinite-scroll",
3 | "description": "An infinite scroll directive for vue.js.",
4 | "keywords": [
5 | "infinite-scroll",
6 | "vue"
7 | ],
8 | "main": [
9 | "vue-infinite-scroll.js"
10 | ],
11 | "dependencies": {
12 | "vue": "~1.0.10"
13 | },
14 | "devDependencies": {
15 | "babel-preset-es2015-rollup": "~1.1.1",
16 | "jasmine-core": "~2.4.1",
17 | "karma": "~0.13.21",
18 | "karma-chrome-launcher": "~0.2.2",
19 | "karma-jasmine": "~0.3.7",
20 | "karma-phantomjs-launcher": "~1.0.0",
21 | "karma-rollup-preprocessor": "~2.0.1",
22 | "phantomjs-prebuilt": "~2.1.5",
23 | "rollup": "~0.25.4",
24 | "rollup-plugin-babel": "~2.4.0",
25 | "rollup-plugin-commonjs": "~2.2.1",
26 | "rollup-plugin-env": "~0.21.2",
27 | "rollup-plugin-node-resolve": "~1.5.0",
28 | "xo": "~0.12.1"
29 | },
30 | "authors": "long.zhang@ele.me",
31 | "repository": {
32 | "type": "git",
33 | "url": "https://github.com/ElemeFE/vue-infinite-scroll.git"
34 | },
35 | "license": "MIT",
36 | "ignore": [
37 | "**/*.txt",
38 | "README",
39 | "package.json",
40 | ".gitignore",
41 | "bower.json"
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-infinite-scroll",
3 | "version": "2.0.2",
4 | "description": "An infinite scroll directive for vue.js.",
5 | "main": "vue-infinite-scroll.js",
6 | "jsnext:main": "./src/index.js",
7 | "scripts": {
8 | "test": "npm run build && xo src/**/* && karma start",
9 | "build": "rollup -c",
10 | "prepublish": "npm run build"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "https://github.com/ElemeFE/vue-infinite-scroll.git"
15 | },
16 | "keywords": [
17 | "infinite-scroll",
18 | "vue"
19 | ],
20 | "author": "long.zhang@ele.me",
21 | "license": "MIT",
22 | "devDependencies": {
23 | "babel-preset-es2015-rollup": "^3.0.0",
24 | "jasmine-core": "^2.4.1",
25 | "karma": "^0.13.21",
26 | "karma-chrome-launcher": "^0.2.2",
27 | "karma-jasmine": "^0.3.7",
28 | "karma-phantomjs-launcher": "^1.0.0",
29 | "karma-rollup-preprocessor": "^2.0.1",
30 | "phantomjs-prebuilt": "^2.1.5",
31 | "rollup": "^0.41.4",
32 | "rollup-plugin-babel": "^2.7.1",
33 | "rollup-plugin-uglify": "1.0.1",
34 | "rollup-plugin-node-resolve": "^2.0.0",
35 | "vue": "^2.0.3",
36 | "xo": "^0.12.1"
37 | },
38 | "xo": {
39 | "envs": [
40 | "browser"
41 | ],
42 | "space": true
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Tue Mar 08 2016 13:37:35 GMT+0800 (CST)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'test/**/*.js'
19 | ],
20 |
21 |
22 | // list of files to exclude
23 | exclude: [
24 | ],
25 |
26 |
27 | // preprocess matching files before serving them to the browser
28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
29 | preprocessors: {
30 | 'test/**/*.js': ['rollup']
31 | },
32 |
33 | rollupPreprocessor: {
34 | rollup: {
35 | plugins: [
36 | require('rollup-plugin-babel')({
37 | exclude: 'node_modules/**',
38 | presets: [
39 | require('babel-preset-es2015-rollup')
40 | ]
41 | }),
42 | require('rollup-plugin-node-resolve')({
43 | jsnext: true,
44 | main: true
45 | }),
46 | require('rollup-plugin-commonjs')(),
47 | require('rollup-plugin-env')({})
48 | ]
49 | },
50 | bundle: {
51 | sourceMap: 'inline'
52 | }
53 | },
54 |
55 |
56 | // test results reporter to use
57 | // possible values: 'dots', 'progress'
58 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
59 | reporters: ['progress'],
60 |
61 |
62 | // web server port
63 | port: 9876,
64 |
65 |
66 | // enable / disable colors in the output (reporters and logs)
67 | colors: true,
68 |
69 |
70 | // level of logging
71 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
72 | logLevel: config.LOG_INFO,
73 |
74 |
75 | // enable / disable watching file and executing tests whenever any file changes
76 | autoWatch: true,
77 |
78 |
79 | // start these browsers
80 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
81 | browsers: ['PhantomJS_custom'],
82 |
83 | // you can define custom flags
84 | customLaunchers: {
85 | 'PhantomJS_custom': {
86 | base: 'PhantomJS',
87 | options: {
88 | viewportSize: { width: 480, height: 800 }
89 | }
90 | }
91 | },
92 |
93 |
94 | phantomjsLauncher: {
95 | // Have phantomjs exit if a ResourceError is encountered (useful if karma exits without killing phantom)
96 | exitOnResourceError: true
97 | },
98 |
99 |
100 | // Continuous Integration mode
101 | // if true, Karma captures browsers, runs the tests and exits
102 | singleRun: true,
103 |
104 | // Concurrency level
105 | // how many browser should be started simultaneous
106 | concurrency: Infinity
107 | })
108 | }
109 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # vue-infinite-scroll
2 |
3 | vue-infinite-scroll is an infinite scroll directive for vue.js.
4 |
5 | # Install
6 |
7 | ```Bash
8 | npm install vue-infinite-scroll --save
9 | ```
10 |
11 | ### CommonJS
12 |
13 | You can use any build tool which supports `commonjs`:
14 |
15 | ```JavaScript
16 | // register globally
17 | var infiniteScroll = require('vue-infinite-scroll');
18 | Vue.use(infiniteScroll)
19 |
20 | // or for a single instance
21 | var infiniteScroll = require('vue-infinite-scroll');
22 | new Vue({
23 | directives: {infiniteScroll}
24 | })
25 |
26 | ```
27 |
28 | Or in ES2015:
29 |
30 | ```JavaScript
31 | // register globally
32 | import infiniteScroll from 'vue-infinite-scroll'
33 | Vue.use(infiniteScroll)
34 |
35 | // or for a single instance
36 | import infiniteScroll from 'vue-infinite-scroll'
37 | new Vue({
38 | directives: {infiniteScroll}
39 | })
40 |
41 | ```
42 |
43 | ### Direct include
44 |
45 | You can use the CDN: https://unpkg.com/vue-infinite-scroll, `infiniteScroll` is exposed to `window` and will automatically install itself. Also you can use your local copy:
46 |
47 | ```HTML
48 |
49 | ```
50 |
51 | ## Usage
52 |
53 | Use v-infinite-scroll to enable the infinite scroll, and use infinite-scroll-* attributes to define its options.
54 |
55 | The method appointed as the value of v-infinite-scroll will be executed when the bottom of the element reaches the bottom of the viewport.
56 |
57 | ```HTML
58 |
59 | ...
60 |
61 | ```
62 |
63 | ```JavaScript
64 | var count = 0;
65 |
66 | new Vue({
67 | el: '#app',
68 | data: {
69 | data: [],
70 | busy: false
71 | },
72 | methods: {
73 | loadMore: function() {
74 | this.busy = true;
75 |
76 | setTimeout(() => {
77 | for (var i = 0, j = 10; i < j; i++) {
78 | this.data.push({ name: count++ });
79 | }
80 | this.busy = false;
81 | }, 1000);
82 | }
83 | }
84 | });
85 | ```
86 |
87 | # Options
88 |
89 | | Option | Description |
90 | | ----- | ----- |
91 | | infinite-scroll-disabled | infinite scroll will be disabled if the value of this attribute is true. |
92 | | infinite-scroll-distance | Number(default = 0) - the minimum distance between the bottom of the element and the bottom of the viewport before the v-infinite-scroll method is executed. |
93 | | infinite-scroll-immediate-check | Boolean(default = true) - indicates that the directive should check immediately after bind. Useful if it's possible that the content is not tall enough to fill up the scrollable container. |
94 | | infinite-scroll-listen-for-event | infinite scroll will check again when the event is emitted in Vue instance. |
95 | | infinite-scroll-throttle-delay | Number(default = 200) - interval(ms) between next time checking and this time |
96 |
97 | ## Development
98 |
99 | |Command|Description|
100 | |---|---|
101 | |npm run build|Build in umd format|
102 | |npm test|Lint code|
103 |
104 | # License
105 |
106 | MIT
107 |
--------------------------------------------------------------------------------
/test/unit/vue-infinite-scroll.test.js:
--------------------------------------------------------------------------------
1 | import infiniteScroll from './../../vue-infinite-scroll';
2 | import Vue from 'vue';
3 |
4 | const scrollToBottom = (targetElement, distance = 0) => {
5 | if (targetElement === 'parentNode') {
6 | const element = document.querySelector('.app');
7 |
8 | element.scrollTop = element.getBoundingClientRect().top + element.getBoundingClientRect().bottom - distance;
9 | } else {
10 | const element = document.querySelector('.app');
11 |
12 | element.scrollTop = element.scrollHeight - element.offsetHeight - distance;
13 | }
14 | };
15 | const scrollToTop = (targetElement) => {
16 | document.querySelector('.app').scrollTop = 0;
17 | };
18 | const createVM = (targetElement = 'window', distance = 0, immediate = true) => {
19 | let template;
20 | switch(targetElement) {
21 | case 'window':
22 | template = `
29 |
`;
30 | break;
31 | case 'parentNode':
32 | template = ``;
42 | break;
43 | case 'currentNode':
44 | default:
45 | template = ``;
54 | break;
55 | }
56 |
57 | return new Vue({
58 | el() {
59 | const element = document.createElement('div');
60 |
61 | document.querySelector('body').appendChild(element);
62 | return element;
63 | },
64 | data() {
65 | return {
66 | busy: false
67 | };
68 | },
69 | template,
70 | methods: {
71 | loadMore() {
72 | this.busy = true;
73 | console.log('loaded!');
74 | }
75 | },
76 | events: {
77 | ['docheck']() {
78 | console.log('tick');
79 | }
80 | }
81 | })
82 | }
83 |
84 | describe('init infinite-scroll directive', () => {
85 | beforeAll(done => {
86 | Vue.use(infiniteScroll);
87 | done();
88 | });
89 |
90 | it('directive installed', done => {
91 | expect(infiniteScroll.installed).toBe(true);
92 | done();
93 | });
94 | });
95 |
96 | const scrollTargetElements = ['parentNode', 'currentNode'];
97 |
98 | scrollTargetElements.forEach(targetElement => {
99 | describe(`${targetElement} scroll test`, () => {
100 | let vm;
101 |
102 | beforeEach(done =>{
103 | vm = createVM(targetElement);
104 |
105 | vm.$nextTick(() => {
106 | spyOn(vm, 'loadMore');
107 |
108 | scrollToBottom(targetElement);
109 | scrollToTop(targetElement);
110 | scrollToBottom(targetElement);
111 |
112 | setTimeout(done);
113 | });
114 | });
115 |
116 | it('the function should be called once', done => {
117 | expect(vm.loadMore.calls.count()).toEqual(1);
118 | done();
119 | });
120 |
121 | it('test "infinite-scroll-listen-for-event"', done => {
122 | vm.$emit('docheck');
123 | expect(vm.loadMore.calls.count()).toEqual(2);
124 | done();
125 | });
126 |
127 | afterEach(done => {
128 | vm.$destroy(true);
129 | done();
130 | });
131 | });
132 |
133 | describe(`${targetElement} scroll distance test`, () => {
134 | let vm;
135 |
136 | beforeEach(done => {
137 | vm = createVM(targetElement, 50);
138 |
139 | vm.$nextTick(() => {
140 | spyOn(vm, 'loadMore');
141 | setTimeout(done);
142 | });
143 | });
144 |
145 | it('the function should be called when scroll to bottom', done => {
146 | scrollToBottom(targetElement, 0);
147 |
148 | setTimeout(() => {
149 | expect(vm.loadMore).toHaveBeenCalled();
150 | done();
151 | });
152 | });
153 |
154 | it('the function should be called when scroll to the bottom of 50px distance', done => {
155 | scrollToBottom(targetElement, 50);
156 | setTimeout(() => {
157 | expect(vm.loadMore).toHaveBeenCalled();
158 | done();
159 | });
160 | });
161 |
162 | it('the function should not be called', done => {
163 | scrollToBottom(targetElement, 51);
164 | setTimeout(() => {
165 | expect(vm.loadMore).not.toHaveBeenCalled();
166 | done();
167 | });
168 | });
169 |
170 | afterEach(done => {
171 | vm.$destroy(true);
172 | done();
173 | });
174 | });
175 | });
176 |
--------------------------------------------------------------------------------
/src/directive.js:
--------------------------------------------------------------------------------
1 | const ctx = '@@InfiniteScroll';
2 |
3 | var throttle = function (fn, delay) {
4 | var now, lastExec, timer, context, args; //eslint-disable-line
5 |
6 | var execute = function () {
7 | fn.apply(context, args);
8 | lastExec = now;
9 | };
10 |
11 | return function () {
12 | context = this;
13 | args = arguments;
14 |
15 | now = Date.now();
16 |
17 | if (timer) {
18 | clearTimeout(timer);
19 | timer = null;
20 | }
21 |
22 | if (lastExec) {
23 | var diff = delay - (now - lastExec);
24 | if (diff < 0) {
25 | execute();
26 | } else {
27 | timer = setTimeout(() => {
28 | execute();
29 | }, diff);
30 | }
31 | } else {
32 | execute();
33 | }
34 | };
35 | };
36 |
37 | var getScrollTop = function (element) {
38 | if (element === window) {
39 | return Math.max(window.pageYOffset || 0, document.documentElement.scrollTop);
40 | }
41 |
42 | return element.scrollTop;
43 | };
44 |
45 | var getComputedStyle = document.defaultView.getComputedStyle;
46 |
47 | var getScrollEventTarget = function (element) {
48 | var currentNode = element;
49 | // bugfix, see http://w3help.org/zh-cn/causes/SD9013 and http://stackoverflow.com/questions/17016740/onscroll-function-is-not-working-for-chrome
50 | while (currentNode && currentNode.tagName !== 'HTML' && currentNode.tagName !== 'BODY' && currentNode.nodeType === 1) {
51 | var overflowY = getComputedStyle(currentNode).overflowY;
52 | if (overflowY === 'scroll' || overflowY === 'auto') {
53 | return currentNode;
54 | }
55 | currentNode = currentNode.parentNode;
56 | }
57 | return window;
58 | };
59 |
60 | var getVisibleHeight = function (element) {
61 | if (element === window) {
62 | return document.documentElement.clientHeight;
63 | }
64 |
65 | return element.clientHeight;
66 | };
67 |
68 | var getElementTop = function (element) {
69 | if (element === window) {
70 | return getScrollTop(window);
71 | }
72 | return element.getBoundingClientRect().top + getScrollTop(window);
73 | };
74 |
75 | var isAttached = function (element) {
76 | var currentNode = element.parentNode;
77 | while (currentNode) {
78 | if (currentNode.tagName === 'HTML') {
79 | return true;
80 | }
81 | if (currentNode.nodeType === 11) {
82 | return false;
83 | }
84 | currentNode = currentNode.parentNode;
85 | }
86 | return false;
87 | };
88 |
89 | var doBind = function () {
90 | if (this.binded) return; // eslint-disable-line
91 | this.binded = true;
92 |
93 | var directive = this;
94 | var element = directive.el;
95 |
96 | var throttleDelayExpr = element.getAttribute('infinite-scroll-throttle-delay');
97 | var throttleDelay = 200;
98 | if (throttleDelayExpr) {
99 | throttleDelay = Number(directive.vm[throttleDelayExpr] || throttleDelayExpr);
100 | if (isNaN(throttleDelay) || throttleDelay < 0) {
101 | throttleDelay = 200;
102 | }
103 | }
104 | directive.throttleDelay = throttleDelay;
105 |
106 | directive.scrollEventTarget = getScrollEventTarget(element);
107 | directive.scrollListener = throttle(doCheck.bind(directive), directive.throttleDelay);
108 | directive.scrollEventTarget.addEventListener('scroll', directive.scrollListener);
109 |
110 | this.vm.$on('hook:beforeDestroy', function () {
111 | directive.scrollEventTarget.removeEventListener('scroll', directive.scrollListener);
112 | });
113 |
114 | var disabledExpr = element.getAttribute('infinite-scroll-disabled');
115 | var disabled = false;
116 |
117 | if (disabledExpr) {
118 | this.vm.$watch(disabledExpr, function(value) {
119 | directive.disabled = value;
120 | if (!value && directive.immediateCheck) {
121 | doCheck.call(directive);
122 | }
123 | });
124 | disabled = Boolean(directive.vm[disabledExpr]);
125 | }
126 | directive.disabled = disabled;
127 |
128 | var distanceExpr = element.getAttribute('infinite-scroll-distance');
129 | var distance = 0;
130 | if (distanceExpr) {
131 | distance = Number(directive.vm[distanceExpr] || distanceExpr);
132 | if (isNaN(distance)) {
133 | distance = 0;
134 | }
135 | }
136 | directive.distance = distance;
137 |
138 | var immediateCheckExpr = element.getAttribute('infinite-scroll-immediate-check');
139 | var immediateCheck = true;
140 | if (immediateCheckExpr) {
141 | immediateCheck = Boolean(directive.vm[immediateCheckExpr]);
142 | }
143 | directive.immediateCheck = immediateCheck;
144 |
145 | if (immediateCheck) {
146 | doCheck.call(directive);
147 | }
148 |
149 | var eventName = element.getAttribute('infinite-scroll-listen-for-event');
150 | if (eventName) {
151 | directive.vm.$on(eventName, function() {
152 | doCheck.call(directive);
153 | });
154 | }
155 | };
156 |
157 | var doCheck = function (force) {
158 | var scrollEventTarget = this.scrollEventTarget;
159 | var element = this.el;
160 | var distance = this.distance;
161 |
162 | if (force !== true && this.disabled) return; //eslint-disable-line
163 | var viewportScrollTop = getScrollTop(scrollEventTarget);
164 | var viewportBottom = viewportScrollTop + getVisibleHeight(scrollEventTarget);
165 |
166 | var shouldTrigger = false;
167 |
168 | if (scrollEventTarget === element) {
169 | shouldTrigger = scrollEventTarget.scrollHeight - viewportBottom <= distance;
170 | } else {
171 | var elementBottom = getElementTop(element) - getElementTop(scrollEventTarget) + element.offsetHeight + viewportScrollTop;
172 |
173 | shouldTrigger = viewportBottom + distance >= elementBottom;
174 | }
175 |
176 | if (shouldTrigger && this.expression) {
177 | this.expression();
178 | }
179 | };
180 |
181 | export default {
182 | bind(el, binding, vnode) {
183 | el[ctx] = {
184 | el,
185 | vm: vnode.context,
186 | expression: binding.value
187 | };
188 | const args = arguments;
189 | el[ctx].vm.$on('hook:mounted', function () {
190 | el[ctx].vm.$nextTick(function () {
191 | if (isAttached(el)) {
192 | doBind.call(el[ctx], args);
193 | }
194 |
195 | el[ctx].bindTryCount = 0;
196 |
197 | var tryBind = function () {
198 | if (el[ctx].bindTryCount > 10) return; //eslint-disable-line
199 | el[ctx].bindTryCount++;
200 | if (isAttached(el)) {
201 | doBind.call(el[ctx], args);
202 | } else {
203 | setTimeout(tryBind, 50);
204 | }
205 | };
206 |
207 | tryBind();
208 | });
209 | });
210 | },
211 |
212 | unbind(el) {
213 | if (el && el[ctx] && el[ctx].scrollEventTarget)
214 | el[ctx].scrollEventTarget.removeEventListener('scroll', el[ctx].scrollListener);
215 | }
216 | };
217 |
--------------------------------------------------------------------------------