@deepkit/desktop-ui
Version:
Library for desktop UI widgets in Angular 10+
1,203 lines (1,014 loc) • 42.1 kB
text/typescript
/*
* Deepkit Framework
* Copyright (C) 2021 Deepkit UG, Marc J. Schmidt
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the MIT License.
*
* You should have received a copy of the MIT License along with this program.
*/
import {
AfterViewInit,
ApplicationRef,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
HostBinding,
HostListener,
inject,
Input,
NgZone,
OnChanges,
OnDestroy,
OnInit,
Output,
QueryList,
SimpleChanges,
SkipSelf,
TemplateRef,
ViewChild,
ViewChildren,
} from '@angular/core';
import {
arrayClear,
arrayHasItem,
arrayRemoveItem,
eachPair,
empty,
first,
getPathValue,
indexOf,
isArray,
isNumber,
nextTick,
} from '@deepkit/core';
import { isObservable, Observable } from 'rxjs';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { DropdownComponent } from '../button';
import { detectChangesNextFrame } from '../app/utils';
import { findParentWithClass, getHammer } from '../../core/utils';
/**
* Directive to allow dynamic content in a cell.
*
* ```html
* <dui-table-column>
* <ng-container *duiTableCell="let item">
* {{item.fieldName | date}}
* </ng-container>
* </dui-table-column>
* ```
*/
export class TableCellDirective {
constructor(public template: TemplateRef<any>) {
}
}
/**
* Can be used to define own dropdown items once the user opens the header context menu.
*/
export class TableCustomHeaderContextMenuDirective {
constructor(public readonly dropdown: DropdownComponent) {
}
}
/**
* Can be used to define own dropdown items once the user opens the row context menu.
*/
export class TableCustomRowContextMenuDirective {
constructor(public readonly dropdown: DropdownComponent) {
}
}
/**
* Directive to allow dynamic content in a column header.
*
* ```html
* <dui-table-column name="fieldName">
* <ng-container *duiTableHead>
* <strong>Header</strong>
* </ng-container>
* </dui-table-column>
* ```
*/
export class TableHeaderDirective {
constructor(public template: TemplateRef<any>) {
}
}
/**
* Defines a new column.
*/
export class TableColumnDirective {
/**
* The name of the column. Needs to be unique. If no renderer (*duiTableCell) is specified, this
* name is used to render the content T[name].
*/
name: string = '';
/**
* A different header name. Use dui-table-header to render HTML there.
*/
header?: string;
/**
* Default width.
*/
width?: number | string = 100;
/**
* Adds additional class to the columns cells.
*/
class: string = '';
/**
* Whether this column is start hidden. User can unhide it using the context menu on the header.
*/
hidden: boolean | '' = false;
sortable: boolean = true;
hideable: boolean = true;
/**
* At which position this column will be placed.
*/
position?: number;
/**
* This is the new position when the user moved it manually.
* @hidden
*/
overwrittenPosition?: number;
cell?: TableCellDirective;
headerDirective?: TableHeaderDirective;
isHidden() {
return this.hidden !== false;
}
toggleHidden() {
this.hidden = !this.isHidden();
}
/**
* @hidden
*/
getWidth(): string | undefined {
if (!this.width) return undefined;
if (isNumber(this.width)) {
return this.width + 'px';
}
return this.width;
}
/**
* @hidden
*/
public getPosition() {
if (this.overwrittenPosition !== undefined) {
return this.overwrittenPosition;
}
return this.position;
}
}
export class TableComponent<T> implements AfterViewInit, OnInit, OnChanges, OnDestroy {
protected app = inject(ApplicationRef);
/**
* @hidden
*/
tabindex = 0;
borderless: boolean | '' = false;
/**
* Array of items that should be used for each row.
*/
public items!: T[] | Observable<T[]>;
/**
* Since dui-table has virtual-scroll active per default, it's required to define the itemHeight to
* make scrolling actually workable correctly.
*/
public itemHeight: number = 25;
/**
* Whether the table height just prints all rows, or if virtual scrolling is enabled.
*/
public autoHeight: boolean = false;
/**
* Whether the table row should have a hover effect.
*/
public hover: boolean | '' = false;
/**
* Current calculated height, used only when autoHeight is given.
*/
public height: number = 23;
/**
* Whether the header should be shown.
*/
public showHeader: boolean = true;
/**
* How many columns (from the left) are frozen (stay visible even if user scrolls horizontally).
*/
public freezeColumns: number = 0;
/**
* Default field of T for sorting.
*/
public defaultSort: string = '';
/**
* Default sorting order.
*/
public defaultSortDirection: 'asc' | 'desc' = 'asc';
/**
* Whether rows are selectable.
*/
public selectable: boolean | '' = false;
/**
* Whether multiple rows are selectable at the same time.
*/
public multiSelect: boolean | '' = false;
/**
* TrackFn for ngFor to improve performance. Default is order by index.
*/
public trackFn?: (index: number, item: T) => any;
/**
* Not used yet.
*/
public displayInitial: number = 20;
/**
* Not used yet.
*/
public increaseBy: number = 10;
/**
* Filter function.
*/
public filter?: (item: T) => boolean;
public rowClass?: (item: T) => string | undefined;
public cellClass?: (item: T, column: string) => string | undefined;
/**
* When the user changes the order or width of the columns, the information is stored
* in localStorage using this key, prefixed with `@dui/table/`.
*/
public preferenceKey: string = 'root';
/**
* Filter query.
*/
public filterQuery?: string;
public columnState: { name: string, position: number, visible: boolean }[] = [];
/**
* Against which fields filterQuery should run.
*/
public filterFields?: string[];
/**
* Alternate object value fetcher, important for sorting and filtering.
*/
public valueFetcher = (object: any, path: string): any => {
return getPathValue(object, path);
};
/**
* A hook to provide custom sorting behavior for certain columns.
*/
public sortFunction?: (sort: { [name: string]: 'asc' | 'desc' }) => (((a: T, b: T) => number) | undefined);
/**
* Whether sorting is enabled (clicking headers trigger sort).
*/
public sorting: boolean = true;
noFocusOutline: boolean | '' = false;
public sort: { [column: string]: 'asc' | 'desc' } = {};
public rawItems: T[] = [];
public sorted: T[] = [];
public selectedMap = new Map<T, boolean>();
/**
* Elements that are selected, by reference.
*/
public selected: T[] = [];
protected selectedHistory: T[] = [];
/**
* Elements that are selected, by reference.
*/
public selectedChange: EventEmitter<T[]> = new EventEmitter();
public sortedChange: EventEmitter<T[]> = new EventEmitter();
public cellSelect: EventEmitter<{ item: T, cell: string } | undefined> = new EventEmitter();
/**
* When a row gets double clicked.
*/
public dbclick: EventEmitter<T> = new EventEmitter();
public customSort: EventEmitter<{ [column: string]: 'asc' | 'desc' }> = new EventEmitter();
public cellDblClick: EventEmitter<{ item: T, column: string }> = new EventEmitter();
public cellClick: EventEmitter<{ item: T, column: string }> = new EventEmitter();
header?: ElementRef;
ths?: QueryList<ElementRef<HTMLElement>>;
columnDefs?: QueryList<TableColumnDirective>;
customHeaderDropdown?: TableCustomHeaderContextMenuDirective;
customRowDropdown?: TableCustomRowContextMenuDirective;
viewport!: CdkVirtualScrollViewport;
viewportElement!: ElementRef;
public sortedColumnDefs: TableColumnDirective[] = [];
visibleColumns: TableColumnDirective[] = [];
columnMap: { [name: string]: TableColumnDirective } = {};
public displayedColumns?: string[] = [];
protected ignoreThisSort = false;
public scrollTop = 0;
constructor(
protected element: ElementRef,
protected cd: ChangeDetectorRef,
protected parentCd: ChangeDetectorRef,
protected zone: NgZone,
) {
}
public setColumnWidth(column: TableColumnDirective, width: number) {
column.width = width;
detectChangesNextFrame(this.cd, () => {
this.storePreference();
});
}
ngOnInit() {
if (this.defaultSort) {
this.sort[this.defaultSort] = this.defaultSortDirection;
}
}
ngOnDestroy(): void {
}
freezeLeft(columns: TableColumnDirective[], untilIndex: number): number {
let left = 0;
for (let i = 0; i < untilIndex; i++) {
const width = columns[i].width;
if (width === undefined) continue;
left += 'number' === typeof width ? width : parseInt(width);
}
return left;
}
onResize() {
nextTick(() => {
this.viewport.checkViewportSize();
});
}
resetAll() {
localStorage.removeItem('@dui/table/preferences-' + this.preferenceKey);
if (!this.columnDefs) return;
for (const column of this.columnDefs.toArray()) {
column.width = 100;
column.hidden = false;
column.overwrittenPosition = undefined;
}
this.visibleColumns = this.getVisibleColumns(this.sortedColumnDefs);
}
storePreference() {
if ('undefined' === typeof localStorage) return;
const preferences: { [name: string]: { hidden: boolean | '', width?: number | string, order?: number } } = {};
if (!this.columnDefs) return;
for (const column of this.columnDefs.toArray()) {
preferences[column.name] = {
width: column.width,
order: column.overwrittenPosition,
hidden: column.hidden
};
}
this.visibleColumns = this.getVisibleColumns(this.sortedColumnDefs);
localStorage.setItem('@dui/table/preferences-' + this.preferenceKey, JSON.stringify(preferences));
}
loadPreference() {
if ('undefined' === typeof localStorage) return;
const preferencesJSON = localStorage.getItem('@dui/table/preferences-' + this.preferenceKey);
if (!preferencesJSON) return;
const preferences = JSON.parse(preferencesJSON);
for (const i in preferences) {
if (!preferences.hasOwnProperty(i)) continue;
if (!this.columnMap[i]) continue;
if (preferences[i].width !== undefined) this.columnMap[i].width = preferences[i].width;
if (preferences[i].order !== undefined) this.columnMap[i].overwrittenPosition = preferences[i].order;
if (preferences[i].hidden !== undefined) this.columnMap[i].hidden = preferences[i].hidden;
}
}
dblClickCell(event: MouseEvent) {
if (!this.cellDblClick.observers.length) return;
if (!event.target) return;
const cell = findParentWithClass(event.target as HTMLElement, 'table-cell');
if (!cell) return;
const i = parseInt(cell.getAttribute('row-i') || '', 10);
const column = cell.getAttribute('row-column') || '';
this.cellDblClick.emit({ item: this.sorted[i], column });
}
clickCell(event: MouseEvent) {
if (!this.cellClick.observers.length) return;
if (!event.target) return;
const cell = findParentWithClass(event.target as HTMLElement, 'table-cell');
if (!cell) return;
const i = parseInt(cell.getAttribute('row-i') || '', 10);
const column = cell.getAttribute('row-column') || '';
this.cellClick.emit({ item: this.sorted[i], column });
}
/**
* Toggles the sort by the given column name.
*/
public sortBy(name: string, $event?: MouseEvent) {
if (!this.sorting) return;
if (this.ignoreThisSort) {
this.ignoreThisSort = false;
return;
}
if ($event && $event.button === 2) return;
//only when shift is pressed do we activate multi-column sort
if (!$event || !$event.shiftKey) {
for (const member in this.sort) if (member !== name) delete this.sort[member];
}
if (this.columnMap[name]) {
const headerDef = this.columnMap[name];
if (!headerDef.sortable) {
return;
}
}
if (!this.sort[name]) {
this.sort[name] = 'asc';
} else {
if (this.sort[name] === 'asc') this.sort[name] = 'desc';
else if (this.sort[name] === 'desc') delete this.sort[name];
}
if (this.customSort.observers.length) {
this.customSort.emit(this.sort);
} else {
this.doSort();
}
}
/**
* @hidden
*/
trackByFn = (index: number, item: any) => {
return this.trackFn ? this.trackFn(index, item) : index;
};
/**
* @hidden
*/
trackByColumn(index: number, column: TableColumnDirective) {
return column.name;
}
/**
* @hidden
*/
filterSorted(items: T[]): T[] {
//apply filter
if (this.filter || (this.filterQuery && this.filterFields)) {
return items.filter((v) => this.filterFn(v));
}
return items;
}
async initHeaderMovement() {
if ('undefined' !== typeof window && this.header && this.ths) {
const Hammer = await getHammer();
if (!Hammer) return;
const mc = new Hammer(this.header!.nativeElement);
mc.add(new Hammer.Pan({ direction: Hammer.DIRECTION_ALL, threshold: 1 }));
interface Box {
left: number;
width: number;
element: HTMLElement;
directive: TableColumnDirective;
}
const THsBoxes: Box[] = [];
let element: HTMLElement | undefined;
let elementCells: HTMLElement[] = [];
let originalPosition = -1;
let foundBox: Box | undefined;
let rowCells: { cells: HTMLElement[] }[] = [];
let startOffsetLeft = 0;
let offsetLeft = 0;
let startOffsetWidth = 0;
let animationFrame: any;
mc.on('panstart', (event: HammerInput) => {
foundBox = undefined;
if (this.ths && event.target.classList.contains('th')) {
element = event.target as HTMLElement;
element.style.zIndex = '1000000';
element.style.opacity = '0.8';
startOffsetLeft = element.offsetLeft;
offsetLeft = element.offsetLeft;
startOffsetWidth = element.offsetWidth;
arrayClear(THsBoxes);
rowCells = [];
for (const th of this.ths.toArray()) {
const directive: TableColumnDirective = this.sortedColumnDefs.find((v) => v.name === th.nativeElement.getAttribute('name')!)!;
const cells = [...this.element.nativeElement.querySelectorAll('div[row-column="' + directive.name + '"]')] as any as HTMLElement[];
if (th.nativeElement === element) {
originalPosition = this.sortedColumnDefs.indexOf(directive);
elementCells = cells;
for (const cell of elementCells) {
cell.classList.add('active-drop');
}
} else {
for (const cell of cells) {
cell.classList.add('other-cell');
}
th.nativeElement.classList.add('other-cell');
}
THsBoxes.push({
left: th.nativeElement.offsetLeft,
width: th.nativeElement.offsetWidth,
element: th.nativeElement,
directive: directive,
});
rowCells.push({ cells: cells });
}
}
});
mc.on('panend', (event: HammerInput) => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
if (element) {
element.style.left = '';
element.style.zIndex = '';
element.style.opacity = '';
for (const t of rowCells) {
for (const cell of t.cells) {
cell.classList.remove('active-drop');
cell.classList.remove('other-cell');
cell.style.left = '0px';
}
}
this.ignoreThisSort = true;
for (const box of THsBoxes) {
box.element.style.left = '0px';
box.element.classList.remove('other-cell');
}
if (foundBox) {
const newPosition = this.sortedColumnDefs.indexOf(foundBox.directive);
if (originalPosition !== newPosition) {
const directive = this.sortedColumnDefs[originalPosition];
this.sortedColumnDefs.splice(originalPosition, 1);
this.sortedColumnDefs.splice(newPosition, 0, directive);
for (let [i, v] of eachPair(this.sortedColumnDefs)) {
v.overwrittenPosition = i;
}
this.sortColumnDefs();
}
}
this.storePreference();
element = undefined;
}
});
mc.on('pan', (event: HammerInput) => {
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
animationFrame = nextTick(() => {
if (element) {
element!.style.left = (event.deltaX) + 'px';
const offsetLeft = startOffsetLeft + event.deltaX;
for (const cell of elementCells) {
cell.style.left = (event.deltaX) + 'px';
}
let afterElement = false;
foundBox = undefined;
for (const [i, box] of eachPair(THsBoxes)) {
if (box.element === element) {
afterElement = true;
continue;
}
box.element.style.left = '0px';
for (const cell of rowCells[i].cells) {
cell.style.left = '0px';
}
if (!afterElement && box.left + (box.width / 2) > offsetLeft) {
//the dragged element is before the current
box.element.style.left = startOffsetWidth + 'px';
for (const cell of rowCells[i].cells) {
cell.style.left = startOffsetWidth + 'px';
}
if (foundBox && box.left > foundBox.left) {
//we found already a box that fits and that is more left
continue;
}
foundBox = box;
} else if (afterElement && box.left + (box.width / 2) < offsetLeft + startOffsetWidth) {
//the dragged element is after the current
box.element.style.left = -startOffsetWidth + 'px';
for (const cell of rowCells[i].cells) {
cell.style.left = -startOffsetWidth + 'px';
}
foundBox = box;
}
}
}
});
});
}
}
ngAfterViewInit(): void {
this.viewport.renderedRangeStream.subscribe(() => {
this.cd.detectChanges();
});
this.zone.runOutsideAngular(() => {
this.viewportElement.nativeElement.addEventListener('scroll', () => {
const scrollLeft = this.viewportElement.nativeElement.scrollLeft;
this.header!.nativeElement.scrollLeft = scrollLeft;
});
});
this.initHeaderMovement();
if (this.columnDefs) {
setTimeout(() => {
this.columnDefs!.changes.subscribe(() => {
this.updateDisplayColumns();
this.loadPreference();
this.sortColumnDefs();
});
this.updateDisplayColumns();
this.loadPreference();
this.sortColumnDefs();
});
}
}
public sortColumnDefs() {
if (this.columnDefs) {
const originalDefs = this.columnDefs.toArray();
this.sortedColumnDefs = this.columnDefs.toArray().slice(0);
this.sortedColumnDefs = this.sortedColumnDefs.sort((a: TableColumnDirective, b: TableColumnDirective) => {
const aPosition = a.getPosition() === undefined ? originalDefs.indexOf(a) : a.getPosition()!;
const bPosition = b.getPosition() === undefined ? originalDefs.indexOf(b) : b.getPosition()!;
if (aPosition > bPosition) return 1;
if (aPosition < bPosition) return -1;
return 0;
});
detectChangesNextFrame(this.cd);
}
}
getVisibleColumns(t: TableColumnDirective[]): TableColumnDirective[] {
return t.filter(v => !v.isHidden());
}
filterFn(item: T) {
if (this.filter) {
return this.filter(item);
}
if (this.filterQuery && this.filterFields) {
const q = this.filterQuery!.toLowerCase();
for (const field of this.filterFields) {
if (-1 !== String((item as any)[field]).toLowerCase().indexOf(q)) {
return true;
}
}
return false;
}
return true;
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.preferenceKey) {
this.loadPreference();
}
if (changes.items) {
if (isObservable(this.items)) {
this.items.subscribe((items: T[]) => {
this.rawItems = items;
this.sorted = items;
this.doSort();
this.viewport.checkViewportSize();
});
} else if (isArray(this.items)) {
this.rawItems = this.items;
this.sorted = this.items;
this.doSort();
this.viewport.checkViewportSize();
} else {
this.rawItems = [];
this.sorted = [];
this.doSort();
this.viewport.checkViewportSize();
}
}
if (changes.selected) {
this.selectedMap.clear();
if (this.selected) {
for (const v of this.selected) {
this.selectedMap.set(v, true);
}
}
}
this.visibleColumns = this.getVisibleColumns(this.sortedColumnDefs);
setTimeout(() => {
this.viewport.checkViewportSize();
});
}
private updateDisplayColumns() {
this.displayedColumns = [];
this.columnMap = {};
this.sortColumnDefs();
if (this.columnDefs) {
for (const column of this.columnDefs.toArray()) {
this.displayedColumns.push(column.name!);
this.columnMap[column.name!] = column;
}
this.doSort();
}
this.visibleColumns = this.getVisibleColumns(this.sortedColumnDefs);
}
private doSort() {
if (this.customSort.observers.length) return;
if (empty(this.sorted)) {
this.sorted = this.rawItems;
}
if (this.sortFunction) {
this.sorted.sort(this.sortFunction(this.sort));
} else {
const sort = Object.entries(this.sort);
sort.reverse(); //we start from bottom
let sortRoot = (a: any, b: any) => 0;
for (const [name, dir] of sort) {
sortRoot = this.createSortFunction(name, dir, sortRoot);
}
this.sorted.sort(sortRoot);
}
this.sortedChange.emit(this.sorted);
this.height = (this.sorted.length * this.itemHeight) + (this.showHeader ? 23 : 0) + 10; //10 is scrollbar padding
this.sorted = this.sorted.slice(0);
detectChangesNextFrame(this.parentCd);
}
protected createSortFunction(sortField: string, dir: 'asc' | 'desc', next?: (a: any, b: any) => number) {
return (a: T, b: T) => {
const aV = this.valueFetcher(a, sortField);
const bV = this.valueFetcher(b, sortField);
if (aV === undefined && bV === undefined) return next ? next(a, b) : 0;
if (aV === undefined && bV !== undefined) return +1;
if (aV !== undefined && bV === undefined) return -1;
if (dir === 'asc') {
if (aV > bV) return 1;
if (aV < bV) return -1;
} else {
if (aV > bV) return -1;
if (aV < bV) return 1;
}
return next ? next(a, b) : 0;
};
}
/**
* @hidden
*/
onFocus(event: KeyboardEvent) {
if (event.key === 'Enter') {
const firstSelected = first(this.selected);
if (firstSelected) {
this.dbclick.emit(firstSelected);
}
}
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.preventDefault();
const firstSelected = first(this.selected);
if (!firstSelected) {
this.select(this.sorted[0]);
return;
}
let index = indexOf(this.sorted, firstSelected);
// if (-1 === index) {
// this.select(this.sorted[0]);
// this.paginator.pageIndex = 0;
// return;
// }
if (event.key === 'ArrowUp') {
if (0 === index) {
return;
}
index--;
}
if (event.key === 'ArrowDown') {
if (empty(this.sorted)) {
return;
}
index++;
}
if (this.sorted[index]) {
const item = this.sorted[index];
// if (event.shiftKey) {
// this.selectedMap[item.id] = true;
// this.selected.push(item);
// } else {
this.select(item);
const scrollTop = this.viewport.measureScrollOffset();
const viewportSize = this.viewport.getViewportSize();
const itemTop = this.itemHeight * index;
if (itemTop + this.itemHeight > viewportSize + scrollTop) {
const diff = (itemTop + this.itemHeight) - (viewportSize + scrollTop);
this.viewport.scrollToOffset(scrollTop + diff);
}
if (itemTop < scrollTop) {
const diff = (itemTop) - (scrollTop);
this.viewport.scrollToOffset(scrollTop + diff);
}
}
this.selectedChange.emit(this.selected.slice(0));
this.cd.markForCheck();
}
}
public deselect(item: T) {
arrayRemoveItem(this.selected, item);
this.selectedMap.delete(item);
detectChangesNextFrame(this.parentCd);
}
public select(item: T, $event?: MouseEvent) {
if (this.selectable === false) {
return;
}
let cellSelectFired = false;
if ($event && $event.target) {
const cell = findParentWithClass($event.target as HTMLElement, 'table-cell');
if (cell) {
const column = cell.getAttribute('row-column') || '';
if (column) {
this.cellSelect.emit({ item, cell: column });
cellSelectFired = true;
}
}
}
if (!cellSelectFired) {
this.cellSelect.emit();
}
if (this.multiSelect === false) {
this.selected = [item];
this.selectedMap.clear();
this.selectedMap.set(item, true);
} else {
if ($event && $event.shiftKey) {
const indexSelected = this.sorted.indexOf(item);
if (this.selected[0]) {
const firstSelected = this.sorted.indexOf(this.selected[0]);
this.selectedMap.clear();
this.selected = [];
if (firstSelected < indexSelected) {
//we select all from index -> indexSelected, downwards
for (let i = firstSelected; i <= indexSelected; i++) {
this.selected.push(this.sorted[i]);
this.selectedMap.set(this.sorted[i], true);
}
} else {
//we select all from indexSelected -> index, upwards
for (let i = firstSelected; i >= indexSelected; i--) {
this.selected.push(this.sorted[i]);
this.selectedMap.set(this.sorted[i], true);
}
}
} else {
//we start at 0 and select all until index
for (let i = 0; i <= indexSelected; i++) {
this.selected.push(this.sorted[i]);
this.selectedMap.set(this.sorted[i], true);
}
}
} else if ($event && $event.metaKey) {
if (arrayHasItem(this.selected, item)) {
arrayRemoveItem(this.selected, item);
this.selectedMap.delete(item);
} else {
this.selectedMap.set(item, true);
this.selected.push(item);
}
} else {
const isRightButton = $event && $event.button == 2;
const isItemSelected = arrayHasItem(this.selected, item);
const resetSelection = !isItemSelected || !isRightButton;
if (resetSelection) {
this.selected = [item];
this.selectedMap.clear();
this.selectedMap.set(item, true);
}
}
}
this.selectedChange.emit(this.selected);
detectChangesNextFrame(this.parentCd);
}
}