ng-virtual-list 15.0.21 → 15.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/ng-package.json +7 -0
- package/package.json +29 -48
- package/src/lib/components/ng-virtual-list-item.component.html +9 -0
- package/src/lib/components/ng-virtual-list-item.component.scss +17 -0
- package/src/lib/components/ng-virtual-list-item.component.spec.ts +23 -0
- package/src/lib/components/ng-virtual-list-item.component.ts +111 -0
- package/src/lib/const/index.ts +67 -0
- package/{lib/enums/direction.d.ts → src/lib/enums/direction.ts} +1 -0
- package/{lib/enums/directions.d.ts → src/lib/enums/directions.ts} +4 -4
- package/{lib/enums/index.d.ts → src/lib/enums/index.ts} +4 -1
- package/{lib/models/collection.model.d.ts → src/lib/models/collection.model.ts} +2 -2
- package/{lib/models/index.d.ts → src/lib/models/index.ts} +8 -1
- package/{lib/models/item.model.d.ts → src/lib/models/item.model.ts} +2 -1
- package/{lib/models/render-collection.model.d.ts → src/lib/models/render-collection.model.ts} +2 -2
- package/{lib/models/render-item-config.model.d.ts → src/lib/models/render-item-config.model.ts} +2 -2
- package/{lib/models/render-item.model.d.ts → src/lib/models/render-item.model.ts} +2 -1
- package/{lib/models/scroll-event.model.d.ts → src/lib/models/scroll-event.model.ts} +1 -0
- package/{lib/models/sticky-map.model.d.ts → src/lib/models/sticky-map.model.ts} +1 -1
- package/src/lib/ng-virtual-list.component.html +5 -0
- package/src/lib/ng-virtual-list.component.scss +28 -0
- package/src/lib/ng-virtual-list.component.spec.ts +23 -0
- package/src/lib/ng-virtual-list.component.ts +671 -0
- package/src/lib/ng-virtual-list.module.ts +12 -0
- package/{lib/types/id.d.ts → src/lib/types/id.ts} +1 -1
- package/{lib/types/index.d.ts → src/lib/types/index.ts} +6 -1
- package/{lib/types/rect.d.ts → src/lib/types/rect.ts} +2 -1
- package/{lib/types/size.d.ts → src/lib/types/size.ts} +1 -1
- package/src/lib/utils/cacheMap.ts +224 -0
- package/src/lib/utils/debounce.ts +31 -0
- package/src/lib/utils/disposableComponent.ts +22 -0
- package/src/lib/utils/eventEmitter.ts +119 -0
- package/{lib/utils/index.d.ts → src/lib/utils/index.ts} +9 -1
- package/src/lib/utils/isDirection.ts +17 -0
- package/src/lib/utils/scrollEvent.ts +62 -0
- package/src/lib/utils/toggleClassName.ts +14 -0
- package/src/lib/utils/trackBox.ts +839 -0
- package/src/lib/utils/tracker.ts +126 -0
- package/{public-api.d.ts → src/public-api.ts} +4 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +15 -0
- package/esm2020/lib/components/ng-virtual-list-item.component.mjs +0 -87
- package/esm2020/lib/const/index.mjs +0 -35
- package/esm2020/lib/enums/direction.mjs +0 -2
- package/esm2020/lib/enums/directions.mjs +0 -18
- package/esm2020/lib/enums/index.mjs +0 -3
- package/esm2020/lib/models/collection.model.mjs +0 -3
- package/esm2020/lib/models/index.mjs +0 -2
- package/esm2020/lib/models/item.model.mjs +0 -3
- package/esm2020/lib/models/render-collection.model.mjs +0 -3
- package/esm2020/lib/models/render-item-config.model.mjs +0 -2
- package/esm2020/lib/models/render-item.model.mjs +0 -3
- package/esm2020/lib/models/scroll-direction.model.mjs +0 -2
- package/esm2020/lib/models/scroll-event.model.mjs +0 -2
- package/esm2020/lib/models/sticky-map.model.mjs +0 -2
- package/esm2020/lib/ng-virtual-list.component.mjs +0 -508
- package/esm2020/lib/ng-virtual-list.module.mjs +0 -20
- package/esm2020/lib/types/id.mjs +0 -2
- package/esm2020/lib/types/index.mjs +0 -2
- package/esm2020/lib/types/rect.mjs +0 -2
- package/esm2020/lib/types/size.mjs +0 -2
- package/esm2020/lib/utils/cacheMap.mjs +0 -167
- package/esm2020/lib/utils/debounce.mjs +0 -31
- package/esm2020/lib/utils/disposableComponent.mjs +0 -29
- package/esm2020/lib/utils/eventEmitter.mjs +0 -106
- package/esm2020/lib/utils/index.mjs +0 -8
- package/esm2020/lib/utils/isDirection.mjs +0 -15
- package/esm2020/lib/utils/scrollEvent.mjs +0 -42
- package/esm2020/lib/utils/toggleClassName.mjs +0 -15
- package/esm2020/lib/utils/trackBox.mjs +0 -623
- package/esm2020/lib/utils/tracker.mjs +0 -92
- package/esm2020/ng-virtual-list.mjs +0 -5
- package/esm2020/public-api.mjs +0 -8
- package/fesm2015/ng-virtual-list.mjs +0 -1767
- package/fesm2015/ng-virtual-list.mjs.map +0 -1
- package/fesm2020/ng-virtual-list.mjs +0 -1776
- package/fesm2020/ng-virtual-list.mjs.map +0 -1
- package/index.d.ts +0 -5
- package/lib/components/ng-virtual-list-item.component.d.ts +0 -30
- package/lib/const/index.d.ts +0 -33
- package/lib/ng-virtual-list.component.d.ts +0 -165
- package/lib/ng-virtual-list.module.d.ts +0 -9
- package/lib/utils/cacheMap.d.ts +0 -60
- package/lib/utils/debounce.d.ts +0 -16
- package/lib/utils/disposableComponent.d.ts +0 -15
- package/lib/utils/eventEmitter.d.ts +0 -40
- package/lib/utils/isDirection.d.ts +0 -8
- package/lib/utils/scrollEvent.d.ts +0 -39
- package/lib/utils/toggleClassName.d.ts +0 -7
- package/lib/utils/trackBox.d.ts +0 -176
- package/lib/utils/tracker.d.ts +0 -44
- /package/{lib/models/scroll-direction.model.d.ts → src/lib/models/scroll-direction.model.ts} +0 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentRef, ElementRef, EventEmitter, Input,
|
|
3
|
+
OnDestroy, OnInit, Output, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation,
|
|
4
|
+
} from '@angular/core';
|
|
5
|
+
import { BehaviorSubject, combineLatest, distinctUntilChanged, filter, map, Observable, of, switchMap, takeUntil, tap } from 'rxjs';
|
|
6
|
+
import { NgVirtualListItemComponent } from './components/ng-virtual-list-item.component';
|
|
7
|
+
import {
|
|
8
|
+
BEHAVIOR_AUTO, BEHAVIOR_INSTANT, CLASS_LIST_HORIZONTAL, CLASS_LIST_VERTICAL, DEFAULT_DIRECTION, DEFAULT_DYNAMIC_SIZE, DEFAULT_ENABLED_BUFFER_OPTIMIZATION, DEFAULT_ITEM_SIZE,
|
|
9
|
+
DEFAULT_ITEMS_OFFSET, DEFAULT_SNAP, HEIGHT_PROP_NAME, LEFT_PROP_NAME, MAX_SCROLL_TO_ITERATIONS, PX, SCROLL, SCROLL_END, TOP_PROP_NAME,
|
|
10
|
+
TRACK_BY_PROPERTY_NAME, WIDTH_PROP_NAME,
|
|
11
|
+
} from './const';
|
|
12
|
+
import { IScrollEvent, IVirtualListCollection, IVirtualListItem, IVirtualListStickyMap } from './models';
|
|
13
|
+
import { Id, ISize } from './types';
|
|
14
|
+
import { IRenderVirtualListCollection } from './models/render-collection.model';
|
|
15
|
+
import { Direction, Directions } from './enums';
|
|
16
|
+
import { ScrollEvent, TrackBox, isDirection, toggleClassName } from './utils';
|
|
17
|
+
import { IGetItemPositionOptions, IUpdateCollectionOptions, TRACK_BOX_CHANGE_EVENT_NAME } from './utils/trackBox';
|
|
18
|
+
import { DisposableComponent } from './utils/disposableComponent';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Virtual list component.
|
|
22
|
+
* Maximum performance for extremely large lists.
|
|
23
|
+
* It is based on algorithms for virtualization of screen objects.
|
|
24
|
+
* @link https://github.com/DjonnyX/ng-virtual-list/blob/15.x/projects/ng-virtual-list/src/lib/ng-virtual-list.component.ts
|
|
25
|
+
* @author Evgenii Grebennikov
|
|
26
|
+
* @email djonnyx@gmail.com
|
|
27
|
+
*/
|
|
28
|
+
@Component({
|
|
29
|
+
selector: 'ng-virtual-list',
|
|
30
|
+
templateUrl: './ng-virtual-list.component.html',
|
|
31
|
+
styleUrls: ['./ng-virtual-list.component.scss'],
|
|
32
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
33
|
+
encapsulation: ViewEncapsulation.ShadowDom,
|
|
34
|
+
})
|
|
35
|
+
export class NgVirtualListComponent extends DisposableComponent implements AfterViewInit, OnInit, OnDestroy {
|
|
36
|
+
private static __nextId: number = 0;
|
|
37
|
+
|
|
38
|
+
private _id: number = NgVirtualListComponent.__nextId;
|
|
39
|
+
/**
|
|
40
|
+
* Readonly. Returns the unique identifier of the component.
|
|
41
|
+
*/
|
|
42
|
+
get id() { return this._id; }
|
|
43
|
+
|
|
44
|
+
@ViewChild('renderersContainer', { read: ViewContainerRef })
|
|
45
|
+
protected _listContainerRef: ViewContainerRef | undefined;
|
|
46
|
+
|
|
47
|
+
@ViewChild('container', { read: ElementRef<HTMLDivElement> })
|
|
48
|
+
protected _container: ElementRef<HTMLDivElement> | undefined;
|
|
49
|
+
|
|
50
|
+
@ViewChild('list', { read: ElementRef<HTMLDivElement> })
|
|
51
|
+
protected _list: ElementRef<HTMLUListElement> | undefined;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fires when the list has been scrolled.
|
|
55
|
+
*/
|
|
56
|
+
@Output()
|
|
57
|
+
onScroll = new EventEmitter<IScrollEvent>();
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Fires when the list has completed scrolling.
|
|
61
|
+
*/
|
|
62
|
+
@Output()
|
|
63
|
+
onScrollEnd = new EventEmitter<IScrollEvent>();
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
private _$items = new BehaviorSubject<IVirtualListCollection | undefined>(undefined);
|
|
67
|
+
readonly $items = this._$items.asObservable();
|
|
68
|
+
|
|
69
|
+
private _itemsTransform = (v: IVirtualListCollection | undefined) => {
|
|
70
|
+
this._trackBox.resetCollection(v, this._$itemSize.getValue());
|
|
71
|
+
return v;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Collection of list items.
|
|
76
|
+
*/
|
|
77
|
+
@Input()
|
|
78
|
+
set items(v: IVirtualListCollection) {
|
|
79
|
+
if (this._$items.getValue() === v) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const transformedValue = this._itemsTransform(v);
|
|
84
|
+
|
|
85
|
+
this._$items.next(transformedValue);
|
|
86
|
+
|
|
87
|
+
this._cdr.markForCheck();
|
|
88
|
+
};
|
|
89
|
+
get items() { return this._$items.getValue() as IVirtualListCollection; }
|
|
90
|
+
|
|
91
|
+
private _$snap = new BehaviorSubject<boolean>(DEFAULT_SNAP);
|
|
92
|
+
readonly $snap = this._$snap.asObservable();
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Determines whether elements will snap. Default value is "true".
|
|
96
|
+
*/
|
|
97
|
+
@Input()
|
|
98
|
+
set snap(v: boolean) {
|
|
99
|
+
if (this._$snap.getValue() === v) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this._$snap.next(v);
|
|
104
|
+
|
|
105
|
+
this._cdr.markForCheck();
|
|
106
|
+
};
|
|
107
|
+
get snap() { return this._$snap.getValue(); }
|
|
108
|
+
|
|
109
|
+
private _$enabledBufferOptimization = new BehaviorSubject<boolean>(DEFAULT_ENABLED_BUFFER_OPTIMIZATION);
|
|
110
|
+
readonly $enabledBufferOptimization = this._$enabledBufferOptimization.asObservable();
|
|
111
|
+
/**
|
|
112
|
+
* Experimental!
|
|
113
|
+
* Enables buffer optimization.
|
|
114
|
+
* Can only be used if items in the collection are not added or updated. Otherwise, artifacts in the form of twitching of the scroll area are possible.
|
|
115
|
+
* Works only if the property dynamic = true
|
|
116
|
+
*/
|
|
117
|
+
@Input()
|
|
118
|
+
set enabledBufferOptimization(v: boolean) {
|
|
119
|
+
if (this._$enabledBufferOptimization.getValue() === v) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this._$enabledBufferOptimization.next(v);
|
|
124
|
+
|
|
125
|
+
this._cdr.markForCheck();
|
|
126
|
+
};
|
|
127
|
+
get enabledBufferOptimization() { return this._$enabledBufferOptimization.getValue(); }
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
private _$itemRenderer = new BehaviorSubject<TemplateRef<any> | undefined>(undefined);
|
|
131
|
+
readonly $itemRenderer = this._$itemRenderer.asObservable();
|
|
132
|
+
/**
|
|
133
|
+
* Rendering element template.
|
|
134
|
+
*/
|
|
135
|
+
@Input()
|
|
136
|
+
set itemRenderer(v: TemplateRef<any>) {
|
|
137
|
+
if (this._$itemRenderer.getValue() === v) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this._$itemRenderer.next(v);
|
|
142
|
+
|
|
143
|
+
this._cdr.markForCheck();
|
|
144
|
+
};
|
|
145
|
+
get itemRenderer() { return this._$itemRenderer.getValue() as TemplateRef<any>; }
|
|
146
|
+
|
|
147
|
+
private _$stickyMap = new BehaviorSubject<IVirtualListStickyMap>({});
|
|
148
|
+
readonly $stickyMap = this._$stickyMap.asObservable();
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Dictionary zIndex by id of the list element. If the value is not set or equal to 0,
|
|
152
|
+
* then a simple element is displayed, if the value is greater than 0, then the sticky position mode is enabled for the element.
|
|
153
|
+
*/
|
|
154
|
+
@Input()
|
|
155
|
+
set stickyMap(v: IVirtualListStickyMap) {
|
|
156
|
+
if (this._$stickyMap.getValue() === v) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this._$stickyMap.next(v);
|
|
161
|
+
|
|
162
|
+
this._cdr.markForCheck();
|
|
163
|
+
};
|
|
164
|
+
get stickyMap() { return this._$stickyMap.getValue(); }
|
|
165
|
+
|
|
166
|
+
private _itemSizeOptions = (v: number | undefined) => {
|
|
167
|
+
if (v === undefined) {
|
|
168
|
+
return DEFAULT_ITEM_SIZE;
|
|
169
|
+
}
|
|
170
|
+
const val = Number(v);
|
|
171
|
+
return Number.isNaN(val) || val <= 0 ? DEFAULT_ITEM_SIZE : val;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
private _$itemSize = new BehaviorSubject<number>(DEFAULT_ITEM_SIZE);
|
|
175
|
+
readonly $itemSize = this._$itemSize.asObservable();
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* If direction = 'vertical', then the height of a typical element. If direction = 'horizontal', then the width of a typical element.
|
|
179
|
+
* Ignored if the dynamicSize property is true.
|
|
180
|
+
*/
|
|
181
|
+
@Input()
|
|
182
|
+
set itemSize(v: number) {
|
|
183
|
+
if (this._$itemSize.getValue() === v) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this._$itemSize.next(this._itemSizeOptions(v));
|
|
188
|
+
|
|
189
|
+
this._cdr.markForCheck();
|
|
190
|
+
};
|
|
191
|
+
get itemSize() { return this._$itemSize.getValue(); }
|
|
192
|
+
|
|
193
|
+
private _$dynamicSize = new BehaviorSubject<boolean>(DEFAULT_DYNAMIC_SIZE);
|
|
194
|
+
readonly $dynamicSize = this._$dynamicSize.asObservable();
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* If true then the items in the list can have different sizes and the itemSize property is ignored.
|
|
198
|
+
* If false then the items in the list have a fixed size specified by the itemSize property. The default value is false.
|
|
199
|
+
*/
|
|
200
|
+
@Input()
|
|
201
|
+
set dynamicSize(v: boolean) {
|
|
202
|
+
if (this._$dynamicSize.getValue() === v) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this._$dynamicSize.next(v);
|
|
207
|
+
|
|
208
|
+
this._cdr.markForCheck();
|
|
209
|
+
};
|
|
210
|
+
get dynamicSize() { return this._$dynamicSize.getValue(); }
|
|
211
|
+
|
|
212
|
+
private _$direction = new BehaviorSubject<Direction>(DEFAULT_DIRECTION);
|
|
213
|
+
readonly $direction = this._$direction.asObservable();
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Determines the direction in which elements are placed. Default value is "vertical".
|
|
217
|
+
*/
|
|
218
|
+
@Input()
|
|
219
|
+
set direction(v: Direction) {
|
|
220
|
+
if (this._$direction.getValue() === v) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this._$direction.next(v);
|
|
225
|
+
|
|
226
|
+
this._cdr.markForCheck();
|
|
227
|
+
};
|
|
228
|
+
get direction() { return this._$direction.getValue(); }
|
|
229
|
+
|
|
230
|
+
private _$itemsOffset = new BehaviorSubject<number>(DEFAULT_ITEMS_OFFSET);
|
|
231
|
+
readonly $itemsOffset = this._$itemsOffset.asObservable();
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Number of elements outside the scope of visibility. Default value is 2.
|
|
235
|
+
*/
|
|
236
|
+
@Input()
|
|
237
|
+
set itemsOffset(v: number) {
|
|
238
|
+
if (this._$itemsOffset.getValue() === v) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this._$itemsOffset.next(v);
|
|
243
|
+
};
|
|
244
|
+
get itemsOffset() { return this._$itemsOffset.getValue(); }
|
|
245
|
+
|
|
246
|
+
private _$trackBy = new BehaviorSubject<string>(TRACK_BY_PROPERTY_NAME);
|
|
247
|
+
readonly $trackBy = this._$trackBy.asObservable();
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The name of the property by which tracking is performed
|
|
251
|
+
*/
|
|
252
|
+
@Input()
|
|
253
|
+
set trackBy(v: string) {
|
|
254
|
+
if (this._$trackBy.getValue() === v) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this._$trackBy.next(v);
|
|
259
|
+
};
|
|
260
|
+
get trackBy() { return this._$trackBy.getValue(); }
|
|
261
|
+
|
|
262
|
+
private _isVertical = this.getIsVertical();
|
|
263
|
+
|
|
264
|
+
protected _displayComponents: Array<ComponentRef<NgVirtualListItemComponent>> = [];
|
|
265
|
+
|
|
266
|
+
protected _$bounds = new BehaviorSubject<DOMRect | null>(null);
|
|
267
|
+
|
|
268
|
+
protected _$scrollSize = new BehaviorSubject<number>(0);
|
|
269
|
+
|
|
270
|
+
private _resizeObserver: ResizeObserver | null = null;
|
|
271
|
+
|
|
272
|
+
private _onResizeHandler = () => {
|
|
273
|
+
this._$bounds.next(this._container?.nativeElement?.getBoundingClientRect() ?? null);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private _onScrollHandler = (e?: Event) => {
|
|
277
|
+
this.clearScrollToRepeatExecutionTimeout();
|
|
278
|
+
|
|
279
|
+
const container = this._container?.nativeElement;
|
|
280
|
+
if (container) {
|
|
281
|
+
const scrollSize = (this._isVertical ? container.scrollTop : container.scrollLeft);
|
|
282
|
+
|
|
283
|
+
this._$scrollSize.next(scrollSize);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private _$initialized = new BehaviorSubject<boolean>(false);
|
|
288
|
+
|
|
289
|
+
readonly $initialized: Observable<boolean>;
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Dictionary of element sizes by their id
|
|
293
|
+
*/
|
|
294
|
+
private _trackBox = new TrackBox(this.trackBy);
|
|
295
|
+
|
|
296
|
+
private _onTrackBoxChangeHandler = (v: number) => {
|
|
297
|
+
this._$cacheVersion.next(v);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private _$cacheVersion = new BehaviorSubject<number>(-1);
|
|
301
|
+
get $cacheVersion() { return this._$cacheVersion.asObservable(); }
|
|
302
|
+
|
|
303
|
+
constructor(
|
|
304
|
+
private _cdr: ChangeDetectorRef,
|
|
305
|
+
private _elementRef: ElementRef<HTMLDivElement>
|
|
306
|
+
) {
|
|
307
|
+
super();
|
|
308
|
+
NgVirtualListComponent.__nextId = NgVirtualListComponent.__nextId + 1 === Number.MAX_SAFE_INTEGER
|
|
309
|
+
? 0 : NgVirtualListComponent.__nextId + 1;
|
|
310
|
+
this._id = NgVirtualListComponent.__nextId;
|
|
311
|
+
|
|
312
|
+
this._$initialized = new BehaviorSubject<boolean>(false);
|
|
313
|
+
this.$initialized = this._$initialized.asObservable();
|
|
314
|
+
|
|
315
|
+
this._trackBox.displayComponents = this._displayComponents;
|
|
316
|
+
|
|
317
|
+
const $trackBy = this.$trackBy;
|
|
318
|
+
|
|
319
|
+
$trackBy.pipe(
|
|
320
|
+
takeUntil(this._$unsubscribe),
|
|
321
|
+
tap(v => {
|
|
322
|
+
this._trackBox.trackingPropertyName = v;
|
|
323
|
+
}),
|
|
324
|
+
).subscribe();
|
|
325
|
+
|
|
326
|
+
const $bounds = this._$bounds.asObservable().pipe(
|
|
327
|
+
filter(b => !!b),
|
|
328
|
+
), $items = this.$items.pipe(
|
|
329
|
+
map(i => !i ? [] : i),
|
|
330
|
+
), $scrollSize = this._$scrollSize.asObservable(),
|
|
331
|
+
$itemSize = this.$itemSize.pipe(
|
|
332
|
+
map(v => v <= 0 ? DEFAULT_ITEM_SIZE : v),
|
|
333
|
+
),
|
|
334
|
+
$itemsOffset = this.$itemsOffset.pipe(
|
|
335
|
+
map(v => v < 0 ? DEFAULT_ITEMS_OFFSET : v),
|
|
336
|
+
),
|
|
337
|
+
$stickyMap = this.$stickyMap.pipe(
|
|
338
|
+
map(v => !v ? {} : v),
|
|
339
|
+
),
|
|
340
|
+
$snap = this.$snap,
|
|
341
|
+
$isVertical = this.$direction.pipe(
|
|
342
|
+
map(v => this.getIsVertical(v || DEFAULT_DIRECTION)),
|
|
343
|
+
),
|
|
344
|
+
$dynamicSize = this.$dynamicSize,
|
|
345
|
+
$enabledBufferOptimization = this.$enabledBufferOptimization,
|
|
346
|
+
$cacheVersion = this.$cacheVersion;
|
|
347
|
+
|
|
348
|
+
$isVertical.pipe(
|
|
349
|
+
takeUntil(this._$unsubscribe),
|
|
350
|
+
tap(v => {
|
|
351
|
+
this._isVertical = v;
|
|
352
|
+
const el: HTMLElement = this._elementRef.nativeElement;
|
|
353
|
+
toggleClassName(el, v ? CLASS_LIST_VERTICAL : CLASS_LIST_HORIZONTAL, true);
|
|
354
|
+
}),
|
|
355
|
+
).subscribe();
|
|
356
|
+
|
|
357
|
+
$dynamicSize.pipe(
|
|
358
|
+
takeUntil(this._$unsubscribe),
|
|
359
|
+
tap(dynamicSize => {
|
|
360
|
+
this.listenCacheChangesIfNeed(dynamicSize);
|
|
361
|
+
})
|
|
362
|
+
).subscribe();
|
|
363
|
+
|
|
364
|
+
combineLatest([this.$initialized, $bounds, $items, $stickyMap, $scrollSize, $itemSize,
|
|
365
|
+
$itemsOffset, $snap, $isVertical, $dynamicSize, $enabledBufferOptimization, $cacheVersion,
|
|
366
|
+
]).pipe(
|
|
367
|
+
takeUntil(this._$unsubscribe),
|
|
368
|
+
distinctUntilChanged(),
|
|
369
|
+
filter(([initialized]) => !!initialized),
|
|
370
|
+
switchMap(([,
|
|
371
|
+
bounds, items, stickyMap, _scrollSize, itemSize,
|
|
372
|
+
itemsOffset, snap, isVertical, dynamicSize, enabledBufferOptimization, cacheVersion,
|
|
373
|
+
]) => {
|
|
374
|
+
const { width, height } = bounds as DOMRect, delta = this._trackBox.delta,
|
|
375
|
+
scrollSize = (this._isVertical ? this._container?.nativeElement.scrollTop ?? 0 : this._container?.nativeElement.scrollLeft) ?? 0,
|
|
376
|
+
actualScrollSize = scrollSize + delta;
|
|
377
|
+
const opts: IUpdateCollectionOptions<IVirtualListItem, IVirtualListCollection> = {
|
|
378
|
+
bounds: { width, height }, dynamicSize, isVertical, itemSize,
|
|
379
|
+
itemsOffset, scrollSize: actualScrollSize, snap, enabledBufferOptimization,
|
|
380
|
+
};
|
|
381
|
+
const { displayItems, totalSize } = this._trackBox.updateCollection(items, stickyMap, opts);
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
this.resetBoundsSize(isVertical, totalSize);
|
|
385
|
+
|
|
386
|
+
this.createDisplayComponentsIfNeed(displayItems);
|
|
387
|
+
|
|
388
|
+
this.tracking();
|
|
389
|
+
|
|
390
|
+
const container = this._container;
|
|
391
|
+
|
|
392
|
+
if (container) {
|
|
393
|
+
this._trackBox.clearDelta();
|
|
394
|
+
|
|
395
|
+
if (scrollSize !== actualScrollSize) {
|
|
396
|
+
const params: ScrollToOptions = {
|
|
397
|
+
[this._isVertical ? TOP_PROP_NAME : LEFT_PROP_NAME]: actualScrollSize,
|
|
398
|
+
behavior: BEHAVIOR_INSTANT as ScrollBehavior
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
container.nativeElement.scrollTo(params);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return of(displayItems);
|
|
406
|
+
}),
|
|
407
|
+
).subscribe();
|
|
408
|
+
|
|
409
|
+
combineLatest([this.$initialized, this.$itemRenderer]).pipe(
|
|
410
|
+
takeUntil(this._$unsubscribe),
|
|
411
|
+
distinctUntilChanged(),
|
|
412
|
+
filter(([initialized]) => !!initialized),
|
|
413
|
+
tap(([, itemRenderer]) => {
|
|
414
|
+
this.resetRenderers(itemRenderer);
|
|
415
|
+
})
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** @internal */
|
|
420
|
+
ngOnInit() {
|
|
421
|
+
this._$initialized.next(true);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private listenCacheChangesIfNeed(value: boolean) {
|
|
425
|
+
if (value) {
|
|
426
|
+
if (!this._trackBox.hasEventListener(TRACK_BOX_CHANGE_EVENT_NAME, this._onTrackBoxChangeHandler)) {
|
|
427
|
+
this._trackBox.addEventListener(TRACK_BOX_CHANGE_EVENT_NAME, this._onTrackBoxChangeHandler);
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
if (this._trackBox.hasEventListener(TRACK_BOX_CHANGE_EVENT_NAME, this._onTrackBoxChangeHandler)) {
|
|
431
|
+
this._trackBox.removeEventListener(TRACK_BOX_CHANGE_EVENT_NAME, this._onTrackBoxChangeHandler);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private getIsVertical(d?: Direction) {
|
|
437
|
+
const dir = d || this.direction;
|
|
438
|
+
return isDirection(dir, Directions.VERTICAL);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private _componentsResizeObserver = new ResizeObserver(() => {
|
|
442
|
+
this._trackBox.changes();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
private createDisplayComponentsIfNeed(displayItems: IRenderVirtualListCollection | null) {
|
|
446
|
+
if (!displayItems || !this._listContainerRef) {
|
|
447
|
+
this._trackBox.setDisplayObjectIndexMapById({});
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this._trackBox.items = displayItems;
|
|
452
|
+
|
|
453
|
+
const _listContainerRef = this._listContainerRef;
|
|
454
|
+
|
|
455
|
+
const maxLength = displayItems.length, components = this._displayComponents;
|
|
456
|
+
|
|
457
|
+
while (components.length < maxLength) {
|
|
458
|
+
if (_listContainerRef) {
|
|
459
|
+
const comp = _listContainerRef.createComponent(NgVirtualListItemComponent);
|
|
460
|
+
components.push(comp);
|
|
461
|
+
|
|
462
|
+
this._componentsResizeObserver.observe(comp.instance.element);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
this.resetRenderers();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private resetRenderers(itemRenderer?: TemplateRef<HTMLElement>) {
|
|
470
|
+
const doMap: { [id: number]: number } = {};
|
|
471
|
+
for (let i = 0, l = this._displayComponents.length; i < l; i++) {
|
|
472
|
+
const item = this._displayComponents[i];
|
|
473
|
+
item.instance.renderer = itemRenderer || this.itemRenderer;
|
|
474
|
+
doMap[item.instance.id] = i;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
this._trackBox.setDisplayObjectIndexMapById(doMap);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Tracking by id
|
|
482
|
+
*/
|
|
483
|
+
protected tracking() {
|
|
484
|
+
this._trackBox.track();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private resetBoundsSize(isVertical: boolean, totalSize: number) {
|
|
488
|
+
const l = this._list;
|
|
489
|
+
if (l) {
|
|
490
|
+
l.nativeElement.style[isVertical ? HEIGHT_PROP_NAME : WIDTH_PROP_NAME] = `${totalSize}${PX}`;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Returns the bounds of an element with a given id
|
|
496
|
+
*/
|
|
497
|
+
getItemBounds(id: Id): ISize | undefined {
|
|
498
|
+
return this._trackBox.getItemBounds(id);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The method scrolls the list to the element with the given id and returns the value of the scrolled area.
|
|
503
|
+
* Behavior accepts the values "auto", "instant" and "smooth".
|
|
504
|
+
*/
|
|
505
|
+
scrollTo(id: Id, behavior: ScrollBehavior = BEHAVIOR_AUTO as ScrollBehavior) {
|
|
506
|
+
this.scrollToExecutor(id, behavior);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private _scrollToRepeatExecutionTimeout: any;
|
|
510
|
+
|
|
511
|
+
private clearScrollToRepeatExecutionTimeout() {
|
|
512
|
+
clearTimeout(this._scrollToRepeatExecutionTimeout);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
protected scrollToExecutor(id: Id, behavior: ScrollBehavior, iteration: number = 0, isLastIteration = false) {
|
|
516
|
+
const items = this.items;
|
|
517
|
+
if (!items || !items.length) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const dynamicSize = this.dynamicSize, container = this._container, itemSize = this.itemSize;
|
|
522
|
+
if (container) {
|
|
523
|
+
this.clearScrollToRepeatExecutionTimeout();
|
|
524
|
+
|
|
525
|
+
if (dynamicSize) {
|
|
526
|
+
if (container) {
|
|
527
|
+
container.nativeElement.removeEventListener(SCROLL, this._onScrollHandler);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const { width, height } = this._$bounds.getValue() || { width: 0, height: 0 },
|
|
531
|
+
stickyMap = this.stickyMap, items = this.items, isVertical = this._isVertical, delta = this._trackBox.delta,
|
|
532
|
+
opts: IGetItemPositionOptions<IVirtualListItem, IVirtualListCollection> = {
|
|
533
|
+
bounds: { width, height }, collection: items, dynamicSize, isVertical: this._isVertical, itemSize,
|
|
534
|
+
itemsOffset: this.itemsOffset, scrollSize: (isVertical ? container.nativeElement.scrollTop : container.nativeElement.scrollLeft) + delta,
|
|
535
|
+
snap: this.snap, fromItemId: id, enabledBufferOptimization: this.enabledBufferOptimization,
|
|
536
|
+
},
|
|
537
|
+
scrollSize = this._trackBox.getItemPosition(id, stickyMap, opts),
|
|
538
|
+
params: ScrollToOptions = { [isVertical ? TOP_PROP_NAME : LEFT_PROP_NAME]: scrollSize, behavior };
|
|
539
|
+
this._trackBox.clearDelta();
|
|
540
|
+
|
|
541
|
+
if (container) {
|
|
542
|
+
const { displayItems, totalSize } = this._trackBox.updateCollection(items, stickyMap, {
|
|
543
|
+
...opts, scrollSize, fromItemId: isLastIteration ? undefined : id,
|
|
544
|
+
}), delta = this._trackBox.delta;
|
|
545
|
+
|
|
546
|
+
this._trackBox.clearDelta();
|
|
547
|
+
|
|
548
|
+
let actualScrollSize = scrollSize + delta;
|
|
549
|
+
|
|
550
|
+
this.resetBoundsSize(isVertical, totalSize);
|
|
551
|
+
|
|
552
|
+
this.createDisplayComponentsIfNeed(displayItems);
|
|
553
|
+
|
|
554
|
+
this.tracking();
|
|
555
|
+
|
|
556
|
+
const _scrollSize = this._trackBox.getItemPosition(id, stickyMap, { ...opts, scrollSize: actualScrollSize, fromItemId: id });
|
|
557
|
+
|
|
558
|
+
const notChanged = actualScrollSize === _scrollSize
|
|
559
|
+
|
|
560
|
+
if (!notChanged || iteration < MAX_SCROLL_TO_ITERATIONS) {
|
|
561
|
+
this.clearScrollToRepeatExecutionTimeout();
|
|
562
|
+
this._scrollToRepeatExecutionTimeout = setTimeout(() => {
|
|
563
|
+
this.scrollToExecutor(id, BEHAVIOR_INSTANT as ScrollBehavior, iteration + 1, notChanged);
|
|
564
|
+
});
|
|
565
|
+
} else {
|
|
566
|
+
this._$scrollSize.next(actualScrollSize);
|
|
567
|
+
|
|
568
|
+
container.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
container.nativeElement.scrollTo(params);
|
|
573
|
+
|
|
574
|
+
this._$scrollSize.next(scrollSize);
|
|
575
|
+
} else {
|
|
576
|
+
const index = items.findIndex(item => item.id === id), scrollSize = index * this.itemSize;
|
|
577
|
+
const params: ScrollToOptions = { [this._isVertical ? TOP_PROP_NAME : LEFT_PROP_NAME]: scrollSize, behavior };
|
|
578
|
+
container.nativeElement.scrollTo(params);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Scrolls the scroll area to the desired element with the specified ID.
|
|
585
|
+
*/
|
|
586
|
+
scrollToEnd(behavior: ScrollBehavior = BEHAVIOR_INSTANT as ScrollBehavior) {
|
|
587
|
+
const items = this.items, latItem = items[items.length > 0 ? items.length - 1 : 0];
|
|
588
|
+
this.scrollTo(latItem.id, behavior);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private _onContainerScrollHandler = (e: Event) => {
|
|
592
|
+
const containerEl = this._container;
|
|
593
|
+
if (containerEl) {
|
|
594
|
+
const scrollSize = (this._isVertical ? containerEl.nativeElement.scrollTop : containerEl.nativeElement.scrollLeft);
|
|
595
|
+
this._trackBox.deltaDirection = this._$scrollSize.getValue() > scrollSize ? -1 : this._$scrollSize.getValue() < scrollSize ? 1 : 0;
|
|
596
|
+
|
|
597
|
+
const event = new ScrollEvent({
|
|
598
|
+
direction: this._trackBox.scrollDirection, container: containerEl.nativeElement,
|
|
599
|
+
list: this._list!.nativeElement, delta: this._trackBox.delta,
|
|
600
|
+
scrollDelta: this._trackBox.scrollDelta, isVertical: this._isVertical,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
this.onScroll.emit(event);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private _onContainerScrollEndHandler = (e: Event) => {
|
|
608
|
+
const containerEl = this._container;
|
|
609
|
+
if (containerEl) {
|
|
610
|
+
const scrollSize = (this._isVertical ? containerEl.nativeElement.scrollTop : containerEl.nativeElement.scrollLeft);
|
|
611
|
+
this._trackBox.deltaDirection = this._$scrollSize.getValue() > scrollSize ? -1 : 0;
|
|
612
|
+
|
|
613
|
+
const event = new ScrollEvent({
|
|
614
|
+
direction: this._trackBox.scrollDirection, container: containerEl.nativeElement,
|
|
615
|
+
list: this._list!.nativeElement, delta: this._trackBox.delta,
|
|
616
|
+
scrollDelta: this._trackBox.scrollDelta, isVertical: this._isVertical,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
this.onScrollEnd.emit(event);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** @internal */
|
|
624
|
+
ngAfterViewInit(): void {
|
|
625
|
+
const containerEl = this._container;
|
|
626
|
+
if (containerEl) {
|
|
627
|
+
// for direction calculation
|
|
628
|
+
containerEl.nativeElement.addEventListener(SCROLL, this._onContainerScrollHandler);
|
|
629
|
+
containerEl.nativeElement.addEventListener(SCROLL_END, this._onContainerScrollEndHandler);
|
|
630
|
+
|
|
631
|
+
containerEl.nativeElement.addEventListener(SCROLL, this._onScrollHandler);
|
|
632
|
+
|
|
633
|
+
this._resizeObserver = new ResizeObserver(this._onResizeHandler);
|
|
634
|
+
this._resizeObserver.observe(containerEl.nativeElement);
|
|
635
|
+
|
|
636
|
+
this._onResizeHandler();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** @internal */
|
|
641
|
+
override ngOnDestroy(): void {
|
|
642
|
+
super.ngOnDestroy();
|
|
643
|
+
this.clearScrollToRepeatExecutionTimeout();
|
|
644
|
+
|
|
645
|
+
if (this._trackBox) {
|
|
646
|
+
this._trackBox.dispose();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const containerEl = this._container;
|
|
650
|
+
if (containerEl) {
|
|
651
|
+
containerEl.nativeElement.removeEventListener(SCROLL, this._onScrollHandler);
|
|
652
|
+
containerEl.nativeElement.removeEventListener(SCROLL, this._onContainerScrollHandler);
|
|
653
|
+
containerEl.nativeElement.removeEventListener(SCROLL_END, this._onContainerScrollEndHandler);
|
|
654
|
+
|
|
655
|
+
if (this._componentsResizeObserver) {
|
|
656
|
+
this._componentsResizeObserver.disconnect();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (this._resizeObserver) {
|
|
660
|
+
this._resizeObserver.disconnect();
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (this._displayComponents) {
|
|
665
|
+
while (this._displayComponents.length > 0) {
|
|
666
|
+
const comp = this._displayComponents.pop();
|
|
667
|
+
comp?.destroy();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { NgVirtualListComponent } from './ng-virtual-list.component';
|
|
4
|
+
import { NgVirtualListItemComponent } from './components/ng-virtual-list-item.component';
|
|
5
|
+
|
|
6
|
+
@NgModule({
|
|
7
|
+
declarations: [NgVirtualListComponent, NgVirtualListItemComponent],
|
|
8
|
+
exports: [NgVirtualListComponent],
|
|
9
|
+
imports: [CommonModule],
|
|
10
|
+
schemas: [NO_ERRORS_SCHEMA],
|
|
11
|
+
})
|
|
12
|
+
export class NgVirtualListModule { }
|