Browse Source

UI: Implement pagination support on overflow for toggle select/header component.

pull/9033/head
Igor Kulikov 3 years ago
parent
commit
20db421a8a
  1. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  2. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  3. 33
      ui-ngx/src/app/shared/components/toggle-header.component.html
  4. 26
      ui-ngx/src/app/shared/components/toggle-header.component.scss
  5. 179
      ui-ngx/src/app/shared/components/toggle-header.component.ts
  6. 1
      ui-ngx/src/app/shared/components/toggle-select.component.html
  7. 9
      ui-ngx/src/app/shared/components/toggle-select.component.ts

2
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -20,7 +20,7 @@
<h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-select *ngIf="hasBasicMode" appearance="fill-invert" [(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<tb-toggle-select *ngIf="hasBasicMode" appearance="fill-invert" disablePagination [(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -360,7 +360,7 @@
[isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()">
<div class="details-buttons" fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-toggle-select *ngIf="tbEditWidget.hasBasicMode" appearance="fill-invert" [(ngModel)]="tbEditWidget.widgetConfigMode">
<tb-toggle-select *ngIf="tbEditWidget.hasBasicMode" appearance="fill-invert" disablePagination [(ngModel)]="tbEditWidget.widgetConfigMode">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>

33
ui-ngx/src/app/shared/components/toggle-header.component.html

@ -15,14 +15,31 @@
limitations under the License.
-->
<mat-button-toggle-group *ngIf="!isMdLg || !useSelectOnMdLg; else select" class="tb-toggle-header"
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
'tb-invert': appearance === 'fill-invert',
'tb-ignore-md-lg': ignoreMdLgSize,
'tb-disabled': disabled }" [name]="name" [(ngModel)]="value"
(ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value" [disabled]="disabled">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>
<button mat-icon-button
[disabled]="!leftPaginationEnabled"
(click)="handlePaginatorClick('before', $event)"
(touchstart)="handlePaginatorTouchStart('before', $event)"
class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': (ignoreMdLgSize || !isMdLg), 'tb-mat-24': !ignoreMdLgSize && isMdLg}">
<mat-icon>chevron_left</mat-icon>
</button>
<div #toggleGroupContainer class="tb-toggle-container" [class]="{'tb-disable-pagination': disablePagination}" *ngIf="!isMdLg || !useSelectOnMdLg; else select" >
<mat-button-toggle-group #toggleGroup
class="tb-toggle-header"
[ngClass]="{'tb-fill': (appearance === 'fill' || appearance === 'fill-invert'),
'tb-invert': appearance === 'fill-invert',
'tb-ignore-md-lg': ignoreMdLgSize,
'tb-disabled': disabled }" [name]="name" [(ngModel)]="value"
(ngModelChange)="valueChange.emit(value)">
<mat-button-toggle *ngFor="let option of options; trackBy: trackByHeaderOption" [value]="option.value" [disabled]="disabled">{{ option.name }}</mat-button-toggle>
</mat-button-toggle-group>
</div>
<button mat-icon-button
[disabled]="!rightPaginationEnabled"
(click)="handlePaginatorClick('after', $event)"
(touchstart)="handlePaginatorTouchStart('after', $event)"
class="tb-toggle-header-pagination-button" [class]="{'tb-mat-32': (ignoreMdLgSize || !isMdLg), 'tb-mat-24': !ignoreMdLgSize && isMdLg}">
<mat-icon>chevron_right</mat-icon>
</button>
<ng-template #select>
<mat-form-field appearance="outline" class="tb-toggle-header-select" subscriptSizing="dynamic">
<mat-select [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)" [disabled]="disabled">

26
ui-ngx/src/app/shared/components/toggle-header.component.scss

@ -17,8 +17,34 @@
@import "../../../theme";
@import "../../../scss/constants";
:host {
max-width: 100%;
display: grid;
grid-template-columns: min-content minmax(auto, 1fr) min-content;
.tb-toggle-header-pagination-button {
display: none;
}
&.tb-toggle-header-pagination-controls-enabled {
.tb-toggle-header-pagination-button {
display: block;
}
}
.tb-toggle-container {
display: inline-grid;
grid-column: 2;
overflow: hidden;
&.tb-disable-pagination {
overflow: visible;
}
}
.tb-toggle-header {
transition: transform 500ms cubic-bezier(0.35, 0, 0.25, 1);
}
}
:host ::ng-deep {
.mat-button-toggle-group.mat-button-toggle-group-appearance-standard.tb-toggle-header {
overflow: visible;
width: 100%;
border-radius: 100px;
height: 32px;

179
ui-ngx/src/app/shared/components/toggle-header.component.ts

@ -15,18 +15,22 @@
///
import {
AfterContentChecked,
AfterContentInit,
AfterViewInit,
ChangeDetectorRef,
Component,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
HostBinding,
Input,
OnDestroy,
OnInit,
Output,
QueryList
QueryList,
ViewChild
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
@ -36,6 +40,8 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { coerceBoolean } from '@shared/decorators/coercion';
import { startWith, takeUntil } from 'rxjs/operators';
import { Platform } from '@angular/cdk/platform';
import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle';
export interface ToggleHeaderOption {
name: string;
@ -44,6 +50,8 @@ export interface ToggleHeaderOption {
export type ToggleHeaderAppearance = 'fill' | 'fill-invert' | 'stroked';
export type ScrollDirection = 'after' | 'before';
@Directive(
{
// eslint-disable-next-line @angular-eslint/directive-selector
@ -72,7 +80,7 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI
@Input()
options: ToggleHeaderOption[] = [];
private _destroyed = new Subject<void>();
protected _destroyed = new Subject<void>();
protected constructor(protected store: Store<AppState>) {
super(store);
@ -109,7 +117,34 @@ export abstract class _ToggleBase extends PageComponent implements AfterContentI
templateUrl: './toggle-header.component.html',
styleUrls: ['./toggle-header.component.scss']
})
export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterContentInit, OnDestroy {
export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterViewInit, AfterContentInit, AfterContentChecked, OnDestroy {
@ViewChild('toggleGroup', {static: false})
toggleGroup: ElementRef<HTMLElement>;
@ViewChild(MatButtonToggleGroup, {static: false})
buttonToggleGroup: MatButtonToggleGroup;
@ViewChild('toggleGroupContainer', {static: false})
toggleGroupContainer: ElementRef<HTMLElement>;
@HostBinding('class.tb-toggle-header-pagination-controls-enabled')
private showPaginationControls = false;
private toggleGroupResize$: ResizeObserver;
leftPaginationEnabled = false;
rightPaginationEnabled = false;
private _scrollDistance = 0;
private _scrollDistanceChanged: boolean;
get scrollDistance(): number {
return this._scrollDistance;
}
set scrollDistance(value: number) {
this._scrollTo(value);
}
@Input()
value: any;
@ -120,6 +155,10 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC
@Input()
name: string;
@Input()
@coerceBoolean()
disablePagination = false;
@Input()
@coerceBoolean()
useSelectOnMdLg = true;
@ -141,6 +180,7 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
private platform: Platform,
private breakpointObserver: BreakpointObserver) {
super(store);
}
@ -154,9 +194,142 @@ export class ToggleHeaderComponent extends _ToggleBase implements OnInit, AfterC
this.cd.markForCheck();
}
);
if (!this.disablePagination) {
this.valueChange.pipe(takeUntil(this._destroyed)).subscribe(() => {
this.scrollToToggleOptionValue();
});
}
}
ngOnDestroy() {
if (this.toggleGroupResize$) {
this.toggleGroupResize$.disconnect();
}
super.ngOnDestroy();
}
ngAfterViewInit() {
if (!this.disablePagination && !this.useSelectOnMdLg) {
this.toggleGroupResize$ = new ResizeObserver(() => {
this.updatePagination();
});
this.toggleGroupResize$.observe(this.toggleGroupContainer.nativeElement);
}
}
ngAfterContentChecked() {
if (this._scrollDistanceChanged) {
this.updateToggleHeaderScrollPosition();
this._scrollDistanceChanged = false;
this.cd.markForCheck();
}
}
trackByHeaderOption(index: number, option: ToggleHeaderOption){
return option.value;
}
handlePaginatorClick(direction: ScrollDirection, $event: Event) {
if ($event) {
$event.stopPropagation();
}
this.scrollHeader(direction);
}
handlePaginatorTouchStart(direction: ScrollDirection, $event: Event) {
if (direction === 'before' && !this.leftPaginationEnabled ||
direction === 'after' && !this.rightPaginationEnabled) {
$event.preventDefault();
}
}
private scrollHeader(direction: ScrollDirection) {
const viewLength = this.toggleGroup.nativeElement.offsetWidth;
// Move the scroll distance one-third the length of the tab list's viewport.
const scrollAmount = ((direction === 'before' ? -1 : 1) * viewLength) / 3;
return this._scrollTo(this._scrollDistance + scrollAmount);
}
private scrollToToggleOptionValue() {
if (this.buttonToggleGroup && this.buttonToggleGroup.selected) {
const selectedToggleButton = this.buttonToggleGroup.selected as MatButtonToggle;
const viewLength = this.toggleGroupContainer.nativeElement.offsetWidth;
const {offsetLeft, offsetWidth} = (selectedToggleButton._buttonElement.nativeElement.offsetParent as HTMLElement);
const labelBeforePos = offsetLeft; // this.toggleGroup.nativeElement.offsetWidth - offsetLeft;
const labelAfterPos = labelBeforePos + offsetWidth;
const beforeVisiblePos = this.scrollDistance;
const afterVisiblePos = this.scrollDistance + viewLength;
if (labelBeforePos < beforeVisiblePos) {
this.scrollDistance -= beforeVisiblePos - labelBeforePos;
} else if (labelAfterPos > afterVisiblePos) {
this.scrollDistance += Math.min(
labelAfterPos - afterVisiblePos,
labelBeforePos - beforeVisiblePos,
);
}
}
}
private updatePagination() {
this.checkPaginationEnabled();
this.checkPaginationControls();
this.updateToggleHeaderScrollPosition();
}
private checkPaginationEnabled() {
if (this.toggleGroupContainer) {
const isEnabled = this.toggleGroup.nativeElement.scrollWidth > this.toggleGroupContainer.nativeElement.offsetWidth;
if (isEnabled !== this.showPaginationControls) {
if (!isEnabled) {
this.scrollDistance = 0;
} else {
setTimeout(() => {
this.scrollToToggleOptionValue();
}, 0);
}
this.cd.markForCheck();
this.showPaginationControls = isEnabled;
}
} else {
this.showPaginationControls = false;
}
}
private checkPaginationControls() {
if (!this.showPaginationControls) {
this.leftPaginationEnabled = this.rightPaginationEnabled = false;
} else {
// Check if the pagination arrows should be activated.
this.leftPaginationEnabled = this.scrollDistance > 0;
this.rightPaginationEnabled = this.scrollDistance < this.getMaxScrollDistance();
this.cd.markForCheck();
}
}
private getMaxScrollDistance(): number {
const lengthOfToggleGroup = this.toggleGroup.nativeElement.scrollWidth;
const viewLength = this.toggleGroupContainer.nativeElement.offsetWidth;
return lengthOfToggleGroup - viewLength || 0;
}
private _scrollTo(position: number) {
if (!this.showPaginationControls) {
return {maxScrollDistance: 0, distance: 0};
} else {
const maxScrollDistance = this.getMaxScrollDistance();
this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position));
this._scrollDistanceChanged = true;
this.checkPaginationControls();
return {maxScrollDistance, distance: this._scrollDistance};
}
}
private updateToggleHeaderScrollPosition() {
const scrollDistance = this.scrollDistance;
const translateX = -scrollDistance;
this.toggleGroup.nativeElement.style.transform = `translateX(${Math.round(translateX)}px)`;
if (this.platform.TRIDENT || this.platform.EDGE) {
this.toggleGroupContainer.nativeElement.scrollLeft = 0;
}
}
}

1
ui-ngx/src/app/shared/components/toggle-select.component.html

@ -20,6 +20,7 @@
useSelectOnMdLg="false"
[disabled]="disabled"
[appearance]="appearance"
[disablePagination]="disablePagination"
[options]="options"
[value]="modelValue"
(valueChange)="updateModel($event)">

9
ui-ngx/src/app/shared/components/toggle-select.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, forwardRef, Input } from '@angular/core';
import { Component, forwardRef, HostBinding, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@ -35,6 +35,9 @@ import { coerceBoolean } from '@shared/decorators/coercion';
})
export class ToggleSelectComponent extends _ToggleBase implements ControlValueAccessor {
@HostBinding('style.maxWidth')
get maxWidth() { return '100%'; }
@Input()
@coerceBoolean()
disabled: boolean;
@ -42,6 +45,10 @@ export class ToggleSelectComponent extends _ToggleBase implements ControlValueAc
@Input()
appearance: ToggleHeaderAppearance = 'stroked';
@Input()
@coerceBoolean()
disablePagination = false;
modelValue: any;
private propagateChange = null;

Loading…
Cancel
Save