├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── dist
├── vue-scroll-list.common.js
├── vue-scroll-list.esm.js
└── vue-scroll-list.js
├── example
├── App.vue
├── componentA.vue
├── componentB.vue
├── index.html
└── index.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
└── index.js
├── webpack.config.base.js
├── webpack.config.demo.js
└── webpack.config.dev.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "es2015",
5 | {
6 | "modules": false
7 | }
8 | ]
9 | ],
10 | "plugins": [
11 | "external-helpers"
12 | ]
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | npm-debug*
4 | .idea/
5 | .vscode/
6 | onlineDemo/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 KyLeo
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # vue-scroll-list
6 | > A vue component support infinite scroll list.Different item height is also supported.
7 |
8 | note: Vue version >= 2.3 is needed.
9 |
10 | ## Install
11 |
12 | ```bash
13 | $ npm install vue-scroll-list --save-dev
14 | ```
15 |
16 | ## Demos
17 |
18 | [infinite data](http://freeui.org/vue-scroll-list/)
19 |
20 | ## Usage
21 |
22 | ```html
23 |
24 |
25 |
vue-scroll-list with infinite data
26 |
random height
27 |
total: {{count}}
28 |
29 |
36 |
41 | index:{{item.index}} / height:{{item.itemHeight}}
42 |
43 |
44 |
45 |
46 |
47 |
95 |
120 | ```
121 | You can define the height of container(such as the `ul` tag above) by the css height.
122 | note: You can run this demo by `npm run dev`.
123 |
124 | ## Props and Events
125 |
126 | Available `Prop` :
127 |
128 | *Prop* | *Type* | *Required* | *Description* |
129 | :--- | :--- | :--- | :--- |
130 | | heights | Array | * | An array contains all height of your item.If you want to use `data-height`,please ignore this option. |
131 | | remain | Number | * | The number of item that show in view port.(default `10`) |
132 | | keep | Boolean | * | Work with `keep-alive` component,keep scroll position after activated.(default `false`) |
133 | | enabled | Boolean | * | If you want to render all data directly,please set 'false' for this option.But `toTop`、`toBottom` and `scrolling` event is still available.(default `true`) |
134 | | debounce | Number | * | Milliseconds of using debounce function to ensure scroll event doesn't fire so often.(disabled by default) |
135 | | step | Number | * | Pixel of using throttle theory to decrease the frequency of scroll event.(disabled by default) |
136 |
137 | Available `Event` :
138 |
139 | *Event* | *Description* |
140 | :--- | :--- |
141 | | toTop | An event emit by this library when this list is scrolled on top. |
142 | | toBottom | An event emit by this library when this list is scrolled on bottom. |
143 | | scrolling | An event emit by this library when this list is scrolling. |
144 |
145 | ## About heights prop
146 | `heights` property is an array contains all height of your item,but you can tell us the height of each item by setting the `data-height` property.
147 | ```html
148 |
151 |
152 | ```
153 | Sometimes you may need to change the height of each item or filter your item.This may cause some blank problems.So you'd better call `update` function to tell us.
154 | ```html
155 |
164 |
169 | index:{{item.index}} / height:{{item.itemHeight}}
170 |
171 |
172 | ```
173 | ```js
174 | this.$refs.vueScrollList && this.$refs.vueScrollList.update();
175 | ```
176 | ## License
177 |
178 | [MIT License](https://github.com/KyLeoHC/vue-scroll-list/blob/master/LICENSE)
179 |
--------------------------------------------------------------------------------
/dist/vue-scroll-list.common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var _debounce = function _debounce(fn, wait) {
4 | var timeoutId = null;
5 | return function () {
6 | var _this = this,
7 | _arguments = arguments;
8 |
9 | var laterFn = function laterFn() {
10 | fn.apply(_this, _arguments);
11 | };
12 | clearTimeout(timeoutId);
13 | timeoutId = setTimeout(laterFn, wait);
14 | };
15 | };
16 |
17 | var component = {
18 | props: {
19 | heights: {
20 | type: Array
21 | },
22 | remain: {
23 | type: Number,
24 | default: 10
25 | },
26 | enabled: {
27 | type: Boolean,
28 | default: true
29 | },
30 | keep: {
31 | type: Boolean,
32 | default: false
33 | },
34 | debounce: {
35 | type: Number
36 | },
37 | step: { // throttle
38 | type: Number
39 | }
40 | },
41 | methods: {
42 | handleScroll: function handleScroll(event) {
43 | var scrollTop = this.$el.scrollTop;
44 | if (!this.ignoreStep && this.step && Math.abs(scrollTop - this.scrollTop) < this.step) return;
45 | this.ignoreStep = false;
46 | this.scrollTop = scrollTop;
47 | this.$emit('scrolling', event);
48 | this.updateZone(scrollTop);
49 | },
50 | updateHeightList: function updateHeightList() {
51 | if (this.heights) {
52 | this.heightList = this.heights;
53 | } else {
54 | var list = this.$slots.default || [];
55 | if (list.length !== this.heightList.length) {
56 | this.heightList = list.map(function (vnode) {
57 | return parseInt(vnode.data.attrs['data-height']);
58 | });
59 | }
60 | }
61 | },
62 | updateZoneNormally: function updateZoneNormally(offset) {
63 | // handle the scroll event normally
64 | var scrollHeight = this.$el.scrollHeight;
65 | var clientHeight = this.$el.clientHeight;
66 | if (offset === 0) {
67 | this.$emit('toTop');
68 | } else if (offset + clientHeight + 5 >= scrollHeight) {
69 | this.$emit('toBottom');
70 | }
71 | },
72 | findOvers: function findOvers(offset) {
73 | // compute overs by comparing offset with the height of each item
74 | // @todo: need to optimize this searching efficiency
75 | var heightList = this.heightList;
76 | var overs = 0;
77 | var height = heightList[0];
78 | var topReserve = Math.floor(this.reserve / 2);
79 | for (var length = heightList.length; overs < length; overs++) {
80 | if (offset >= height) {
81 | height += heightList[overs + 1];
82 | } else {
83 | break;
84 | }
85 | }
86 | return overs > topReserve - 1 ? overs - topReserve : 0;
87 | },
88 | updateZone: function updateZone(offset) {
89 | if (this.enabled) {
90 | this.updateHeightList();
91 | var overs = this.findOvers(offset);
92 |
93 | // scroll to top
94 | if (!offset && this.total) {
95 | this.$emit('toTop');
96 | }
97 |
98 | var start = overs || 0;
99 | var end = start + this.keeps;
100 | var totalHeight = this.heightList.reduce(function (a, b) {
101 | return a + b;
102 | });
103 |
104 | // scroll to bottom
105 | if (offset && offset + this.$el.clientHeight >= totalHeight) {
106 | start = this.total - this.keeps;
107 | end = this.total - 1;
108 | this.$emit('toBottom');
109 | }
110 |
111 | if (this.start !== start || this.end !== end) {
112 | this.start = start;
113 | this.end = end;
114 | this.$forceUpdate();
115 | }
116 | } else {
117 | this.updateZoneNormally(offset);
118 | }
119 | },
120 | filter: function filter(slots) {
121 | var _this2 = this;
122 |
123 | this.updateHeightList();
124 | if (!slots) {
125 | slots = [];
126 | this.start = 0;
127 | }
128 |
129 | var slotList = slots.filter(function (slot, index) {
130 | return index >= _this2.start && index <= _this2.end;
131 | });
132 | var topList = this.heightList.slice(0, this.start);
133 | var bottomList = this.heightList.slice(this.end + 1);
134 | this.total = slots.length;
135 | // consider that the height of item may change in any case
136 | // so we compute paddingTop and paddingBottom every time
137 | this.paddingTop = topList.length ? topList.reduce(function (a, b) {
138 | return a + b;
139 | }) : 0;
140 | this.paddingBottom = bottomList.length ? bottomList.reduce(function (a, b) {
141 | return a + b;
142 | }) : 0;
143 |
144 | return slotList;
145 | },
146 | update: function update() {
147 | var _this3 = this;
148 |
149 | this.$nextTick(function () {
150 | _this3.updateZone(_this3.scrollTop);
151 | });
152 | }
153 | },
154 | beforeCreate: function beforeCreate() {
155 | // vue won't observe this properties
156 | Object.assign(this, {
157 | heightList: [], // list of each item height
158 | scrollTop: 0, // current scroll position
159 | start: 0, // start index
160 | end: 0, // end index
161 | total: 0, // all items count
162 | keeps: 0, // number of item keeping in real dom
163 | paddingTop: 0, // all padding of top dom
164 | paddingBottom: 0, // all padding of bottom dom
165 | reserve: 10 // number of reserve dom for pre-render
166 | });
167 | },
168 | beforeMount: function beforeMount() {
169 | if (this.enabled) {
170 | var remains = this.remain;
171 | this.start = 0;
172 | this.end = remains + this.reserve - 1;
173 | this.keeps = remains + this.reserve;
174 | }
175 | },
176 | activated: function activated() {
177 | // while work with keep-alive component
178 | // set scroll position after 'activated'
179 | this.ignoreStep = true;
180 | this.$el.scrollTop = this.keep ? this.scrollTop || 1 : 1;
181 | },
182 | render: function render(h) {
183 | var showList = this.enabled ? this.filter(this.$slots.default) : this.$slots.default;
184 | var debounce = this.debounce;
185 |
186 | return h('div', {
187 | class: ['scroll-container'],
188 | style: {
189 | 'display': 'block',
190 | 'overflow-y': 'auto',
191 | 'height': '100%'
192 | },
193 | on: { // '&' support passive event
194 | '&scroll': debounce ? _debounce(this.handleScroll.bind(this), debounce) : this.handleScroll
195 | }
196 | }, [h('div', {
197 | style: {
198 | 'display': 'block',
199 | 'padding-top': this.paddingTop + 'px',
200 | 'padding-bottom': this.paddingBottom + 'px'
201 | }
202 | }, showList)]);
203 | }
204 | };
205 |
206 | module.exports = component;
207 |
--------------------------------------------------------------------------------
/dist/vue-scroll-list.esm.js:
--------------------------------------------------------------------------------
1 | var _debounce = function _debounce(fn, wait) {
2 | var timeoutId = null;
3 | return function () {
4 | var _this = this,
5 | _arguments = arguments;
6 |
7 | var laterFn = function laterFn() {
8 | fn.apply(_this, _arguments);
9 | };
10 | clearTimeout(timeoutId);
11 | timeoutId = setTimeout(laterFn, wait);
12 | };
13 | };
14 |
15 | var component = {
16 | props: {
17 | heights: {
18 | type: Array
19 | },
20 | remain: {
21 | type: Number,
22 | default: 10
23 | },
24 | enabled: {
25 | type: Boolean,
26 | default: true
27 | },
28 | keep: {
29 | type: Boolean,
30 | default: false
31 | },
32 | debounce: {
33 | type: Number
34 | },
35 | step: { // throttle
36 | type: Number
37 | }
38 | },
39 | methods: {
40 | handleScroll: function handleScroll(event) {
41 | var scrollTop = this.$el.scrollTop;
42 | if (!this.ignoreStep && this.step && Math.abs(scrollTop - this.scrollTop) < this.step) return;
43 | this.ignoreStep = false;
44 | this.scrollTop = scrollTop;
45 | this.$emit('scrolling', event);
46 | this.updateZone(scrollTop);
47 | },
48 | updateHeightList: function updateHeightList() {
49 | if (this.heights) {
50 | this.heightList = this.heights;
51 | } else {
52 | var list = this.$slots.default || [];
53 | if (list.length !== this.heightList.length) {
54 | this.heightList = list.map(function (vnode) {
55 | return parseInt(vnode.data.attrs['data-height']);
56 | });
57 | }
58 | }
59 | },
60 | updateZoneNormally: function updateZoneNormally(offset) {
61 | // handle the scroll event normally
62 | var scrollHeight = this.$el.scrollHeight;
63 | var clientHeight = this.$el.clientHeight;
64 | if (offset === 0) {
65 | this.$emit('toTop');
66 | } else if (offset + clientHeight + 5 >= scrollHeight) {
67 | this.$emit('toBottom');
68 | }
69 | },
70 | findOvers: function findOvers(offset) {
71 | // compute overs by comparing offset with the height of each item
72 | // @todo: need to optimize this searching efficiency
73 | var heightList = this.heightList;
74 | var overs = 0;
75 | var height = heightList[0];
76 | var topReserve = Math.floor(this.reserve / 2);
77 | for (var length = heightList.length; overs < length; overs++) {
78 | if (offset >= height) {
79 | height += heightList[overs + 1];
80 | } else {
81 | break;
82 | }
83 | }
84 | return overs > topReserve - 1 ? overs - topReserve : 0;
85 | },
86 | updateZone: function updateZone(offset) {
87 | if (this.enabled) {
88 | this.updateHeightList();
89 | var overs = this.findOvers(offset);
90 |
91 | // scroll to top
92 | if (!offset && this.total) {
93 | this.$emit('toTop');
94 | }
95 |
96 | var start = overs || 0;
97 | var end = start + this.keeps;
98 | var totalHeight = this.heightList.reduce(function (a, b) {
99 | return a + b;
100 | });
101 |
102 | // scroll to bottom
103 | if (offset && offset + this.$el.clientHeight >= totalHeight) {
104 | start = this.total - this.keeps;
105 | end = this.total - 1;
106 | this.$emit('toBottom');
107 | }
108 |
109 | if (this.start !== start || this.end !== end) {
110 | this.start = start;
111 | this.end = end;
112 | this.$forceUpdate();
113 | }
114 | } else {
115 | this.updateZoneNormally(offset);
116 | }
117 | },
118 | filter: function filter(slots) {
119 | var _this2 = this;
120 |
121 | this.updateHeightList();
122 | if (!slots) {
123 | slots = [];
124 | this.start = 0;
125 | }
126 |
127 | var slotList = slots.filter(function (slot, index) {
128 | return index >= _this2.start && index <= _this2.end;
129 | });
130 | var topList = this.heightList.slice(0, this.start);
131 | var bottomList = this.heightList.slice(this.end + 1);
132 | this.total = slots.length;
133 | // consider that the height of item may change in any case
134 | // so we compute paddingTop and paddingBottom every time
135 | this.paddingTop = topList.length ? topList.reduce(function (a, b) {
136 | return a + b;
137 | }) : 0;
138 | this.paddingBottom = bottomList.length ? bottomList.reduce(function (a, b) {
139 | return a + b;
140 | }) : 0;
141 |
142 | return slotList;
143 | },
144 | update: function update() {
145 | var _this3 = this;
146 |
147 | this.$nextTick(function () {
148 | _this3.updateZone(_this3.scrollTop);
149 | });
150 | }
151 | },
152 | beforeCreate: function beforeCreate() {
153 | // vue won't observe this properties
154 | Object.assign(this, {
155 | heightList: [], // list of each item height
156 | scrollTop: 0, // current scroll position
157 | start: 0, // start index
158 | end: 0, // end index
159 | total: 0, // all items count
160 | keeps: 0, // number of item keeping in real dom
161 | paddingTop: 0, // all padding of top dom
162 | paddingBottom: 0, // all padding of bottom dom
163 | reserve: 10 // number of reserve dom for pre-render
164 | });
165 | },
166 | beforeMount: function beforeMount() {
167 | if (this.enabled) {
168 | var remains = this.remain;
169 | this.start = 0;
170 | this.end = remains + this.reserve - 1;
171 | this.keeps = remains + this.reserve;
172 | }
173 | },
174 | activated: function activated() {
175 | // while work with keep-alive component
176 | // set scroll position after 'activated'
177 | this.ignoreStep = true;
178 | this.$el.scrollTop = this.keep ? this.scrollTop || 1 : 1;
179 | },
180 | render: function render(h) {
181 | var showList = this.enabled ? this.filter(this.$slots.default) : this.$slots.default;
182 | var debounce = this.debounce;
183 |
184 | return h('div', {
185 | class: ['scroll-container'],
186 | style: {
187 | 'display': 'block',
188 | 'overflow-y': 'auto',
189 | 'height': '100%'
190 | },
191 | on: { // '&' support passive event
192 | '&scroll': debounce ? _debounce(this.handleScroll.bind(this), debounce) : this.handleScroll
193 | }
194 | }, [h('div', {
195 | style: {
196 | 'display': 'block',
197 | 'padding-top': this.paddingTop + 'px',
198 | 'padding-bottom': this.paddingBottom + 'px'
199 | }
200 | }, showList)]);
201 | }
202 | };
203 |
204 | export default component;
205 |
--------------------------------------------------------------------------------
/dist/vue-scroll-list.js:
--------------------------------------------------------------------------------
1 | (function (global, factory) {
2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 | typeof define === 'function' && define.amd ? define(factory) :
4 | (global['vue-scroll-list'] = factory());
5 | }(this, (function () { 'use strict';
6 |
7 | var _debounce = function _debounce(fn, wait) {
8 | var timeoutId = null;
9 | return function () {
10 | var _this = this,
11 | _arguments = arguments;
12 |
13 | var laterFn = function laterFn() {
14 | fn.apply(_this, _arguments);
15 | };
16 | clearTimeout(timeoutId);
17 | timeoutId = setTimeout(laterFn, wait);
18 | };
19 | };
20 |
21 | var component = {
22 | props: {
23 | heights: {
24 | type: Array
25 | },
26 | remain: {
27 | type: Number,
28 | default: 10
29 | },
30 | enabled: {
31 | type: Boolean,
32 | default: true
33 | },
34 | keep: {
35 | type: Boolean,
36 | default: false
37 | },
38 | debounce: {
39 | type: Number
40 | },
41 | step: { // throttle
42 | type: Number
43 | }
44 | },
45 | methods: {
46 | handleScroll: function handleScroll(event) {
47 | var scrollTop = this.$el.scrollTop;
48 | if (!this.ignoreStep && this.step && Math.abs(scrollTop - this.scrollTop) < this.step) return;
49 | this.ignoreStep = false;
50 | this.scrollTop = scrollTop;
51 | this.$emit('scrolling', event);
52 | this.updateZone(scrollTop);
53 | },
54 | updateHeightList: function updateHeightList() {
55 | if (this.heights) {
56 | this.heightList = this.heights;
57 | } else {
58 | var list = this.$slots.default || [];
59 | if (list.length !== this.heightList.length) {
60 | this.heightList = list.map(function (vnode) {
61 | return parseInt(vnode.data.attrs['data-height']);
62 | });
63 | }
64 | }
65 | },
66 | updateZoneNormally: function updateZoneNormally(offset) {
67 | // handle the scroll event normally
68 | var scrollHeight = this.$el.scrollHeight;
69 | var clientHeight = this.$el.clientHeight;
70 | if (offset === 0) {
71 | this.$emit('toTop');
72 | } else if (offset + clientHeight + 5 >= scrollHeight) {
73 | this.$emit('toBottom');
74 | }
75 | },
76 | findOvers: function findOvers(offset) {
77 | // compute overs by comparing offset with the height of each item
78 | // @todo: need to optimize this searching efficiency
79 | var heightList = this.heightList;
80 | var overs = 0;
81 | var height = heightList[0];
82 | var topReserve = Math.floor(this.reserve / 2);
83 | for (var length = heightList.length; overs < length; overs++) {
84 | if (offset >= height) {
85 | height += heightList[overs + 1];
86 | } else {
87 | break;
88 | }
89 | }
90 | return overs > topReserve - 1 ? overs - topReserve : 0;
91 | },
92 | updateZone: function updateZone(offset) {
93 | if (this.enabled) {
94 | this.updateHeightList();
95 | var overs = this.findOvers(offset);
96 |
97 | // scroll to top
98 | if (!offset && this.total) {
99 | this.$emit('toTop');
100 | }
101 |
102 | var start = overs || 0;
103 | var end = start + this.keeps;
104 | var totalHeight = this.heightList.reduce(function (a, b) {
105 | return a + b;
106 | });
107 |
108 | // scroll to bottom
109 | if (offset && offset + this.$el.clientHeight >= totalHeight) {
110 | start = this.total - this.keeps;
111 | end = this.total - 1;
112 | this.$emit('toBottom');
113 | }
114 |
115 | if (this.start !== start || this.end !== end) {
116 | this.start = start;
117 | this.end = end;
118 | this.$forceUpdate();
119 | }
120 | } else {
121 | this.updateZoneNormally(offset);
122 | }
123 | },
124 | filter: function filter(slots) {
125 | var _this2 = this;
126 |
127 | this.updateHeightList();
128 | if (!slots) {
129 | slots = [];
130 | this.start = 0;
131 | }
132 |
133 | var slotList = slots.filter(function (slot, index) {
134 | return index >= _this2.start && index <= _this2.end;
135 | });
136 | var topList = this.heightList.slice(0, this.start);
137 | var bottomList = this.heightList.slice(this.end + 1);
138 | this.total = slots.length;
139 | // consider that the height of item may change in any case
140 | // so we compute paddingTop and paddingBottom every time
141 | this.paddingTop = topList.length ? topList.reduce(function (a, b) {
142 | return a + b;
143 | }) : 0;
144 | this.paddingBottom = bottomList.length ? bottomList.reduce(function (a, b) {
145 | return a + b;
146 | }) : 0;
147 |
148 | return slotList;
149 | },
150 | update: function update() {
151 | var _this3 = this;
152 |
153 | this.$nextTick(function () {
154 | _this3.updateZone(_this3.scrollTop);
155 | });
156 | }
157 | },
158 | beforeCreate: function beforeCreate() {
159 | // vue won't observe this properties
160 | Object.assign(this, {
161 | heightList: [], // list of each item height
162 | scrollTop: 0, // current scroll position
163 | start: 0, // start index
164 | end: 0, // end index
165 | total: 0, // all items count
166 | keeps: 0, // number of item keeping in real dom
167 | paddingTop: 0, // all padding of top dom
168 | paddingBottom: 0, // all padding of bottom dom
169 | reserve: 10 // number of reserve dom for pre-render
170 | });
171 | },
172 | beforeMount: function beforeMount() {
173 | if (this.enabled) {
174 | var remains = this.remain;
175 | this.start = 0;
176 | this.end = remains + this.reserve - 1;
177 | this.keeps = remains + this.reserve;
178 | }
179 | },
180 | activated: function activated() {
181 | // while work with keep-alive component
182 | // set scroll position after 'activated'
183 | this.ignoreStep = true;
184 | this.$el.scrollTop = this.keep ? this.scrollTop || 1 : 1;
185 | },
186 | render: function render(h) {
187 | var showList = this.enabled ? this.filter(this.$slots.default) : this.$slots.default;
188 | var debounce = this.debounce;
189 |
190 | return h('div', {
191 | class: ['scroll-container'],
192 | style: {
193 | 'display': 'block',
194 | 'overflow-y': 'auto',
195 | 'height': '100%'
196 | },
197 | on: { // '&' support passive event
198 | '&scroll': debounce ? _debounce(this.handleScroll.bind(this), debounce) : this.handleScroll
199 | }
200 | }, [h('div', {
201 | style: {
202 | 'display': 'block',
203 | 'padding-top': this.paddingTop + 'px',
204 | 'padding-bottom': this.paddingBottom + 'px'
205 | }
206 | }, showList)]);
207 | }
208 | };
209 |
210 | return component;
211 |
212 | })));
213 |
--------------------------------------------------------------------------------
/example/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | vue-scroll-list with infinite data
5 | random height
6 | total: {{count}}
7 |
8 |
10 | switch view
11 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
37 |
46 |
--------------------------------------------------------------------------------
/example/componentA.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
18 | index:{{item.index}} / height:{{item.itemHeight}}
19 |
20 |
21 |
22 |
23 |
88 |
--------------------------------------------------------------------------------
/example/componentB.vue:
--------------------------------------------------------------------------------
1 |
2 | I am component B!
3 |
4 |
9 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue-scroll-list
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import App from './App';
3 |
4 | new Vue({
5 | el: '#app',
6 | template: '',
7 | components: {App}
8 | });
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-scroll-list",
3 | "version": "0.7.0",
4 | "description": "support infinite scroll list with vue",
5 | "main": "dist/vue-scroll-list.common.js",
6 | "directories": {
7 | "example": "example"
8 | },
9 | "scripts": {
10 | "dist": "rollup -c",
11 | "buildDemo": "webpack --config webpack.config.demo.js",
12 | "dev": "webpack-dev-server --config webpack.config.dev.js --open --inline --hot"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/KyLeoHC/vue-scroll-list.git"
17 | },
18 | "keywords": [
19 | "vue",
20 | "infinite",
21 | "scroll-list"
22 | ],
23 | "author": "KyLeo",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/KyLeoHC/vue-scroll-list/issues"
27 | },
28 | "homepage": "https://github.com/KyLeoHC/vue-scroll-list#readme",
29 | "devDependencies": {
30 | "babel-core": "^6.25.0",
31 | "babel-loader": "^7.1.0",
32 | "babel-plugin-external-helpers": "^6.22.0",
33 | "babel-plugin-transform-runtime": "^6.23.0",
34 | "babel-preset-es2015": "^6.24.1",
35 | "babel-runtime": "^6.23.0",
36 | "css-loader": "^0.28.4",
37 | "css-select": "^1.3.0-rc0",
38 | "html-webpack-plugin": "^2.28.0",
39 | "htmlparser2": "^3.9.2",
40 | "pretty-error": "^2.1.1",
41 | "renderkid": "^2.0.1",
42 | "rollup": "^0.49.2",
43 | "rollup-plugin-babel": "^3.0.2",
44 | "vue": "^2.3.4",
45 | "vue-loader": "^12.2.1",
46 | "vue-template-compiler": "^2.3.4",
47 | "webpack": "^3.0.0",
48 | "webpack-dev-server": "^2.5.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 |
3 | const distPath = 'dist/';
4 |
5 | export default {
6 | input: 'src/index.js',
7 | output: [{
8 | file: distPath + 'vue-scroll-list.js',
9 | format: 'umd',
10 | name: 'vue-scroll-list'
11 | }, {
12 | file: distPath + 'vue-scroll-list.common.js',
13 | format: 'cjs'
14 | }, {
15 | file: distPath + 'vue-scroll-list.esm.js',
16 | format: 'es'
17 | }],
18 | plugins: [
19 | babel()
20 | ]
21 | };
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const _debounce = function (fn, wait) {
2 | let timeoutId = null;
3 | return function () {
4 | const laterFn = () => {
5 | fn.apply(this, arguments);
6 | };
7 | clearTimeout(timeoutId);
8 | timeoutId = setTimeout(laterFn, wait);
9 | };
10 | };
11 |
12 | const component = {
13 | props: {
14 | heights: {
15 | type: Array
16 | },
17 | remain: {
18 | type: Number,
19 | default: 10
20 | },
21 | enabled: {
22 | type: Boolean,
23 | default: true
24 | },
25 | keep: {
26 | type: Boolean,
27 | default: false
28 | },
29 | debounce: {
30 | type: Number
31 | },
32 | step: { // throttle
33 | type: Number
34 | }
35 | },
36 | methods: {
37 | handleScroll(event) {
38 | const scrollTop = this.$el.scrollTop;
39 | if (!this.ignoreStep && this.step && Math.abs(scrollTop - this.scrollTop) < this.step) return;
40 | this.ignoreStep = false;
41 | this.scrollTop = scrollTop;
42 | this.$emit('scrolling', event);
43 | this.updateZone(scrollTop);
44 | },
45 | updateHeightList() {
46 | if (this.heights) {
47 | this.heightList = this.heights;
48 | } else {
49 | const list = this.$slots.default || [];
50 | if (list.length !== this.heightList.length) {
51 | this.heightList = list.map(vnode => parseInt(vnode.data.attrs['data-height']));
52 | }
53 | }
54 | },
55 | updateZoneNormally(offset) {
56 | // handle the scroll event normally
57 | const scrollHeight = this.$el.scrollHeight;
58 | const clientHeight = this.$el.clientHeight;
59 | if (offset === 0) {
60 | this.$emit('toTop');
61 | } else if (offset + clientHeight + 5 >= scrollHeight) {
62 | this.$emit('toBottom');
63 | }
64 | },
65 | findOvers(offset) {
66 | // compute overs by comparing offset with the height of each item
67 | // @todo: need to optimize this searching efficiency
68 | const heightList = this.heightList;
69 | let overs = 0;
70 | let height = heightList[0];
71 | let topReserve = Math.floor(this.reserve / 2);
72 | for (let length = heightList.length; overs < length; overs++) {
73 | if (offset >= height) {
74 | height += heightList[overs + 1];
75 | } else {
76 | break;
77 | }
78 | }
79 | return overs > topReserve - 1 ? overs - topReserve : 0;
80 | },
81 | updateZone(offset) {
82 | if (this.enabled) {
83 | this.updateHeightList();
84 | const overs = this.findOvers(offset);
85 |
86 | // scroll to top
87 | if (!offset && this.total) {
88 | this.$emit('toTop');
89 | }
90 |
91 | let start = overs || 0;
92 | let end = start + this.keeps;
93 | let totalHeight = this.heightList.reduce((a, b) => {
94 | return a + b;
95 | });
96 |
97 | // scroll to bottom
98 | if (offset && offset + this.$el.clientHeight >= totalHeight) {
99 | start = this.total - this.keeps;
100 | end = this.total - 1;
101 | this.$emit('toBottom');
102 | }
103 |
104 | if (this.start !== start || this.end !== end) {
105 | this.start = start;
106 | this.end = end;
107 | this.$forceUpdate();
108 | }
109 | } else {
110 | this.updateZoneNormally(offset);
111 | }
112 | },
113 | filter(slots) {
114 | this.updateHeightList();
115 | if (!slots) {
116 | slots = [];
117 | this.start = 0;
118 | }
119 |
120 | const slotList = slots.filter((slot, index) => {
121 | return index >= this.start && index <= this.end;
122 | });
123 | const topList = this.heightList.slice(0, this.start);
124 | const bottomList = this.heightList.slice(this.end + 1);
125 | this.total = slots.length;
126 | // consider that the height of item may change in any case
127 | // so we compute paddingTop and paddingBottom every time
128 | this.paddingTop = topList.length ? topList.reduce((a, b) => {
129 | return a + b;
130 | }) : 0;
131 | this.paddingBottom = bottomList.length ? bottomList.reduce((a, b) => {
132 | return a + b;
133 | }) : 0;
134 |
135 | return slotList;
136 | },
137 | update() {
138 | this.$nextTick(() => {
139 | this.updateZone(this.scrollTop);
140 | });
141 | }
142 | },
143 | beforeCreate() {
144 | // vue won't observe this properties
145 | Object.assign(this, {
146 | heightList: [], // list of each item height
147 | scrollTop: 0, // current scroll position
148 | start: 0, // start index
149 | end: 0, // end index
150 | total: 0, // all items count
151 | keeps: 0, // number of item keeping in real dom
152 | paddingTop: 0, // all padding of top dom
153 | paddingBottom: 0, // all padding of bottom dom
154 | reserve: 10 // number of reserve dom for pre-render
155 | });
156 | },
157 | beforeMount() {
158 | if (this.enabled) {
159 | let remains = this.remain;
160 | this.start = 0;
161 | this.end = remains + this.reserve - 1;
162 | this.keeps = remains + this.reserve;
163 | }
164 | },
165 | activated() {
166 | // while work with keep-alive component
167 | // set scroll position after 'activated'
168 | this.ignoreStep = true;
169 | this.$el.scrollTop = this.keep ? (this.scrollTop || 1) : 1;
170 | },
171 | render(h) {
172 | const showList = this.enabled ? this.filter(this.$slots.default) : this.$slots.default;
173 | const debounce = this.debounce;
174 |
175 | return h('div', {
176 | class: ['scroll-container'],
177 | style: {
178 | 'display': 'block',
179 | 'overflow-y': 'auto',
180 | 'height': '100%'
181 | },
182 | on: { // '&' support passive event
183 | '&scroll': debounce
184 | ? _debounce(this.handleScroll.bind(this), debounce)
185 | : this.handleScroll
186 | }
187 | }, [
188 | h('div', {
189 | style: {
190 | 'display': 'block',
191 | 'padding-top': this.paddingTop + 'px',
192 | 'padding-bottom': this.paddingBottom + 'px'
193 | }
194 | }, showList)
195 | ]);
196 | }
197 | };
198 |
199 | export default component;
200 |
--------------------------------------------------------------------------------
/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 |
4 | let config = {
5 | entry: {
6 | index: './example/index'
7 | },
8 | resolve: {
9 | extensions: ['.js', '.vue'],
10 | alias: {
11 | 'vue$': 'vue/dist/vue.esm.js',
12 | 'vue-scroll-list': path.resolve(__dirname, 'src/index.js')
13 | }
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.vue$/,
19 | loader: 'vue-loader',
20 | options: {}
21 | },
22 | {
23 | test: /\.js$/,
24 | exclude: /node_modules/,
25 | use: [
26 | {
27 | loader: 'babel-loader',
28 | options: {
29 | babelrc: false, // don't read '.babelrc' file
30 | presets: ['es2015'],
31 | plugins: ['transform-runtime']
32 | }
33 | }
34 | ]
35 | }
36 | ]
37 | },
38 | plugins: [
39 | // new webpack.LoaderOptionsPlugin({
40 | // options: {
41 | // babel: {
42 | // presets: ['es2015'],
43 | // plugins: ['transform-runtime']
44 | // }
45 | // }
46 | // })
47 | ]
48 | };
49 |
50 | module.exports = config;
--------------------------------------------------------------------------------
/webpack.config.demo.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const baseConfig = require('./webpack.config.base');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | baseConfig.output = {
7 | path: path.resolve(__dirname, 'onlineDemo'),
8 | publicPath: '/vue-scroll-list/',
9 | filename: '[name].[hash].js',
10 | sourceMapFilename: '[file].map'
11 | };
12 |
13 | baseConfig.plugins.push(
14 | new webpack.DefinePlugin({
15 | 'process.env': {
16 | NODE_ENV: '"production"'
17 | }
18 | })
19 | );
20 |
21 | baseConfig.plugins.push(
22 | new webpack.optimize.UglifyJsPlugin({
23 | compress: {
24 | warnings: false
25 | },
26 | comments: false,
27 | sourceMap: true
28 | })
29 | );
30 |
31 | baseConfig.plugins.push(
32 | new HtmlWebpackPlugin({
33 | title: 'vue-scroll-list',
34 | template: 'example/index.html',
35 | filename: 'index.html'
36 | })
37 | );
38 |
39 | baseConfig.devtool = 'source-map';
40 |
41 | module.exports = baseConfig;
--------------------------------------------------------------------------------
/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const baseConfig = require('./webpack.config.base');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | baseConfig.output = {
7 | path: path.resolve(__dirname, 'build'),
8 | publicPath: '/',
9 | filename: '[name].js'
10 | };
11 |
12 | baseConfig.plugins.push(
13 | new HtmlWebpackPlugin({
14 | title: 'vue-scroll-list',
15 | template: 'example/index.html',
16 | filename: 'index.html'
17 | })
18 | );
19 |
20 | baseConfig.devServer = {
21 | host: '0.0.0.0',
22 | port: '8686',
23 | noInfo: true,
24 | disableHostCheck: true
25 | };
26 |
27 | module.exports = baseConfig;
--------------------------------------------------------------------------------