Skip to content

Commit 6408d57

Browse files
committed
New variable height mode & Updated deps
1 parent e194688 commit 6408d57

13 files changed

+152
-29
lines changed

Diff for: README.md

+33-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ Vue.component('virtual-scroller', VirtualScroller)
111111
The virtual scroller has three main props:
112112

113113
- `items` is the list of items you want to display in the scroller. There can be several types of item.
114-
- `itemHeight` is the display height of the items in pixels used to calculate the scroll height and position.
114+
- `itemHeight` is the display height of the items in pixels used to calculate the scroll height and position. If it set to null (default value), it will use [variable height mode](#variable-height-mode).
115115
- `renderers` is a map of component definitions objects or names for each item type ([more details](#renderers)). If you don't define `renderers`, the scroller will use *scoped slots* ([see below](#scoped-slots)).
116116

117117
You need to set the size of the virtual-scroller element and the items elements (for example, with CSS). All items should have the same height to prevent display glitches.
@@ -173,6 +173,38 @@ The page mode expand the virtual-scroller and use the page viewport to compute w
173173
</footer>
174174
```
175175

176+
## Variable height mode
177+
178+
**⚠️ This mode can be performance heavy with a lot of items. Use with caution.**
179+
180+
If the `itemHeight` prop is not set or set to `null`, the virtual scroller will switch to Variable height mode. You then need to expose a number field on the item objects with the height of the item element.
181+
182+
**⚠️ You still need to set the height of the items with CSS correctly (with classes for example).**
183+
184+
Use the `heightField` prop (default is `'height'`) to set the field used by the scroller to get the height for each item.
185+
186+
Example:
187+
188+
```javascript
189+
const items = [
190+
{
191+
id: 1,
192+
label: 'Title',
193+
height: 64,
194+
},
195+
{
196+
id: 2,
197+
label: 'Foo',
198+
height: 32,
199+
},
200+
{
201+
id: 3,
202+
label: 'Bar',
203+
height: 32,
204+
},
205+
]
206+
```
207+
176208
## Customizing the tags
177209

178210
These are optional props you can use to change the DOM tags used in the virtual scroller:

Diff for: config/webpack.base.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
var webpack = require('webpack')
2+
var path = require('path')
23
var ExtractTextPlugin = require('extract-text-webpack-plugin')
34

45
var outputFile = 'vue-virtual-scroller'
@@ -9,7 +10,7 @@ var config = require('../package.json')
910
module.exports = {
1011
entry: './src/index.js',
1112
output: {
12-
path: './dist',
13+
path: path.resolve(__dirname, '../dist'),
1314
filename: outputFile + '.js',
1415
library: globalName,
1516
libraryTarget: 'umd',

Diff for: dist/vue-virtual-scroller.css

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
background-color: transparent;
2323
pointer-events: none;
2424
display: block;
25-
overflow: hidden;
25+
overflow: hidden;
26+
opacity: 0;
2627
}
2728

2829
.test h1[data-v-5ee0142a] {

Diff for: dist/vue-virtual-scroller.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: docs-src/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"private": true,
77
"scripts": {
88
"dev": "cross-env NODE_ENV=development webpack-dev-server --open --inline --hot",
9+
"devp": "cross-env NODE_ENV=production webpack-dev-server --open --inline --hot",
910
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
1011
},
1112
"dependencies": {

Diff for: docs-src/src/App.vue

+18-8
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
<span>
55
<input v-model="countInput" type="number" min="0" max="500000" /> items
66
</span>
7+
<span>
8+
<input v-model="buffer" type="number" min="1" max="500000" /> buffer
9+
</span>
10+
<span>
11+
<input v-model="poolSize" type="number" min="1" max="500000" /> poolSize
12+
</span>
713
<span v-if="generateTime !== null">
814
Items generation: {{ generateTime }} ms
915
</span>
10-
<span v-if="updateTime !== null">
11-
Virtual scroller update: {{ updateTime }} ms
12-
</span>
1316
<span>
1417
<button @mousedown="showScroller = !showScroller">Toggle scroller</button>
1518
<label><input type="checkbox" v-model="scopedSlots" /> Scoped slots</label>
@@ -18,23 +21,23 @@
1821
<div class="content" v-if="showScroller">
1922
<div class="wrapper">
2023
<!-- Scoped slots -->
21-
<virtual-scroller v-if="scopedSlots" class="scroller" :items="items" item-height="42" main-tag="section" content-tag="table">
24+
<virtual-scroller v-if="scopedSlots" class="scroller" :items="items" main-tag="section" content-tag="table" :buffer="buffer" :pool-size="poolSize">
2225
<template scope="props">
2326
<!-- <letter v-if="props.item.type === 'letter'" :item="props.item"></letter>-->
24-
<tr v-if="props.item.type === 'letter'" class="letter">
27+
<tr v-if="props.item.type === 'letter'" class="letter" :key="props.itemKey">
2528
<td class="index">
2629
{{props.item.index}}
2730
</td>
2831
<td>
2932
{{props.item.value}} Scoped
3033
</td>
3134
</tr>
32-
<item v-if="props.item.type === 'person'" :item="props.item"></item>
35+
<item v-if="props.item.type === 'person'" :item="props.item" :key="props.itemKey"></item>
3336
</template>
3437
</virtual-scroller>
3538

3639
<!-- Renderers -->
37-
<virtual-scroller v-else class="scroller" :items="items" :renderers="renderers" item-height="42" type-field="type" key-field="index" main-tag="section" content-tag="table"></virtual-scroller>
40+
<virtual-scroller v-else class="scroller" :items="items" :renderers="renderers" type-field="type" key-field="index" main-tag="section" content-tag="table"></virtual-scroller>
3841
</div>
3942
</div>
4043
</div>
@@ -65,6 +68,8 @@ export default {
6568
updateTime: null,
6669
showScroller: true,
6770
scopedSlots: false,
71+
buffer: 0,
72+
poolSize: 1,
6873
}),
6974
7075
watch: {
@@ -191,7 +196,12 @@ body {
191196
.letter {
192197
text-transform: uppercase;
193198
color: grey;
194-
font-weight: bold;
199+
font-weight: lighter;
200+
height: 200px;
201+
}
202+
203+
.letter .value {
204+
font-size: 120px;
195205
}
196206
197207
.index {

Diff for: docs-src/src/Item.vue

+7
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,12 @@ export default {
1717
this.item.value.name += '#'
1818
},
1919
},
20+
/* beforeCreate () {
21+
let d = 0
22+
for (let i = 0; i < 9999999; i++) {
23+
d++
24+
}
25+
return d
26+
}, */
2027
}
2128
</script>

Diff for: docs-src/src/Letter.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<td class="index">
44
{{item.index}}
55
</td>
6-
<td>
6+
<td class="value">
77
{{item.value}}
88
</td>
99
</tr>

Diff for: docs-src/src/data.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,22 @@ export function getData (count) {
1818
}
1919

2020
const data = []
21-
let index = 0
21+
let index = 1
2222

2323
for (const l of alphabet) {
2424
raw[l] = raw[l].sort((a, b) => a.name < b.name ? -1 : 1)
2525
data.push({
2626
index: index++,
2727
type: 'letter',
2828
value: l,
29+
height: 200,
2930
})
3031
for (var item of raw[l]) {
3132
data.push({
3233
index: index++,
3334
type: 'person',
3435
value: item,
36+
height: 42,
3537
})
3638
}
3739
}

Diff for: docs/build.js

+5-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: docs/build.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vue-virtual-scroller",
33
"description": "Smooth scrolling for any amount of data",
4-
"version": "0.6.1",
4+
"version": "0.7.0",
55
"author": {
66
"name": "Guillaume Chau",
77
"email": "[email protected]"
@@ -52,6 +52,6 @@
5252
},
5353
"dependencies": {
5454
"vue-observe-visibility": "^0.1.3",
55-
"vue-resize": "^0.1.2"
55+
"vue-resize": "^0.1.3"
5656
}
5757
}

Diff for: src/components/VirtualScroller.vue

+76-7
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default {
4343
},
4444
itemHeight: {
4545
type: [Number, String],
46-
required: true,
46+
default: null,
4747
},
4848
typeField: {
4949
type: String,
@@ -53,6 +53,10 @@ export default {
5353
type: String,
5454
default: 'id',
5555
},
56+
heightField: {
57+
type: String,
58+
default: 'height',
59+
},
5660
mainTag: {
5761
type: String,
5862
default: 'div',
@@ -98,6 +102,18 @@ export default {
98102
'page-mode': this.pageMode,
99103
}
100104
},
105+
106+
heights () {
107+
const heights = {}
108+
const items = this.items
109+
const field = this.heightField
110+
let accumulator = 0
111+
for (let i = 0; i < items.length; i++) {
112+
accumulator += items[i][field]
113+
heights[i] = accumulator
114+
}
115+
return heights
116+
},
101117
},
102118
103119
watch: {
@@ -147,32 +163,85 @@ export default {
147163
updateVisibleItems () {
148164
const l = this.items.length
149165
const scroll = this.getScroll()
166+
let containerHeight, offsetTop
150167
if (scroll) {
151-
let startIndex = Math.floor((Math.floor(scroll.top / this.itemHeight) - this.buffer) / this.poolSize) * this.poolSize
152-
let endIndex = Math.floor((Math.ceil(scroll.bottom / this.itemHeight) + this.buffer) / this.poolSize) * this.poolSize
168+
let startIndex = -1
169+
let endIndex = -1
170+
171+
// Variable height mode
172+
if (this.itemHeight === null) {
173+
const heights = this.heights
174+
let h
175+
let a = 0
176+
let b = l
177+
let i = ~~(l / 2)
178+
let oldI
179+
180+
// Searching for startIndex
181+
do {
182+
oldI = i
183+
h = heights[i]
184+
if (h < scroll.top) {
185+
a = i
186+
} else if (i < l && heights[i + 1] > scroll.top) {
187+
b = i
188+
}
189+
i = ~~((a + b) / 2)
190+
} while (i !== oldI)
191+
startIndex = i
192+
193+
// For containers style
194+
offsetTop = i > 0 ? heights[i - 1] : 0
195+
containerHeight = heights[l - 1]
196+
197+
// Searching for endIndex
198+
for (endIndex = i; endIndex < l && heights[endIndex] < scroll.bottom; endIndex++);
199+
if (endIndex === -1) {
200+
endIndex = this.items.length - 1
201+
} else {
202+
endIndex++
203+
}
204+
} else {
205+
// Fixed height mode
206+
startIndex = Math.floor((Math.floor(scroll.top / this.itemHeight) - this.buffer) / this.poolSize) * this.poolSize
207+
endIndex = Math.floor((Math.ceil(scroll.bottom / this.itemHeight) + this.buffer) / this.poolSize) * this.poolSize
208+
containerHeight = l * this.itemHeight
209+
offsetTop = startIndex * this.itemHeight
210+
}
211+
153212
if (startIndex < 0) {
154213
startIndex = 0
155214
}
156215
if (endIndex > l) {
157216
endIndex = l
158217
}
159-
if (startIndex !== this._startIndex || endIndex !== this.endIndex) {
218+
219+
if (startIndex !== this._startIndex || endIndex !== this._endIndex) {
160220
this.keysEnabled = !(startIndex > this._endIndex || endIndex < this._startIndex)
161221
this._startIndex = startIndex
162222
this._endIndex = endIndex
163223
this.visibleItems = this.items.slice(startIndex, endIndex)
164224
this.itemContainerStyle = {
165-
height: l * this.itemHeight + 'px',
225+
height: containerHeight + 'px',
166226
}
167227
this.itemsStyle = {
168-
marginTop: startIndex * this.itemHeight + 'px',
228+
marginTop: offsetTop + 'px',
169229
}
170230
}
171231
}
172232
},
173233
174234
scrollToItem (index) {
175-
this.$el.scrollTop = index * this.itemHeight
235+
let scrollTop
236+
if (this.itemHeight === null) {
237+
scrollTop = 0
238+
for (let i = 0; i < index; i++) {
239+
scrollTop += this.items[i][this.heightField]
240+
}
241+
} else {
242+
scrollTop = index * this.itemHeight
243+
}
244+
this.$el.scrollTop = scrollTop
176245
},
177246
178247
handleVisibilityChange (isVisible, entry) {

0 commit comments

Comments
 (0)