forked from bvaughn/infinite-list-reflow-examples
-
Notifications
You must be signed in to change notification settings - Fork 0
/
list-absolute-positioning.js
189 lines (148 loc) · 5.89 KB
/
list-absolute-positioning.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
function createList(container, itemsCount, createItem, updateItem) {
const indexToCachedSizeMap = new Map();
const indexToCachedOffsetMap = new Map();
const itemPool = new Set();
const visibleItems = new Map();
let listOuter = null;
let listInner = null;
let estimatedItemHeight = 30;
let lastMeasuredIndex = -1;
let totalMeasuredItemHeights = 0;
let previousScrollTop = 0;
function findItemIndexForOffset(offset) {
// If we've already positioned and measured past this point,
// Use a binary search to find the closest item.
if (offset <= totalMeasuredItemHeights) {
return findNearestItemBinarySearch(lastMeasuredIndex, 0, offset);
}
// Otherwise start rendering where we left off.
return lastMeasuredIndex + 1;
}
function findNearestItemBinarySearch(indexHigh, indexLow, targetOffset) {
while (indexLow <= indexHigh) {
const indexMiddle = indexLow + Math.floor((indexHigh - indexLow) / 2);
const itemOffset = indexToCachedOffsetMap.get(indexMiddle);
if (itemOffset === targetOffset) {
return indexMiddle;
} else if (itemOffset < targetOffset) {
indexLow = indexMiddle + 1;
} else if (itemOffset > targetOffset) {
indexHigh = indexMiddle - 1;
}
}
if (indexLow > 0) {
return indexLow - 1;
} else {
return 0;
}
};
function estimateTotalScrollHeight() {
const numUnmeasuredItems = itemsCount - lastMeasuredIndex - 1;
const estimatedUnmeasuredItemHeights = numUnmeasuredItems * estimatedItemHeight;
const estimatedHeight = totalMeasuredItemHeights + estimatedUnmeasuredItemHeights;
if (lastMeasuredIndex === itemsCount -1) {
return Math.min(
estimatedHeight,
indexToCachedOffsetMap.get(lastMeasuredIndex) + indexToCachedSizeMap.get(lastMeasuredIndex)
);
}
return estimatedHeight;
}
function init() {
listOuter = document.createElement('div');
listOuter.className = 'list-outer';
listInner = document.createElement('div');
listInner.className = 'list-inner';
listInner.style.setProperty('height', itemsCount * estimatedItemHeight);
listOuter.appendChild(listInner);
container.appendChild(listOuter);
window.addEventListener('resize', renderItems);
listOuter.addEventListener('scroll', renderItems);
}
function renderItems() {
const scrollTop = listOuter.scrollTop;
const listHeight = listOuter.clientHeight;
const startIndex = findItemIndexForOffset(scrollTop);
let index = startIndex;
let offset = indexToCachedOffsetMap.get(startIndex) || 0;
let scrollTopAdjustments = 0;
while (index < itemsCount && offset < scrollTop + listHeight) {
let prevItemOffset = indexToCachedOffsetMap.has(index - 1) ? indexToCachedOffsetMap.get(index - 1) : 0;
let prevItemSize = indexToCachedSizeMap.has(index - 1) ? indexToCachedSizeMap.get(index - 1) : 0;
offset = prevItemOffset + prevItemSize;
let itemSize;
let item;
if (visibleItems.has(index)) {
item = visibleItems.get(index);
item.style.setProperty('top', offset); // TODO Is this necessary?
} else {
if (itemPool.size > 0) {
item = itemPool.values().next().value;
itemPool.delete(item);
} else {
item = document.createElement('div');
item.className = 'list-item';
item.style.setProperty('width', '100%');
item.style.setProperty('position', 'absolute');
createItem(item);
}
item.style.setProperty('top', offset);
updateItem(item, index);
visibleItems.set(index, item);
listInner.appendChild(item);
}
itemSize = item.offsetHeight;
if (indexToCachedSizeMap.has(index)) {
let itemSizeDelta = itemSize - indexToCachedSizeMap.get(index);
if (itemSizeDelta !== 0) {
totalMeasuredItemHeights += itemSizeDelta;
// If we're scrolling up and item size has changed, note the delta.
// We'll need to adjust scroll by this amount to preserve the appearance of smooth scrolling.
// Else items will appear to jump around while the user scrolls.
if (previousScrollTop > scrollTop) {
scrollTopAdjustments += itemSizeDelta;
}
}
} else {
totalMeasuredItemHeights += itemSize;
}
indexToCachedSizeMap.set(index, itemSize);
indexToCachedOffsetMap.set(index, offset);
lastMeasuredIndex = Math.max(index, lastMeasuredIndex);
estimatedItemHeight = Math.round(totalMeasuredItemHeights / (lastMeasuredIndex + 1));
index++;
offset += itemSize;
}
const stopIndex = index - 1;
// Remove items that are no longer visible and return them to the pool.
for (let [index, item] of visibleItems.entries()) {
if (index < startIndex || index > stopIndex) {
let item = visibleItems.get(index);
visibleItems.delete(index);
listInner.removeChild(item);
itemPool.add(item);
}
}
// If item sizes have changed, adjust scroll to preserve the appearance of smooth scrolling.
if (scrollTopAdjustments !== 0) {
// Adjusting scroll offset directly interrupts smooth scrolling for some browsers (e.g. Firefox)
// but works well in other browsers tested (e.g. Chrome, Safari).
// The relative scrollBy() method does not cause this interrupt for Firefox v65+
// so if it's available, use it instead.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1502059
if (typeof listOuter.scrollBy === 'function') {
listOuter.scrollBy({
top: scrollTopAdjustments,
left: 0,
behavior: 'auto'
});
} else {
listOuter.scrollTop = scrollTop + scrollTopAdjustments;
}
}
previousScrollTop = scrollTop;
listInner.style.setProperty('height', estimateTotalScrollHeight());
}
init();
renderItems();
}