UNPKG

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
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/