Skip to content

Commit 9357dec

Browse files
committed
chore: small changes
1 parent 14d90d1 commit 9357dec

File tree

3 files changed

+143
-88
lines changed

3 files changed

+143
-88
lines changed

src/virtual-list/index.ts

+139-83
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
import Component, { IComponentOptions } from '../share/Component'
22
import $ from 'licia/$'
3-
import types from 'licia/types'
43
import throttle from 'licia/throttle'
54
import isHidden from 'licia/isHidden'
65
import now from 'licia/now'
6+
import ResizeSensor from 'licia/ResizeSensor'
7+
import isEmpty from 'licia/isEmpty'
8+
import unique from 'licia/unique'
9+
import map from 'licia/map'
10+
import debounce from 'licia/debounce'
11+
import each from 'licia/each'
712

813
/** IOptions */
914
export interface IOptions extends IComponentOptions {
1015
/** Auto scroll if at bottom. */
1116
autoScroll?: boolean
1217
}
1318

14-
interface IItem {
15-
el: HTMLElement
16-
height: number
17-
width: number
18-
}
19-
2019
/**
2120
* Vertical list with virtual scrolling.
2221
*
@@ -27,8 +26,7 @@ interface IItem {
2726
* virtualList.append(document.createElement('div'))
2827
*/
2928
export default class VirtualList extends Component<IOptions> {
30-
render: types.AnyFn
31-
private items: IItem[] = []
29+
private items: Item[] = []
3230
private $el: $.$
3331
private el: HTMLElement
3432
private $fakeEl: $.$
@@ -45,6 +43,10 @@ export default class VirtualList extends Component<IOptions> {
4543
private maxSpeedTolerance = 2000
4644
private minSpeedTolerance = 100
4745
private isAtBottom = true
46+
private updateTimer: NodeJS.Timeout | null = null
47+
private updateItems: Item[] = []
48+
private resizeSensor: ResizeSensor
49+
private scrollTimer: NodeJS.Timeout | null = null
4850
constructor(container: HTMLElement, options: IOptions = {}) {
4951
super(container, { compName: 'virtual-list' }, options)
5052

@@ -61,21 +63,63 @@ export default class VirtualList extends Component<IOptions> {
6163
this.$space = this.find('.items-space')
6264
this.space = this.$space.get(0) as HTMLElement
6365

64-
this.render = throttle((options: any) => this._render(options), 16)
66+
this.resizeSensor = new ResizeSensor(this.space)
6567

6668
this.bindEvent()
6769
}
6870
clear() {
6971
this.items = []
7072
this.render()
7173
}
72-
append(item: HTMLElement) {
73-
this.items.push({
74-
el: item,
75-
height: 0,
76-
width: 0,
77-
})
74+
append(el: HTMLElement) {
75+
const item = new Item(el, this.el)
76+
this.items.push(item)
77+
this.updateSize(item)
78+
}
79+
setItems(els: HTMLElement[]) {
80+
each(this.items, (item) => item.destroy())
81+
this.items = map(els, (el) => new Item(el, this.el))
82+
this.updateItems = []
83+
this.updateAllSize()
84+
}
85+
private updateAllSize = debounce(() => {
86+
this.updateItems.push(...this.items)
87+
this.updateItems = unique(this.updateItems)
88+
if (!this.updateTimer) {
89+
this._updateSize()
90+
}
91+
}, 1000)
92+
private updateSize(item: Item) {
93+
this.updateItems.push(item)
94+
if (!this.updateTimer) {
95+
this._updateSize()
96+
}
97+
}
98+
private _updateSize = () => {
99+
const items = this.updateItems.splice(0, 1000)
100+
if (isEmpty(items)) {
101+
return
102+
}
103+
104+
const len = items.length
105+
const { fakeEl } = this
106+
const fakeFrag = document.createDocumentFragment()
107+
for (let i = 0; i < len; i++) {
108+
fakeFrag.appendChild(items[i].el)
109+
}
110+
fakeEl.appendChild(fakeFrag)
111+
for (let i = 0; i < len; i++) {
112+
items[i].updateSize()
113+
}
114+
fakeEl.textContent = ''
115+
78116
this.render()
117+
118+
if (!isEmpty(this.updateItems)) {
119+
this.updateTimer = setTimeout(() => this._updateSize(), 100)
120+
} else {
121+
this.updateTimer = null
122+
}
79123
}
80124
private initTpl() {
81125
this.$container.html(
@@ -97,77 +141,64 @@ export default class VirtualList extends Component<IOptions> {
97141
this.space.style.height = height + 'px'
98142
}
99143
private bindEvent() {
144+
this.resizeSensor.addListener(
145+
throttle(() => {
146+
this.updateAllSize()
147+
}, 100)
148+
)
100149
this.$container.on('scroll', this.onScroll)
101150
}
102-
private _render({ topTolerance = 500, bottomTolerance = 500 } = {}) {
103-
const { el, container, space } = this
104-
if (isHidden(container)) return
105-
const { scrollTop, offsetHeight } = container as HTMLElement
106-
const containerWidth = space.getBoundingClientRect().width
107-
const top = scrollTop - topTolerance
108-
const bottom = scrollTop + offsetHeight + bottomTolerance
109-
110-
const { items } = this
151+
private render = throttle(
152+
({ topTolerance = 500, bottomTolerance = 500 } = {}) => {
153+
const { el, container } = this
154+
if (isHidden(container)) {
155+
return
156+
}
111157

112-
let topSpaceHeight = 0
113-
let bottomSpaceHeight = 0
114-
let currentHeight = 0
158+
const { scrollTop, offsetHeight } = container as HTMLElement
159+
const top = scrollTop - topTolerance
160+
const bottom = scrollTop + offsetHeight + bottomTolerance
115161

116-
const len = items.length
162+
const { items } = this
117163

118-
const { fakeEl } = this
119-
const fakeFrag = document.createDocumentFragment()
120-
const updateItems = []
121-
for (let i = 0; i < len; i++) {
122-
const item = items[i]
123-
const { width, height } = item
124-
if (height === 0 || width !== containerWidth) {
125-
fakeFrag.appendChild(item.el)
126-
updateItems.push(item)
127-
}
128-
}
129-
if (updateItems.length > 0) {
130-
fakeEl.appendChild(fakeFrag)
131-
for (let i = 0, len = updateItems.length; i < len; i++) {
132-
this.updateItemSize(updateItems[i])
133-
}
134-
fakeEl.textContent = ''
135-
}
164+
let topSpaceHeight = 0
165+
let bottomSpaceHeight = 0
166+
let currentHeight = 0
136167

137-
const frag = document.createDocumentFragment()
138-
for (let i = 0; i < len; i++) {
139-
const item = items[i]
140-
const { el, height } = item
141-
142-
if (currentHeight > bottom) {
143-
bottomSpaceHeight += height
144-
} else if (currentHeight + height > top) {
145-
frag.appendChild(el)
146-
} else if (currentHeight < top) {
147-
topSpaceHeight += height
148-
}
168+
const len = items.length
149169

150-
currentHeight += height
151-
}
170+
const frag = document.createDocumentFragment()
171+
for (let i = 0; i < len; i++) {
172+
const item = items[i]
173+
const { el, height } = item
152174

153-
this.updateSpace(currentHeight)
154-
this.updateTopSpace(topSpaceHeight)
155-
this.updateBottomSpace(bottomSpaceHeight)
175+
if (currentHeight > bottom) {
176+
bottomSpaceHeight += height
177+
} else if (currentHeight + height > top) {
178+
frag.appendChild(el)
179+
} else if (currentHeight < top) {
180+
topSpaceHeight += height
181+
}
156182

157-
while (el.firstChild) {
158-
if (el.lastChild) {
159-
el.removeChild(el.lastChild)
183+
currentHeight += height
160184
}
161-
}
162-
el.appendChild(frag)
163185

164-
if (this.options.autoScroll) {
165-
const { scrollHeight } = container
166-
if (this.isAtBottom && scrollTop <= scrollHeight - offsetHeight) {
167-
container.scrollTop = 10000000
186+
this.updateSpace(currentHeight)
187+
this.updateTopSpace(topSpaceHeight)
188+
this.updateBottomSpace(bottomSpaceHeight)
189+
190+
el.textContent = ''
191+
el.appendChild(frag)
192+
193+
if (this.options.autoScroll) {
194+
const { scrollHeight } = container
195+
if (this.isAtBottom && scrollTop <= scrollHeight - offsetHeight) {
196+
container.scrollTop = 10000000
197+
}
168198
}
169-
}
170-
}
199+
},
200+
16
201+
)
171202
private onScroll = () => {
172203
const { scrollHeight, offsetHeight, scrollTop } = this
173204
.container as HTMLElement
@@ -225,14 +256,39 @@ export default class VirtualList extends Component<IOptions> {
225256
topTolerance: topTolerance * 2,
226257
bottomTolerance: bottomTolerance * 2,
227258
})
228-
}
229-
private updateItemSize(item: IItem) {
230-
const { width, height } = item.el.getBoundingClientRect()
231-
if (item.height !== height) {
232-
item.height = height
233-
}
234-
if (item.width !== width) {
235-
item.width = width
259+
260+
if (this.scrollTimer) {
261+
clearTimeout(this.scrollTimer)
236262
}
263+
this.scrollTimer = setTimeout(() => {
264+
this.render()
265+
}, 100)
266+
}
267+
}
268+
269+
class Item {
270+
el: HTMLElement
271+
width: number
272+
height: number
273+
private resizeSensor: ResizeSensor
274+
constructor(el: HTMLElement, container: HTMLElement) {
275+
this.el = el
276+
this.width = 0
277+
this.height = 0
278+
279+
this.resizeSensor = new ResizeSensor(el)
280+
this.resizeSensor.addListener(() => {
281+
if (el.parentNode === container && !isHidden(el)) {
282+
this.updateSize()
283+
}
284+
})
285+
}
286+
destroy() {
287+
this.resizeSensor.destroy()
288+
}
289+
updateSize() {
290+
const { width, height } = this.el.getBoundingClientRect()
291+
this.width = width
292+
this.height = height
237293
}
238294
}

src/virtual-list/story.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ const def = story(
3535
{
3636
style: {
3737
color: '#fff',
38+
width: '100%',
3839
background: randomColor({ lightness: 0.5 }),
39-
wordBreak: 'break-all',
40+
minHeight: random(30, 100) + 'px',
41+
lineHeight: '1.5em',
4042
},
4143
},
4244
toStr(i) + ' ' + randomId(random(30, 1000))

src/virtual-list/style.scss

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
@use '../share/mixin' as mixin;
22

33
.luna-virtual-list {
4-
@include mixin.overflow-auto(y);
4+
@include mixin.overflow-auto();
55
height: 100%;
66
position: relative;
77
will-change: scroll-position;
8-
-webkit-overflow-scrolling: touch;
98
}
109

1110
.fake-items {
@@ -14,10 +13,8 @@
1413
top: 0;
1514
pointer-events: none;
1615
visibility: hidden;
17-
width: 100%;
1816
}
1917

2018
.items {
2119
position: absolute;
22-
width: 100%;
2320
}

0 commit comments

Comments
 (0)