add table header and sync width directive

This commit is contained in:
Dan Percic 2021-08-10 19:51:44 +03:00
parent f0c4408764
commit 7e9b28a10c
10 changed files with 200 additions and 9 deletions

View File

@ -18,3 +18,37 @@
padding-left: 10px;
}
}
.header-item {
background-color: $btn-bg;
height: 50px;
padding: 0 24px;
display: flex;
align-items: center;
z-index: 1;
border-bottom: 1px solid $separator;
box-sizing: border-box;
> *:not(:last-child) {
margin-right: 10px;
}
.actions {
display: flex;
align-items: center;
justify-content: flex-end;
> *:not(:last-child) {
margin-right: 16px;
}
}
&.selection-enabled {
padding-left: 10px;
}
}
.scrollbar-placeholder {
width: 11px;
padding: 0 !important;
}

View File

@ -8,6 +8,7 @@ export * from './lib/utils/types/utility-types';
export * from './lib/utils/types/tooltip-positions.type';
export * from './lib/utils/decorators/bind.decorator';
export * from './lib/utils/decorators/required.decorator';
export * from './lib/utils/decorators/debounce.decorator';
export * from './lib/buttons/circle-button/circle-button.type';
export * from './lib/buttons/circle-button/circle-button.component';
export * from './lib/filtering/filter-utils';
@ -25,3 +26,4 @@ export * from './lib/tables/entities.service';
export * from './lib/tables/listing-component.directive';
export * from './lib/tables/models/table-column-config.model';
export * from './lib/tables/table-column-name/table-column-name.component';
export * from './lib/tables/table-header/table-header.component';

View File

@ -12,6 +12,9 @@ import { SortByPipe } from './sorting/sort-by.pipe';
import { HumanizePipe } from './utils/pipes/humanize.pipe';
import { TableColumnNameComponent } from './tables/table-column-name/table-column-name.component';
import { QuickFiltersComponent } from './filtering/quick-filters/quick-filters.component';
import { TableHeaderComponent } from './tables/table-header/table-header.component';
import { TranslateModule } from '@ngx-translate/core';
import { SyncWidthDirective } from './tables/sync-width.directive';
const buttons = [IconButtonComponent, ChevronButtonComponent, CircleButtonComponent];
@ -19,14 +22,16 @@ const inputs = [RoundCheckboxComponent];
const matModules = [MatIconModule, MatButtonModule, MatTooltipModule];
const components = [...buttons, ...inputs, TableColumnNameComponent, QuickFiltersComponent];
const modules = [...matModules, TranslateModule];
const pipes = [SortByPipe, HumanizePipe];
const components = [...buttons, ...inputs, TableColumnNameComponent, QuickFiltersComponent, TableHeaderComponent];
const utils = [SortByPipe, HumanizePipe, SyncWidthDirective];
@NgModule({
declarations: [...components, ...pipes],
imports: [CommonModule, ...matModules],
exports: [...components, ...pipes, ...matModules]
declarations: [...components, ...utils],
imports: [CommonModule, ...modules],
exports: [...components, ...utils, ...modules]
})
export class CommonUiModule {
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) {

View File

@ -10,7 +10,7 @@ import { FilterService } from '../filter.service';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class QuickFiltersComponent {
@Input() @Required() quickFilters!: Set<NestedFilter>;
@Input() @Required() quickFilters!: readonly NestedFilter[];
constructor(readonly filterService: FilterService) {}
}

View File

@ -1,4 +1,4 @@
import { Directive, Injector, OnDestroy, Provider } from '@angular/core';
import { Directive, Injector, OnDestroy } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { FilterService } from '../filtering/filter.service';
@ -11,7 +11,7 @@ import { KeysOf } from '../utils/types/utility-types';
import { TableColumnConfig } from './models/table-column-config.model';
import { EntitiesService } from './entities.service';
export const DefaultListingServices = new Set<Provider>().add(FilterService).add(SearchService).add(EntitiesService).add(SortingService);
export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const;
@Directive()
export abstract class ListingComponent<T extends object> extends AutoUnsubscribe implements OnDestroy {
@ -23,7 +23,7 @@ export abstract class ListingComponent<T extends object> extends AutoUnsubscribe
readonly noMatch$ = this._noMatch$;
readonly sortedDisplayedEntities$ = this._sortedDisplayedEntities$;
abstract readonly tableColumnConfigs: TableColumnConfig<T>[];
abstract readonly tableColumnConfigs: readonly TableColumnConfig<T>[];
/**
* Key used in the *trackBy* function with **ngFor* or **cdkVirtualFor*
* and in the default sorting and as the search field

View File

@ -0,0 +1,71 @@
import { Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/core';
@Directive({
selector: '[iqserSyncWidth]'
})
export class SyncWidthDirective implements OnDestroy {
@Input() iqserSyncWidth!: string;
private readonly _interval: number;
constructor(private readonly _elementRef: ElementRef) {
this._interval = setInterval(() => {
this._matchWidth();
}, 1000);
}
ngOnDestroy(): void {
clearInterval(this._interval);
}
private _matchWidth() {
const headerItems = this._elementRef.nativeElement.children;
const tableRows = this._elementRef.nativeElement.parentElement.parentElement.getElementsByClassName(this.iqserSyncWidth);
if (!tableRows || !tableRows.length) {
return;
}
this._elementRef.nativeElement.setAttribute('synced', true);
const { tableRow, length } = this._sampleRow(tableRows);
if (!tableRow) return;
const hasExtraColumns = headerItems.length !== length ? 1 : 0;
for (let idx = 0; idx < length - hasExtraColumns - 1; ++idx) {
if (headerItems[idx]) {
headerItems[idx].style.width = `${tableRow.children[idx].getBoundingClientRect().width}px`;
headerItems[idx].style.minWidth = `${tableRow.children[idx].getBoundingClientRect().width}px`;
}
}
for (let idx = length - hasExtraColumns - 1; idx < headerItems.length; ++idx) {
if (headerItems[idx]) {
headerItems[idx].style.minWidth = `0`;
}
}
}
@HostListener('window:resize')
onResize() {
this._matchWidth();
}
private _sampleRow(tableRows: HTMLCollectionOf<Element>): {
tableRow?: Element;
length: number;
} {
let length = 0;
let tableRow: Element | undefined;
for (let idx = 0; idx < tableRows.length; ++idx) {
const row = tableRows.item(idx);
if (row && row.children.length > length) {
length = row.children.length;
tableRow = row;
}
}
return { tableRow, length };
}
}

View File

@ -0,0 +1,42 @@
<div [class.selection-enabled]="selectionEnabled" class="header-item">
<iqser-round-checkbox
(click)="entitiesService.selectAll()"
*ngIf="selectionEnabled"
[active]="entitiesService.areAllSelected$ | async"
[indeterminate]="entitiesService.notAllSelected$ | async"
></iqser-round-checkbox>
<span class="all-caps-label">
{{ tableHeaderLabel | translate: { length: (entitiesService.displayedLength$ | async) } }}
</span>
<ng-container [ngTemplateOutlet]="bulkActions"></ng-container>
<iqser-quick-filters *ngIf="quickFilters$ | async as quickFilters" [quickFilters]="quickFilters"></iqser-quick-filters>
<!-- Custom content-->
<ng-content></ng-content>
</div>
<div
[class.no-data]="entitiesService.noData$ | async"
[class.selection-enabled]="selectionEnabled"
class="table-header"
iqserSyncWidth="table-item"
>
<div *ngIf="selectionEnabled" class="select-oval-placeholder"></div>
<iqser-table-column-name
*ngFor="let config of tableColumnConfigs"
[class]="config.class"
[sortByKey]="config.sortByKey"
[label]="config.label | translate"
[leftIcon]="config.leftIcon"
[rightIconTooltip]="config.rightIconTooltip"
[rightIcon]="config.rightIcon"
></iqser-table-column-name>
<div *ngIf="hasEmptyColumn"></div>
<div class="scrollbar-placeholder"></div>
</div>

View File

@ -0,0 +1,23 @@
import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core';
import { Required } from '../../utils/decorators/required.decorator';
import { FilterService } from '../../filtering/filter.service';
import { TableColumnConfig } from '../models/table-column-config.model';
import { EntitiesService } from '../entities.service';
@Component({
selector: 'iqser-table-header',
templateUrl: './table-header.component.html',
styleUrls: ['./table-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableHeaderComponent<T extends object> {
@Input() @Required() tableHeaderLabel!: string;
@Input() @Required() tableColumnConfigs!: readonly TableColumnConfig<T>[];
@Input() hasEmptyColumn = false;
@Input() selectionEnabled = false;
@Input() bulkActions?: TemplateRef<any>;
readonly quickFilters$ = this.filterService.getFilterModels$('quickFilters');
constructor(readonly entitiesService: EntitiesService<T>, readonly filterService: FilterService) {}
}

View File

@ -0,0 +1,14 @@
export function Debounce(delay = 300): MethodDecorator {
return function (target: Object, propertyKey: PropertyKey, descriptor: PropertyDescriptor) {
let timeout: number | undefined;
const original = descriptor.value;
descriptor.value = function (...args: unknown[]) {
clearTimeout(timeout);
timeout = setTimeout(() => original.apply(this, args), delay);
};
return descriptor;
};
}