argos/dmp-frontend/src/common/modules/hybrid-listing/hybrid-listing.component.ts

745 lines
18 KiB
TypeScript

import { ChangeDetectorRef, Component, ContentChild, ElementRef, EventEmitter, Input, NgZone, OnChanges, OnInit, Output, PipeTransform, SimpleChanges, TemplateRef, ViewChild, ViewEncapsulation, inject } from '@angular/core';
import { BaseComponent } from '@common/base/base.component';
import { TranslateService } from '@ngx-translate/core';
import { DatatableComponent, SelectionType, TableColumn } from '@swimlane/ngx-datatable';
import { debounceTime, filter, takeUntil } from 'rxjs/operators';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { BatchActionsDirective } from '@common/modules/hybrid-listing/directives/batch-actions.directive';
import { PageEvent } from '@angular/material/paginator';
import { nameof } from 'ts-simple-nameof';
import { Subject } from 'rxjs';
import { CollectionUtils } from '@app/core/services/utilities/collection-utils.service';
@Component({
selector: 'app-hybrid-listing',
templateUrl: './hybrid-listing.component.html',
styleUrls: ['./hybrid-listing.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class HybridListingComponent extends BaseComponent implements OnInit, OnChanges {
@ViewChild('wrapperCard', {static: true, read: ElementRef}) wrapperCard: ElementRef<any>;
@ViewChild(DatatableComponent) table: DatatableComponent;
private changeDetectorRef = inject(ChangeDetectorRef);
private ngZone = inject(NgZone);
@ViewChild('functionValueTemplate', { static: true }) functionValueTemplate: TemplateRef<any>;
private _batchActions?: BatchActionsDirective
@ContentChild(BatchActionsDirective) set batchActions(batchActions: BatchActionsDirective){
this._batchActions = batchActions;
this.refreshColumnDefinitionData();
if(this._batchActions){
this._batchActions.getSelections = () =>{
return this.selected;
}
}
}
get batchActions(): BatchActionsDirective{
return this._batchActions;
}
private _listItemTemplate: TemplateRef<any>;
@Input() public set listItemTemplate(value: TemplateRef<any>){
if(!value){
this.mode = HybridListingMode.Table;
}
this._listItemTemplate = value;
}
public get listItemTemplate(): TemplateRef<any>{
return this._listItemTemplate;
}
private _cardItemTemplate: TemplateRef<any>;
@Input() public set cardItemTemplate(value: TemplateRef<any>){
if(!value){
this.mode = HybridListingMode.Table;
}
this._cardItemTemplate = value;
}
public get cardItemTemplate(): TemplateRef<any>{
return this._cardItemTemplate;
}
protected _rowIdentity = x => x
@Input()
trackBy: ((item: any) => any ) = this._rowIdentity;
@Input() class: TableClass = TableClass.Material;
@Input() forceShowShorting = false;
@Input() columns: ColumnDefinition[];
@Input() rows: any[];
@Input() columnMode: ColumnMode = ColumnMode.Force;
@Input() headerHeight = 50;
@Input() footerHeight = 50;
@Input() rowHeight: number | string = 'auto';
@Input() messages: StaticTableMessages;
@Input() externalPaging = true;
@Input() count = 0;
@Input() offset = 0;
@Input() limit: number = undefined;
@Input() propertySelectable: boolean = true;
@Input() hideModeSelection: boolean = false;
public _defaultSort: any;
@Input() set defaultSort(input: string[]) {
if (input && input.length > 0) {
this._defaultSort = input.map(x => {
let sortProp: String = x;
let sortDir: String = SortDirection.Ascending;
if (x.startsWith('+')) {
sortProp = x.substring(1);
sortDir = SortDirection.Ascending;
} else if (x.startsWith('-')) {
sortProp = x.substring(1);
sortDir = SortDirection.Descending;
}
return { prop: sortProp, dir: sortDir };
});
} else {
this._defaultSort = undefined;
}
}
@Input() loadingIndicator = false;
@Input() externalSorting = false;
@Input() initialMode: HybridListingMode;
@Output() rowActivated: EventEmitter<RowActivateEvent> = new EventEmitter<RowActivateEvent>();
@Output() pageLoad: EventEmitter<PageLoadEvent> = new EventEmitter<PageLoadEvent>();
@Output() columnSort: EventEmitter<ColumnSortEvent> = new EventEmitter<ColumnSortEvent>();
@Output() columnsChanged: EventEmitter<ColumnsChangedEvent> = new EventEmitter<ColumnsChangedEvent>();
@Output() modeChanged: EventEmitter<HybridListingMode> = new EventEmitter<HybridListingMode>();
public internalColumns: TableColumn[];
@Input() visibleColumns: TableColumnProp[];
private columnSortKeys: Map<TableColumnProp, string[]>;
protected sortableListColumns: {property: TableColumnProp, name: string}[];
protected availableColumns: {property: TableColumnProp, name: string}[];
protected columnSelections: Set<TableColumnProp>;
SelectionType = SelectionType;
protected selected=[]
get rowIdentity(){
return this.batchActions?.rowIdentity ?? this.trackBy ?? this._rowIdentity;
}
HybridListingMode = HybridListingMode;
SortDirection = SortDirection;
protected mode: HybridListingMode = HybridListingMode.Table;
constructor(
private collectionUtils: CollectionUtils,
private language: TranslateService,
) {
super();
}
ngOnChanges(changes: SimpleChanges) {
if (changes['columns']) {
this.refreshColumnDefinitionData();
}
if (changes['visibleColumns'] && !changes['visibleColumns'].isFirstChange()) {
this.refreshColumnDefinitionData();
}
const modeChanges = changes[nameof<HybridListingComponent>(x => x.initialMode)];
if(modeChanges?.isFirstChange){
if(![null, undefined].includes(this.initialMode)){
this.mode = this.initialMode;
}
}
}
ngOnDestroy(): void {
super.ngOnDestroy();
this.resizeObserver?.unobserve(this.wrapperCard.nativeElement)
this.resizeObserver?.disconnect();
this.resizeObserver = null;
this.resizeSubject$.complete();
}
private resizeObserver: ResizeObserver;
private resizeSubject$: Subject<void>;
ngOnInit() {
this.setTableMessages();
this.language.onLangChange.pipe(takeUntil(this._destroyed)).subscribe(event => {
this.setTableMessages();
this.refreshColumnDefinitionData();
});
this.ngZone.runOutsideAngular(() => {
this.resizeSubject$ = new Subject<void>;
this.resizeSubject$.pipe(
debounceTime(500),
filter(() => !!this.table)
).subscribe(() => {
this.table.recalculate();
this.changeDetectorRef.detectChanges();
})
this.resizeObserver = new ResizeObserver(
(resize) => {
this.resizeSubject$.next();
},
)
this.resizeObserver.observe(this.wrapperCard.nativeElement)
})
}
public setModeSilently(mode: HybridListingMode):void{
this.mode = mode;
}
public getMode(): HybridListingMode{
return this.mode;
}
public isColumnSelected = (columnProp: string) => !!this.columnSelections?.has(columnProp)
protected onListItemActivated(row: any){
this.rowActivated.emit({
type: 'click',
row: row,
} as any);
}
protected emptySelections(): void{
this.selected = [];
}
protected toggleMode(): void{
if(!this.listItemTemplate && !this.cardItemTemplate){
this.mode = HybridListingMode.Table;
this.modeChanged.emit(this.mode);
return;
}
switch(this.mode){
case HybridListingMode.Table:{
if(this.listItemTemplate){
this.mode = HybridListingMode.List;
this.modeChanged.emit(this.mode);
return;
}
}
case HybridListingMode.List:{
if(this.cardItemTemplate){
this.mode = HybridListingMode.Card;
this.modeChanged.emit(this.mode);
return;
}
}
case HybridListingMode.Card:
default: {
this.mode = HybridListingMode.Table;
this.modeChanged.emit(this.mode);
return;
}
}
}
onsomePage(event: PageEvent){
if(!event){
return;
}
this.pageLoad.emit({
count: event.length,
pageSize: event.pageSize,
limit: event.pageSize,
offset: event.pageIndex,
});
}
protected isListingItemSelected(item: any){
return !!this.selected?.find(x => this.rowIdentity && (this.rowIdentity(item) === this.rowIdentity(x)));
}
onSelectionListChange(event: MatCheckboxChange, item: any){
this.selected = this.selected?.filter(x => this.rowIdentity && (this.rowIdentity(item) !== this.rowIdentity(x)))
if(event.checked){
this.selected = [ ...(this.selected ?? []), item].filter(x => x);
}
}
private setTableMessages() {
this.messages = {
emptyMessage: this.language.instant('COMMONS.LISTING-COMPONENT.MESSAGE.EMPTY'),
totalMessage: this.language.instant('COMMONS.LISTING-COMPONENT.MESSAGE.TOTAL'),
selectedMessage: this.language.instant('COMMONS.LISTING-COMPONENT.MESSAGE.SELECTED')
};
}
private refreshColumnDefinitionData() {
const visibleColumns = this.getVisibleColumns();
this.internalColumns = this.columns.filter(x => x.alwaysShown || visibleColumns.includes(x.prop)).map(x => {
const item = {
checkboxable: x.checkboxable,
frozenLeft: x.frozenLeft,
frozenRight: x.frozenRight,
flexGrow: x.flexGrow,
minWidth: x.minWidth,
maxWidth: x.maxWidth,
width: x.width,
resizeable: x.resizeable,
comparator: x.comparator,
pipe: x.pipe,
sortable: x.sortable,
draggable: x.draggable,
canAutoResize: x.canAutoResize,
name: x.languageName ? this.language.instant(x.languageName) : x.name,
prop: x.prop,
cellTemplate: x.cellTemplate,
headerTemplate: x.headerTemplate,
cellClass: x.cellClass,
headerClass: x.headerClass,
headerCheckboxable: x.headerCheckboxable,
summaryFunc: x.summaryFunc,
summaryTemplate: x.summaryTemplate
} as TableColumn;
if (x.valueFunction) {
item.cellTemplate = this.functionValueTemplate;
item['valueFunction'] = x.valueFunction;
}
return item;
});
this.internalColumns = [ this.batchActions?.rowIdentity && {
checkboxable: true,
sortable: false,
width: 30,
maxWidth: 30,
canAutoRisize: false,
headerCheckboxable: false
} ,...this.internalColumns].filter(x => !!x)
this.columnSortKeys = new Map<TableColumnProp, string[]>(this.columns.map(buildColumnSortKeys));
this.availableColumns = this.columns?.filter(x => !x?.alwaysShown).map(x => ({
name: x.languageName ? this.language.instant(x.languageName) : x.name,
property: x.prop
}));
this.sortableListColumns = this.internalColumns.filter(x => x?.sortable).map(x => ({
name: x.name,
property: x.prop
}))
this.columnSelections = new Set(visibleColumns);
}
protected getPropertyName(string): string{
return this.sortableListColumns?.find(x => x.property === string)?.name;
}
protected onColumnSelectionChange(event: MatCheckboxChange, property: TableColumnProp){
if(event.checked){
this.columnSelections.add(property)
}else{
this.columnSelections.delete(property)
}
this.visibleColumns = [...this.columnSelections].filter(x => x);
this.refreshColumnDefinitionData();
const visibleColumns = this.getVisibleColumns();
this.columnSelections = new Set(visibleColumns);
this.columnsChanged.emit({
properties: visibleColumns
})
}
onRowActivated(event) {
if (this.rowActivated && event) {
this.rowActivated.emit({
type: event.type,
event: event.event,
row: event.row,
column: event.column,
value: event.value,
cellElement: event.cellElement,
rowElement: event.rowElement,
});
}
}
onColumnSort(event) {
if (this.columnSort && event) {
const sortDescriptorsCollection: SortDescriptor[][] = event.sorts.map(x => getColumnSortDescriptors(x, this.columnSortKeys.get(x.prop)));
const sortEvent = {
column: event.column,
previousValue: event.prevValue,
newValue: event.newValue,
sortDescriptors: this.collectionUtils.flatten(sortDescriptorsCollection)
};
this.columnSort.emit(sortEvent);
}
}
protected toggleSortListViewSort(): void{
if(this.columnSort){
const previousSort = this._defaultSort?.[0]?.dir as SortDirection;
const sortField = this._defaultSort?.[0]?.prop as string;
if(!sortField){
return ;
}
switch(previousSort){
case SortDirection.Ascending:{
this.columnSort.emit({
newValue: SortDirection.Descending,
previousValue: SortDirection.Ascending,
sortDescriptors: [{
direction: SortDirection.Descending,
property: sortField
}]
})
return;
}
case SortDirection.Descending:{
this.columnSort.emit({
newValue: SortDirection.Ascending,
previousValue: SortDirection.Descending,
sortDescriptors: [{
direction: SortDirection.Ascending,
property: sortField
}]
})
return;
}
default: return;
}
}
}
protected onListViewSort(byProperty: string): void{
if(!byProperty){
return;
}
if(!this.columnSort){
return;
}
const previousSort = this._defaultSort?.[0]?.dir;
const sort = previousSort ?? SortDirection.Descending;
this.columnSort.emit({
newValue: sort,
previousValue: previousSort,
sortDescriptors:[
{
direction: sort,
property: byProperty
}
]
})
}
getVisibleColumns(): TableColumnProp[] {
if (!this.visibleColumns || this.visibleColumns.length === 0) { return this.columns.map(x => x.prop).filter(x => x); }
return this.visibleColumns;
}
}
export enum TableClass {
Material = 'material',
Bootstrap = 'bootstrap',
Dark = 'dark'
}
export enum ColumnMode {
Standard = 'standard',
Flex = 'flex',
Force = 'force'
}
export interface RowActivateEvent<T = any> {
type: 'keydown' | 'click' | 'dblclick' | 'mouseenter';
event: MouseEvent;
row: T;
column: ColumnDefinition;
value: any;
cellElement: Element;
rowElement: Element;
}
export interface PageLoadEvent {
count: number;
pageSize: number;
limit: number;
offset: number;
}
export interface ColumnSortEvent {
sortDescriptors: SortDescriptor[];
column?: ColumnDefinition;
previousValue: SortDirection;
newValue: SortDirection;
}
export interface ColumnsChangedEvent {
properties: TableColumnProp[];
}
export interface SortDescriptor {
direction: SortDirection;
property: string;
}
export enum SortDirection {
Ascending = 'asc',
Descending = 'desc'
}
export declare type TableColumnProp = string | number;
export interface ColumnDefinition {
/**
* Determines if column is checkbox
*
* @type {boolean}
* @memberOf TableColumn
*/
checkboxable?: boolean;
/**
* Determines if the column is frozen to the left
*
* @type {boolean}
* @memberOf TableColumn
*/
frozenLeft?: boolean;
/**
* Determines if the column is frozen to the right
*
* @type {boolean}
* @memberOf TableColumn
*/
frozenRight?: boolean;
/**
* The grow factor relative to other columns. Same as the flex-grow
* API from http =//www.w3.org/TR/css3-flexbox/. Basically;
* take any available extra width and distribute it proportionally
* according to all columns' flexGrow values.
*
* @type {number}
* @memberOf TableColumn
*/
flexGrow?: number;
/**
* Min width of the column
*
* @type {number}
* @memberOf TableColumn
*/
minWidth?: number;
/**
* Max width of the column
*
* @type {number}
* @memberOf TableColumn
*/
maxWidth?: number;
/**
* The default width of the column, in pixels
*
* @type {number}
* @memberOf TableColumn
*/
width?: number;
/**
* Can the column be resized
*
* @type {boolean}
* @memberOf TableColumn
*/
resizeable?: boolean;
/**
* Custom sort comparator
*
* @type {*}
* @memberOf TableColumn
*/
comparator?: any;
/**
* Custom pipe transforms
*
* @type {PipeTransform}
* @memberOf TableColumn
*/
pipe?: PipeTransform;
/**
* Can the column be sorted
*
* @type {boolean}
* @memberOf TableColumn
*/
sortable?: boolean;
/**
* Can the column be re-arranged by dragging
*
* @type {boolean}
* @memberOf TableColumn
*/
/**
* Override for the sort property that will be used (default is column.prop)
*
* @type {string}
* @memberOf TableColumn
*/
sortKeys?: string[];
draggable?: boolean;
/**
* Whether the column can automatically resize to fill space in the table.
*
* @type {boolean}
* @memberOf TableColumn
*/
canAutoResize?: boolean;
/**
* Column name or label
*
* @type {string}
* @memberOf TableColumn
*/
name?: string;
/**
* Language Key Column name or label
*
* @type {string}
* @memberOf TableColumn
*/
languageName?: string;
/**
* Property to bind to the row. Example:
*
* `someField` or `some.field.nested`, 0 (numeric)
*
* If left blank, will use the name as camel case conversion
*
* @type {TableColumnProp}
* @memberOf TableColumn
*/
prop?: TableColumnProp;
/**
* Cell template ref
*
* @type {*}
* @memberOf TableColumn
*/
cellTemplate?: any;
/**
* Header template ref
*
* @type {*}
* @memberOf TableColumn
*/
headerTemplate?: any;
/**
* CSS Classes for the cell
*
*
* @memberOf TableColumn
*/
cellClass?: string | ((data: any) => string | any);
/**
* CSS classes for the header
*
*
* @memberOf TableColumn
*/
headerClass?: string | ((data: any) => string | any);
/**
* Header checkbox enabled
*
* @type {boolean}
* @memberOf TableColumn
*/
headerCheckboxable?: boolean;
/**
* Summary function
*
* @type {(cells: any[]) => any}
* @memberOf TableColumn
*/
summaryFunc?: (cells: any[]) => any;
/**
* Summary cell template ref
*
* @type {*}
* @memberOf TableColumn
*/
summaryTemplate?: any;
alwaysShown?: boolean;
valueFunction?: (cell: any) => any;
}
export interface StaticTableMessages {
emptyMessage: string;
totalMessage: string;
selectedMessage: string;
}
function getColumnSortDescriptors(internalSortDescriptor, sortKeys: string[]): SortDescriptor[] {
const direction = internalSortDescriptor.dir;
return sortKeys.map(x => ({
direction: direction,
property: x
}));
}
function buildColumnSortKeys(column: ColumnDefinition): [TableColumnProp, string[]] {
return [column.prop, getColumnSortKeys(column)];
}
function getColumnSortKeys(column: ColumnDefinition): string[] {
const sortKeys = extractSortKeys(column.sortKeys);
if (sortKeys.length === 0) { sortKeys.push(String(column.prop)); }
return sortKeys;
}
function extractSortKeys(sortKeys: string[]): string[] {
sortKeys = sortKeys || [];
return sortKeys.filter(x => Boolean(x));
}
export enum HybridListingMode{
Table,
List,
Card
}