don't use mouseenter if not needed
This commit is contained in:
parent
aa7926da8d
commit
710d014455
@ -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));
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
43
src/lib/listing/table-content/table-content.component.html
Normal file
43
src/lib/listing/table-content/table-content.component.html
Normal 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>
|
||||
149
src/lib/listing/table-content/table-content.component.scss
Normal file
149
src/lib/listing/table-content/table-content.component.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/lib/listing/table-content/table-content.component.ts
Normal file
68
src/lib/listing/table-content/table-content.component.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user