Skip to content

Commit 1a485d5

Browse files
author
Guillaume Chau
committed
New RecycleList experimental component
1 parent 254ff7e commit 1a485d5

File tree

5 files changed

+512
-128
lines changed

5 files changed

+512
-128
lines changed

src/components/RecycleList.vue

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
<template>
2+
<div
3+
class="recycle-list"
4+
:class="cssClass"
5+
@scroll.passive="handleScroll"
6+
v-observe-visibility="handleVisibilityChange"
7+
>
8+
<div
9+
class="item-wrapper"
10+
:style="'height:' + totalHeight + 'px'"
11+
>
12+
<div
13+
v-for="view of pool"
14+
:key="view.nr.id"
15+
class="item-view"
16+
:style="'transform:translateY(' + view.top + 'px)'"
17+
>
18+
<slot
19+
:item="view.item"
20+
/>
21+
</div>
22+
</div>
23+
24+
<resize-observer @notify="handleResize" />
25+
</div>
26+
</template>
27+
28+
<script>
29+
import Scroller from '../mixins/scroller'
30+
import { clearTimeout, setTimeout } from 'timers';
31+
32+
let uid = 0
33+
34+
export default {
35+
name: 'RecycleList',
36+
37+
mixins: [
38+
Scroller,
39+
],
40+
41+
props: {
42+
itemHeight: {
43+
type: Number,
44+
default: null,
45+
},
46+
},
47+
48+
data () {
49+
return {
50+
pool: [],
51+
totalHeight: 0,
52+
}
53+
},
54+
55+
watch: {
56+
items: {
57+
handler () {
58+
this.updateVisibleItems({
59+
checkItem: true,
60+
})
61+
},
62+
},
63+
pageMode () {
64+
this.applyPageMode()
65+
this.updateVisibleItems({
66+
checkItem: false,
67+
})
68+
},
69+
heights () {
70+
this.updateVisibleItems({
71+
checkItem: false,
72+
})
73+
},
74+
},
75+
76+
created () {
77+
this.$_ready = false
78+
this.$_startIndex = 0
79+
this.$_endIndex = 0
80+
this.$_views = new Map()
81+
this.$_unusedViews = new Map()
82+
this.$_scrollDirty = false
83+
84+
// TODO prerender
85+
},
86+
87+
mounted () {
88+
this.applyPageMode()
89+
this.$nextTick(() => {
90+
this.updateVisibleItems({
91+
checkItem: true,
92+
})
93+
this.$_ready = true
94+
})
95+
},
96+
97+
methods: {
98+
addView (index, item) {
99+
const view = {
100+
item,
101+
top: 0,
102+
}
103+
const nonReactive = {
104+
id: uid++,
105+
index,
106+
used: true,
107+
}
108+
Object.defineProperty(view, 'nr', {
109+
configurable: false,
110+
value: nonReactive,
111+
})
112+
this.pool.push(view)
113+
return view
114+
},
115+
116+
unuseView (view, fake = false) {
117+
const unusedViews = this.$_unusedViews
118+
const type = view.item[this.typeField]
119+
let unusedPool = unusedViews.get(type)
120+
if (!unusedPool) {
121+
unusedPool = []
122+
unusedViews.set(type, unusedPool)
123+
}
124+
unusedPool.push(view)
125+
if (!fake) {
126+
view.nr.used = false
127+
view.top = this.totalHeight
128+
this.$_views.delete(view.item)
129+
}
130+
},
131+
132+
handleResize () {
133+
this.$emit('resize')
134+
this.$_ready && this.updateVisibleItems({
135+
checkItem: false,
136+
})
137+
},
138+
139+
handleScroll (event) {
140+
if (!this.$_scrollDirty) {
141+
this.$_scrollDirty = true
142+
requestAnimationFrame(() => {
143+
this.$_scrollDirty = false
144+
const { continuous } = this.updateVisibleItems({
145+
checkItem: false,
146+
})
147+
148+
// It seems sometimes chrome doesn't fire scroll event :/
149+
// When non continous scrolling is ending, we force a refresh
150+
if (!continuous) {
151+
clearTimeout(this.$_refreshTimout)
152+
this.$_refreshTimout = setTimeout(this.handleScroll, 100)
153+
}
154+
})
155+
}
156+
},
157+
158+
handleVisibilityChange (isVisible, entry) {
159+
if (this.$_ready && (isVisible || entry.boundingClientRect.width !== 0 || entry.boundingClientRect.height !== 0)) {
160+
this.$emit('visible')
161+
requestAnimationFrame(() => {
162+
this.updateVisibleItems({
163+
checkItem: false,
164+
})
165+
})
166+
}
167+
},
168+
169+
updateVisibleItems ({ checkItem }) {
170+
const scroll = this.getScroll()
171+
const buffer = parseInt(this.buffer)
172+
scroll.top -= buffer
173+
scroll.bottom += buffer
174+
175+
const itemHeight = this.itemHeight
176+
const typeField = this.typeField
177+
const items = this.items
178+
const count = items.length
179+
const heights = this.heights
180+
const views = this.$_views
181+
let unusedViews = this.$_unusedViews
182+
const pool = this.pool
183+
let startIndex, endIndex
184+
let totalHeight
185+
186+
// Variable height mode
187+
if (itemHeight === null) {
188+
let h
189+
let a = 0
190+
let b = count - 1
191+
let i = ~~(count / 2)
192+
let oldI
193+
194+
// Searching for startIndex
195+
do {
196+
oldI = i
197+
h = heights[i]
198+
if (h < scroll.top) {
199+
a = i
200+
} else if (i < count && heights[i + 1] > scroll.top) {
201+
b = i
202+
}
203+
i = ~~((a + b) / 2)
204+
} while (i !== oldI)
205+
i < 0 && (i = 0)
206+
startIndex = i
207+
208+
// For container style
209+
totalHeight = heights[count - 1]
210+
211+
// Searching for endIndex
212+
for (endIndex = i; endIndex < count && heights[endIndex] < scroll.bottom; endIndex++);
213+
if (endIndex === -1) {
214+
endIndex = items.length - 1
215+
} else {
216+
endIndex++
217+
// Bounds
218+
endIndex > count && (endIndex = count)
219+
}
220+
} else {
221+
// Fixed height mode
222+
startIndex = ~~(scroll.top / itemHeight)
223+
endIndex = Math.ceil(scroll.bottom / itemHeight)
224+
225+
// Bounds
226+
startIndex < 0 && (startIndex = 0)
227+
endIndex > count && (endIndex = count)
228+
229+
totalHeight = count * itemHeight
230+
}
231+
232+
this.totalHeight = totalHeight
233+
234+
let view
235+
236+
const continuous = startIndex < this.$_endIndex && endIndex > this.$_startIndex
237+
let unusedIndex
238+
239+
if (this.$_continuous !== continuous) {
240+
if (continuous) {
241+
this.$_views.clear()
242+
this.$_unusedViews.clear()
243+
for (let i = 0, l = pool.length; i < l; i++) {
244+
view = pool[i]
245+
this.unuseView(view)
246+
}
247+
}
248+
this.$_continuous = continuous
249+
} else if (continuous) {
250+
for (let i = 0, l = pool.length; i < l; i++) {
251+
view = pool[i]
252+
if (view.nr.used && (
253+
view.nr.index < startIndex ||
254+
view.nr.index > endIndex ||
255+
(checkItem && !items.includes(view.item))
256+
)) {
257+
this.unuseView(view)
258+
}
259+
}
260+
}
261+
262+
if (!continuous) {
263+
unusedIndex = new Map()
264+
}
265+
266+
let item, type, unusedPool
267+
let v
268+
for (let i = startIndex; i < endIndex; i++) {
269+
item = items[i]
270+
view = views.get(item)
271+
272+
// No view assigned to item
273+
if (!view) {
274+
type = item[typeField]
275+
276+
if (continuous) {
277+
unusedPool = unusedViews.get(type)
278+
// Reuse existing view
279+
if (unusedPool && unusedPool.length) {
280+
view = unusedPool.pop()
281+
view.item = item
282+
view.nr.used = true
283+
view.nr.index = i
284+
} else {
285+
view = this.addView(i, item, type)
286+
}
287+
} else {
288+
unusedPool = unusedViews.get(type)
289+
v = unusedIndex.get(type) || 0
290+
// Use existing view
291+
// We don't care if they are already used
292+
// because we are not in continous scrolling
293+
if (unusedPool && v < unusedPool.length) {
294+
view = unusedPool[v]
295+
view.item = item
296+
view.nr.used = true
297+
view.nr.index = i
298+
unusedIndex.set(type, v + 1)
299+
} else {
300+
view = this.addView(i, item, type)
301+
this.unuseView(view, true)
302+
}
303+
v++
304+
}
305+
views.set(item, view)
306+
} else {
307+
view.nr.used = true
308+
}
309+
310+
// Update position
311+
if (itemHeight === null) {
312+
view.top = heights[i - 1]
313+
} else {
314+
view.top = i * itemHeight
315+
}
316+
}
317+
318+
this.$_startIndex = startIndex
319+
this.$_endIndex = endIndex
320+
321+
this.emitUpdate && this.$emit('update', startIndex, endIndex)
322+
323+
return {
324+
continuous,
325+
}
326+
},
327+
},
328+
}
329+
</script>
330+
331+
<style scoped>
332+
.recycle-list:not(.page-mode) {
333+
overflow-y: auto;
334+
}
335+
336+
.item-wrapper {
337+
box-sizing: border-box;
338+
width: 100%;
339+
overflow: hidden;
340+
position: relative;
341+
}
342+
343+
.item-view {
344+
width: 100%;
345+
position: absolute;
346+
top: 0;
347+
left: 0;
348+
will-change: transform;
349+
}
350+
</style>

0 commit comments

Comments
 (0)