ng-virtual-list
Version:
🚀 High-performance virtual scrolling for Angular apps. Render 100,000+ items in Angular without breaking a sweat. Smooth, customizable, and developer-friendly.
1,323 lines (1,304 loc) • 151 kB
JavaScript
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { Injectable, inject, signal, ElementRef, ChangeDetectionStrategy, Component, viewChild, output, input, ViewContainerRef, ViewChild, ViewEncapsulation } from '@angular/core';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { Subject, tap, fromEvent, combineLatest, map, filter, distinctUntilChanged, debounceTime, switchMap, of, BehaviorSubject as BehaviorSubject$1, delay, take } from 'rxjs';
/**
* Action modes for collection elements.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/collection-modes.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
var CollectionModes;
(function (CollectionModes) {
/**
* When adding elements to the beginning of the collection, the scroll remains at the current position.
*/
CollectionModes["NORMAL"] = "normal";
/**
* When adding elements to the beginning of the collection, the scroll is shifted by the sum of the sizes of the new elements.
*/
CollectionModes["LAZY"] = "lazy";
})(CollectionModes || (CollectionModes = {}));
/**
* Axis of the arrangement of virtual list elements.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/directions.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
var Directions;
(function (Directions) {
/**
* Horizontal axis.
*/
Directions["HORIZONTAL"] = "horizontal";
/**
* Vertical axis.
*/
Directions["VERTICAL"] = "vertical";
})(Directions || (Directions = {}));
/**
* Methods for selecting list items.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/methods-for-selecting.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
var MethodsForSelecting;
(function (MethodsForSelecting) {
/**
* List items are not selectable.
*/
MethodsForSelecting["NONE"] = "none";
/**
* List items are selected one by one.
*/
MethodsForSelecting["SELECT"] = "select";
/**
* Multiple selection of list items.
*/
MethodsForSelecting["MULTI_SELECT"] = "multi-select";
})(MethodsForSelecting || (MethodsForSelecting = {}));
/**
* Snapping method.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/snapping-method.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
var SnappingMethods;
(function (SnappingMethods) {
/**
* Normal group rendering.
*/
SnappingMethods["NORMAL"] = "normal";
/**
* The group is rendered on a transparent background. List items below the group are not rendered.
*/
SnappingMethods["ADVANCED"] = "advanced";
})(SnappingMethods || (SnappingMethods = {}));
/**
* Focus Alignments.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/focus-alignments.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
var FocusAlignments;
(function (FocusAlignments) {
FocusAlignments["NONE"] = "none";
FocusAlignments["START"] = "start";
FocusAlignments["CENTER"] = "center";
FocusAlignments["END"] = "end";
})(FocusAlignments || (FocusAlignments = {}));
const DEFAULT_ITEM_SIZE = 24;
const DEFAULT_BUFFER_SIZE = 2;
const DEFAULT_MAX_BUFFER_SIZE = 10;
const DEFAULT_LIST_SIZE = 400;
const DEFAULT_SNAP = false;
const DEFAULT_SELECT_BY_CLICK = true;
const DEFAULT_COLLAPSE_BY_CLICK = true;
const DEFAULT_ENABLED_BUFFER_OPTIMIZATION = false;
const DEFAULT_DYNAMIC_SIZE = false;
const TRACK_BY_PROPERTY_NAME = 'id';
const DEFAULT_DIRECTION = Directions.VERTICAL;
const DEFAULT_COLLECTION_MODE = CollectionModes.NORMAL;
const DISPLAY_OBJECTS_LENGTH_MESUREMENT_ERROR = 1;
const MAX_SCROLL_TO_ITERATIONS = 5;
const DEFAULT_SNAPPING_METHOD = SnappingMethods.NORMAL;
const DEFAULT_SELECT_METHOD = MethodsForSelecting.NONE;
const DEFAULT_SCREEN_READER_MESSAGE = 'Showing items $1 to $2';
// presets
const BEHAVIOR_AUTO = 'auto';
const BEHAVIOR_INSTANT = 'instant';
const BEHAVIOR_SMOOTH = 'smooth';
const DISPLAY_BLOCK = 'block';
const DISPLAY_NONE = 'none';
const OPACITY_0 = '0';
const OPACITY_100 = '100';
const VISIBILITY_VISIBLE = 'visible';
const VISIBILITY_HIDDEN = 'hidden';
const SIZE_100_PERSENT = '100%';
const SIZE_AUTO = 'auto';
const POSITION_ABSOLUTE = 'absolute';
const POSITION_STICKY = 'sticky';
const TRANSLATE_3D = 'translate3d';
const ZEROS_TRANSLATE_3D = `${TRANSLATE_3D}(0,0,0)`;
const HIDDEN_ZINDEX = '-1';
const DEFAULT_ZINDEX = '0';
const TOP_PROP_NAME = 'top';
const LEFT_PROP_NAME = 'left';
const X_PROP_NAME = 'x';
const Y_PROP_NAME = 'y';
const WIDTH_PROP_NAME = 'width';
const HEIGHT_PROP_NAME = 'height';
const PX = 'px';
const SCROLL = 'scroll';
const SCROLL_END = 'scrollend';
const CLASS_LIST_VERTICAL = 'vertical';
const CLASS_LIST_HORIZONTAL = 'horizontal';
// styles
const PART_DEFAULT_ITEM = 'item';
const PART_ITEM_NEW = ' item-new';
const PART_ITEM_ODD = ' item-odd';
const PART_ITEM_EVEN = ' item-even';
const PART_ITEM_SNAPPED = ' item-snapped';
const PART_ITEM_SELECTED = ' item-selected';
const PART_ITEM_COLLAPSED = ' item-collapsed';
const PART_ITEM_FOCUSED = ' item-focused';
/**
* Virtual List Item Interface
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/models/base-virtual-list-item-component.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class BaseVirtualListItemComponent {
}
/**
* Methods for selecting list items.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/enums/method-for-selecting-types.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
var MethodsForSelectingTypes;
(function (MethodsForSelectingTypes) {
/**
* List items are not selectable.
*/
MethodsForSelectingTypes[MethodsForSelectingTypes["NONE"] = 0] = "NONE";
/**
* List items are selected one by one.
*/
MethodsForSelectingTypes[MethodsForSelectingTypes["SELECT"] = 1] = "SELECT";
/**
* Multiple selection of list items.
*/
MethodsForSelectingTypes[MethodsForSelectingTypes["MULTI_SELECT"] = 2] = "MULTI_SELECT";
})(MethodsForSelectingTypes || (MethodsForSelectingTypes = {}));
class NgVirtualListService {
_nextComponentId = 0;
_$itemClick = new Subject();
$itemClick = this._$itemClick.asObservable();
_$selectedIds = new BehaviorSubject(undefined);
$selectedIds = this._$selectedIds.asObservable();
_$collapsedIds = new BehaviorSubject([]);
$collapsedIds = this._$collapsedIds.asObservable();
_$methodOfSelecting = new BehaviorSubject(0);
$methodOfSelecting = this._$methodOfSelecting.asObservable();
set methodOfSelecting(v) {
this._$methodOfSelecting.next(v);
}
_$focusedId = new BehaviorSubject(null);
$focusedId = this._$focusedId.asObservable();
get focusedId() { return this._$focusedId.getValue(); }
selectByClick = DEFAULT_SELECT_BY_CLICK;
collapseByClick = DEFAULT_COLLAPSE_BY_CLICK;
_trackBox;
listElement = null;
_$displayItems = new BehaviorSubject([]);
$displayItems = this._$displayItems.asObservable();
_collection = [];
set collection(v) {
if (this._collection === v) {
return;
}
this._collection = v;
this._$displayItems.next(v);
}
get collection() { return this._collection; }
constructor() {
this._$methodOfSelecting.pipe(takeUntilDestroyed(), tap(v => {
switch (v) {
case MethodsForSelectingTypes.SELECT: {
const curr = this._$selectedIds.getValue();
if (typeof curr !== 'number' && typeof curr !== 'string') {
this._$selectedIds.next(undefined);
}
break;
}
case MethodsForSelectingTypes.MULTI_SELECT: {
if (!Array.isArray(this._$selectedIds.getValue())) {
this._$selectedIds.next([]);
}
break;
}
case MethodsForSelectingTypes.NONE:
default: {
this._$selectedIds.next(undefined);
break;
}
}
})).subscribe();
}
setSelectedIds(ids) {
if (JSON.stringify(this._$selectedIds.getValue()) !== JSON.stringify(ids)) {
this._$selectedIds.next(ids);
}
}
setCollapsedIds(ids) {
if (JSON.stringify(this._$collapsedIds.getValue()) !== JSON.stringify(ids)) {
this._$collapsedIds.next(ids);
}
}
itemClick(data) {
this._$itemClick.next(data);
if (this.collapseByClick) {
this.collapse(data);
}
if (this.selectByClick) {
this.select(data);
}
}
update() {
this._trackBox?.changes();
}
/**
* Selects a list item
* @param data
* @param selected - If the value is undefined, then the toggle method is executed, if false or true, then the selection/deselection is performed.
*/
select(data, selected = undefined) {
if (data && data.config.selectable) {
switch (this._$methodOfSelecting.getValue()) {
case MethodsForSelectingTypes.SELECT: {
const curr = this._$selectedIds.getValue();
if (selected === undefined) {
this._$selectedIds.next(curr !== data?.id ? data?.id : undefined);
}
else {
this._$selectedIds.next(selected ? data?.id : undefined);
}
break;
}
case MethodsForSelectingTypes.MULTI_SELECT: {
const curr = [...(this._$selectedIds.getValue() || [])], index = curr.indexOf(data.id);
if (selected === undefined) {
if (index > -1) {
curr.splice(index, 1);
this._$selectedIds.next(curr);
}
else {
this._$selectedIds.next([...curr, data.id]);
}
}
else if (selected) {
if (index > -1) {
this._$selectedIds.next(curr);
}
else {
this._$selectedIds.next([...curr, data.id]);
}
}
else {
if (index > -1) {
curr.splice(index, 1);
this._$selectedIds.next(curr);
}
else {
this._$selectedIds.next(curr);
}
}
break;
}
case MethodsForSelectingTypes.NONE:
default: {
this._$selectedIds.next(undefined);
}
}
}
}
/**
* Collapse list items
* @param data
* @param collapsed - If the value is undefined, then the toggle method is executed, if false or true, then the collapse/expand is performed.
*/
collapse(data, collapsed = undefined) {
if (data && data.config.sticky > 0 && data.config.collapsable) {
const curr = [...(this._$collapsedIds.getValue() || [])], index = curr.indexOf(data.id);
if (collapsed === undefined) {
if (index > -1) {
curr.splice(index, 1);
this._$collapsedIds.next(curr);
}
else {
this._$collapsedIds.next([...curr, data.id]);
}
}
else if (collapsed) {
if (index > -1) {
this._$collapsedIds.next(curr);
}
else {
this._$collapsedIds.next([...curr, data.id]);
}
}
else {
if (index > -1) {
curr.splice(index, 1);
this._$collapsedIds.next(curr);
}
else {
this._$collapsedIds.next(curr);
}
}
}
}
itemToFocus;
focus(element, align = FocusAlignments.CENTER) {
element.focus({ preventScroll: true });
if (element.parentElement) {
const pos = parseFloat(element.parentElement?.getAttribute('position') ?? '0');
this.itemToFocus?.(element, pos, align);
}
}
areaFocus(id) {
this._$focusedId.next(id);
}
initialize(trackBox) {
this._trackBox = trackBox;
}
generateComponentId() {
return this._nextComponentId = this._nextComponentId === Number.MAX_SAFE_INTEGER
? 0 : this._nextComponentId + 1;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
const isUndefinable = (value) => {
return value === undefined;
};
const isNullable = (value) => {
return value === null;
};
/**
* Int validator
* @param value Int
*/
const validateInt = (value, undefinable = false) => {
return (undefinable && isUndefinable(value)) || !Number.isNaN(Number.parseInt(`${value}`));
};
/**
* Float validator
* @param value Float
*/
const validateFloat = (value, undefinable = false) => {
return (undefinable && isUndefinable(value)) || !Number.isNaN(Number.parseFloat(`${value}`));
};
/**
* String validator
* @param value String
*/
const validateString = (value, undefinable = false, nullable = false) => {
return (undefinable && isUndefinable(value)) || (nullable && isNullable(value)) || typeof value === 'string';
};
/**
* Boolean validator
* @param value Boolean
*/
const validateBoolean = (value, undefinable = false) => {
return (undefinable && isUndefinable(value)) || typeof value === 'boolean';
};
/**
* Array validator
* @param value Array
*/
const validateArray = (value, undefinable = false, nullable = false) => {
return (undefinable && isUndefinable(value)) || (nullable && isNullable(value)) || Array.isArray(value);
};
/**
* Object validator
* @param value Object
*/
const validateObject = (value, undefinable = false, nullable = false) => {
return (undefinable && isUndefinable(value)) || (nullable && isNullable(value)) || typeof value === 'object';
};
/**
* Function validator
* @param value Function
*/
const validateFunction = (value, undefinable = false, nullable = false) => {
return (undefinable && isUndefinable(value)) || (nullable && isNullable(value)) || typeof value === 'function';
};
const ATTR_AREA_SELECTED = 'area-selected', TABINDEX = 'ng-vl-index', POSITION = 'position', POSITION_ZERO = '0', ID = 'item-id', KEY_SPACE = " ", KEY_ARR_LEFT = "ArrowLeft", KEY_ARR_UP = "ArrowUp", KEY_ARR_RIGHT = "ArrowRight", KEY_ARR_DOWN = "ArrowDown", EVENT_FOCUS_IN = 'focusin', EVENT_FOCUS_OUT = 'focusout', EVENT_KEY_DOWN = 'keydown';
const getElementByIndex = (index) => {
return `[${TABINDEX}="${index}"]`;
};
/**
* Virtual list item component
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/components/ng-virtual-list-item.component.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class NgVirtualListItemComponent extends BaseVirtualListItemComponent {
_id;
get id() {
return this._id;
}
_service = inject(NgVirtualListService);
_isSelected = false;
_isCollapsed = false;
config = signal({});
measures = signal(undefined);
focused = signal(false);
part = signal(PART_DEFAULT_ITEM);
regular = false;
data = signal(undefined);
_data = undefined;
set item(v) {
if (this._data === v) {
return;
}
this._data = v;
this.updatePartStr(v, this._isSelected, this._isCollapsed);
this.updateConfig(v);
this.updateMeasures(v);
this.update();
this.data.set(v);
}
_regularLength = SIZE_100_PERSENT;
set regularLength(v) {
if (this._regularLength === v) {
return;
}
this._regularLength = v;
this.update();
}
get item() {
return this._data;
}
get itemId() {
return this._data?.id;
}
itemRenderer = signal(undefined);
set renderer(v) {
this.itemRenderer.set(v);
}
_elementRef = inject((ElementRef));
get element() {
return this._elementRef.nativeElement;
}
_selectHandler = (data) =>
/**
* Selects a list item
* @param selected - If the value is undefined, then the toggle method is executed, if false or true, then the selection/deselection is performed.
*/
(selected = undefined) => {
const valid = validateBoolean(selected, true);
if (!valid) {
console.error('The "selected" parameter must be of type `boolean` or `undefined`.');
return;
}
this._service.select(data, selected);
};
_collapseHandler = (data) =>
/**
* Collapse list items
* @param collapsed - If the value is undefined, then the toggle method is executed, if false or true, then the collapse/expand is performed.
*/
(collapsed = undefined) => {
const valid = validateBoolean(collapsed, true);
if (!valid) {
console.error('The "collapsed" parameter must be of type `boolean` or `undefined`.');
return;
}
this._service.collapse(data, collapsed);
};
_focusHandler = () =>
/**
* Focus a list item
*/
(align = FocusAlignments.CENTER) => {
this.focus(align);
};
constructor() {
super();
this._id = this._service.generateComponentId();
this._elementRef.nativeElement.setAttribute('id', String(this._id));
const $data = toObservable(this.data), $focused = toObservable(this.focused);
$focused.pipe(takeUntilDestroyed(), tap(v => {
this._service.areaFocus(v ? this._id : this._service.focusedId === this._id ? null : this._service.focusedId);
})).subscribe();
fromEvent(this.element, EVENT_FOCUS_IN).pipe(takeUntilDestroyed(), tap(e => {
this.focused.set(true);
this.updateConfig(this._data);
this.updatePartStr(this._data, this._isSelected, this._isCollapsed);
})).subscribe(),
fromEvent(this.element, EVENT_FOCUS_OUT).pipe(takeUntilDestroyed(), tap(e => {
this.focused.set(false);
this.updateConfig(this._data);
this.updatePartStr(this._data, this._isSelected, this._isCollapsed);
})).subscribe(),
fromEvent(this.element, EVENT_KEY_DOWN).pipe(takeUntilDestroyed(), tap(e => {
switch (e.key) {
case KEY_SPACE: {
e.stopImmediatePropagation();
e.preventDefault();
if (this._service.selectByClick) {
this._service.select(this._data);
}
if (this._service.collapseByClick) {
this._service.collapse(this._data);
}
break;
}
case KEY_ARR_LEFT:
if (!this.config().isVertical) {
e.stopImmediatePropagation();
e.preventDefault();
this.focusPrev();
}
break;
case KEY_ARR_UP:
if (this.config().isVertical) {
e.stopImmediatePropagation();
e.preventDefault();
this.focusPrev();
}
break;
case KEY_ARR_RIGHT:
if (!this.config().isVertical) {
e.stopImmediatePropagation();
e.preventDefault();
this.focusNext();
}
break;
case KEY_ARR_DOWN:
if (this.config().isVertical) {
e.stopImmediatePropagation();
e.preventDefault();
this.focusNext();
}
break;
}
})).subscribe();
combineLatest([$data, this._service.$methodOfSelecting, this._service.$selectedIds, this._service.$collapsedIds]).pipe(takeUntilDestroyed(), map(([, m, selectedIds, collapsedIds]) => ({ method: m, selectedIds, collapsedIds })), tap(({ method, selectedIds, collapsedIds }) => {
switch (method) {
case MethodsForSelectingTypes.SELECT: {
const id = selectedIds, isSelected = id === this.itemId;
this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
this._isSelected = isSelected;
break;
}
case MethodsForSelectingTypes.MULTI_SELECT: {
const actualIds = selectedIds, isSelected = this.itemId !== undefined && actualIds && actualIds.includes(this.itemId);
this.element.setAttribute(ATTR_AREA_SELECTED, String(isSelected));
this._isSelected = isSelected;
break;
}
case MethodsForSelectingTypes.NONE:
default: {
this.element.removeAttribute(ATTR_AREA_SELECTED);
this._isSelected = false;
break;
}
}
const actualIds = collapsedIds, isCollapsed = this.itemId !== undefined && actualIds && actualIds.includes(this.itemId);
this._isCollapsed = isCollapsed;
this.updatePartStr(this._data, this._isSelected, isCollapsed);
this.updateConfig(this._data);
this.updateMeasures(this._data);
})).subscribe();
}
focusNext() {
if (this._service.listElement) {
const tabIndex = this._data?.config?.tabIndex ?? 0, length = this._service.collection?.length ?? 0;
let index = tabIndex;
while (index <= length) {
index++;
const el = this._service.listElement.querySelector(getElementByIndex(index));
if (el) {
this._service.focus(el);
break;
}
}
}
}
focusPrev() {
if (this._service.listElement) {
const tabIndex = this._data?.config?.tabIndex ?? 0;
let index = tabIndex;
while (index >= 0) {
index--;
const el = this._service.listElement.querySelector(getElementByIndex(index));
if (el) {
this._service.focus(el);
break;
}
}
}
}
focus(align = FocusAlignments.CENTER) {
if (this._service.listElement) {
const tabIndex = this._data?.config?.tabIndex ?? 0;
let index = tabIndex;
const el = this._service.listElement.querySelector(getElementByIndex(index));
if (el) {
this._service.focus(el, align);
}
}
}
updateMeasures(v) {
this.measures.set(v?.measures ? { ...v.measures } : undefined);
}
updateConfig(v) {
this.config.set({
...v?.config || {}, selected: this._isSelected, collapsed: this._isCollapsed, focused: this.focused(),
collapse: this._collapseHandler(v), select: this._selectHandler(v), focus: this._focusHandler(),
});
}
update() {
const data = this._data, regular = this.regular, length = this._regularLength;
if (data) {
this._elementRef.nativeElement.setAttribute(ID, `${data.id}`);
const styles = this._elementRef.nativeElement.style;
styles.zIndex = data.config.zIndex;
if (data.config.snapped) {
this._elementRef.nativeElement.setAttribute(POSITION, data.config.sticky === 1 ? POSITION_ZERO : `${data.config.isVertical ? data.measures.y : data.measures.x}`);
styles.transform = data.config.sticky === 1 ? ZEROS_TRANSLATE_3D : `${TRANSLATE_3D}(${data.config.isVertical ? 0 : data.measures.x}${PX}, ${data.config.isVertical ? data.measures.y : 0}${PX}, ${POSITION_ZERO})`;
;
if (!data.config.isSnappingMethodAdvanced) {
styles.position = POSITION_STICKY;
}
}
else {
styles.position = POSITION_ABSOLUTE;
if (regular) {
this._elementRef.nativeElement.setAttribute(POSITION, POSITION_ZERO);
styles.transform = `${TRANSLATE_3D}(${data.config.isVertical ? 0 : data.measures.delta}${PX}, ${data.config.isVertical ? data.measures.delta : 0}${PX}, ${POSITION_ZERO})`;
}
else {
this._elementRef.nativeElement.setAttribute(POSITION, `${data.config.isVertical ? data.measures.y : data.measures.x}`);
styles.transform = `${TRANSLATE_3D}(${data.config.isVertical ? 0 : data.measures.x}${PX}, ${data.config.isVertical ? data.measures.y : 0}${PX}, ${POSITION_ZERO})`;
}
}
styles.height = data.config.isVertical ? data.config.dynamic ? SIZE_AUTO : `${data.measures.height}${PX}` : regular ? length : SIZE_100_PERSENT;
styles.width = data.config.isVertical ? regular ? length : SIZE_100_PERSENT : data.config.dynamic ? SIZE_AUTO : `${data.measures.width}${PX}`;
}
else {
this._elementRef.nativeElement.removeAttribute(ID);
}
}
updatePartStr(v, isSelected, isCollapsed) {
let odd = false;
if (v?.index !== undefined) {
odd = v.index % 2 === 0;
}
let part = PART_DEFAULT_ITEM;
part += odd ? PART_ITEM_ODD : PART_ITEM_EVEN;
if (v ? v.config.snapped : false) {
part += PART_ITEM_SNAPPED;
}
if (isSelected) {
part += PART_ITEM_SELECTED;
}
if (isCollapsed) {
part += PART_ITEM_COLLAPSED;
}
if (v ? v.config.new : false) {
part += PART_ITEM_NEW;
}
if (this.focused()) {
part += PART_ITEM_FOCUSED;
}
this.part.set(part);
}
getBounds() {
const el = this._elementRef.nativeElement, { width, height } = el.getBoundingClientRect();
return { width, height };
}
show() {
const styles = this._elementRef.nativeElement.style;
if (this.regular) {
if (styles.display === DISPLAY_BLOCK) {
return;
}
styles.display = DISPLAY_BLOCK;
}
else {
if (styles.visibility === VISIBILITY_VISIBLE) {
return;
}
styles.visibility = VISIBILITY_VISIBLE;
}
styles.zIndex = this._data?.config?.zIndex ?? DEFAULT_ZINDEX;
}
hide() {
const styles = this._elementRef.nativeElement.style;
if (this.regular) {
if (styles.display === DISPLAY_NONE) {
return;
}
styles.display = DISPLAY_NONE;
}
else {
if (styles.visibility === VISIBILITY_HIDDEN) {
return;
}
styles.visibility = VISIBILITY_HIDDEN;
}
styles.position = POSITION_ABSOLUTE;
styles.transform = ZEROS_TRANSLATE_3D;
styles.zIndex = HIDDEN_ZINDEX;
}
onClickHandler() {
this._service.itemClick(this._data);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
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 _part = part();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"_part\" [attr.ng-vl-index]=\"_config.tabIndex || -1\" tabindex=\"0\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut, 'focus': focused()}\" (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 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: NgVirtualListItemComponent, decorators: [{
type: Component,
args: [{ selector: 'ng-virtual-list-item', imports: [CommonModule], host: {
'class': 'ngvl__item',
'role': 'listitem',
}, changeDetection: ChangeDetectionStrategy.OnPush, template: "@let item = data();\r\n@let _config = config();\r\n@let _part = part();\r\n@let _measures = measures();\r\n@let renderer = itemRenderer();\r\n\r\n@if (item) {\r\n <div #listItem [part]=\"_part\" [attr.ng-vl-index]=\"_config.tabIndex || -1\" tabindex=\"0\" class=\"ngvl-item__container\"\r\n [ngClass]=\"{'snapped': item.config.snapped, 'snapped-out': item.config.snappedOut, 'focus': focused()}\" (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"] }]
}], ctorParameters: () => [] });
/**
* Simple debounce function.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/debounce.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
const debounce = (cb, debounceTime = 0) => {
let timeout;
const dispose = () => {
if (timeout !== undefined) {
clearTimeout(timeout);
}
};
const execute = (...args) => {
dispose();
timeout = setTimeout(() => {
cb(...args);
}, debounceTime);
};
return {
/**
* Call handling method
*/
execute,
/**
* Method of destroying handlers
*/
dispose,
};
};
/**
* Switch css classes
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/toggleClassName.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
const toggleClassName = (el, className, removeClassName) => {
if (!el.classList.contains(className)) {
el.classList.add(className);
}
if (removeClassName) {
el.classList.remove(removeClassName);
}
};
/**
* Scroll event.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/scrollEvent.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class ScrollEvent {
_direction = 1;
get direction() { return this._direction; }
_scrollSize = 0;
get scrollSize() { return this._scrollSize; }
_scrollWeight = 0;
get scrollWeight() { return this._scrollWeight; }
_isVertical = true;
get isVertical() { return this._isVertical; }
_listSize = 0;
get listSize() { return this._listSize; }
_size = 0;
get size() { return this._size; }
_isStart = true;
get isStart() { return this._isStart; }
_isEnd = false;
get isEnd() { return this._isEnd; }
_delta = 0;
get delta() { return this._delta; }
_scrollDelta = 0;
get scrollDelta() { return this._scrollDelta; }
constructor(params) {
const { direction, isVertical, container, list, delta, scrollDelta } = params;
this._direction = direction;
this._isVertical = isVertical;
this._scrollSize = isVertical ? container.scrollTop : container.scrollLeft;
this._scrollWeight = isVertical ? container.scrollHeight : container.scrollWidth;
this._listSize = isVertical ? list.offsetHeight : list.offsetWidth;
this._size = isVertical ? container.offsetHeight : container.offsetWidth;
this._isEnd = (this._scrollSize + this._size) === this._scrollWeight;
this._delta = delta;
this._scrollDelta = scrollDelta;
this._isStart = this._scrollSize === 0;
}
}
/**
* Simple event emitter
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/eventEmitter.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class EventEmitter {
_listeners = {};
_disposed = false;
constructor() { }
/**
* Emits the event
*/
dispatch(event, ...args) {
const ctx = this;
const listeners = this._listeners[event];
if (Array.isArray(listeners)) {
for (let i = 0, l = listeners.length; i < l; i++) {
const listener = listeners[i];
if (listener) {
listener.apply(ctx, args);
}
}
}
}
/**
* Emits the event async
*/
dispatchAsync(event, ...args) {
queueMicrotask(() => {
if (this._disposed) {
return;
}
this.dispatch(event, ...args);
});
}
/**
* Returns true if the event listener is already subscribed.
*/
hasEventListener(eventName, handler) {
const event = eventName;
if (this._listeners.hasOwnProperty(event)) {
const listeners = this._listeners[event];
const index = listeners.findIndex(v => v === handler);
if (index > -1) {
return true;
}
}
return false;
}
/**
* Add event listener
*/
addEventListener(eventName, handler) {
const event = eventName;
if (!this._listeners.hasOwnProperty(event)) {
this._listeners[event] = [];
}
this._listeners[event].push(handler);
}
/**
* Remove event listener
*/
removeEventListener(eventName, handler) {
const event = eventName;
if (!this._listeners.hasOwnProperty(event)) {
return;
}
const listeners = this._listeners[event], index = listeners.findIndex(v => v === handler);
if (index > -1) {
listeners.splice(index, 1);
if (listeners.length === 0) {
delete this._listeners[event];
}
}
}
/**
* Remove all listeners
*/
removeAllListeners() {
const events = Object.keys(this._listeners);
while (events.length > 0) {
const event = events.pop();
if (event) {
const listeners = this._listeners[event];
if (Array.isArray(listeners)) {
while (listeners.length > 0) {
const listener = listeners.pop();
if (listener) {
this.removeEventListener(event, listener);
}
}
}
}
}
}
/**
* Method of destroying handlers
*/
dispose() {
this._disposed = true;
this.removeAllListeners();
}
}
class CMap {
_dict = {};
constructor(dict) {
if (dict) {
this._dict = { ...dict._dict };
}
}
get(key) {
const k = String(key);
return this._dict[k];
}
set(key, value) {
const k = String(key);
this._dict[k] = value;
return this;
}
has(key) {
return this._dict.hasOwnProperty(String(key));
}
delete(key) {
const k = String(key);
delete this._dict[k];
}
clear() {
this._dict = {};
}
}
const CACHE_BOX_CHANGE_EVENT_NAME = 'change';
const MAX_SCROLL_DIRECTION_POOL = 50, CLEAR_SCROLL_DIRECTION_TO = 10, DIR_BACK = '-1', DIR_NONE = '0', DIR_FORWARD = '1';
/**
* Cache map.
* Emits a change event on each mutation.
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/cacheMap.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class CacheMap extends EventEmitter {
_map = new CMap();
_snapshot = new CMap();
_version = 0;
_previousVersion = this._version;
_lifeCircleTimeout;
_delta = 0;
get delta() {
return this._delta;
}
_deltaDirection = 0;
set deltaDirection(v) {
this._deltaDirection = v;
this._scrollDirection = this.calcScrollDirection(v);
}
get deltaDirection() {
return this._deltaDirection;
}
_scrollDirectionCache = [];
_scrollDirection = 0;
get scrollDirection() {
return this._scrollDirection;
}
get version() {
return this._version;
}
_clearScrollDirectionDebounce = debounce(() => {
while (this._scrollDirectionCache.length > CLEAR_SCROLL_DIRECTION_TO) {
this._scrollDirectionCache.shift();
}
}, 10);
constructor() {
super();
this.lifeCircle();
}
changesDetected() {
return this._version !== this._previousVersion;
}
stopLifeCircle() {
clearTimeout(this._lifeCircleTimeout);
}
nextTick(cb) {
if (this._disposed) {
return;
}
this._lifeCircleTimeout = setTimeout(() => {
cb();
});
return this._lifeCircleTimeout;
}
lifeCircle() {
this.fireChangeIfNeed();
this.lifeCircleDo();
}
lifeCircleDo() {
this._previousVersion = this._version;
this.nextTick(() => {
this.lifeCircle();
});
}
clearScrollDirectionCache() {
this._clearScrollDirectionDebounce.execute();
}
calcScrollDirection(v) {
while (this._scrollDirectionCache.length >= MAX_SCROLL_DIRECTION_POOL) {
this._scrollDirectionCache.shift();
}
this._scrollDirectionCache.push(v);
const dict = { [DIR_BACK]: 0, [DIR_NONE]: 0, [DIR_FORWARD]: 0 };
for (let i = 0, l = this._scrollDirectionCache.length, li = l - 1; i < l; i++) {
const dir = String(this._scrollDirectionCache[i]);
dict[dir] += 1;
if (i === li) {
for (let d in dict) {
if (d === String(v)) {
continue;
}
dict[d] -= 1;
}
}
}
if (dict[DIR_BACK] > dict[DIR_NONE] && dict[DIR_BACK] > dict[DIR_FORWARD]) {
return -1;
}
else if (dict[DIR_FORWARD] > dict[DIR_BACK] && dict[DIR_FORWARD] > dict[DIR_NONE]) {
return 1;
}
return 0;
}
bumpVersion() {
if (this.changesDetected()) {
return;
}
const v = this._version === Number.MAX_SAFE_INTEGER ? 0 : this._version + 1;
this._version = v;
}
fireChangeIfNeed() {
if (this.changesDetected()) {
this.dispatch(CACHE_BOX_CHANGE_EVENT_NAME, this.version);
}
}
set(id, bounds) {
if (this._map.has(id)) {
const b = this._map.get(id), bb = bounds;
if (b.width === bb.width && b.height === bb.height) {
return this._map;
}
return this._map;
}
const v = this._map.set(id, bounds);
this.bumpVersion();
return v;
}
has(id) {
return this._map.has(id);
}
get(id) {
return this._map.get(id);
}
snapshot() {
this._snapshot = new CMap(this._map);
}
dispose() {
super.dispose();
this.stopLifeCircle();
this._snapshot.clear();
this._map.clear();
}
}
/**
* Tracks display items by property
* @link https://github.com/DjonnyX/ng-virtual-list/blob/20.x/projects/ng-virtual-list/src/lib/utils/tracker.ts
* @author Evgenii Grebennikov
* @email djonnyx@gmail.com
*/
class Tracker {
/**
* display objects dictionary of indexes by id
*/
_displayObjectIndexMapById = {};
set displayObjectIndexMapById(v) {
if (this._displayObjectIndexMapById === v) {
return;
}
this._displayObjectIndexMapById = v;
}
get displayObjectIndexMapById() {
return this._displayObjectIndexMapById;
}
/**
* Dictionary displayItems propertyNameId by items propertyNameId
*/
_trackMap = new CMap();
get trackMap() {
return this._trackMap;
}
_trackingPropertyName;
set trackingPropertyName(v) {
this._trackingPropertyName = v;
}
constructor(trackingPropertyName) {
this._trackingPropertyName = trackingPropertyName;
}
/**
* tracking by propName
*/
track(items, components, snapedComponent, direction) {
if (!items) {
return;
}
const idPropName = this._trackingPropertyName, untrackedItems = [...components], newTrackItems = [], isDown = direction === 0 || direction === 1;
let isRegularSnapped = false;
for (let i = isDown ? 0 : items.length - 1, l = isDown ? items.length : 0; isDown ? i < l : i >= l; isDown ? i++ : i--) {
const item = items[i], itemTrackingProperty = item[idPropName];
if (this._trackMap) {
if (this._trackMap.has(itemTrackingProperty)) {
const diId = this._trackMap.get(itemTrackingProperty), compIndex = this._displayObjectIndexMapById[diId], comp = components[compIndex];
const compId = comp?.instance?.id;
if (comp !== undefined && compId === diId) {
const indexByUntrackedItems = untrackedItems.findIndex(v => {
return v.instance.id === compId;
});
if (indexByUntrackedItems > -1) {
if (snapedComponent) {
if (item['config']['snapped'] || item['config']['snappedOut']) {
isRegularSnapped = true;
snapedComponent.instance.item = item;
snapedComponent.instance.show();
}
}
if (snapedComponent) {
if (item['config']['snapped'] || item['config']['snappedOut']) {
comp.instance.item = null;
comp.instance.hide();
}
else {
comp.instance.item = item;
comp.instance.show();
}
}
else {
comp.instance.item = item;
comp.instance.show();
}
untrackedItems.splice(indexByUntrackedItems, 1);
continue;
}
}
}
else {
this._trackMap.delete(itemTrackingProperty);
}
}
if (untrackedItems.length > 0) {
newTrackItems.push(item);
}
}
for (let i = 0, l = newTrackItems.length; i < l; i++) {
if (untrackedItems.length > 0) {
const comp = untrackedItems.shift(), item = newTrackItems[i], itemTrackingProperty = item[idPropName];
if (comp) {
if (snapedComponent) {
if (item['config']['snapped'] || item['config']['snappedOut']) {
isRegularSnapped = true;
snapedComponent.instance.item = item;
snapedComponent.instance.show();
}
}
if (snapedComponent) {
if (item['config']['snapped'] || item['config']['snappedOut']) {
comp.instance.item = null;
comp.instance.hide();
}
else {
comp.instance.item = item;
comp.instance.show();
}
}
else {
comp.instance.item = item;
comp.instance.show();
}
if (this._trackMap) {
this._trackMap.set(itemTrackingProperty, comp.instance.id);
}
}
}
}
if (untrackedItems.length) {
for (let i = 0, l = untrackedItems.length; i < l; i++) {
const comp = untrackedItems[i];
comp.instance.item = null;
comp.instance.hide();
}
}
if (!isRegularSnapped) {
if (snapedComponent) {
snapedComponent.instance.item = null;
snapedComponent.instance.hide();
}
}
}
untrackComponentByIdProperty(component) {
if (!component) {
return;
}
const propertyIdName = this._trackingPropertyName;
if (this._trackMap && component[propertyIdName] !== undefined) {
this._trackMap.delete(propertyIdName);
}
}
dispose() {
if (this._trackMap) {
this._trackMap.clear();
}
}
}
const DEFAULT_EXTRA = {
extremumThreshold: 2,
bufferSize: 10,
};
const bufferInterpolation = (currentBufferValue, array, value, extra) => {
const { extremumThreshold = DEFAULT_EXTRA.extremumThreshold, bufferSize = DEFAULT_EXTRA.bufferSize, } = extra ?? DEFAULT_EXTRA;
if (currentBufferValue < value) {
let i = 0;
while (i < extremumThreshold) {
array.push(value);
i++;
}
}
else {
array.push(value);
}
while (array.length >= bufferSize) {
array.shift();
}
const l = array.length;
let buffer = 0;
for (let i = 0; i < l; i++) {
buffer += array[i];
}
return Math.ceil(buffer / l);
};
var TrackBoxEvents;
(function (TrackBoxEvents) {
TrackBoxEvents["CHANGE"] = "change";
TrackBoxEvents["RESET"] = "reset";
})(TrackBoxEvents || (TrackBoxEvents = {}));
var ItemDisplayMethods;
(function (ItemDisplayMethods) {
ItemDisplayMethods[ItemDisplayMethods["CREATE"] = 0] = "CREATE";
ItemDisplayMethods[ItemDisplayMethods["UPDATE"] = 1] = "UPDATE";
ItemDisplayMethods[ItemDisplayMethods["DELETE"] = 2] = "DELETE";
ItemDisplayMethods[ItemDisplayMethods["NOT_CHANGED"] = 3] = "NOT_CHANGED";
})(ItemDisplayMethods || (ItemDisplayMethods = {}));
const DEFAULT_BUFFER_EXTREMUM_THRESHOLD = 15, DEFAULT_MAX_BUFFER_SEQUENCE_LENGTH = 30, DEFAULT_RESET_BUFFER_SIZE_TIMEOUT = 10000, IS_NEW = 'isNew';
/**
* An object that performs tracking, calculations and caching.
* @link https://github.com/DjonnyX/