-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Expand file tree
/
Copy pathfixed-size-virtual-scroll.ts
More file actions
249 lines (218 loc) · 8.74 KB
/
fixed-size-virtual-scroll.ts
File metadata and controls
249 lines (218 loc) · 8.74 KB
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {coerceNumberProperty, NumberInput} from '../coercion';
import {Directive, forwardRef, Input, OnChanges} from '@angular/core';
import {Observable, Subject} from 'rxjs';
import {distinctUntilChanged} from 'rxjs/operators';
import {VIRTUAL_SCROLL_STRATEGY, VirtualScrollStrategy} from './virtual-scroll-strategy';
import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
/** Virtual scrolling strategy for lists with items of known fixed size. */
export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
private readonly _scrolledIndexChange = new Subject<number>();
/** @docs-private Implemented as part of VirtualScrollStrategy. */
scrolledIndexChange: Observable<number> = this._scrolledIndexChange.pipe(distinctUntilChanged());
/** The attached viewport. */
private _viewport: CdkVirtualScrollViewport | null = null;
/** The size of the items in the virtually scrolling list. */
private _itemSize: number;
/** The minimum amount of buffer rendered beyond the viewport (in pixels). */
private _minBufferPx: number;
/** The number of buffer items to render beyond the edge of the viewport (in pixels). */
private _maxBufferPx: number;
/**
* @param itemSize The size of the items in the virtually scrolling list.
* @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more
* @param maxBufferPx The amount of buffer (in pixels) to render when rendering more.
*/
constructor(itemSize: number, minBufferPx: number, maxBufferPx: number) {
this._itemSize = itemSize;
this._minBufferPx = minBufferPx;
this._maxBufferPx = maxBufferPx;
}
/**
* Attaches this scroll strategy to a viewport.
* @param viewport The viewport to attach this strategy to.
*/
attach(viewport: CdkVirtualScrollViewport) {
this._viewport = viewport;
this._updateTotalContentSize();
this._updateRenderedRange();
}
/** Detaches this scroll strategy from the currently attached viewport. */
detach() {
this._scrolledIndexChange.complete();
this._viewport = null;
}
/**
* Update the item size and buffer size.
* @param itemSize The size of the items in the virtually scrolling list.
* @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more
* @param maxBufferPx The amount of buffer (in pixels) to render when rendering more.
*/
updateItemAndBufferSize(itemSize: number, minBufferPx: number, maxBufferPx: number) {
if (maxBufferPx < minBufferPx && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw Error('CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx');
}
this._itemSize = itemSize;
this._minBufferPx = minBufferPx;
this._maxBufferPx = maxBufferPx;
this._updateTotalContentSize();
this._updateRenderedRange();
}
/** @docs-private Implemented as part of VirtualScrollStrategy. */
onContentScrolled() {
this._updateRenderedRange();
}
/** @docs-private Implemented as part of VirtualScrollStrategy. */
onDataLengthChanged() {
this._updateTotalContentSize();
this._updateRenderedRange();
}
/** @docs-private Implemented as part of VirtualScrollStrategy. */
onContentRendered() {
/* no-op */
}
/** @docs-private Implemented as part of VirtualScrollStrategy. */
onRenderedOffsetChanged() {
/* no-op */
}
/**
* Scroll to the offset for the given index.
* @param index The index of the element to scroll to.
* @param behavior The ScrollBehavior to use when scrolling.
*/
scrollToIndex(index: number, behavior: ScrollBehavior): void {
if (this._viewport) {
this._viewport.scrollToOffset(index * this._itemSize, behavior, 'viewport');
}
}
/** Update the viewport's total content size. */
private _updateTotalContentSize() {
if (!this._viewport) {
return;
}
this._viewport.setTotalContentSize(this._viewport.getDataLength() * this._itemSize);
}
/** Update the viewport's rendered range. */
private _updateRenderedRange() {
if (!this._viewport) {
return;
}
const renderedRange = this._viewport.getRenderedRange();
const newRange = {start: renderedRange.start, end: renderedRange.end};
const viewportSize = this._viewport.getViewportSize();
const dataLength = this._viewport.getDataLength();
let scrollOffset = this._viewport.measureScrollOffset();
// Prevent NaN as result when dividing by zero.
let firstVisibleIndex = this._itemSize > 0 ? scrollOffset / this._itemSize : 0;
// If user scrolls to the bottom of the list and data changes to a smaller list
if (newRange.end > dataLength) {
// We have to recalculate the first visible index based on new data length and viewport size.
const maxVisibleItems = Math.ceil(viewportSize / this._itemSize);
const newVisibleIndex = Math.max(
0,
Math.min(firstVisibleIndex, dataLength - maxVisibleItems),
);
// If first visible index changed we must update scroll offset to handle start/end buffers
// Current range must also be adjusted to cover the new position (bottom of new list).
if (firstVisibleIndex != newVisibleIndex) {
firstVisibleIndex = newVisibleIndex;
scrollOffset = newVisibleIndex * this._itemSize;
newRange.start = Math.floor(firstVisibleIndex);
}
newRange.end = Math.max(0, Math.min(dataLength, newRange.start + maxVisibleItems));
}
const startBuffer = scrollOffset - newRange.start * this._itemSize;
if (startBuffer < this._minBufferPx && newRange.start != 0) {
const expandStart = Math.ceil((this._maxBufferPx - startBuffer) / this._itemSize);
newRange.start = Math.max(0, newRange.start - expandStart);
newRange.end = Math.min(
dataLength,
Math.ceil(firstVisibleIndex + (viewportSize + this._minBufferPx) / this._itemSize),
);
} else {
const endBuffer = newRange.end * this._itemSize - (scrollOffset + viewportSize);
if (endBuffer < this._minBufferPx && newRange.end != dataLength) {
const expandEnd = Math.ceil((this._maxBufferPx - endBuffer) / this._itemSize);
if (expandEnd > 0) {
newRange.end = Math.min(dataLength, newRange.end + expandEnd);
newRange.start = Math.max(
0,
Math.floor(firstVisibleIndex - this._minBufferPx / this._itemSize),
);
}
}
}
this._viewport.setRenderedRange(newRange);
this._viewport.setRenderedContentOffset(Math.round(this._itemSize * newRange.start));
this._scrolledIndexChange.next(Math.floor(firstVisibleIndex));
}
}
/**
* Provider factory for `FixedSizeVirtualScrollStrategy` that simply extracts the already created
* `FixedSizeVirtualScrollStrategy` from the given directive.
* @param fixedSizeDir The instance of `CdkFixedSizeVirtualScroll` to extract the
* `FixedSizeVirtualScrollStrategy` from.
*/
export function _fixedSizeVirtualScrollStrategyFactory(fixedSizeDir: CdkFixedSizeVirtualScroll) {
return fixedSizeDir._scrollStrategy;
}
/** A virtual scroll strategy that supports fixed-size items. */
@Directive({
selector: 'cdk-virtual-scroll-viewport[itemSize]',
providers: [
{
provide: VIRTUAL_SCROLL_STRATEGY,
useFactory: _fixedSizeVirtualScrollStrategyFactory,
deps: [forwardRef(() => CdkFixedSizeVirtualScroll)],
},
],
})
export class CdkFixedSizeVirtualScroll implements OnChanges {
/** The size of the items in the list (in pixels). */
@Input()
get itemSize(): number {
return this._itemSize;
}
set itemSize(value: NumberInput) {
this._itemSize = coerceNumberProperty(value);
}
_itemSize = 20;
/**
* The minimum amount of buffer rendered beyond the viewport (in pixels).
* If the amount of buffer dips below this number, more items will be rendered. Defaults to 100px.
*/
@Input()
get minBufferPx(): number {
return this._minBufferPx;
}
set minBufferPx(value: NumberInput) {
this._minBufferPx = coerceNumberProperty(value);
}
_minBufferPx = 100;
/**
* The number of pixels worth of buffer to render for when rendering new items. Defaults to 200px.
*/
@Input()
get maxBufferPx(): number {
return this._maxBufferPx;
}
set maxBufferPx(value: NumberInput) {
this._maxBufferPx = coerceNumberProperty(value);
}
_maxBufferPx = 200;
/** The scroll strategy used by this directive. */
_scrollStrategy = new FixedSizeVirtualScrollStrategy(
this.itemSize,
this.minBufferPx,
this.maxBufferPx,
);
ngOnChanges() {
this._scrollStrategy.updateItemAndBufferSize(this.itemSize, this.minBufferPx, this.maxBufferPx);
}
}