Table component WIP

This commit is contained in:
Adina Țeudan 2021-08-27 00:09:37 +03:00
parent 1afafe0e28
commit 1b7104ba14
28 changed files with 451 additions and 61 deletions

View File

@ -0,0 +1,18 @@
<svg
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.8285 12.0259L16.2427 13.4402L12 17.6828L7.7574 13.4402L9.17161 12.0259L11 13.8544V6.31724H13V13.8544L14.8285 12.0259Z"
fill="currentColor"
/>
<path
clip-rule="evenodd"
d="M19.7782 19.7782C15.4824 24.0739 8.51759 24.0739 4.22183 19.7782C-0.0739417 15.4824 -0.0739417 8.51759 4.22183 4.22183C8.51759 -0.0739419 15.4824 -0.0739419 19.7782 4.22183C24.0739 8.51759 24.0739 15.4824 19.7782 19.7782ZM18.364 18.364C14.8492 21.8787 9.15076 21.8787 5.63604 18.364C2.12132 14.8492 2.12132 9.15076 5.63604 5.63604C9.15076 2.12132 14.8492 2.12132 18.364 5.63604C21.8787 9.15076 21.8787 14.8492 18.364 18.364Z"
fill="currentColor"
fill-rule="evenodd"
/>
</svg>

After

Width:  |  Height:  |  Size: 841 B

View File

@ -14,6 +14,7 @@ import { IqserInputsModule } from './inputs';
import { IqserHelpModeModule } from './help-mode';
import { IqserIconsModule } from './icons';
import { IqserButtonsModule } from './buttons';
import { IqserScrollbarModule } from './scrollbar';
const matModules = [MatIconModule, MatProgressSpinnerModule];
const modules = [
@ -23,7 +24,8 @@ const modules = [
IqserListingModule,
IqserFiltersModule,
IqserInputsModule,
IqserHelpModeModule
IqserHelpModeModule,
IqserScrollbarModule
];
const components = [StatusBarComponent, FullPageLoadingIndicatorComponent, FullPageErrorComponent];
const pipes = [SortByPipe, HumanizePipe];

View File

@ -12,6 +12,7 @@ export class IqserIconsModule {
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) {
const icons: Set<string> = new Set([
'arrow-down',
'arrow-down-o',
'check',
'close',
'edit',

View File

@ -1,8 +1,12 @@
export * from './tables';
export * from './workflow-listing';
export * from './models';
export * from './services';
export * from './scroll-button/scroll-button.component';
export * from './table/table.component';
export * from './table-column-name/table-column-name.component';
export * from './table-header/table-header.component';
export * from './sync-width.directive';
export * from './listing.module';
export * from './entities.service';
export * from './listing-component.directive';
export * from './table-header/table-header.component';
export * from './models/listable';

View File

@ -5,9 +5,8 @@ import { FilterService } from '../filtering';
import { SortingOrders, SortingService } from '../sorting';
import { AutoUnsubscribe, Bind, KeysOf } from '../utils';
import { SearchService } from '../search';
import { TableColumnConfig } from './tables';
import { EntitiesService } from './entities.service';
import { Listable } from './models/listable';
import { EntitiesService } from './services';
import { Listable, TableColumnConfig } from './models';
export const DefaultListingServices = [FilterService, SearchService, EntitiesService, SortingService] as const;

View File

@ -1,18 +1,35 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { TablesModule } from './tables';
import { TableHeaderComponent } from './table-header/table-header.component';
import { WorkflowListingModule } from './workflow-listing';
import { IqserFiltersModule } from '../filtering';
import { IqserInputsModule } from '../inputs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TableColumnNameComponent } from './table-column-name/table-column-name.component';
import { ScrollButtonComponent } from './scroll-button/scroll-button.component';
import { TableComponent } from './table/table.component';
import { SyncWidthDirective } from './sync-width.directive';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { IqserIconsModule } from '../icons';
import { IqserScrollbarModule } from '../scrollbar';
import { RouterModule } from '@angular/router';
const components = [TableHeaderComponent];
const modules = [TranslateModule, TablesModule, WorkflowListingModule, IqserFiltersModule, IqserInputsModule];
const matModules = [MatTooltipModule];
const components = [TableHeaderComponent, TableComponent, TableColumnNameComponent, ScrollButtonComponent];
const modules = [
TranslateModule,
IqserFiltersModule,
IqserInputsModule,
IqserIconsModule,
IqserScrollbarModule,
ScrollingModule,
RouterModule
];
const utils = [SyncWidthDirective];
@NgModule({
declarations: [...components],
exports: [...components, TablesModule, WorkflowListingModule],
imports: [CommonModule, ...modules]
declarations: [...components, ...utils],
exports: [...components, ...utils],
imports: [CommonModule, ...modules, ...matModules]
})
export class IqserListingModule {}

View File

@ -0,0 +1,2 @@
export * from './listable';
export * from './table-column-config.model';

View File

@ -1,4 +1,5 @@
import { KeysOf } from '../../../utils';
import { KeysOf } from '../../utils';
import { TemplateRef } from '@angular/core';
export interface TableColumnConfig<T> {
readonly label: string;
@ -8,4 +9,6 @@ export interface TableColumnConfig<T> {
readonly rightIcon?: string;
readonly rightIconTooltip?: string;
readonly notTranslatable?: boolean;
readonly width?: string; // TODO: make required
readonly template?: TemplateRef<unknown>; // TODO: make required
}

View File

@ -0,0 +1,7 @@
<button (click)="scroll(buttonType.top)" [hidden]="(showScrollUp$ | async) === false" class="scroll-button top pointer">
<mat-icon svgIcon="iqser:arrow-down-o"></mat-icon>
</button>
<button (click)="scroll(buttonType.bottom)" [hidden]="(showScrollDown$ | async) === false" class="scroll-button bottom pointer">
<mat-icon svgIcon="iqser:arrow-down-o"></mat-icon>
</button>

View File

@ -0,0 +1,30 @@
@import '../../../assets/styles/common';
.scroll-button {
background-color: $white;
position: absolute;
right: 0;
height: 40px;
width: 44px;
border: none;
border-radius: 8px 0 0 8px;
box-shadow: -1px 1px 5px 0 rgba(40, 50, 65, 0.25);
&.bottom {
bottom: 30px;
}
&.top {
top: 100px;
mat-icon {
transform: rotate(180deg);
}
}
}
mat-icon {
width: 22px;
height: 22px;
color: $grey-7;
}

View File

@ -0,0 +1,61 @@
import { ChangeDetectionStrategy, Component, HostListener, Input, OnInit } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { concatMap, delay, distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { Required } from '../../utils';
const ButtonTypes = {
top: 'top',
bottom: 'bottom'
} as const;
type ButtonType = keyof typeof ButtonTypes;
@Component({
selector: 'iqser-scroll-button',
templateUrl: './scroll-button.component.html',
styleUrls: ['./scroll-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ScrollButtonComponent implements OnInit {
readonly buttonType = ButtonTypes;
@Input() @Required() scrollViewport!: CdkVirtualScrollViewport;
@Input() @Required() itemSize!: number;
showScrollUp$?: Observable<boolean>;
showScrollDown$?: Observable<boolean>;
ngOnInit(): void {
const scrollSize = () => this.scrollViewport.getDataLength() * this.itemSize;
const scrollIsNeeded = () => this.scrollViewport.getViewportSize() < scrollSize();
const reachedEnd = (type: ButtonType) => this.scrollViewport.measureScrollOffset(type) === 0;
const showScrollUp = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.top);
const showScrollDown = () => scrollIsNeeded() && !reachedEnd(ButtonTypes.bottom);
const scroll$ = this.scrollViewport.elementScrolled().pipe(
startWith(''),
/** Delay first value so that we can wait for items to be rendered in viewport and get correct values */
concatMap((value, index) => (index === 0 ? of(value).pipe(delay(0)) : of(value)))
);
this.showScrollUp$ = scroll$.pipe(map(showScrollUp), distinctUntilChanged());
this.showScrollDown$ = scroll$.pipe(map(showScrollDown), distinctUntilChanged());
}
scroll(type: ButtonType): void {
const viewportSize = (this.scrollViewport?.getViewportSize() - this.itemSize) * (type === ButtonTypes.top ? -1 : 1);
const scrollOffset = this.scrollViewport?.measureScrollOffset('top');
this.scrollViewport?.scrollToOffset(scrollOffset + viewportSize, 'smooth');
}
@HostListener('document:keyup', ['$event'])
spaceAndPageDownScroll(event: KeyboardEvent): void {
const target = event.target as EventTarget & { tagName: string };
if (['Space', 'PageDown'].includes(event.code) && target.tagName === 'BODY') {
this.scroll(ButtonTypes.bottom);
} else if (['PageUp'].includes(event.code) && target.tagName === 'BODY') {
this.scroll(ButtonTypes.top);
}
}
}

View File

@ -1,9 +1,9 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, pipe } from 'rxjs';
import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { FilterService, getFilteredEntities } from '../filtering';
import { SearchService } from '../search';
import { Listable } from './models/listable';
import { FilterService, getFilteredEntities } from '../../filtering';
import { SearchService } from '../../search';
import { Listable } from '../models';
const toLengthValue = (entities: unknown[]) => entities?.length ?? 0;
const getLength = pipe(map(toLengthValue), distinctUntilChanged());

View File

@ -0,0 +1 @@
export * from './entities.service';

View File

@ -1,4 +1,4 @@
@import '../../../../assets/styles/common';
@import '../../../assets/styles/common';
:host {
display: flex;

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, Optional } from '@angular/core';
import { SortingOrders, SortingService } from '../../../sorting';
import { KeysOf, Required } from '../../../utils';
import { SortingOrders, SortingService } from '../../sorting';
import { KeysOf, Required } from '../../utils';
const ifHasRightIcon = <T>(thisArg: TableColumnNameComponent<T>) => !!thisArg.rightIcon;

View File

@ -1,9 +1,8 @@
import { ChangeDetectionStrategy, Component, Input, TemplateRef } from '@angular/core';
import { Required } from '../../utils';
import { FilterService } from '../../filtering';
import { EntitiesService } from '../entities.service';
import { Listable } from '../models/listable';
import { TableColumnConfig } from '../tables';
import { EntitiesService } from '../services';
import { Listable, TableColumnConfig } from '../models';
export const ListingModes = {
list: 'list',

View File

@ -0,0 +1,52 @@
<iqser-table-header
[bulkActions]="bulkActions"
[hasEmptyColumn]="!!emptyColumnWidth"
[selectionEnabled]="selectionEnabled"
[tableColumnConfigs]="tableColumnConfigs"
[tableHeaderLabel]="tableHeaderLabel"
></iqser-table-header>
<!--TODO: Empty states-->
<!--<redaction-empty-state-->
<!-- (action)="openAddEditDictionaryDialog()"-->
<!-- *ngIf="entitiesService.noData$ | async"-->
<!-- [buttonLabel]="'dictionary-listing.no-data.action' | translate"-->
<!-- [showButton]="currentUser.isAdmin"-->
<!-- [text]="'dictionary-listing.no-data.title' | translate"-->
<!-- icon="red:dictionary"-->
<!--&gt;</redaction-empty-state>-->
<!--<redaction-empty-state-->
<!-- *ngIf="noMatch$ | async"-->
<!-- [text]="'dictionary-listing.no-match.title' | translate"-->
<!--&gt;</redaction-empty-state>-->
<cdk-virtual-scroll-viewport #scrollViewport [itemSize]="itemSize" iqserHasScrollbar>
<div
*cdkVirtualFor="let entity of listingComponent.sortedDisplayedEntities$ | async; trackBy: listingComponent.trackByPrimaryKey"
[class.pointer]="!!routerLinkFn"
[routerLink]="routerLinkFn(entity)"
class="table-item"
>
<div (click)="listingComponent.toggleEntitySelected($event, entity)" *ngIf="selectionEnabled" class="selection-column">
<iqser-round-checkbox [active]="listingComponent.isSelected(entity)"></iqser-round-checkbox>
</div>
<ng-container *ngFor="let column of tableColumnConfigs">
<ng-container *ngIf="!!column.template">
<ng-container *ngTemplateOutlet="column.template; context: { entity: entity }"></ng-container>
</ng-container>
<!-- TODO: Remove-->
<div *ngIf="!column.template">n</div>
</ng-container>
<div class="actions-container">
<ng-container *ngTemplateOutlet="actionsTemplate; context: { entity: entity }"></ng-container>
</div>
<div class="scrollbar-placeholder"></div>
</div>
</cdk-virtual-scroll-viewport>
<iqser-scroll-button *ngIf="hasScrollButton" [itemSize]="itemSize" [scrollViewport]="scrollViewport"></iqser-scroll-button>

View File

@ -0,0 +1,121 @@
//iqser-table-header::ng-deep .header-item {
// padding-right: 16px;
//}
@import '../../../assets/styles/common';
cdk-virtual-scroll-viewport {
height: calc(100vh - 50px - 31px - 111px);
overflow-y: hidden !important;
&.has-scrollbar:hover ::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: var(--gridTemplateColumnsHover);
}
::ng-deep.cdk-virtual-scroll-content-wrapper {
grid-template-columns: var(--gridTemplateColumns);
display: grid;
.table-item {
display: contents;
> div {
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
box-sizing: border-box;
border-bottom: 1px solid $separator;
height: var(--itemSize);
padding: 0 24px 0 var(--paddingLeft);
&:not(.scrollbar-placeholder):not(.selection-column) {
min-width: 110px;
}
&.selection-column {
padding-right: 0 !important;
iqser-round-checkbox .wrapper {
opacity: 0;
transition: opacity 0.2s;
&.active {
opacity: 1;
}
}
}
}
.table-item-title {
font-weight: 600;
@include line-clamp(1);
}
.action-buttons {
position: absolute;
display: none;
right: -11px;
top: 0;
height: 100%;
width: fit-content;
flex-direction: row;
align-items: center;
padding-left: 100px;
padding-right: 24px;
z-index: 1;
background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, $grey-2 35%);
mat-icon {
width: 14px;
}
iqser-circle-button:not(:last-child) {
margin-right: 2px;
}
&.active {
display: flex;
// compensate for scroll
padding-right: 23px;
}
}
input,
mat-select {
margin-top: 0;
}
&:hover {
> div {
background-color: $grey-8;
&.selection-column iqser-round-checkbox .wrapper {
opacity: 1;
}
}
.action-buttons {
display: flex;
}
}
}
}
&:hover {
overflow-y: auto !important;
@include scroll-bar;
&.has-scrollbar {
.table-item {
.action-buttons {
right: 0;
padding-right: 13px;
}
.scrollbar-placeholder {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,66 @@
import { ChangeDetectionStrategy, Component, forwardRef, Inject, Input, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { Required } from '../../utils';
import { Listable, TableColumnConfig } from '../models';
import { ListingComponent } from '../listing-component.directive';
@Component({
selector: 'iqser-table',
templateUrl: './table.component.html',
styleUrls: ['./table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TableComponent<T extends Listable> implements OnInit {
@Input() bulkActions?: TemplateRef<unknown>;
@Input() actionsTemplate?: TemplateRef<unknown>;
@Input() @Required() itemSize!: number;
@Input() @Required() tableColumnConfigs!: readonly TableColumnConfig<T>[];
@Input() @Required() tableHeaderLabel!: string;
@Input() selectionEnabled = false;
@Input() hasScrollButton = false;
@Input() emptyColumnWidth?: string;
@Input() classes?: string;
@Input() routerLinkFn?: (entity: T) => string | string[];
@ViewChild(CdkVirtualScrollViewport, { static: true }) private readonly _viewport!: CdkVirtualScrollViewport;
constructor(@Inject(forwardRef(() => ListingComponent)) private _parent: ListingComponent<T>) {}
get listingComponent(): ListingComponent<T> {
return this._parent;
}
ngOnInit(): void {
this._setStyles();
}
private _setStyles(): void {
const element = this._viewport.elementRef.nativeElement;
this._setColumnsWidth(element);
this._setItemSize(element);
this._setPadding(element);
}
private _setColumnsWidth(element: HTMLElement) {
let gridTemplateColumnsHover = '';
if (this.selectionEnabled) {
gridTemplateColumnsHover += 'auto ';
}
for (const config of this.tableColumnConfigs) {
gridTemplateColumnsHover += `${config.width as string} `; // TODO remove cast
}
gridTemplateColumnsHover += this.emptyColumnWidth;
const gridTemplateColumns = gridTemplateColumnsHover + ' 11px';
element.style.setProperty('--gridTemplateColumns', gridTemplateColumns);
element.style.setProperty('--gridTemplateColumnsHover', gridTemplateColumnsHover);
}
private _setItemSize(element: HTMLElement) {
element.style.setProperty('--itemSize', `${this.itemSize}px`);
}
private _setPadding(element: HTMLElement) {
const paddingLeft = this.selectionEnabled ? 10 : 24;
element.style.setProperty('--paddingLeft', `${paddingLeft}px`);
}
}

View File

@ -1,4 +0,0 @@
export * from './tables-module';
export * from './table-column-name/table-column-name.component';
export * from './models/table-column-config.model';
export * from './sync-width.directive';

View File

@ -1,18 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { TableColumnNameComponent } from './table-column-name/table-column-name.component';
import { SyncWidthDirective } from './sync-width.directive';
import { IqserIconsModule } from '../../icons';
const matModules = [MatTooltipModule];
const components = [TableColumnNameComponent];
const utils = [SyncWidthDirective];
@NgModule({
declarations: [...components, ...utils],
exports: [...components, ...utils],
imports: [CommonModule, TranslateModule, IqserIconsModule, ...matModules]
})
export class TablesModule {}

View File

@ -1 +0,0 @@
export * from './workflow-listing.module';

View File

@ -1,12 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { TranslateModule } from '@ngx-translate/core';
const matModules = [DragDropModule];
@NgModule({
declarations: [],
imports: [CommonModule, TranslateModule, ...matModules]
})
export class WorkflowListingModule {}

View File

@ -0,0 +1,27 @@
import { AfterContentChecked, Directive, ElementRef, HostBinding } from '@angular/core';
@Directive({
selector: '[iqserHasScrollbar]',
exportAs: 'iqserHasScrollbar'
})
export class HasScrollbarDirective implements AfterContentChecked {
@HostBinding('class') class = '';
constructor(private readonly _elementRef: ElementRef) {}
get hasScrollbar(): boolean {
const element = this._elementRef?.nativeElement as HTMLElement;
return element.clientHeight < element.scrollHeight;
}
ngAfterContentChecked(): void {
this._process();
}
_process(): void {
const newClass = this.hasScrollbar ? 'has-scrollbar' : '';
if (this.class !== newClass) {
this.class = newClass;
}
}
}

View File

@ -0,0 +1,2 @@
export * from '../scrollbar/has-scrollbar.directive';
export * from './scrollbar.module';

View File

@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HasScrollbarDirective } from './has-scrollbar.directive';
const utils = [HasScrollbarDirective];
@NgModule({
declarations: [...utils],
exports: [...utils],
imports: [CommonModule],
providers: [HasScrollbarDirective]
})
export class IqserScrollbarModule {}