ng-virtual-list 19.7.2 → 19.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 CHANGED
@@ -44,6 +44,10 @@ As each item may contain images, nested components, or interactions, virtual ren
44
44
 
45
45
  Single and multiple selection of elements
46
46
 
47
+ Navigating with the keyboard
48
+
49
+ Support for element animation
50
+
47
51
  <br/>
48
52
 
49
53
  ## Installation
@@ -536,7 +540,8 @@ Inputs
536
540
  | 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
541
  | itemRenderer | TemplateRef | Rendering element template. |
538
542
  | methodForSelecting | [MethodForSelecting](https://github.com/DjonnyX/ng-virtual-list/blob/19.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, 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`. |
543
+ | itemConfigMap | [IVirtualListItemConfigMap?](https://github.com/DjonnyX/ng-virtual-list/blob/19.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`. |
544
+ | 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`. |
540
545
  | snap | boolean? = false | Determines whether elements will snap. Default value is "false". |
541
546
  | snappingMethod | [SnappingMethod? = 'normal'](https://github.com/DjonnyX/ng-virtual-list/blob/19.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. |
542
547
  | direction | [Direction? = 'vertical'](https://github.com/DjonnyX/ng-virtual-list/blob/19.x/projects/ng-virtual-list/src/lib/enums/direction.ts) | Determines the direction in which elements are placed. Default value is "vertical". |
@@ -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
 
@@ -69,6 +69,7 @@ const DEFAULT_BUFFER_SIZE = 2;
69
69
  const DEFAULT_MAX_BUFFER_SIZE = 100;
70
70
  const DEFAULT_LIST_SIZE = 400;
71
71
  const DEFAULT_SNAP = false;
72
+ const DEFAULT_SELECT_BY_CLICK = true;
72
73
  const DEFAULT_ENABLED_BUFFER_OPTIMIZATION = false;
73
74
  const DEFAULT_DYNAMIC_SIZE = false;
74
75
  const TRACK_BY_PROPERTY_NAME = 'id';
@@ -108,10 +109,11 @@ const CLASS_LIST_VERTICAL = 'vertical';
108
109
  const CLASS_LIST_HORIZONTAL = 'horizontal';
109
110
  // styles
110
111
  const PART_DEFAULT_ITEM = 'item';
111
- const PART_ITEM_ODD = ' odd';
112
- const PART_ITEM_EVEN = ' even';
113
- const PART_ITEM_SNAPPED = ' snapped';
114
- 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';
115
117
 
116
118
  /**
117
119
  * Virtual List Item Interface
@@ -155,7 +157,9 @@ class NgVirtualListService {
155
157
  set methodOfSelecting(v) {
156
158
  this._$methodOfSelecting.next(v);
157
159
  }
160
+ selectByClick = DEFAULT_SELECT_BY_CLICK;
158
161
  _trackBox;
162
+ listElement = null;
159
163
  constructor() {
160
164
  this._$methodOfSelecting.pipe(takeUntilDestroyed(), tap(v => {
161
165
  switch (v) {
@@ -181,25 +185,61 @@ class NgVirtualListService {
181
185
  })).subscribe();
182
186
  }
183
187
  setSelectedIds(ids) {
184
- this._$selectedIds.next(ids);
188
+ if (JSON.stringify(this._$selectedIds.getValue()) !== JSON.stringify(ids)) {
189
+ this._$selectedIds.next(ids);
190
+ }
185
191
  }
186
192
  itemClick(data) {
187
193
  this._$itemClick.next(data);
194
+ if (this.selectByClick) {
195
+ this.select(data);
196
+ }
197
+ }
198
+ /**
199
+ * Selects a list item
200
+ * @param data
201
+ * @param selected - If the value is undefined, then the toggle method is executed, if false or true, then the selection/deselection is performed.
202
+ */
203
+ select(data, selected = undefined) {
188
204
  if (data && data.config.selectable) {
189
205
  switch (this._$methodOfSelecting.getValue()) {
190
206
  case MethodsForSelectingTypes.SELECT: {
191
207
  const curr = this._$selectedIds.getValue();
192
- this._$selectedIds.next(curr !== data?.id ? data?.id : undefined);
208
+ if (selected === undefined) {
209
+ this._$selectedIds.next(curr !== data?.id ? data?.id : undefined);
210
+ }
211
+ else {
212
+ this._$selectedIds.next(selected ? data?.id : undefined);
213
+ }
193
214
  break;
194
215
  }
195
216
  case MethodsForSelectingTypes.MULTI_SELECT: {
196
217
  const curr = [...(this._$selectedIds.getValue() || [])], index = curr.indexOf(data.id);
197
- if (index > -1) {
198
- curr.splice(index, 1);
199
- this._$selectedIds.next(curr);
218
+ if (selected === undefined) {
219
+ if (index > -1) {
220
+ curr.splice(index, 1);
221
+ this._$selectedIds.next(curr);
222
+ }
223
+ else {
224
+ this._$selectedIds.next([...curr, data.id]);
225
+ }
226
+ }
227
+ else if (selected) {
228
+ if (index > -1) {
229
+ this._$selectedIds.next(curr);
230
+ }
231
+ else {
232
+ this._$selectedIds.next([...curr, data.id]);
233
+ }
200
234
  }
201
235
  else {
202
- this._$selectedIds.next([...curr, data.id]);
236
+ if (index > -1) {
237
+ curr.splice(index, 1);
238
+ this._$selectedIds.next(curr);
239
+ }
240
+ else {
241
+ this._$selectedIds.next(curr);
242
+ }
203
243
  }
204
244
  break;
205
245
  }
@@ -227,7 +267,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImpo
227
267
  }]
228
268
  }], ctorParameters: () => [] });
229
269
 
230
- 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";
231
271
  /**
232
272
  * Virtual list item component
233
273
  * @link https://github.com/DjonnyX/ng-virtual-list/blob/19.x/projects/ng-virtual-list/src/lib/components/ng-virtual-list-item.component.ts
@@ -242,6 +282,8 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
242
282
  _service = inject(NgVirtualListService);
243
283
  _isSelected = false;
244
284
  config = signal({});
285
+ measures = signal(undefined);
286
+ focus = signal(false);
245
287
  _part = PART_DEFAULT_ITEM;
246
288
  get part() { return this._part; }
247
289
  regular = false;
@@ -254,6 +296,7 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
254
296
  this._data = v;
255
297
  this.updatePartStr(v, this._isSelected);
256
298
  this.updateConfig(v);
299
+ this.updateMeasures(v);
257
300
  this.update();
258
301
  this.data.set(v);
259
302
  }
@@ -279,37 +322,115 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
279
322
  get element() {
280
323
  return this._elementRef.nativeElement;
281
324
  }
325
+ _selectHandler = (data) =>
326
+ /**
327
+ * Selects a list item
328
+ * @param selected - If the value is undefined, then the toggle method is executed, if false or true, then the selection/deselection is performed.
329
+ */
330
+ (selected = undefined) => {
331
+ this._service.select(data, selected);
332
+ };
282
333
  constructor() {
283
334
  super();
284
335
  this._id = this._service.generateComponentId();
285
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();
286
385
  combineLatest([$data, this._service.$methodOfSelecting, this._service.$selectedIds]).pipe(takeUntilDestroyed(), map(([, m, ids]) => ({ method: m, ids })), tap(({ method, ids }) => {
287
386
  switch (method) {
288
387
  case MethodsForSelectingTypes.SELECT: {
289
388
  const id = ids, isSelected = id === this.itemId;
290
- this._elementRef.nativeElement.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
389
+ this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
291
390
  this._isSelected = isSelected;
292
391
  break;
293
392
  }
294
393
  case MethodsForSelectingTypes.MULTI_SELECT: {
295
394
  const actualIds = ids, isSelected = this.itemId !== undefined && actualIds && actualIds.includes(this.itemId);
296
- this._elementRef.nativeElement.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
395
+ this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
297
396
  this._isSelected = isSelected;
298
397
  break;
299
398
  }
300
399
  case MethodsForSelectingTypes.NONE:
301
400
  default: {
302
- this._elementRef.nativeElement.removeAttribute(ATTR_AREA_SELECTED);
401
+ this.element.removeAttribute(ATTR_AREA_SELECTED);
303
402
  this._isSelected = false;
304
403
  break;
305
404
  }
306
405
  }
307
406
  this.updatePartStr(this._data, this._isSelected);
308
407
  this.updateConfig(this._data);
408
+ this.updateMeasures(this._data);
309
409
  })).subscribe();
310
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
+ }
429
+ updateMeasures(v) {
430
+ this.measures.set(v?.measures ? { ...v.measures } : undefined);
431
+ }
311
432
  updateConfig(v) {
312
- this.config.set({ ...v?.config || {}, selected: this._isSelected });
433
+ this.config.set({ ...v?.config || {}, selected: this._isSelected, select: this._selectHandler(v), focus: this.focus() });
313
434
  }
314
435
  update() {
315
436
  const data = this._data, regular = this.regular, length = this._regularLength;
@@ -349,6 +470,9 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
349
470
  if (isSelected) {
350
471
  part += PART_ITEM_SELECTED;
351
472
  }
473
+ if (this.focus()) {
474
+ part += PART_ITEM_FOCUSED;
475
+ }
352
476
  this._part = part;
353
477
  }
354
478
  getBounds() {
@@ -393,14 +517,14 @@ class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
393
517
  this._service.itemClick(this._data);
394
518
  }
395
519
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
396
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", 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 conf = config();\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 || {}, config: conf}\" />\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: "19.2.14", 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 });
397
521
  }
398
522
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListItemComponent, decorators: [{
399
523
  type: Component,
400
524
  args: [{ selector: 'ng-virtual-list-item', imports: [CommonModule], host: {
401
525
  'class': 'ngvl__item',
402
526
  'role': 'listitem',
403
- }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let conf = config();\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 || {}, config: conf}\" />\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"] }]
404
528
  }], ctorParameters: () => [] });
405
529
 
406
530
  /**
@@ -1496,7 +1620,7 @@ class TrackBox extends CacheMap {
1496
1620
  const { width, height, normalizedItemWidth, normalizedItemHeight, dynamicSize, itemsOnDisplayLength, itemsFromStartToScrollEnd, isVertical, renderItems: renderItemsLength, scrollSize, sizeProperty, snap, snippedPos, startPosition, totalLength, startIndex, typicalItemSize, } = metrics, displayItems = [];
1497
1621
  if (items.length) {
1498
1622
  const actualSnippedPosition = snippedPos, isSnappingMethodAdvanced = this.isSnappingMethodAdvanced, boundsSize = isVertical ? height : width, actualEndSnippedPosition = boundsSize;
1499
- 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;
1500
1624
  if (snap) {
1501
1625
  for (let i = Math.min(itemsFromStartToScrollEnd > 0 ? itemsFromStartToScrollEnd : 0, totalLength - 1); i >= 0; i--) {
1502
1626
  if (!items[i]) {
@@ -1521,6 +1645,7 @@ class TrackBox extends CacheMap {
1521
1645
  snappedOut: false,
1522
1646
  dynamic: dynamicSize,
1523
1647
  isSnappingMethodAdvanced,
1648
+ tabIndex: count,
1524
1649
  zIndex: '1',
1525
1650
  };
1526
1651
  const itemData = items[i];
@@ -1528,6 +1653,7 @@ class TrackBox extends CacheMap {
1528
1653
  stickyItemIndex = i;
1529
1654
  stickyItemSize = size;
1530
1655
  displayItems.push(stickyItem);
1656
+ count++;
1531
1657
  break;
1532
1658
  }
1533
1659
  }
@@ -1559,6 +1685,7 @@ class TrackBox extends CacheMap {
1559
1685
  snappedOut: false,
1560
1686
  dynamic: dynamicSize,
1561
1687
  isSnappingMethodAdvanced,
1688
+ tabIndex: count,
1562
1689
  zIndex: '1',
1563
1690
  };
1564
1691
  const itemData = items[i];
@@ -1566,6 +1693,7 @@ class TrackBox extends CacheMap {
1566
1693
  endStickyItemIndex = i;
1567
1694
  endStickyItemSize = size;
1568
1695
  displayItems.push(endStickyItem);
1696
+ count++;
1569
1697
  break;
1570
1698
  }
1571
1699
  }
@@ -1597,6 +1725,7 @@ class TrackBox extends CacheMap {
1597
1725
  snappedOut: false,
1598
1726
  dynamic: dynamicSize,
1599
1727
  isSnappingMethodAdvanced,
1728
+ tabIndex: count,
1600
1729
  zIndex: '0',
1601
1730
  };
1602
1731
  if (snapped) {
@@ -1621,6 +1750,7 @@ class TrackBox extends CacheMap {
1621
1750
  nextEndSticky.measures.delta = isVertical ? (item.measures.y - scrollSize) : (item.measures.x - scrollSize);
1622
1751
  }
1623
1752
  displayItems.push(item);
1753
+ count++;
1624
1754
  }
1625
1755
  renderItems -= 1;
1626
1756
  pos += size;
@@ -1799,6 +1929,11 @@ class NgVirtualListComponent {
1799
1929
  * Sets the selected items.
1800
1930
  */
1801
1931
  selectedIds = input(undefined);
1932
+ /**
1933
+ * If false, the element is selected using the config.select method passed to the template;
1934
+ * if true, the element is selected by clicking on it. The default value is true.
1935
+ */
1936
+ selectByClick = input(DEFAULT_SELECT_BY_CLICK);
1802
1937
  /**
1803
1938
  * Determines whether elements will snap. Default value is "true".
1804
1939
  */
@@ -1997,7 +2132,13 @@ class NgVirtualListComponent {
1997
2132
  this._initialized = signal(false);
1998
2133
  this.$initialized = toObservable(this._initialized);
1999
2134
  this._trackBox.displayComponents = this._displayComponents;
2000
- const $trackBy = toObservable(this.trackBy);
2135
+ toObservable(this._list).pipe(takeUntilDestroyed(), filter(v => !!v), tap(v => {
2136
+ this._service.listElement = v.nativeElement;
2137
+ })).subscribe();
2138
+ const $trackBy = toObservable(this.trackBy), $selectByClick = toObservable(this.selectByClick);
2139
+ $selectByClick.pipe(takeUntilDestroyed(), tap(v => {
2140
+ this._service.selectByClick = v;
2141
+ })).subscribe();
2001
2142
  $trackBy.pipe(takeUntilDestroyed(), tap(v => {
2002
2143
  this._trackBox.trackingPropertyName = v;
2003
2144
  })).subscribe();
@@ -2079,10 +2220,19 @@ class NgVirtualListComponent {
2079
2220
  this._service.$itemClick.pipe(takeUntilDestroyed(), tap(v => {
2080
2221
  this.onItemClick.emit(v);
2081
2222
  })).subscribe();
2082
- this._service.$selectedIds.pipe(takeUntilDestroyed(), tap(v => {
2083
- this.onSelect.emit(v);
2223
+ let isSelectedIdsFirstEmit = 0;
2224
+ this._service.$selectedIds.pipe(takeUntilDestroyed(), distinctUntilChanged(), tap(v => {
2225
+ if (this.isSingleSelecting || (this.isMultiSelecting && isSelectedIdsFirstEmit >= 2)) {
2226
+ const curr = this.selectedIds();
2227
+ if ((this.isSingleSelecting && JSON.stringify(v) !== JSON.stringify(curr)) || (isSelectedIdsFirstEmit === 2 && JSON.stringify(v) !== JSON.stringify(curr)) || isSelectedIdsFirstEmit > 2) {
2228
+ this.onSelect.emit(v);
2229
+ }
2230
+ }
2231
+ if (isSelectedIdsFirstEmit < 3) {
2232
+ isSelectedIdsFirstEmit++;
2233
+ }
2084
2234
  })).subscribe();
2085
- $selectedIds.pipe(takeUntilDestroyed(), tap(v => {
2235
+ $selectedIds.pipe(takeUntilDestroyed(), distinctUntilChanged(), tap(v => {
2086
2236
  this._service.setSelectedIds(v);
2087
2237
  })).subscribe();
2088
2238
  }
@@ -2346,7 +2496,7 @@ class NgVirtualListComponent {
2346
2496
  }
2347
2497
  }
2348
2498
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2349
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NgVirtualListComponent, isStandalone: true, selector: "ng-virtual-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null }, snap: { classPropertyName: "snap", publicName: "snap", isSignal: true, isRequired: false, transformFunction: null }, enabledBufferOptimization: { classPropertyName: "enabledBufferOptimization", publicName: "enabledBufferOptimization", isSignal: true, isRequired: false, transformFunction: null }, itemRenderer: { classPropertyName: "itemRenderer", publicName: "itemRenderer", isSignal: true, isRequired: true, transformFunction: null }, stickyMap: { classPropertyName: "stickyMap", publicName: "stickyMap", isSignal: true, isRequired: false, transformFunction: null }, itemConfigMap: { classPropertyName: "itemConfigMap", publicName: "itemConfigMap", isSignal: true, isRequired: false, transformFunction: null }, itemSize: { classPropertyName: "itemSize", publicName: "itemSize", isSignal: true, isRequired: false, transformFunction: null }, dynamicSize: { classPropertyName: "dynamicSize", publicName: "dynamicSize", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, itemsOffset: { classPropertyName: "itemsOffset", publicName: "itemsOffset", isSignal: true, isRequired: false, transformFunction: null }, bufferSize: { classPropertyName: "bufferSize", publicName: "bufferSize", isSignal: true, isRequired: false, transformFunction: null }, maxBufferSize: { classPropertyName: "maxBufferSize", publicName: "maxBufferSize", isSignal: true, isRequired: false, transformFunction: null }, snappingMethod: { classPropertyName: "snappingMethod", publicName: "snappingMethod", isSignal: true, isRequired: false, transformFunction: null }, methodForSelecting: { classPropertyName: "methodForSelecting", publicName: "methodForSelecting", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onScroll: "onScroll", onScrollEnd: "onScrollEnd", onViewportChange: "onViewportChange", onItemClick: "onItemClick", onSelect: "onSelect" }, providers: [NgVirtualListService], viewQueries: [{ propertyName: "_snappedContainer", first: true, predicate: ["snapped"], descendants: true, isSignal: true }, { propertyName: "_container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "_list", first: true, predicate: ["list"], descendants: true, isSignal: true }, { propertyName: "_listContainerRef", first: true, predicate: ["renderersContainer"], descendants: true, read: ViewContainerRef }, { propertyName: "_snapContainerRef", first: true, predicate: ["snapRendererContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: "@if (snap()) {\r\n <div #snapped part=\"snapped-item\" class=\"ngvl__list-snapper\">\r\n <ng-container #snapRendererContainer></ng-container>\r\n </div>\r\n}\r\n<div #container part=\"scroller\" class=\"ngvl__scroller\">\r\n <div [attr.aria-orientation]=\"orientation\" #list part=\"list\" class=\"ngvl__list\">\r\n <ng-container #renderersContainer></ng-container>\r\n </div>\r\n</div>", styles: [":host{position:relative;display:block;width:400px;overflow:hidden}:host(.horizontal){height:48px}:host(.horizontal) .ngvl__list{display:inline-flex}:host(.horizontal) .ngvl__scroller{overflow:auto hidden}:host(.vertical) .ngvl__scroller{overflow:hidden auto}:host(.vertical){height:320px}.ngvl__scroller{overflow:auto;width:100%;height:100%}.ngvl__list-snapper{pointer-events:none;position:absolute;list-style:none;left:0;top:0;z-index:1}.ngvl__list{position:relative;list-style:none;padding:0;margin:0;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.ShadowDom });
2499
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.14", type: NgVirtualListComponent, isStandalone: true, selector: "ng-virtual-list", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: true, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null }, selectByClick: { classPropertyName: "selectByClick", publicName: "selectByClick", isSignal: true, isRequired: false, transformFunction: null }, snap: { classPropertyName: "snap", publicName: "snap", isSignal: true, isRequired: false, transformFunction: null }, enabledBufferOptimization: { classPropertyName: "enabledBufferOptimization", publicName: "enabledBufferOptimization", isSignal: true, isRequired: false, transformFunction: null }, itemRenderer: { classPropertyName: "itemRenderer", publicName: "itemRenderer", isSignal: true, isRequired: true, transformFunction: null }, stickyMap: { classPropertyName: "stickyMap", publicName: "stickyMap", isSignal: true, isRequired: false, transformFunction: null }, itemConfigMap: { classPropertyName: "itemConfigMap", publicName: "itemConfigMap", isSignal: true, isRequired: false, transformFunction: null }, itemSize: { classPropertyName: "itemSize", publicName: "itemSize", isSignal: true, isRequired: false, transformFunction: null }, dynamicSize: { classPropertyName: "dynamicSize", publicName: "dynamicSize", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, itemsOffset: { classPropertyName: "itemsOffset", publicName: "itemsOffset", isSignal: true, isRequired: false, transformFunction: null }, bufferSize: { classPropertyName: "bufferSize", publicName: "bufferSize", isSignal: true, isRequired: false, transformFunction: null }, maxBufferSize: { classPropertyName: "maxBufferSize", publicName: "maxBufferSize", isSignal: true, isRequired: false, transformFunction: null }, snappingMethod: { classPropertyName: "snappingMethod", publicName: "snappingMethod", isSignal: true, isRequired: false, transformFunction: null }, methodForSelecting: { classPropertyName: "methodForSelecting", publicName: "methodForSelecting", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onScroll: "onScroll", onScrollEnd: "onScrollEnd", onViewportChange: "onViewportChange", onItemClick: "onItemClick", onSelect: "onSelect" }, providers: [NgVirtualListService], viewQueries: [{ propertyName: "_snappedContainer", first: true, predicate: ["snapped"], descendants: true, isSignal: true }, { propertyName: "_container", first: true, predicate: ["container"], descendants: true, isSignal: true }, { propertyName: "_list", first: true, predicate: ["list"], descendants: true, isSignal: true }, { propertyName: "_listContainerRef", first: true, predicate: ["renderersContainer"], descendants: true, read: ViewContainerRef }, { propertyName: "_snapContainerRef", first: true, predicate: ["snapRendererContainer"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: "@if (snap()) {\r\n <div #snapped part=\"snapped-item\" class=\"ngvl__list-snapper\">\r\n <ng-container #snapRendererContainer></ng-container>\r\n </div>\r\n}\r\n<div #container part=\"scroller\" class=\"ngvl__scroller\">\r\n <div [attr.aria-orientation]=\"orientation\" #list part=\"list\" class=\"ngvl__list\">\r\n <ng-container #renderersContainer></ng-container>\r\n </div>\r\n</div>", styles: [":host{position:relative;display:block;width:400px;overflow:hidden}:host(.horizontal){height:48px}:host(.horizontal) .ngvl__list{display:inline-flex}:host(.horizontal) .ngvl__scroller{overflow:auto hidden}:host(.vertical) .ngvl__scroller{overflow:hidden auto}:host(.vertical){height:320px}.ngvl__scroller{overflow:auto;width:100%;height:100%}.ngvl__list-snapper{pointer-events:none;position:absolute;list-style:none;left:0;top:0;z-index:1}.ngvl__list{position:relative;list-style:none;padding:0;margin:0;width:100%;height:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.ShadowDom });
2350
2500
  }
2351
2501
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: NgVirtualListComponent, decorators: [{
2352
2502
  type: Component,