don't use mouseenter if not needed

This commit is contained in:
Dan Percic 2021-11-16 18:58:10 +02:00
parent aa7926da8d
commit 710d014455
8 changed files with 276 additions and 226 deletions

View File

@ -22,7 +22,6 @@ export abstract class ListingComponent<T extends IListable> extends AutoUnsubscr
readonly noMatch$ = this._noMatch$;
readonly noContent$ = this._noContent$;
readonly sortedDisplayedEntities$ = this._sortedDisplayedEntities$;
readonly listingMode$: Observable<ListingMode>;
abstract readonly tableColumnConfigs: readonly TableColumnConfig<T>[];
abstract readonly tableHeaderLabel: string;
@ -34,21 +33,12 @@ export abstract class ListingComponent<T extends IListable> extends AutoUnsubscr
protected constructor(protected readonly _injector: Injector) {
super();
this.listingMode$ = this._listingMode$.asObservable();
}
get allEntities(): T[] {
return this.entitiesService.all;
}
get listingMode(): ListingMode {
return this._listingMode$.value;
}
set listingMode(listingMode: ListingMode) {
this._listingMode$.next(listingMode);
}
private get _sortedDisplayedEntities$(): Observable<readonly T[]> {
const sort = (entities: T[]) => this.sortingService.defaultSort(entities);
const sortedEntities$ = this.listingService.displayed$.pipe(map(sort));

View File

@ -19,6 +19,7 @@ import { DragDropModule } from '@angular/cdk/drag-drop';
import { PageHeaderComponent } from './page-header/page-header.component';
import { IqserButtonsModule } from '../buttons';
import { IqserHelpModeModule } from '../help-mode';
import { TableContentComponent } from './table-content/table-content.component';
const matModules = [MatTooltipModule];
const components = [
@ -45,7 +46,7 @@ const modules = [
const utils = [SyncWidthDirective];
@NgModule({
declarations: [...components, ...utils],
declarations: [...components, ...utils, TableContentComponent],
exports: [...components, ...utils],
imports: [CommonModule, ...modules, ...matModules],
})

View File

@ -0,0 +1,43 @@
<cdk-virtual-scroll-viewport
[class.no-data]="listingComponent.noContent$ | async"
[itemSize]="itemSize"
[maxBufferPx]="1500"
[minBufferPx]="300"
iqserHasScrollbar
>
<ng-container *ngIf="listingComponent.sortedDisplayedEntities$ | async as entities">
<!-- mouseenter and mouseleave triggers change detection event if itemMouse functions are undefined -->
<!-- this little hack below ensures that change detection won't be triggered if functions are undefined -->
<ng-container *ngIf="itemMouseEnterFn || itemMouseLeaveFn; else withoutMouseEvents">
<div
(mouseenter)="itemMouseEnterFn && itemMouseEnterFn(entity)"
(mouseleave)="itemMouseLeaveFn && itemMouseLeaveFn(entity)"
*cdkVirtualFor="let entity of entities; trackBy: trackById"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink"
>
<ng-container *ngTemplateOutlet="tableItem; context: { entity: entity }"></ng-container>
</div>
</ng-container>
<ng-template #withoutMouseEvents>
<div
*cdkVirtualFor="let entity of entities; trackBy: trackById"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink"
>
<ng-container *ngTemplateOutlet="tableItem; context: { entity: entity }"></ng-container>
</div>
</ng-template>
</ng-container>
</cdk-virtual-scroll-viewport>
<ng-template #tableItem let-entity="entity">
<div (click)="listingComponent.toggleEntitySelected($event, entity)" *ngIf="selectionEnabled" class="selection-column">
<iqser-round-checkbox [active]="listingComponent.isSelected(entity)"></iqser-round-checkbox>
</div>
<ng-container *ngTemplateOutlet="listingComponent.tableItemTemplate; context: { entity: entity }"></ng-container>
<div class="scrollbar-placeholder"></div>
</ng-template>

View File

@ -0,0 +1,149 @@
@use '../../../assets/styles/common-mixins' as mixins;
:host cdk-virtual-scroll-viewport {
height: calc(100vh - 50px - 31px - 111px);
overflow-y: hidden !important;
@include mixins.scroll-bar;
&.no-data {
display: none;
}
&.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;
> *:not(.selection-column):not(.scrollbar-placeholder) {
display: contents;
}
> div,
.cell {
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
box-sizing: border-box;
border-bottom: 1px solid var(--iqser-separator);
height: var(--itemSize);
padding: 0 10px;
&.center {
align-items: center;
justify-content: center;
}
}
.cell {
min-width: 110px;
&:first-of-type {
padding: 0 24px;
}
}
.selection-column {
padding-right: 0 !important;
iqser-round-checkbox .wrapper {
opacity: 0;
transition: opacity 0.2s;
&.active {
opacity: 1;
}
}
& + * > .cell:first-of-type {
padding: 0 10px;
}
}
&.disabled > div,
&.disabled .cell {
background-color: var(--iqser-grey-2);
color: var(--iqser-disabled);
.action-buttons {
color: initial;
}
}
.table-item-title {
font-weight: 600;
@include mixins.line-clamp(1);
width: fit-content;
max-width: 100%;
}
.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: 21px;
z-index: 1;
background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, var(--iqser-grey-2) 35%);
mat-icon {
width: 14px;
}
iqser-circle-button:not(:last-child) {
margin-right: 2px;
}
}
input,
mat-select {
margin-top: 0;
}
&:hover {
.selection-column iqser-round-checkbox .wrapper {
opacity: 1;
}
.action-buttons {
display: flex;
}
}
&:hover:not(.disabled) {
> div,
> * > div {
background-color: var(--iqser-not-disabled-table-item);
}
}
}
}
&:hover {
overflow-y: auto !important;
&.has-scrollbar {
.table-item {
.action-buttons {
right: 0;
padding-right: 13px;
}
.scrollbar-placeholder {
display: none;
}
}
}
}
}

View File

@ -0,0 +1,68 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, Inject, Input, OnDestroy, ViewChild } from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { tap } from 'rxjs/operators';
import { AutoUnsubscribe } from '../../utils';
import { IListable } from '../models';
import { ListingComponent, ListingService } from '../index';
import { HasScrollbarDirective } from '../../scrollbar';
@Component({
selector: 'iqser-table-content',
templateUrl: './table-content.component.html',
styleUrls: ['./table-content.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TableContentComponent<T extends IListable> extends AutoUnsubscribe implements OnDestroy, AfterViewInit {
@Input() itemSize!: number;
@Input() itemMouseEnterFn?: (entity: T) => void;
@Input() itemMouseLeaveFn?: (entity: T) => void;
@Input() tableItemClasses?: Record<string, (e: T) => boolean>;
@Input() selectionEnabled!: boolean;
@ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport;
@ViewChild(HasScrollbarDirective, { static: true }) readonly hasScrollbarDirective!: HasScrollbarDirective;
private _lastScrolledIndex = 0;
constructor(
@Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<T>,
readonly listingService: ListingService<T>,
) {
super();
this.addSubscription = this.listingComponent.noContent$.subscribe(() => {
setTimeout(() => {
this.scrollViewport?.checkViewportSize();
}, 0);
});
}
ngAfterViewInit(): void {
this.addSubscription = this.scrollViewport.scrolledIndexChange.pipe(tap(index => (this._lastScrolledIndex = index))).subscribe();
this.addSubscription = this.listingService.displayedLength$.subscribe(() => {
setTimeout(() => {
this.hasScrollbarDirective.process();
}, 0);
});
}
scrollToLastIndex(): void {
this.scrollViewport.scrollToIndex(this._lastScrolledIndex, 'smooth');
}
trackById(index: number, entity: T): string {
return entity.id;
}
getTableItemClasses(entity: T): Record<string, boolean> {
const classes: Record<string, boolean> = {
'table-item': true,
pointer: !!entity.routerLink && entity.routerLink.length > 0,
};
for (const key in this.tableItemClasses) {
if (Object.prototype.hasOwnProperty.call(this.tableItemClasses, key)) {
classes[key] = this.tableItemClasses[key](entity);
}
}
return classes;
}
}

View File

@ -22,34 +22,17 @@
<iqser-empty-state *ngIf="listingComponent.noMatch$ | async" [text]="noMatchText"></iqser-empty-state>
<cdk-virtual-scroll-viewport
[class.no-data]="listingComponent.noContent$ | async"
<iqser-table-content
#tableContent
[itemMouseEnterFn]="itemMouseEnterFn"
[itemMouseLeaveFn]="itemMouseLeaveFn"
[itemSize]="itemSize"
[maxBufferPx]="1500"
[minBufferPx]="300"
iqserHasScrollbar
>
<ng-container *ngIf="listingComponent.sortedDisplayedEntities$ | async as entities">
<div
(mouseenter)="itemMouseEnterFn && itemMouseEnterFn(entity)"
(mouseleave)="itemMouseLeaveFn && itemMouseLeaveFn(entity)"
*cdkVirtualFor="let entity of entities; trackBy: trackById"
[ngClass]="getTableItemClasses(entity)"
[routerLink]="entity.routerLink"
>
<div (click)="listingComponent.toggleEntitySelected($event, entity)" *ngIf="selectionEnabled" class="selection-column">
<iqser-round-checkbox [active]="listingComponent.isSelected(entity)"></iqser-round-checkbox>
</div>
<ng-container *ngTemplateOutlet="listingComponent.tableItemTemplate; context: { entity: entity }"></ng-container>
<div class="scrollbar-placeholder"></div>
</div>
</ng-container>
</cdk-virtual-scroll-viewport>
[selectionEnabled]="selectionEnabled"
[tableItemClasses]="tableItemClasses"
></iqser-table-content>
<iqser-scroll-button
*ngIf="hasScrollButton && scrollViewport"
*ngIf="hasScrollButton && tableContent?.scrollViewport"
[itemSize]="itemSize"
[scrollViewport]="scrollViewport"
[scrollViewport]="tableContent.scrollViewport"
></iqser-scroll-button>

View File

@ -1,149 +0,0 @@
@use '../../../assets/styles/common-mixins' as mixins;
:host cdk-virtual-scroll-viewport {
height: calc(100vh - 50px - 31px - 111px);
overflow-y: hidden !important;
@include mixins.scroll-bar;
&.no-data {
display: none;
}
&.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;
> *:not(.selection-column):not(.scrollbar-placeholder) {
display: contents;
}
> div,
.cell {
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
box-sizing: border-box;
border-bottom: 1px solid var(--iqser-separator);
height: var(--itemSize);
padding: 0 10px;
&.center {
align-items: center;
justify-content: center;
}
}
.cell {
min-width: 110px;
&:first-of-type {
padding: 0 24px;
}
}
.selection-column {
padding-right: 0 !important;
iqser-round-checkbox .wrapper {
opacity: 0;
transition: opacity 0.2s;
&.active {
opacity: 1;
}
}
& + * > .cell:first-of-type {
padding: 0 10px;
}
}
&.disabled > div,
&.disabled .cell {
background-color: var(--iqser-grey-2);
color: var(--iqser-disabled);
.action-buttons {
color: initial;
}
}
.table-item-title {
font-weight: 600;
@include mixins.line-clamp(1);
width: fit-content;
max-width: 100%;
}
.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: 21px;
z-index: 1;
background: linear-gradient(to right, rgba(244, 245, 247, 0) 0%, var(--iqser-grey-2) 35%);
mat-icon {
width: 14px;
}
iqser-circle-button:not(:last-child) {
margin-right: 2px;
}
}
input,
mat-select {
margin-top: 0;
}
&:hover {
.selection-column iqser-round-checkbox .wrapper {
opacity: 1;
}
.action-buttons {
display: flex;
}
}
&:hover:not(.disabled) {
> div,
> * > div {
background-color: var(--iqser-not-disabled-table-item);
}
}
}
}
&:hover {
overflow-y: auto !important;
&.has-scrollbar {
.table-item {
.action-buttons {
right: 0;
padding-right: 13px;
}
.scrollbar-placeholder {
display: none;
}
}
}
}
}

View File

@ -11,12 +11,11 @@ import {
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { AutoUnsubscribe, Required } from '../../utils';
import { IListable, ListingModes, TableColumnConfig } from '../models';
import { ListingComponent } from '../listing-component.directive';
import { EntitiesService, ListingService } from '../services';
import { HasScrollbarDirective } from '../../scrollbar';
import { EntitiesService } from '../services';
import { TableContentComponent } from '../table-content/table-content.component';
const SCROLLBAR_WIDTH = 11;
@ -43,19 +42,17 @@ export class TableComponent<T extends IListable> extends AutoUnsubscribe impleme
@Input() noDataButtonIcon?: string;
@Input() noDataButtonLabel?: string;
@Input() showNoDataButton = false;
@Output() readonly noDataAction = new EventEmitter<void>();
@Input() noMatchText?: string;
@Input() tableItemClasses?: { [key: string]: (e: T) => boolean };
@Input() tableItemClasses?: Record<string, (e: T) => boolean>;
@Input() itemMouseEnterFn?: (entity: T) => void;
@Input() itemMouseLeaveFn?: (entity: T) => void;
@ViewChild(CdkVirtualScrollViewport, { static: true }) readonly scrollViewport!: CdkVirtualScrollViewport;
@ViewChild(HasScrollbarDirective, { static: true }) hasScrollbarDirective!: HasScrollbarDirective;
@Output() readonly noDataAction = new EventEmitter<void>();
@ViewChild(TableContentComponent, { static: true }) readonly tableContent!: TableContentComponent<T>;
constructor(
@Inject(forwardRef(() => ListingComponent)) readonly listingComponent: ListingComponent<T>,
private readonly _hostRef: ViewContainerRef,
readonly entitiesService: EntitiesService<T>,
readonly listingService: ListingService<T>,
) {
super();
}
@ -68,42 +65,10 @@ export class TableComponent<T extends IListable> extends AutoUnsubscribe impleme
return this.listingComponent.tableHeaderLabel;
}
trackById(index: number, entity: T): string {
return entity.id;
}
ngOnInit(): void {
this.addSubscription = this.listingService.displayedLength$.subscribe(() => {
setTimeout(() => {
this.hasScrollbarDirective.process();
}, 0);
});
this.addSubscription = this.listingComponent.noContent$.subscribe(() => {
setTimeout(() => {
this.scrollViewport?.checkViewportSize();
}, 0);
});
this._patchConfig();
this._setStyles();
}
getTableItemClasses(entity: T): { [key: string]: boolean } {
const classes: { [key: string]: boolean } = {
'table-item': true,
pointer: !!entity.routerLink && entity.routerLink.length > 0,
};
for (const key in this.tableItemClasses) {
if (Object.prototype.hasOwnProperty.call(this.tableItemClasses, key)) {
classes[key] = this.tableItemClasses[key](entity);
}
}
return classes;
}
private _patchConfig() {
this.tableColumnConfigs[this.tableColumnConfigs.length - 1].last = true;
}
private _setStyles(): void {
const element = this._hostRef.element.nativeElement as HTMLElement;
this._setColumnsWidth(element);