diff --git a/src/assets/icons/arrow-down-o.svg b/src/assets/icons/arrow-down-o.svg
new file mode 100644
index 0000000..384b3c9
--- /dev/null
+++ b/src/assets/icons/arrow-down-o.svg
@@ -0,0 +1,18 @@
+
diff --git a/src/lib/common-ui.module.ts b/src/lib/common-ui.module.ts
index 3f025d7..62a5f66 100644
--- a/src/lib/common-ui.module.ts
+++ b/src/lib/common-ui.module.ts
@@ -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];
diff --git a/src/lib/icons/icons.module.ts b/src/lib/icons/icons.module.ts
index f49d0d1..bc0aa02 100644
--- a/src/lib/icons/icons.module.ts
+++ b/src/lib/icons/icons.module.ts
@@ -12,6 +12,7 @@ export class IqserIconsModule {
constructor(private readonly _iconRegistry: MatIconRegistry, private readonly _sanitizer: DomSanitizer) {
const icons: Set = new Set([
'arrow-down',
+ 'arrow-down-o',
'check',
'close',
'edit',
diff --git a/src/lib/listing/index.ts b/src/lib/listing/index.ts
index 004b4bc..29623f2 100644
--- a/src/lib/listing/index.ts
+++ b/src/lib/listing/index.ts
@@ -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';
diff --git a/src/lib/listing/listing-component.directive.ts b/src/lib/listing/listing-component.directive.ts
index c28a074..9c212f5 100644
--- a/src/lib/listing/listing-component.directive.ts
+++ b/src/lib/listing/listing-component.directive.ts
@@ -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;
diff --git a/src/lib/listing/listing.module.ts b/src/lib/listing/listing.module.ts
index 04b5b05..d6b3cda 100644
--- a/src/lib/listing/listing.module.ts
+++ b/src/lib/listing/listing.module.ts
@@ -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 {}
diff --git a/src/lib/listing/models/index.ts b/src/lib/listing/models/index.ts
new file mode 100644
index 0000000..24d6869
--- /dev/null
+++ b/src/lib/listing/models/index.ts
@@ -0,0 +1,2 @@
+export * from './listable';
+export * from './table-column-config.model';
diff --git a/src/lib/listing/tables/models/table-column-config.model.ts b/src/lib/listing/models/table-column-config.model.ts
similarity index 57%
rename from src/lib/listing/tables/models/table-column-config.model.ts
rename to src/lib/listing/models/table-column-config.model.ts
index 997a7cf..2e9d7dc 100644
--- a/src/lib/listing/tables/models/table-column-config.model.ts
+++ b/src/lib/listing/models/table-column-config.model.ts
@@ -1,4 +1,5 @@
-import { KeysOf } from '../../../utils';
+import { KeysOf } from '../../utils';
+import { TemplateRef } from '@angular/core';
export interface TableColumnConfig {
readonly label: string;
@@ -8,4 +9,6 @@ export interface TableColumnConfig {
readonly rightIcon?: string;
readonly rightIconTooltip?: string;
readonly notTranslatable?: boolean;
+ readonly width?: string; // TODO: make required
+ readonly template?: TemplateRef; // TODO: make required
}
diff --git a/src/lib/listing/scroll-button/scroll-button.component.html b/src/lib/listing/scroll-button/scroll-button.component.html
new file mode 100644
index 0000000..b04b249
--- /dev/null
+++ b/src/lib/listing/scroll-button/scroll-button.component.html
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/lib/listing/scroll-button/scroll-button.component.scss b/src/lib/listing/scroll-button/scroll-button.component.scss
new file mode 100644
index 0000000..093aaa9
--- /dev/null
+++ b/src/lib/listing/scroll-button/scroll-button.component.scss
@@ -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;
+}
diff --git a/src/lib/listing/scroll-button/scroll-button.component.ts b/src/lib/listing/scroll-button/scroll-button.component.ts
new file mode 100644
index 0000000..09ad966
--- /dev/null
+++ b/src/lib/listing/scroll-button/scroll-button.component.ts
@@ -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;
+ showScrollDown$?: Observable;
+
+ 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);
+ }
+ }
+}
diff --git a/src/lib/listing/entities.service.ts b/src/lib/listing/services/entities.service.ts
similarity index 96%
rename from src/lib/listing/entities.service.ts
rename to src/lib/listing/services/entities.service.ts
index c73f34b..b4edab1 100644
--- a/src/lib/listing/entities.service.ts
+++ b/src/lib/listing/services/entities.service.ts
@@ -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());
diff --git a/src/lib/listing/services/index.ts b/src/lib/listing/services/index.ts
new file mode 100644
index 0000000..8d6e94c
--- /dev/null
+++ b/src/lib/listing/services/index.ts
@@ -0,0 +1 @@
+export * from './entities.service';
diff --git a/src/lib/listing/tables/sync-width.directive.ts b/src/lib/listing/sync-width.directive.ts
similarity index 100%
rename from src/lib/listing/tables/sync-width.directive.ts
rename to src/lib/listing/sync-width.directive.ts
diff --git a/src/lib/listing/tables/table-column-name/table-column-name.component.html b/src/lib/listing/table-column-name/table-column-name.component.html
similarity index 100%
rename from src/lib/listing/tables/table-column-name/table-column-name.component.html
rename to src/lib/listing/table-column-name/table-column-name.component.html
diff --git a/src/lib/listing/tables/table-column-name/table-column-name.component.scss b/src/lib/listing/table-column-name/table-column-name.component.scss
similarity index 95%
rename from src/lib/listing/tables/table-column-name/table-column-name.component.scss
rename to src/lib/listing/table-column-name/table-column-name.component.scss
index ed0a5d7..87a10b1 100644
--- a/src/lib/listing/tables/table-column-name/table-column-name.component.scss
+++ b/src/lib/listing/table-column-name/table-column-name.component.scss
@@ -1,4 +1,4 @@
-@import '../../../../assets/styles/common';
+@import '../../../assets/styles/common';
:host {
display: flex;
diff --git a/src/lib/listing/tables/table-column-name/table-column-name.component.ts b/src/lib/listing/table-column-name/table-column-name.component.ts
similarity index 87%
rename from src/lib/listing/tables/table-column-name/table-column-name.component.ts
rename to src/lib/listing/table-column-name/table-column-name.component.ts
index 887cb14..8977bf2 100644
--- a/src/lib/listing/tables/table-column-name/table-column-name.component.ts
+++ b/src/lib/listing/table-column-name/table-column-name.component.ts
@@ -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 = (thisArg: TableColumnNameComponent) => !!thisArg.rightIcon;
diff --git a/src/lib/listing/table-header/table-header.component.ts b/src/lib/listing/table-header/table-header.component.ts
index 86bbb4e..c9d9df7 100644
--- a/src/lib/listing/table-header/table-header.component.ts
+++ b/src/lib/listing/table-header/table-header.component.ts
@@ -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',
diff --git a/src/lib/listing/table/table.component.html b/src/lib/listing/table/table.component.html
new file mode 100644
index 0000000..c07daff
--- /dev/null
+++ b/src/lib/listing/table/table.component.html
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ n
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/lib/listing/table/table.component.scss b/src/lib/listing/table/table.component.scss
new file mode 100644
index 0000000..55f6016
--- /dev/null
+++ b/src/lib/listing/table/table.component.scss
@@ -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;
+ }
+ }
+ }
+ }
+}
diff --git a/src/lib/listing/table/table.component.ts b/src/lib/listing/table/table.component.ts
new file mode 100644
index 0000000..8afad4b
--- /dev/null
+++ b/src/lib/listing/table/table.component.ts
@@ -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 implements OnInit {
+ @Input() bulkActions?: TemplateRef;
+ @Input() actionsTemplate?: TemplateRef;
+ @Input() @Required() itemSize!: number;
+ @Input() @Required() tableColumnConfigs!: readonly TableColumnConfig[];
+ @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) {}
+
+ get listingComponent(): ListingComponent {
+ 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`);
+ }
+}
diff --git a/src/lib/listing/tables/index.ts b/src/lib/listing/tables/index.ts
deleted file mode 100644
index 2cad679..0000000
--- a/src/lib/listing/tables/index.ts
+++ /dev/null
@@ -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';
diff --git a/src/lib/listing/tables/tables-module.ts b/src/lib/listing/tables/tables-module.ts
deleted file mode 100644
index 96a9db2..0000000
--- a/src/lib/listing/tables/tables-module.ts
+++ /dev/null
@@ -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 {}
diff --git a/src/lib/listing/workflow-listing/index.ts b/src/lib/listing/workflow-listing/index.ts
deleted file mode 100644
index 87b1b5e..0000000
--- a/src/lib/listing/workflow-listing/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './workflow-listing.module';
diff --git a/src/lib/listing/workflow-listing/workflow-listing.module.ts b/src/lib/listing/workflow-listing/workflow-listing.module.ts
deleted file mode 100644
index 66d6d54..0000000
--- a/src/lib/listing/workflow-listing/workflow-listing.module.ts
+++ /dev/null
@@ -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 {}
diff --git a/src/lib/scrollbar/has-scrollbar.directive.ts b/src/lib/scrollbar/has-scrollbar.directive.ts
new file mode 100644
index 0000000..c2edf2f
--- /dev/null
+++ b/src/lib/scrollbar/has-scrollbar.directive.ts
@@ -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;
+ }
+ }
+}
diff --git a/src/lib/scrollbar/index.ts b/src/lib/scrollbar/index.ts
new file mode 100644
index 0000000..19629b5
--- /dev/null
+++ b/src/lib/scrollbar/index.ts
@@ -0,0 +1,2 @@
+export * from '../scrollbar/has-scrollbar.directive';
+export * from './scrollbar.module';
diff --git a/src/lib/scrollbar/scrollbar.module.ts b/src/lib/scrollbar/scrollbar.module.ts
new file mode 100644
index 0000000..5efb8f1
--- /dev/null
+++ b/src/lib/scrollbar/scrollbar.module.ts
@@ -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 {}