ng-virtual-list 20.7.2 → 20.7.4
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/README.md +109 -37
- package/fesm2022/ng-virtual-list.mjs +94 -13
- package/fesm2022/ng-virtual-list.mjs.map +1 -1
- package/index.d.ts +12 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,29 +1,60 @@
|
|
|
1
1
|
# NgVirtualList
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
🚀 High-performance virtual scrolling for Angular apps. Render 100,000+ items in Angular without breaking a sweat. Smooth, customizable, and developer-friendly.
|
|
4
|
+
|
|
5
|
+
Flexible, and actively maintained Angular library that excels with high-performance, feature-rich virtualized lists—including grouping, sticky headers, snapping, animations, single and multiple selection of elements and both scroll directions. Whether you're rendering millions of items or building interactive list components, it delivers scalability and customization. Angular (14–20) compatibility.
|
|
5
6
|
|
|
6
7
|
<img width="1033" height="171" alt="logo" src="https://github.com/user-attachments/assets/b559cfde-405a-4361-b71b-6715478d997d" />
|
|
7
8
|
|
|
8
|
-
Angular version 20.X.X
|
|
9
|
+
<b>Angular version 20.X.X</b>.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
9
14
|
|
|
10
15
|
[Live Demo](https://ng-virtual-list-chat-demo.eugene-grebennikov.pro/)
|
|
16
|
+
[(Code)](https://github.com/DjonnyX/ng-virtual-list-demo)
|
|
11
17
|
|
|
12
18
|
[Live Examples](https://ng-virtual-list.eugene-grebennikov.pro/)
|
|
19
|
+
[(Code)](https://github.com/DjonnyX/ng-virtual-list-demo/tree/main/src/app)
|
|
20
|
+
|
|
21
|
+
<br/>
|
|
22
|
+
|
|
23
|
+
## ✨ Why use ng-virtual-list?
|
|
24
|
+
|
|
25
|
+
⚡ Blazing fast — only renders what’s visible (plus a smart buffer).<br/>
|
|
26
|
+
📱 Works everywhere — smooth on desktop & mobile.<br/>
|
|
27
|
+
🔀 Flexible layouts — vertical, horizontal, grouped lists, sticky headers.<br/>
|
|
28
|
+
📏 Dynamic sizes — handles items of varying height/width.<br/>
|
|
29
|
+
🎯 Precise control — scroll to an ID, or snap to positions.<br/>
|
|
30
|
+
🔌 Angular-friendly — simple inputs/outputs, trackBy support.<br/>
|
|
13
31
|
|
|
14
32
|
<br/>
|
|
15
33
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
## ⚙️ Key Features
|
|
35
|
+
|
|
36
|
+
Virtualization modes
|
|
37
|
+
- Fixed size (fastest)
|
|
38
|
+
- Dynamic size (auto-measured)
|
|
39
|
+
- Scrolling control
|
|
40
|
+
- Scroll to item ID
|
|
41
|
+
- Smooth or instant scroll
|
|
42
|
+
- Custom snapping behavior
|
|
43
|
+
- Advanced layouts
|
|
44
|
+
- Grouped lists with sticky headers
|
|
45
|
+
- Horizontal or vertical scrolling
|
|
46
|
+
- Advanced layouts
|
|
47
|
+
- Grouped lists with sticky headers
|
|
48
|
+
- Horizontal or vertical scrolling
|
|
49
|
+
- Selecting elements
|
|
50
|
+
- Single selection
|
|
51
|
+
- Multiple selection
|
|
52
|
+
- Performance tuning
|
|
53
|
+
- bufferSize and maxBufferSize for fine-grained control
|
|
23
54
|
|
|
24
55
|
<br/>
|
|
25
56
|
|
|
26
|
-
## When to Use It: Ideal Use Cases
|
|
57
|
+
## 📱 When to Use It: Ideal Use Cases
|
|
27
58
|
|
|
28
59
|
Drawing on general virtual-scroll insights and ng-virtual-list features:
|
|
29
60
|
|
|
@@ -44,28 +75,51 @@ As each item may contain images, nested components, or interactions, virtual ren
|
|
|
44
75
|
|
|
45
76
|
Single and multiple selection of elements
|
|
46
77
|
|
|
78
|
+
Navigating with the keyboard
|
|
79
|
+
|
|
80
|
+
Support for element animation
|
|
81
|
+
|
|
47
82
|
<br/>
|
|
48
83
|
|
|
49
|
-
## Installation
|
|
84
|
+
## 📦 Installation
|
|
50
85
|
|
|
51
86
|
```bash
|
|
52
87
|
npm i ng-virtual-list
|
|
53
88
|
```
|
|
54
89
|
|
|
55
|
-
|
|
90
|
+
<br/>
|
|
91
|
+
|
|
92
|
+
## 🚀 Quick Start
|
|
93
|
+
```html
|
|
94
|
+
<ng-virtual-list [items]="items" [bufferSize]="5" [itemRenderer]="itemRenderer" [itemSize]="64"></ng-virtual-list>
|
|
95
|
+
|
|
96
|
+
<ng-template #itemRenderer let-data="data">
|
|
97
|
+
@if (data) {
|
|
98
|
+
<span>{{data.name}}</span>
|
|
99
|
+
}
|
|
100
|
+
</ng-template>
|
|
101
|
+
```
|
|
102
|
+
```ts
|
|
103
|
+
items = Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `Item #${i}` }));
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
<br/>
|
|
56
107
|
|
|
57
|
-
|
|
108
|
+
## 📱 Examples
|
|
109
|
+
|
|
110
|
+
### Horizontal virtual list (Single selection)
|
|
58
111
|
|
|
59
112
|

|
|
60
113
|
|
|
61
114
|
Template:
|
|
62
115
|
```html
|
|
63
116
|
<ng-virtual-list class="list" direction="horizontal" [items]="horizontalItems" [bufferSize]="50"
|
|
64
|
-
[itemRenderer]="horizontalItemRenderer" [itemSize]="64"
|
|
117
|
+
[itemRenderer]="horizontalItemRenderer" [itemSize]="64" [methodForSelecting]="'select'"
|
|
118
|
+
[selectedIds]="2" (onSelect)="onSelect($event)" (onItemClick)="onItemClick($event)"></ng-virtual-list>
|
|
65
119
|
|
|
66
|
-
<ng-template #horizontalItemRenderer let-data="data">
|
|
120
|
+
<ng-template #horizontalItemRenderer let-data="data" let-config="config">
|
|
67
121
|
@if (data) {
|
|
68
|
-
<div
|
|
122
|
+
<div [ngClass]="{'list__h-container': true, 'selected': config.selected}">
|
|
69
123
|
<span>{{data.name}}</span>
|
|
70
124
|
</div>
|
|
71
125
|
}
|
|
@@ -80,10 +134,7 @@ interface ICollectionItem {
|
|
|
80
134
|
name: string;
|
|
81
135
|
}
|
|
82
136
|
|
|
83
|
-
const HORIZONTAL_ITEMS: IVirtualListCollection<ICollectionItem> =
|
|
84
|
-
for (let i = 0, l = 1000000; i < l; i++) {
|
|
85
|
-
HORIZONTAL_ITEMS.push({ id: i + 1, name: `${i}` });
|
|
86
|
-
}
|
|
137
|
+
const HORIZONTAL_ITEMS: IVirtualListCollection<ICollectionItem> = Array.from({ length: 100000 }, (_, i) => ({ id: i, name: `${i}` }));
|
|
87
138
|
|
|
88
139
|
@Component({
|
|
89
140
|
selector: 'app-root',
|
|
@@ -99,19 +150,24 @@ export class AppComponent {
|
|
|
99
150
|
console.info(`Click: (ID: ${item.id}) Item ${item.data.name}`);
|
|
100
151
|
}
|
|
101
152
|
}
|
|
153
|
+
|
|
154
|
+
onSelect(data: Array<Id> | Id | undefined) {
|
|
155
|
+
console.info(`Select: ${JSON.stringify(data)}`);
|
|
156
|
+
}
|
|
102
157
|
}
|
|
103
158
|
```
|
|
104
159
|
|
|
105
|
-
### Horizontal grouped virtual list
|
|
160
|
+
### Horizontal grouped virtual list (Multiple selection)
|
|
106
161
|
|
|
107
162
|

|
|
108
163
|
|
|
109
164
|
Template:
|
|
110
165
|
```html
|
|
111
166
|
<ng-virtual-list class="list" direction="horizontal" [items]="horizontalGroupItems" [itemRenderer]="horizontalGroupItemRenderer"
|
|
112
|
-
[bufferSize]="50" [itemConfigMap]="horizontalGroupItemConfigMap" [itemSize]="54" [snap]="true"
|
|
167
|
+
[bufferSize]="50" [itemConfigMap]="horizontalGroupItemConfigMap" [itemSize]="54" [snap]="true" [methodForSelecting]="'multi-select'"
|
|
168
|
+
[selectedIds]="[3,2]" (onSelect)="onSelect($event)" (onItemClick)="onItemClick($event)"></ng-virtual-list>
|
|
113
169
|
|
|
114
|
-
<ng-template #horizontalGroupItemRenderer let-data="data">
|
|
170
|
+
<ng-template #horizontalGroupItemRenderer let-data="data" let-config="config">
|
|
115
171
|
@if (data) {
|
|
116
172
|
@switch (data.type) {
|
|
117
173
|
@case ("group-header") {
|
|
@@ -120,7 +176,7 @@ Template:
|
|
|
120
176
|
</div>
|
|
121
177
|
}
|
|
122
178
|
@default {
|
|
123
|
-
<div
|
|
179
|
+
<div [ngClass]="{'list__h-container': true, 'selected': config.selected}">
|
|
124
180
|
<span>{{data.name}}</span>
|
|
125
181
|
</div>
|
|
126
182
|
}
|
|
@@ -168,6 +224,10 @@ export class AppComponent {
|
|
|
168
224
|
console.info(`Click: (ID: ${item.id}) Item ${item.data.name}`);
|
|
169
225
|
}
|
|
170
226
|
}
|
|
227
|
+
|
|
228
|
+
onSelect(data: Array<Id> | Id | undefined) {
|
|
229
|
+
console.info(`Select: ${JSON.stringify(data)}`);
|
|
230
|
+
}
|
|
171
231
|
}
|
|
172
232
|
```
|
|
173
233
|
|
|
@@ -437,7 +497,9 @@ export class AppComponent {
|
|
|
437
497
|
}
|
|
438
498
|
```
|
|
439
499
|
|
|
440
|
-
|
|
500
|
+
<br/>
|
|
501
|
+
|
|
502
|
+
## 🖼️ Stylization
|
|
441
503
|
|
|
442
504
|
List items are encapsulated in shadowDOM, so to override default styles you need to use ::part access
|
|
443
505
|
|
|
@@ -521,7 +583,9 @@ Selecting even elements:
|
|
|
521
583
|
}
|
|
522
584
|
```
|
|
523
585
|
|
|
524
|
-
|
|
586
|
+
<br/>
|
|
587
|
+
|
|
588
|
+
## 📚 API
|
|
525
589
|
|
|
526
590
|
[NgVirtualListComponent](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/ng-virtual-list.component.ts)
|
|
527
591
|
|
|
@@ -536,7 +600,7 @@ Inputs
|
|
|
536
600
|
| maxBufferSize | number? = 100 | Maximum number of elements outside the scope of visibility. Default value is 100. If maxBufferSize is set to be greater than bufferSize, then adaptive buffer mode is enabled. The greater the scroll size, the more elements are allocated for rendering. |
|
|
537
601
|
| itemRenderer | TemplateRef | Rendering element template. |
|
|
538
602
|
| methodForSelecting | [MethodForSelecting](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/method-for-selecting.ts) | Method for selecting list items. Default value is 'none'. 'select' - List items are selected one by one. 'multi-select' - Multiple selection of list items. 'none' - List items are not selectable. |
|
|
539
|
-
| itemConfigMap | [IVirtualListItemConfigMap?](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/models/item-config-map.model.ts) | Sets sticky position and selectable for the list item element. If sticky position is greater than 0
|
|
603
|
+
| itemConfigMap | [IVirtualListItemConfigMap?](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/models/item-config-map.model.ts) | Sets `sticky` position and `selectable` for the list item element. If `sticky` position is greater than `0`, then `sticky` position is applied. If the `sticky` value is greater than `0`, then the `sticky` position mode is enabled for the element. `1` - position start, `2` - position end. Default value is `0`. `selectable` determines whether an element can be selected or not. Default value is `true`. |
|
|
540
604
|
| selectByClick | boolean? = true | If `false`, the element is selected using the config.select method passed to the template; if `true`, the element is selected by clicking on it. The default value is `true`. |
|
|
541
605
|
| snap | boolean? = false | Determines whether elements will snap. Default value is "false". |
|
|
542
606
|
| snappingMethod | [SnappingMethod? = 'normal'](https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/snapping-method.ts) | Snapping method. 'normal' - Normal group rendering. 'advanced' - The group is rendered on a transparent background. List items below the group are not rendered. |
|
|
@@ -571,20 +635,28 @@ Methods
|
|
|
571
635
|
|
|
572
636
|
<br/>
|
|
573
637
|
|
|
574
|
-
## Previous versions
|
|
638
|
+
## 📦 Previous versions
|
|
575
639
|
|
|
576
640
|
| Angular version | ng-virtual-list version | git | npm |
|
|
577
641
|
|--|--|--|--|
|
|
578
|
-
| 19.x | 19.7.
|
|
579
|
-
| 18.x | 18.7.
|
|
580
|
-
| 17.x | 17.7.
|
|
581
|
-
| 16.x | 16.7.
|
|
582
|
-
| 15.x | 15.7.
|
|
583
|
-
| 14.x | 14.7.
|
|
642
|
+
| 19.x | 19.7.6 | [19.x](https://github.com/DjonnyX/ng-virtual-list/tree/19.x) | [19.7.6](https://www.npmjs.com/package/ng-virtual-list/v/19.7.6) |
|
|
643
|
+
| 18.x | 18.7.4 | [18.x](https://github.com/DjonnyX/ng-virtual-list/tree/18.x) | [18.7.4](https://www.npmjs.com/package/ng-virtual-list/v/18.7.4) |
|
|
644
|
+
| 17.x | 17.7.4 | [17.x](https://github.com/DjonnyX/ng-virtual-list/tree/17.x) | [17.7.4](https://www.npmjs.com/package/ng-virtual-list/v/17.7.4) |
|
|
645
|
+
| 16.x | 16.7.4 | [16.x](https://github.com/DjonnyX/ng-virtual-list/tree/16.x) | [16.7.4](https://www.npmjs.com/package/ng-virtual-list/v/16.7.4) |
|
|
646
|
+
| 15.x | 15.7.4 | [15.x](https://github.com/DjonnyX/ng-virtual-list/tree/15.x) | [15.7.4](https://www.npmjs.com/package/ng-virtual-list/v/15.7.4) |
|
|
647
|
+
| 14.x | 14.7.4 | [14.x](https://github.com/DjonnyX/ng-virtual-list/tree/14.x) | [14.7.4](https://www.npmjs.com/package/ng-virtual-list/v/14.7.4) |
|
|
648
|
+
|
|
649
|
+
<br/>
|
|
650
|
+
|
|
651
|
+
## 🤝 Contributing
|
|
652
|
+
|
|
653
|
+
PRs and feature requests are welcome!
|
|
654
|
+
Open an issue or start a discussion to shape the future of [ng-virtual-list](https://github.com/DjonnyX/ng-virtual-list/).
|
|
655
|
+
Try it out, star ⭐ the repo, and let us know what you’re building.
|
|
584
656
|
|
|
585
657
|
<br/>
|
|
586
658
|
|
|
587
|
-
## License
|
|
659
|
+
## 📄 License
|
|
588
660
|
|
|
589
661
|
MIT License
|
|
590
662
|
|
|
@@ -606,4 +678,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
606
678
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
607
679
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
608
680
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
609
|
-
SOFTWARE.
|
|
681
|
+
SOFTWARE.
|
|
@@ -2,7 +2,7 @@ import * as i1 from '@angular/common';
|
|
|
2
2
|
import { CommonModule } from '@angular/common';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
4
|
import { Injectable, inject, signal, ElementRef, ChangeDetectionStrategy, Component, viewChild, output, input, ViewContainerRef, ViewChild, ViewEncapsulation } from '@angular/core';
|
|
5
|
-
import { Subject, tap, combineLatest, map, filter, distinctUntilChanged, switchMap, of } from 'rxjs';
|
|
5
|
+
import { Subject, tap, fromEvent, combineLatest, map, filter, distinctUntilChanged, switchMap, of } from 'rxjs';
|
|
6
6
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
|
7
7
|
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
|
|
8
8
|
|
|
@@ -109,10 +109,11 @@ const CLASS_LIST_VERTICAL = 'vertical';
|
|
|
109
109
|
const CLASS_LIST_HORIZONTAL = 'horizontal';
|
|
110
110
|
// styles
|
|
111
111
|
const PART_DEFAULT_ITEM = 'item';
|
|
112
|
-
const PART_ITEM_ODD = ' odd';
|
|
113
|
-
const PART_ITEM_EVEN = ' even';
|
|
114
|
-
const PART_ITEM_SNAPPED = ' snapped';
|
|
115
|
-
const PART_ITEM_SELECTED = ' selected';
|
|
112
|
+
const PART_ITEM_ODD = ' item-odd';
|
|
113
|
+
const PART_ITEM_EVEN = ' item-even';
|
|
114
|
+
const PART_ITEM_SNAPPED = ' item-snapped';
|
|
115
|
+
const PART_ITEM_SELECTED = ' item-selected';
|
|
116
|
+
const PART_ITEM_FOCUSED = ' item-focused';
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
119
|
* Virtual List Item Interface
|
|
@@ -158,6 +159,7 @@ class NgVirtualListService {
|
|
|
158
159
|
}
|
|
159
160
|
selectByClick = DEFAULT_SELECT_BY_CLICK;
|
|
160
161
|
_trackBox;
|
|
162
|
+
listElement = null;
|
|
161
163
|
constructor() {
|
|
162
164
|
this._$methodOfSelecting.pipe(takeUntilDestroyed(), tap(v => {
|
|
163
165
|
switch (v) {
|
|
@@ -265,7 +267,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImpor
|
|
|
265
267
|
}]
|
|
266
268
|
}], ctorParameters: () => [] });
|
|
267
269
|
|
|
268
|
-
const ATTR_AREA_SELECTED = 'area-selected';
|
|
270
|
+
const ATTR_AREA_SELECTED = 'area-selected', TABINDEX = 'index', KEY_SPACE = " ", KEY_ARR_LEFT = "ArrowLeft", KEY_ARR_UP = "ArrowUp", KEY_ARR_RIGHT = "ArrowRight", KEY_ARR_DOWN = "ArrowDown";
|
|
269
271
|
/**
|
|
270
272
|
* Virtual list item component
|
|
271
273
|
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/components/ng-virtual-list-item.component.ts
|
|
@@ -281,6 +283,7 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
|
|
|
281
283
|
_isSelected = false;
|
|
282
284
|
config = signal({});
|
|
283
285
|
measures = signal(undefined);
|
|
286
|
+
focus = signal(false);
|
|
284
287
|
_part = PART_DEFAULT_ITEM;
|
|
285
288
|
get part() { return this._part; }
|
|
286
289
|
regular = false;
|
|
@@ -331,23 +334,71 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
|
|
|
331
334
|
super();
|
|
332
335
|
this._id = this._service.generateComponentId();
|
|
333
336
|
const $data = toObservable(this.data);
|
|
337
|
+
fromEvent(this.element, 'focusin').pipe(takeUntilDestroyed(), tap(e => {
|
|
338
|
+
this.focus.set(true);
|
|
339
|
+
this.updateConfig(this._data);
|
|
340
|
+
this.updatePartStr(this._data, this._isSelected);
|
|
341
|
+
})).subscribe(),
|
|
342
|
+
fromEvent(this.element, 'focusout').pipe(takeUntilDestroyed(), tap(e => {
|
|
343
|
+
this.focus.set(false);
|
|
344
|
+
this.updateConfig(this._data);
|
|
345
|
+
this.updatePartStr(this._data, this._isSelected);
|
|
346
|
+
})).subscribe(),
|
|
347
|
+
fromEvent(this.element, 'keydown').pipe(takeUntilDestroyed(), tap(e => {
|
|
348
|
+
switch (e.key) {
|
|
349
|
+
case KEY_SPACE: {
|
|
350
|
+
e.stopImmediatePropagation();
|
|
351
|
+
e.preventDefault();
|
|
352
|
+
this._service.select(this._data);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case KEY_ARR_LEFT:
|
|
356
|
+
if (!this.config().isVertical) {
|
|
357
|
+
e.stopImmediatePropagation();
|
|
358
|
+
e.preventDefault();
|
|
359
|
+
this.focusPrev();
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
case KEY_ARR_UP:
|
|
363
|
+
if (this.config().isVertical) {
|
|
364
|
+
e.stopImmediatePropagation();
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
this.focusPrev();
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
case KEY_ARR_RIGHT:
|
|
370
|
+
if (!this.config().isVertical) {
|
|
371
|
+
e.stopImmediatePropagation();
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
this.focusNext();
|
|
374
|
+
}
|
|
375
|
+
break;
|
|
376
|
+
case KEY_ARR_DOWN:
|
|
377
|
+
if (this.config().isVertical) {
|
|
378
|
+
e.stopImmediatePropagation();
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
this.focusNext();
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
})).subscribe();
|
|
334
385
|
combineLatest([$data, this._service.$methodOfSelecting, this._service.$selectedIds]).pipe(takeUntilDestroyed(), map(([, m, ids]) => ({ method: m, ids })), tap(({ method, ids }) => {
|
|
335
386
|
switch (method) {
|
|
336
387
|
case MethodsForSelectingTypes.SELECT: {
|
|
337
388
|
const id = ids, isSelected = id === this.itemId;
|
|
338
|
-
this.
|
|
389
|
+
this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
|
|
339
390
|
this._isSelected = isSelected;
|
|
340
391
|
break;
|
|
341
392
|
}
|
|
342
393
|
case MethodsForSelectingTypes.MULTI_SELECT: {
|
|
343
394
|
const actualIds = ids, isSelected = this.itemId !== undefined && actualIds && actualIds.includes(this.itemId);
|
|
344
|
-
this.
|
|
395
|
+
this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
|
|
345
396
|
this._isSelected = isSelected;
|
|
346
397
|
break;
|
|
347
398
|
}
|
|
348
399
|
case MethodsForSelectingTypes.NONE:
|
|
349
400
|
default: {
|
|
350
|
-
this.
|
|
401
|
+
this.element.removeAttribute(ATTR_AREA_SELECTED);
|
|
351
402
|
this._isSelected = false;
|
|
352
403
|
break;
|
|
353
404
|
}
|
|
@@ -357,11 +408,29 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
|
|
|
357
408
|
this.updateMeasures(this._data);
|
|
358
409
|
})).subscribe();
|
|
359
410
|
}
|
|
411
|
+
focusNext() {
|
|
412
|
+
const tabIndex = this.config()?.tabIndex ?? 0;
|
|
413
|
+
if (this._service.listElement && tabIndex > 0) {
|
|
414
|
+
const el = this._service.listElement.querySelector(`[${TABINDEX}="${tabIndex + 1}"]`);
|
|
415
|
+
if (el) {
|
|
416
|
+
el.focus();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
focusPrev() {
|
|
421
|
+
const tabIndex = this.config()?.tabIndex ?? 0;
|
|
422
|
+
if (this._service.listElement && tabIndex > 1) {
|
|
423
|
+
const el = this._service.listElement.querySelector(`[${TABINDEX}="${tabIndex - 1}"]`);
|
|
424
|
+
if (el) {
|
|
425
|
+
el.focus();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
360
429
|
updateMeasures(v) {
|
|
361
430
|
this.measures.set(v?.measures ? { ...v.measures } : undefined);
|
|
362
431
|
}
|
|
363
432
|
updateConfig(v) {
|
|
364
|
-
this.config.set({ ...v?.config || {}, selected: this._isSelected, select: this._selectHandler(v) });
|
|
433
|
+
this.config.set({ ...v?.config || {}, selected: this._isSelected, select: this._selectHandler(v), focus: this.focus() });
|
|
365
434
|
}
|
|
366
435
|
update() {
|
|
367
436
|
const data = this._data, regular = this.regular, length = this._regularLength;
|
|
@@ -401,6 +470,9 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
|
|
|
401
470
|
if (isSelected) {
|
|
402
471
|
part += PART_ITEM_SELECTED;
|
|
403
472
|
}
|
|
473
|
+
if (this.focus()) {
|
|
474
|
+
part += PART_ITEM_FOCUSED;
|
|
475
|
+
}
|
|
404
476
|
this._part = part;
|
|
405
477
|
}
|
|
406
478
|
getBounds() {
|
|
@@ -445,14 +517,14 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
|
|
|
445
517
|
this._service.itemClick(this._data);
|
|
446
518
|
}
|
|
447
519
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
448
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: NgVirtualListItemComponent, isStandalone: true, selector: "ng-virtual-list-item", host: { attributes: { "role": "listitem" }, classAttribute: "ngvl__item" }, usesInheritance: true, ngImport: i0, template: "@let item = data();\r\n@let _config = config();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"part\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut}\" (click)=\"onClickHandler()\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data, measures: _measures, config: _config}\" />\r\n }\r\n </div>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
520
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: NgVirtualListItemComponent, isStandalone: true, selector: "ng-virtual-list-item", host: { attributes: { "role": "listitem" }, classAttribute: "ngvl__item" }, usesInheritance: true, ngImport: i0, template: "@let item = data();\r\n@let _config = config();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"part\" [attr.index]=\"item.config.tabIndex\" tabindex=\"1\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut, 'focus': focus()}\" (click)=\"onClickHandler()\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data, measures: _measures, config: _config}\" />\r\n }\r\n </div>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit;box-sizing:border-box}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
449
521
|
}
|
|
450
522
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListItemComponent, decorators: [{
|
|
451
523
|
type: Component,
|
|
452
524
|
args: [{ selector: 'ng-virtual-list-item', imports: [CommonModule], host: {
|
|
453
525
|
'class': 'ngvl__item',
|
|
454
526
|
'role': 'listitem',
|
|
455
|
-
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let _config = config();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"part\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut}\" (click)=\"onClickHandler()\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data, measures: _measures, config: _config}\" />\r\n }\r\n </div>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit}\n"] }]
|
|
527
|
+
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let _config = config();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"part\" [attr.index]=\"item.config.tabIndex\" tabindex=\"1\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut, 'focus': focus()}\" (click)=\"onClickHandler()\">\r\n @if (renderer) {\r\n <ng-container [ngTemplateOutlet]=\"renderer\"\r\n [ngTemplateOutletContext]=\"{data: item.data, measures: _measures, config: _config}\" />\r\n }\r\n </div>\r\n}", styles: [":host{display:block;position:absolute;left:0;top:0;box-sizing:border-box;overflow:hidden}.ngvl-item__container{margin:0;padding:0;overflow:hidden;background-color:#fff;width:inherit;height:inherit;box-sizing:border-box}\n"] }]
|
|
456
528
|
}], ctorParameters: () => [] });
|
|
457
529
|
|
|
458
530
|
/**
|
|
@@ -1548,7 +1620,7 @@ class TrackBox extends CacheMap {
|
|
|
1548
1620
|
const { width, height, normalizedItemWidth, normalizedItemHeight, dynamicSize, itemsOnDisplayLength, itemsFromStartToScrollEnd, isVertical, renderItems: renderItemsLength, scrollSize, sizeProperty, snap, snippedPos, startPosition, totalLength, startIndex, typicalItemSize, } = metrics, displayItems = [];
|
|
1549
1621
|
if (items.length) {
|
|
1550
1622
|
const actualSnippedPosition = snippedPos, isSnappingMethodAdvanced = this.isSnappingMethodAdvanced, boundsSize = isVertical ? height : width, actualEndSnippedPosition = boundsSize;
|
|
1551
|
-
let pos = startPosition, renderItems = renderItemsLength, stickyItem, nextSticky, stickyItemIndex = -1, stickyItemSize = 0, endStickyItem, nextEndSticky, endStickyItemIndex = -1, endStickyItemSize = 0;
|
|
1623
|
+
let pos = startPosition, renderItems = renderItemsLength, stickyItem, nextSticky, stickyItemIndex = -1, stickyItemSize = 0, endStickyItem, nextEndSticky, endStickyItemIndex = -1, endStickyItemSize = 0, count = 1;
|
|
1552
1624
|
if (snap) {
|
|
1553
1625
|
for (let i = Math.min(itemsFromStartToScrollEnd > 0 ? itemsFromStartToScrollEnd : 0, totalLength - 1); i >= 0; i--) {
|
|
1554
1626
|
if (!items[i]) {
|
|
@@ -1573,6 +1645,7 @@ class TrackBox extends CacheMap {
|
|
|
1573
1645
|
snappedOut: false,
|
|
1574
1646
|
dynamic: dynamicSize,
|
|
1575
1647
|
isSnappingMethodAdvanced,
|
|
1648
|
+
tabIndex: count,
|
|
1576
1649
|
zIndex: '1',
|
|
1577
1650
|
};
|
|
1578
1651
|
const itemData = items[i];
|
|
@@ -1580,6 +1653,7 @@ class TrackBox extends CacheMap {
|
|
|
1580
1653
|
stickyItemIndex = i;
|
|
1581
1654
|
stickyItemSize = size;
|
|
1582
1655
|
displayItems.push(stickyItem);
|
|
1656
|
+
count++;
|
|
1583
1657
|
break;
|
|
1584
1658
|
}
|
|
1585
1659
|
}
|
|
@@ -1611,6 +1685,7 @@ class TrackBox extends CacheMap {
|
|
|
1611
1685
|
snappedOut: false,
|
|
1612
1686
|
dynamic: dynamicSize,
|
|
1613
1687
|
isSnappingMethodAdvanced,
|
|
1688
|
+
tabIndex: count,
|
|
1614
1689
|
zIndex: '1',
|
|
1615
1690
|
};
|
|
1616
1691
|
const itemData = items[i];
|
|
@@ -1618,6 +1693,7 @@ class TrackBox extends CacheMap {
|
|
|
1618
1693
|
endStickyItemIndex = i;
|
|
1619
1694
|
endStickyItemSize = size;
|
|
1620
1695
|
displayItems.push(endStickyItem);
|
|
1696
|
+
count++;
|
|
1621
1697
|
break;
|
|
1622
1698
|
}
|
|
1623
1699
|
}
|
|
@@ -1649,6 +1725,7 @@ class TrackBox extends CacheMap {
|
|
|
1649
1725
|
snappedOut: false,
|
|
1650
1726
|
dynamic: dynamicSize,
|
|
1651
1727
|
isSnappingMethodAdvanced,
|
|
1728
|
+
tabIndex: count,
|
|
1652
1729
|
zIndex: '0',
|
|
1653
1730
|
};
|
|
1654
1731
|
if (snapped) {
|
|
@@ -1673,6 +1750,7 @@ class TrackBox extends CacheMap {
|
|
|
1673
1750
|
nextEndSticky.measures.delta = isVertical ? (item.measures.y - scrollSize) : (item.measures.x - scrollSize);
|
|
1674
1751
|
}
|
|
1675
1752
|
displayItems.push(item);
|
|
1753
|
+
count++;
|
|
1676
1754
|
}
|
|
1677
1755
|
renderItems -= 1;
|
|
1678
1756
|
pos += size;
|
|
@@ -2054,6 +2132,9 @@ class NgVirtualListComponent {
|
|
|
2054
2132
|
this._initialized = signal(false);
|
|
2055
2133
|
this.$initialized = toObservable(this._initialized);
|
|
2056
2134
|
this._trackBox.displayComponents = this._displayComponents;
|
|
2135
|
+
toObservable(this._list).pipe(takeUntilDestroyed(), filter(v => !!v), tap(v => {
|
|
2136
|
+
this._service.listElement = v.nativeElement;
|
|
2137
|
+
})).subscribe();
|
|
2057
2138
|
const $trackBy = toObservable(this.trackBy), $selectByClick = toObservable(this.selectByClick);
|
|
2058
2139
|
$selectByClick.pipe(takeUntilDestroyed(), tap(v => {
|
|
2059
2140
|
this._service.selectByClick = v;
|