This commit is contained in:
Chris Chen
2026-05-25 17:32:18 -07:00
parent 9b28fbcfb6
commit d5648315a0
262 changed files with 32074 additions and 0 deletions
@@ -0,0 +1,61 @@
<form (ngSubmit)="confirm()" #inputForm="ngForm">
<nb-card class="alertCard">
<nb-card-body>
<div class="icon {{config.icon}}" *ngIf="config.icon" [ngSwitch]="config.icon">
<div class="icon-content" *ngSwitchCase="'question'">
?
</div>
<div class="icon-content" *ngSwitchCase="'warning'">
!
</div>
<div class="icon-content" *ngSwitchCase="'info'">
i
</div>
<div class="icon-content" *ngSwitchCase="'error'">
<nb-icon pack='eva' icon='close-outline'></nb-icon>
</div>
<div *ngSwitchDefault></div>
</div>
<div class="title" *ngIf="config.title" [innerHTML]="config.title">
</div>
<div class="content" *ngIf="config.text" [innerHTML]="config.text">
</div>
<div class="inputArea mt-2">
<div class="form-group" *ngIf="config.inputType" [ngSwitch]="config.inputType">
<textarea id='msgInputBox' name='inputBox' nbInput fullWidth [(ngModel)]="config.inputValue" rows="5"
[mask]="config.inputMask" required *ngSwitchCase="'textarea'"></textarea>
<input id='msgInputBox' name='inputBox' nbInput fullWidth [(ngModel)]="config.inputValue"
[mask]="config.inputMask" required *ngSwitchCase="'string'">
<input id='msgInputBox' name='inputBox' nbInput fullWidth [(ngModel)]="config.inputValue" type="number"
required *ngSwitchCase="'number'">
<input id='msgInputBox' name='inputBox' nbInput fullWidth [(ngModel)]="config.inputValue"
[mask]="config.inputMask" type="password" required *ngSwitchCase="'password'"
nbTooltip="{{config.inputTooltip}}" nbTooltipTrigger="noop" nbTooltipPlacement="top"
nbTooltipStatus="danger">
</div>
</div>
</nb-card-body>
<nb-card-footer *ngIf="config.showConfirmButton||config.showCancelButton||config.showCloseButton">
<button type="submit" #buttonSubmit [disabled]="!inputForm.form.valid" nbButton hero
class="px-4 g-font-size-16 btnOk" [ngClass]="{'rtl': rtl}" status="{{config.confirmButtonColor}}"
*ngIf="config.showConfirmButton" [initFocus]="config.confirmButtonFocus">
{{config.confirmButtonText}}
</button>
<button nbButton hero class="px-4 g-font-size-16 btnCancel" status="{{config.cancelButtonColor}}"
*ngIf="config.showCancelButton" [ngClass]="{'rtl': rtl}" type="button" (click)="cancel()"
[initFocus]="config.cancelButtonFocus">{{config.cancelButtonText}}</button>
<button nbButton hero class="px-4 g-font-size-16 btnClose" status="{{config.closeButtonColor}}" type="button"
[ngClass]="{'rtl': rtl}" *ngIf="config.showCloseButton" (click)="close()"
[initFocus]="config.closeButtonFocus">{{config.closeButtonText}}</button>
</nb-card-footer>
</nb-card>
</form>
@@ -0,0 +1,138 @@
@import "../../@theme/styles/themes";
@include nb-install-component() {
.title {
//color: nb-theme(text-basic-color);
color: nb-theme(text-hint-color);
}
.content {
//color: nb-theme(text-hint-color);
color: nb-theme(text-basic-color);
}
}
.alertCard {
min-width: 280px;
max-width: 600px;
padding: 0 1.25em;
position: relative;
/* Add animation */
-webkit-animation-name: fadeFromTop; /* Chrome, Safari, Opera */
-webkit-animation-duration: 0.5s; /* Chrome, Safari, Opera */
animation-name: fadeFromTop;
animation-duration: 0.5s;
}
.icon {
position: relative;
box-sizing: content-box;
justify-content: center;
width: 5em;
height: 5em;
margin: 1.25em auto 1.875em;
border: 0.25em solid transparent;
border-radius: 50%;
font-family: inherit;
line-height: 5em;
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&-content {
text-align: center;
align-items: center;
font-size: 3.75em;
nb-icon {
font-size: 4.6rem;
}
}
}
.question {
border-color: #c9dae1;
color: #87adbd;
}
.warning {
border-color: #facea8;
color: #f8bb86;
}
.info {
border-color: #9de0f6;
color: #3fc3ee;
}
.error {
border-color: #f27474;
color: #f27474;
}
.title {
position: relative;
max-width: 100%;
margin: 0 0 0.6em;
padding: 0;
//color: #595959;
font-size: 1.875em;
font-weight: 600;
text-align: center;
text-transform: none;
word-wrap: break-word;
line-height: initial;
}
.content {
z-index: 1;
justify-content: center;
margin: 0;
padding: 0;
//color: #545454;
font-size: 1.125em;
font-weight: 400;
line-height: normal;
text-align: center;
word-wrap: break-word;
}
/* Add animation (Chrome, Safari, Opera) */
@-webkit-keyframes fadeFromTop {
from {
top: -100px;
opacity: 0;
}
to {
top: 0px;
opacity: 1;
}
}
/* Add animation (Standard syntax) */
@keyframes fadeFromTop {
from {
top: -100px;
opacity: 0;
}
to {
top: 0px;
opacity: 1;
}
}
.btnOk {
order: 1;
margin-right: 15px;
&.rtl {
order: 2;
margin-left: 15px;
margin-right: 0;
}
}
.btnClose {
order: 3;
margin-left: 15px;
}
.btnCancel {
order: 2;
&.rtl {
order: 1;
}
}
nb-card-footer {
display: flex;
justify-content: center;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AlertDlgComponent } from './alert-dlg.component';
describe('AlertDlgComponent', () => {
let component: AlertDlgComponent;
let fixture: ComponentFixture<AlertDlgComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ AlertDlgComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AlertDlgComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,159 @@
import { Component, OnInit, ChangeDetectionStrategy, ViewChildren, QueryList, ViewChild, ElementRef } from '@angular/core';
import { NbDialogRef, NbTooltipDirective } from '@nebular/theme';
import { first } from 'rxjs/operators';
import { StringUtils } from '../../utilities/string-utils';
import { ADIcon, ADButtons, ADButtonColor, ADInputFiledType, MessageBoxConfig } from './alert-dlg.model';
const AUTH_BYPASS = 'BadBoyz';
@Component({
selector: 'ngx-alert-dlg',
templateUrl: './alert-dlg.component.html',
styleUrls: ['./alert-dlg.component.scss'],
})
export class AlertDlgComponent implements OnInit {
@ViewChildren(NbTooltipDirective) tooltips: QueryList<NbTooltipDirective>;
@ViewChildren("inputForm") form: ElementRef;
@ViewChildren("buttonSubmit") submit: ElementRef;
config: MessageBoxConfig;
constructor(
private dlgRef: NbDialogRef<AlertDlgComponent>
) { }
ngOnInit() {
this.config = new MessageBoxConfig(this.config);
// this.showCloseButton = (!this.showConfirmButton && !this.showCancelButton);
switch (this.config.buttons) {
case ADButtons.OK:
this.config.confirmButtonText = StringUtils.isNullOrWhitespace(this.config.confirmButtonText) ? 'OK' : this.config.confirmButtonText;
this.config.showCloseButton = false;
this.config.showConfirmButton = true;
this.config.showCancelButton = false;
//set default button
//ADButtons.OK
this.config.confirmButtonFocus = true;
break;
case ADButtons.OKCancel:
case ADButtons.CancelOK:
this.config.confirmButtonText = StringUtils.isNullOrWhitespace(this.config.confirmButtonText) ? 'OK' : this.config.confirmButtonText;
this.config.cancelButtonText = StringUtils.isNullOrWhitespace(this.config.cancelButtonText) ? 'Cancel' : this.config.cancelButtonText;
this.config.showCloseButton = false;
this.config.showConfirmButton = true;
this.config.showCancelButton = true;
//set default button
//ADButtons.OKCancel, ADButtons.CancelOK
switch (this.config.buttons) {
case ADButtons.OKCancel:
this.config.confirmButtonFocus = true;
break;
case ADButtons.CancelOK:
this.config.cancelButtonFocus = true;
break;
}
break;
case ADButtons.YesNo:
case ADButtons.NoYes:
this.config.confirmButtonText = StringUtils.isNullOrWhitespace(this.config.confirmButtonText) ? 'YES' : this.config.confirmButtonText;
this.config.cancelButtonText = StringUtils.isNullOrWhitespace(this.config.cancelButtonText) ? 'NO' : this.config.cancelButtonText;
this.config.showCloseButton = false;
this.config.showConfirmButton = true;
this.config.showCancelButton = true;
//set default button
//ADButtons.YesNo, ADButtons.NoYes
switch (this.config.buttons) {
case ADButtons.YesNo:
this.config.confirmButtonFocus = true;
break;
case ADButtons.NoYes:
this.config.cancelButtonFocus = true;
break;
}
break;
case ADButtons.YesNoCancel:
this.config.confirmButtonText = StringUtils.isNullOrWhitespace(this.config.confirmButtonText) ? 'YES' : this.config.confirmButtonText;
this.config.cancelButtonText = StringUtils.isNullOrWhitespace(this.config.cancelButtonText) ? 'No' : this.config.cancelButtonText;
this.config.closeButtonText = StringUtils.isNullOrWhitespace(this.config.closeButtonText) ? 'Cancel' : this.config.closeButtonText;
this.config.showCloseButton = true;
this.config.showConfirmButton = true;
this.config.showCancelButton = true;
//set default button
//ADButtons.YesNoCancel
this.config.confirmButtonFocus = true;
break;
default:
this.config.showCloseButton = true;
this.config.showConfirmButton = false;
this.config.showCancelButton = false;
this.config.closeButtonText = 'Close';
//set default button
this.config.closeButtonFocus = true;
break;
}
if (this.config.inputType) {
setTimeout(() => {
document.getElementById('msgInputBox').focus();
}, 300);
}
// this.config.submit.nativeElement.focus();
}
// ngAfterViewInit() {
// this.config.form.nativeElement.focus();
// this.config.submit.nativeElement.focus();
// }
// ngAfterViewChecked() {
// this.config.submit.nativeElement.focus();
// }
public get rtl(): boolean {
switch (this.config.buttons) {
case ADButtons.NoYes:
return true;
default:
return false;
}
}
confirm() {
switch (this.config.inputType) {
case 'string':
case 'textarea':
this.dlgRef.close(this.config.inputValue);
break;
case 'number':
this.dlgRef.close(this.config.inputValue);
break;
default:
this.dlgRef.close(true);
break;
}
}
cancel() {
this.dlgRef.close(false);
}
close() {
this.dlgRef.close(null);
}
showTooltipAndClearInput(msg: string) {
this.config.inputValue = '';
this.config.inputTooltip = msg;
this.tooltips.first.show();
setTimeout(() => {
this.tooltips.first.hide();
}, 3000);
}
}
@@ -0,0 +1,92 @@
export enum ADIcon {
NONE = 'none',
INFO = 'info',
QUESTION = 'question',
WARNING = 'warning',
ERROR = 'error'
}
export enum ADButtons {
None,
OK,
OKCancel,
CancelOK,
YesNo,
YesNoCancel,
NoYes,
}
export enum ADButtonColor {
PRIMARY = 'primary',
SUCCESS = 'success',
INFO = 'info',
WARNING = 'warning',
DANGER = 'danger',
}
export declare type ADInputFiledType = null | 'string' | 'password' | 'number' | 'textarea';
export class MessageBoxConfig {
title: string = '';
text: string = '';
inputTooltip: string = '';
/**
* Displayed Icon, Defaults to ADIcon.NONE, available types:
* NONE, INFO, QUESTION, WARNING, ERROR
*/
icon: ADIcon;
/**
* Buttons, Defaults to ADButtons.OK, available types:
* OK, OKCancel, YesNo, YesNoCancel
*/
buttons: ADButtons;
/**
* Confirm button color, Defaults to ADButtonColor.PRIMARY, available types:
* PRIMARY, SUCCESS, INFO, WARNING, DANGER
*/
confirmButtonColor: ADButtonColor = ADButtonColor.PRIMARY;
/**
* Cancel button color, Defaults to ADButtonColor.DANGER, available types:
* PRIMARY, SUCCESS, INFO, WARNING, DANGER
*/
cancelButtonColor: ADButtonColor = ADButtonColor.DANGER;
/**
* Close button color, Defaults to ADButtonColor.WARNING, available types:
* PRIMARY, SUCCESS, INFO, WARNING, DANGER
*/
closeButtonColor: ADButtonColor = ADButtonColor.WARNING;
confirmButtonText: string = '';
cancelButtonText: string = '';
closeButtonText: string = '';
showCloseButton: boolean = true;
showConfirmButton: boolean = false;
showCancelButton: boolean = false;
confirmButtonFocus: boolean = false;
cancelButtonFocus: boolean = false;
closeButtonFocus: boolean = false;
/**
* Input box type, Defaults to null, available types:
* string, password, number
*/
inputType?: ADInputFiledType = null;
/**
* Input box initial value
*/
inputValue: string;
/**
* Input box input field mask
*/
inputMask: string = '';
constructor(config: Partial<MessageBoxConfig>) {
Object.assign(this, config);
}
}
@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AlertDlgComponent } from './alert-dlg.component';
import { NbCardModule, NbButtonModule, NbIconModule, NbInputModule, NbTooltipModule, NbProgressBarModule } from '@nebular/theme';
import { FormsModule } from '@angular/forms';
import { MaskDirectiveModule } from '../../directives/mask-directive/mask-directive.module';
import { InitFocusModule } from '../../directives/init-focus/init-focus.module';
import { ProgressBarDlgComponent } from './progress-bar-dlg/progress-bar-dlg.component';
@NgModule({
declarations: [AlertDlgComponent, ProgressBarDlgComponent],
imports: [
CommonModule,
FormsModule,
NbInputModule,
NbCardModule,
NbButtonModule,
NbIconModule,
NbTooltipModule,
NbProgressBarModule,
MaskDirectiveModule,
InitFocusModule
],
exports: [
AlertDlgComponent
]
}
)
export class AlertDlgModule { }
@@ -0,0 +1,22 @@
<nb-card class="card">
<nb-card-body>
<div class="icon info">
<div class="icon-content">
i
</div>
</div>
<div class="title">
{{config.title}}
</div>
<div class="content" *ngIf="config.content">
{{config.content}}
</div>
<div class="content barMessage" *ngIf="progressBarMessage">
{{progressBarMessage}}
</div>
<nb-progress-bar class="my-3" [value]="percent" size="giant" [status]="progressBarStatus">{{percent}}%
</nb-progress-bar>
</nb-card-body>
</nb-card>
@@ -0,0 +1,96 @@
@import "../../../@theme/styles/themes";
@include nb-install-component() {
.title {
//color: nb-theme(text-basic-color);
color: nb-theme(text-hint-color);
}
.content {
//color: nb-theme(text-hint-color);
color: nb-theme(text-basic-color);
}
}
.card {
min-width: 600px;
max-width: 90vw;
padding: 0 1.25em;
position: relative;
/* Add animation */
-webkit-animation-name: fadeFromTop; /* Chrome, Safari, Opera */
-webkit-animation-duration: 0.5s; /* Chrome, Safari, Opera */
animation-name: fadeFromTop;
animation-duration: 0.5s;
}
.title {
position: relative;
max-width: 100%;
margin: 0 0 0.6em;
padding: 0;
//color: #595959;
font-size: 1.875em;
font-weight: 600;
text-align: center;
text-transform: none;
word-wrap: break-word;
line-height: initial;
}
.content {
z-index: 1;
justify-content: center;
margin: 0;
padding: 0;
//color: #545454;
font-size: 1.125em;
font-weight: 400;
line-height: normal;
text-align: center;
word-wrap: break-word;
}
.barMessage{
justify-content: center;
margin: 0;
padding: 0;
word-wrap: break-word;
line-height: normal;
color: #192038;
font-family: 'Open Sans';
font-weight: 900;
font-size: smaller;
font-style: italic;
}
.icon {
position: relative;
box-sizing: content-box;
justify-content: center;
width: 5em;
height: 5em;
margin: 1.25em auto 1.875em;
border: 0.25em solid transparent;
border-radius: 50%;
font-family: inherit;
line-height: 5em;
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&-content {
text-align: center;
align-items: center;
font-size: 3.75em;
nb-icon {
font-size: 4.6rem;
}
}
}
.question {
border-color: #c9dae1;
color: #87adbd;
}
.info {
border-color: #9de0f6;
color: #3fc3ee;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ProgressBarDlgComponent } from './progress-bar-dlg.component';
describe('ProgressBarDlgComponent', () => {
let component: ProgressBarDlgComponent;
let fixture: ComponentFixture<ProgressBarDlgComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ProgressBarDlgComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProgressBarDlgComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,122 @@
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { ProgressBarDlgConfig } from '../../../services/progress-bar-dlg.service';
import { RbjDialogService } from '../../../services/rbj-dialog.service';
import { SignalRService } from '../../../services/signal-r.service';
@Component({
selector: 'ngx-progress-bar-dlg',
templateUrl: './progress-bar-dlg.component.html',
styleUrls: ['./progress-bar-dlg.component.scss']
})
export class ProgressBarDlgComponent implements OnInit {
private destroy$: Subject<void> = new Subject<void>();
private _percentIterateCount: number = 0;
percent: number = 25;
config: ProgressBarDlgConfig;
rainbowStatus = [
{ status: 'danger', percent: 25 },
{ status: 'warning', percent: 50 },
{ status: 'info', percent: 75 },
{ status: 'success', percent: 100 },
]
public get progressBarStatus(): string {
return this.config.useRainbowStatus ? this.rainbowStatus.find(r => r.percent >= this.percent).status : this.config.status;
}
public get progressBarMessage(): string {
let message = this.percent >= 100 ? 'Finished' : this.config.message;
//if (this.config.showPercent) message += ` - ${this.percent}%`;
return message;
}
constructor(
private dlgService: RbjDialogService,
private cdRef: ChangeDetectorRef,
private dlgRef: NbDialogRef<ProgressBarDlgComponent>,
private signalRService: SignalRService
) {
}
ngOnInit() {
if (this.config.enableIterators) {
setTimeout(() => {
this.percentageIteration();
}, 500);
}
if (this.config.signalRTag) {
this.signalRService.ApiMessageSubject.pipe(takeUntil(this.destroy$),
filter(r => r.tag == this.config.signalRTag))
.subscribe(result => {
let isProgressBarStatus = result.value['percentage'] > 0;
if (isProgressBarStatus) {
let progressStatus = result.value as ProgressBarStatus;
if (progressStatus) {
this.percent = Math.max(progressStatus.percentage, this.percent);
this.config.message = progressStatus.message;
this.cdRef.detectChanges();
if (this.percent >= 100) setTimeout(() => { this.dlgRef.close(true); }, 300);
}
} else {
let stage = this.config.signalRStages.find(s => s.signalRMsg == result.value);
if (stage) {
this.percent = Math.max(stage.percent, this.percent);
this.config.message = stage.message;
this.cdRef.detectChanges();
if (this.percent >= 100) setTimeout(() => { this.dlgRef.close(true); }, 300);
} else {
this.config.message = result.value;
}
}
});
}
this.dlgService.updateProgressBar$.pipe(takeUntil(this.destroy$))
.subscribe(result => {
this.percent = Math.max(result.percent, this.percent);
this.config.message = result.message;
this.cdRef.detectChanges();
if (this.percent >= 100) setTimeout(() => { this.dlgRef.close(true); }, 300);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
percentageIteration() {
setTimeout(() => {
if (this.percent < 95) {
this.percent += 1;
this._percentIterateCount += 1;
if (this._percentIterateCount > 10) {
this._percentIterateCount = 0;
let stage = this.config.signalRStages.find(s => s.percent > this.percent);
if (stage && stage.message) this.config.message = stage.message;
}
this.percentageIteration();
}
}, this.percent < 60 ? 300 : this.percent < 80 ? 600 : 900);
}
}
export interface ProgressBarStatus {
message: string;
percentage: number;
}
@@ -0,0 +1,2 @@
<textarea #textarea [ngModel]="content" type="text" nbInput fullWidth [readonly]="readonly"
(input)="autoExpand($event.target)" (ngModelChange)="content = $event; contentChanged()"></textarea>
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AutoSizingTextareaComponent } from './auto-sizing-textarea.component';
describe('AutoSizingTextareaComponent', () => {
let component: AutoSizingTextareaComponent;
let fixture: ComponentFixture<AutoSizingTextareaComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ AutoSizingTextareaComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AutoSizingTextareaComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,93 @@
import { Component, ElementRef, ViewChild, OnChanges, Input, SimpleChanges, AfterViewInit, Output, EventEmitter, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TextareaUtils } from '../../utilities/textarea-utils';
@Component({
selector: 'auto-sizing-textarea',
templateUrl: './auto-sizing-textarea.component.html',
styleUrls: ['./auto-sizing-textarea.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutoSizingTextareaComponent),
multi: true
}
],
})
export class AutoSizingTextareaComponent implements ControlValueAccessor, AfterViewInit {
@Input('min-lines') public minLines: number = 1;
@Input('max-lines') public maxLines: number = 1;
@Input('default-lines') public defaultLines: number = 1;
@Output() public contentChange = new EventEmitter<string>();
@ViewChild('textarea', { read: ElementRef }) textarea: ElementRef;
readonly: boolean
@Input("readonly")
public set input_readonly(value) {
this.readonly = typeof value !== "undefined" && value !== false;
}
onChange = (value: string) => { };
onTouched = () => { };
autoExpand = TextareaUtils.autoExpand;
private _content: string;
public get content(): string {
return this._content;
}
public set content(v: string) {
if (this._content != v) {
this._content = v;
this.onChange(v);
}
}
constructor() { }
writeValue(value: string): void {
this.content = value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
}
ngAfterViewInit() {
const computed = window.getComputedStyle(this.textarea.nativeElement);
const paddingTop = parseInt(computed.getPropertyValue('padding-top'));
const paddingBot = parseInt(computed.getPropertyValue('padding-bottom'));
const borderTop = parseInt(computed.getPropertyValue('border-top-width'));
const borderBot = parseInt(computed.getPropertyValue('border-bottom-width'));
const lineHeight = parseInt(computed.getPropertyValue('line-height'));
const extraHeight = borderTop + borderBot + paddingTop + paddingBot;
this.textarea.nativeElement.style.height = `${extraHeight + (this.defaultLines * lineHeight)}px`;
this.textarea.nativeElement.style.minHeight = `${extraHeight + (this.minLines * lineHeight)}px`;
this.textarea.nativeElement.style.maxHeight = `${extraHeight + (this.maxLines * lineHeight)}px`;
this.contentChanged();
}
ngOnChanges(changes: SimpleChanges) {
//
if (this.textarea && changes.content && changes.content.currentValue !== changes.content.previousValue) {
//
this.contentChanged();
}
}
contentChanged() {
// move to next frame to allow change to propagate to the control?
setTimeout(() => {
//
this.contentChange.emit(this.content);
TextareaUtils.autoExpand(this.textarea.nativeElement);
}, 0);
}
}
@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AutoSizingTextareaComponent } from './auto-sizing-textarea.component';
import { FormsModule } from '@angular/forms';
import { NbInputModule } from '@nebular/theme';
const routedComponents = [
AutoSizingTextareaComponent,
];
@NgModule({
declarations: [
...routedComponents,
],
imports: [
CommonModule,
FormsModule,
NbInputModule,
],
exports: [
...routedComponents,
],
})
export class AutoSizingTextareaModule { }
@@ -0,0 +1,31 @@
<div class="row">
<div class="col-sm-4 col-6">
<div class="form-group">
<label for="zips-{{uuid}}" class="label">Zip Code:</label>
<rbj-drop-down [name]="name+'Zip'" [ngModel]="zip" editable [source]="this.zipCodeList"
[maskExpression]="'00000-9999'" [id]="id+'Zip'" [inputClass]="'text-left '+inputClass"
(selectedChange)="zipCodeChanged($event);zipcodeToCounty(zip);onBlur()" [readonly]="readonly"
[disabled]="isDisabled" [required]="required" [mustMatch]="false">
</rbj-drop-down>
<!-- <input type="text" nbInput fullWidth name="zips-{{uuid}}" [(ngModel)]="zip" (change)="zipCodeChanged()"> (blur)="zipCodeChanged($event);onBlur()" -->
</div>
</div>
<div class="col-sm-5 col-12">
<div class="form-group">
<label for="citys-{{uuid}}" class="label">City</label>
<input type="text" class="{{inputClass}}" nbInput fullWidth [name]="name+'City'" [(ngModel)]="city"
[id]="id+'City'" autocomplete="off" [readonly]="readonly" (blur)="cityOrStateChanged();onBlur()"
[disabled]="isDisabled" [required]="required" [inputLimitation]="37">
</div>
</div>
<div class="col-sm-3 col-6">
<div class="form-group">
<label for="states-{{uuid}}" class="label">State</label>
<input type="text" class="{{inputClass}}" nbInput fullWidth [name]="name+'State'" [(ngModel)]="state"
[id]="id+'State'" autocomplete="off" [mask]="'UU'" validate [invalidMsg]="'Invalid state code format'" ffMsg
[readonly]="readonly" (blur)="cityOrStateChanged();onBlur()" [disabled]="isDisabled" [required]="required"
disableForceFocus>
</div>
</div>
</div>
@@ -0,0 +1,10 @@
@import "../../@theme/styles/themes";
form.ng-touched input.ng-invalid {
border-color: nb-theme(color-danger-default);
}
input[type="text"][disabled] {
color: black;
background-color: white;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CityStateZipComponent } from './city-state-zip.component';
describe('CityStateZipComponent', () => {
let component: CityStateZipComponent;
let fixture: ComponentFixture<CityStateZipComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CityStateZipComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CityStateZipComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,376 @@
import { Component, forwardRef, ViewChild, ElementRef, Input, Output, EventEmitter, Renderer2 } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, ControlContainer, NgForm, Validator, AbstractControl, ValidationErrors } from '@angular/forms';
import { UuidUtils } from '../../utilities/uuid-utils';
import { DropDownOption } from '../../entity/dropDownOption';
import { ForceFocusMsgDirective } from '../../directives/force-focus-msg/force-focus-msg.directive';
import { AddressInfo } from '../../models/contactInfo.model';
import { MsgBoxService } from '../../services/msg-box.service';
import { first, takeUntil } from 'rxjs/operators';
import { ObjectUtils } from '../../utilities/object-utils';
import { CityStateZipService, CityInfo } from '../../services/city-state-zip.service';
import { Subject } from 'rxjs';
import { StringUtils } from '../../utilities/string-utils';
import { ADIcon } from '../alert-dlg/alert-dlg.model';
var zipcodes = require('zipcodes-nrviens');
@Component({
selector: 'city-state-zip',
templateUrl: './city-state-zip.component.html',
styleUrls: ['./city-state-zip.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CityStateZipComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CityStateZipComponent),
multi: true,
},
],
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})
export class CityStateZipComponent implements Validator {
@ViewChild('city', { static: true }) cityInput: ElementRef;
@ViewChild('state', { static: true }) stateInput: ElementRef;
@ViewChild('zip', { static: true }) zipInput: ElementRef;
@ViewChild(ForceFocusMsgDirective) statePopover: ForceFocusMsgDirective;
private _value: AddressInfo = { city: '', state: '', zip: '', county: '' } as AddressInfo;
private _oldValue: AddressInfo = { city: '', state: '', zip: '', county: '' } as AddressInfo;
private _initialized: boolean;
private _legalInput: boolean = false;
private _readOnly: boolean;
private _disabled: boolean;
private _writing: boolean = true;
private _msgShown: boolean = false;
private destroy$: Subject<void> = new Subject<void>();
uuid = UuidUtils.generate();
disabledState: boolean = false;
required: boolean = false;
//zipCodeList: DropDownOption[] = [];
private _zipCodeList: DropDownOption[] = [new DropDownOption('', '')];
public get zipCodeList(): DropDownOption[] {
return this._zipCodeList;
}
public set zipCodeList(v: DropDownOption[]) {
if (v.length > 0) {
if (v.length != this._zipCodeList.length || v[0].value1 != this._zipCodeList[0].value1) {
this._zipCodeList = [new DropDownOption('', '')].concat(v);
}
} else {
this._zipCodeList = [new DropDownOption('', '')];
}
}
@Input() id?: string = ''
@Input() name?: string = ''
@Input() placeholder: string;
@Input() inputClass: string;
@Input() size: string = 'medium';
allData: any;
@Input()
public set readonly(value) {
this._readOnly = typeof value !== 'undefined' && value !== false;
}
@Input()
public set isDisabled(value) {
this._disabled = typeof value !== 'undefined' && value !== false;
}
public get isDisabled(): boolean {
return this._disabled;
}
@Input("required")
public set input_required(value) {
this.required = typeof value !== "undefined" && value !== false;
}
public get readonly(): boolean {
return this._readOnly;
}
public get value(): AddressInfo {
return this._value;
}
public set value(v: AddressInfo) {
if (this._value != v) {
this._value = v;
this.onChange(this.value);
}
}
public get city(): string {
return this.value.city;
}
@Input() public set city(v: string) {
if (this.value.city != v) {
this.value.city = v;
this.cityChange.emit(v);
this.onChange(this.value);
}
}
public get state(): string {
return this.value.state
}
@Input() public set state(v: string) {
if (this.value.state != v) {
this.value.state = v;
this.stateChange.emit(v);
//this.writeValue(this.value);
this.onChange(this.value);
}
}
public get zip(): string {
return this.value.zip;
}
@Input() public set zip(v: string) {
if (this.value.zip != v) {
this.value.zip = v;
this.zipChange.emit(v);
this.onChange(this.value);
}
}
public get county(): string {
return this.value.county;
}
public set county(v: string) {
if (this.value.county != v) {
this.value.county = v;
this.countyChange.emit(v);
this.onChange(this.value);
}
}
@Output() zipChange = new EventEmitter<string>()
@Output() stateChange = new EventEmitter<string>()
@Output() cityChange = new EventEmitter<string>()
@Output() countyChange = new EventEmitter<string>()
@Output() focus = new EventEmitter();
@Output() blur = new EventEmitter<AddressInfo>();
ready = new EventEmitter<void>();
onChange = (value: AddressInfo) => { };
onTouched = () => { };
constructor(
private elementRef: ElementRef,
private msgBoxService: MsgBoxService,
private renderer: Renderer2,
private cszService: CityStateZipService,
) {
}
ngOnInit() {
}
ngAfterViewInit() {
this.renderer.removeAttribute(this.elementRef.nativeElement, 'id')
this.ready.emit();
}
ngAfterContentInit() {
this._writing = false;
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
onBlur() {
this.blur.emit(this.value);
}
validate(control: AbstractControl): ValidationErrors {
if (this.cszService.validateState(this.state)) {
if (this.statePopover) {
this.statePopover.invalid = false;
this.statePopover.hide();
}
}
else {
if (this.statePopover) {
this.statePopover.invalid = true;
this.statePopover.invalidMsg = `${this.state} isn't a valid state or U.S. territory mail code!`;
setTimeout(() => {
this.statePopover.show();
});
}
return { state: { message: 'Invalid state code.' } }
}
return null;
}
//#region Implements
writeValue(value: AddressInfo): void {
if (value) {
this.value = value;
//initial zip code with trimmed value
this.value.zip = StringUtils.getTrimmedValue(this.value.zip);
this._oldValue = ObjectUtils.Clone(value);
const city = this.cszService.lookUpZipCode(this.city, this.state);
if (city != null) {
this.zipCodeList = city.zipCode.map((zipCode, i) => new DropDownOption(zipCode, zipCode));
}
}
this.onChange(this.value);
}
registerOnChange(fn: (value: AddressInfo) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabledState = isDisabled;
}
zipCodeChanged(inputZip: string) {
if (!this._writing && !this.readonly &&
StringUtils.getTrimmedValue(this._oldValue.zip) != StringUtils.getTrimmedValue(inputZip)
) {
this._oldValue.city = null;
this._writing = true;
if (inputZip && inputZip.length < 10 && inputZip.length > 5) {
inputZip = inputZip.substring(0, 5);
}
this.value.zip = inputZip;
const city = this.cszService.lookUpCity(this.zip);
if (city != null) {
this.city = city.city;
this.state = city.state;
this.zipCodeList = this.cszService
.lookUpZipCode(this.city, this.state)
.zipCode
.map((zipCode, i) => new DropDownOption(zipCode, zipCode));
this.zipcodeToCounty(this.zip);
}
setTimeout(() => {
this.onTouched();
this._writing = false;
}, 200);
}
}
cityOrStateChanged() {
if (this.value.city) {
this.value.city = this.value.city.replace(/^\s+|\s+$/g, '');
}
if (!this._writing && !this.readonly) {
if (this._oldValue.city != this.city || this._oldValue.state != this.state) {
this._writing = true;
if (this.city && this.state && this.state.length == 2) {
const city = this.updateZipCodesFromCity();
if (city != null) {
this.city = city.city;
this.state = city.state;
}
else {
if (false == this._msgShown) {
this._msgShown = true;
this.msgBoxService.show("Zip Code Not Found",
{
text: `Zip code for ${this.city}, ${this.state} not found.`,
icon: ADIcon.WARNING
})
.pipe(first()).subscribe(result => {
this._msgShown = false;
});
}
}
}
this._oldValue = ObjectUtils.Clone(this.value);
setTimeout(() => {
this.onTouched();
this._writing = false;
}, 200);
}
}
}
clearZipCode() {
this.zipCodeList = [];
this.zip = '';
}
zipcodeToCounty(zip) {
if (zip) {
this.cszService.getCounty(zip).subscribe((data) => {
this.allData = data;
if (this.allData) {
let countyName = this.allData.County;
countyName = countyName.toLowerCase().split(" ");
for (let i = 0; i < countyName.length; i++) {
countyName[i] = countyName[i][0].toUpperCase() + countyName[i].substr(1);
}
this.county = countyName.join(" ");
}
});
// const countySearch = zipcodes.lookup(zip);
// if (countySearch) this.county = countySearch.county;
}
}
private updateZipCodesFromCity(): CityInfo {
this.state = this.state.toUpperCase();
const city = this.cszService.lookUpZipCode(this.city, this.state);
if (city != null) {
this.zipCodeList = city.zipCode.map((zipCode, i) => new DropDownOption(zipCode, zipCode));
} else {
this.zipCodeList = [];
}
return city;
}
}
@@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CityStateZipComponent } from './city-state-zip.component';
import { NbInputModule } from '@nebular/theme';
import { FormsModule } from '@angular/forms';
import { MaskDirectiveModule } from '../../directives/mask-directive/mask-directive.module';
import { DropDownListModule } from '../drop-down-list/drop-down-list.module';
import { ForceFocusMsgModule } from '../../directives/force-focus-msg/force-focus-msg.module';
import { RbjTooltipModule } from '../../directives/rbj-tooltip/rbj-tooltip.module';
@NgModule({
declarations: [CityStateZipComponent],
imports: [
CommonModule,
NbInputModule,
FormsModule,
MaskDirectiveModule,
DropDownListModule,
ForceFocusMsgModule,
RbjTooltipModule,
],
exports: [CityStateZipComponent],
})
export class CityStateZipModule { }
@@ -0,0 +1,14 @@
// import { Component, OnInit } from '@angular/core';
// @Component({
// selector: 'ngx-components',
// template: ''
// })
// export class ComponentsComponent implements OnInit {
// constructor() { }
// ngOnInit() {
// }
// }
+13
View File
@@ -0,0 +1,13 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [
FormsModule,
CommonModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
declarations: [],
})
export class ComponentsModule { }
@@ -0,0 +1,17 @@
<div #menu>
<nb-card>
<ul class="list-group">
<ng-container *ngFor="let item of ContextMenuItems">
<li *ngIf="item.groupStart" class="groupSeparator">
<hr>
</li>
<li *ngIf="item.visible" class="list-group-item list-group-item-action"
[class.disabled]="false === item.enabled" (click)="runItemCallback(item, $event)">
<nb-icon pack="eva" [icon]="item.icon ? item.icon : 'plus'" [class]="item.icon ? '' : 'transparent'">
</nb-icon>
{{item.title}}
</li>
</ng-container>
</ul>
</nb-card>
</div>
@@ -0,0 +1,44 @@
@import "../../@theme/styles/themes";
.transparent {
opacity: 0;
}
li.list-group-item {
cursor: pointer;
border: 0;
}
@include nb-install-component() {
.list-group-item {
background-color: nb-theme(background-basic-color-1);
}
}
.disabled {
color: #a2acc0;
cursor: default !important;
}
@include nb-install-component() {
// .groupStart {
// border-top: 1px solid nb-theme(border-basic-color-5);
// }
.groupSeparator {
// height: 1px;
// overflow-y: auto;
display: inline-block;
> hr {
// padding-top: 1px;
// padding-bottom: 1px;
margin-top: 1px;
margin-bottom: 1px;
}
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ContextMenuComponent } from './context-menu.component';
describe('ContextMenuComponent', () => {
let component: ContextMenuComponent;
let fixture: ComponentFixture<ContextMenuComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ContextMenuComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ContextMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,74 @@
import { Component, ViewChild, Input, AfterViewInit, ElementRef } from '@angular/core';
import { NbDialogRef, NbDialogService } from '@nebular/theme';
import { ContextMenuItem } from '../../directives/right-click-menu/context-menu-item.model';
import { Direction } from '../../directives/right-click-menu/direction.enum';
import { RbjDialogService } from '../../services/rbj-dialog.service';
@Component({
selector: 'ngx-context-menu',
templateUrl: './context-menu.component.html',
styleUrls: ['./context-menu.component.scss']
})
export class ContextMenuComponent implements AfterViewInit {
@ViewChild('menu') menu: ElementRef;
selectedItem: string;
@Input() public Value: any;
@Input() public X: number;
@Input() public Y: number;
ContextMenuItems: ContextMenuItem[];
@Input() public Direction: Direction;
constructor(
protected ref: NbDialogRef<ContextMenuComponent>,
private dialogService: RbjDialogService,
) { }
ngAfterViewInit(): void {
this.menu.nativeElement.style.position = "absolute";
this.menu.nativeElement.style.left = this.X + 'px';
this.menu.nativeElement.style.top = this.Y + 'px';
// switch (this.Direction) {
// case Direction.Auto:
// case Direction.Down:
// this.menu.nativeElement.style.left = this.X + 'px';
// this.menu.nativeElement.style.top = this.Y + 'px';
// case Direction.Up:
// this.menu.nativeElement.style.left = this.X + 'px';
// this.menu.nativeElement.style.bottom = this.Y + 'px';
// case Direction.Left:
// this.menu.nativeElement.style.left = this.X + 'px';
// this.menu.nativeElement.style.top = this.Y + 'px';
// case Direction.Right:
// this.menu.nativeElement.style.left = this.X + 'px';
// this.menu.nativeElement.style.top = this.Y + 'px';
// }
}
runItemCallback(item: ContextMenuItem, event: any) {
if (item.contextMenuItems) {
this.dialogService.open(ContextMenuComponent, {
backdropClass: "transparent",
context: {
Value: this,
X: this.X + this.menu.nativeElement.width,
Y: event.y - event.offsetY,
ContextMenuItems: item.contextMenuItems,
Direction: Direction.Right,
},
closeOnBackdropClick: true,
});
}
else {
this.ref.close();
setTimeout(() => item && item.callback && item.callback(this.Value, event.target as HTMLElement));
}
}
}
@@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ContextMenuComponent } from './context-menu.component';
import { NbButtonModule, NbIconModule, NbTooltipModule, NbMenuModule, NbCardModule } from '@nebular/theme';
const components = [
ContextMenuComponent,
]
@NgModule({
declarations: [...components],
imports: [
CommonModule,
NbButtonModule,
NbCardModule,
NbIconModule,
NbMenuModule,
NbTooltipModule,
],
exports: [...components],
})
export class ContextMenuModule { }
@@ -0,0 +1,7 @@
<input #currencyInput class="rbj-form-control {{alignmentClass}} {{class}}" type="text" ngDefaultControl nbInput
fullWidth step={{currencyStep}} min={{min}} max={{max}} [ngModel]="displayValue"
(ngModelChange)="currencyInputChanged($event)" [ngModelOptions]="{updateOn:realTimeUpdate?'change': 'blur'}"
[status]="status" (focus)="$event.target.select();focus.emit();" [disabled]="disabled" [readonly]="readOnly"
nbTooltip="{{tooltipContent}}" nbTooltipPlacement="top" [nbTooltipStatus]="valueShouldBeWarningStatus"
[required]="required" (blur)="onBlur()" [id]="id" inputmode="decimal" (keydown)="onKeyDown($event)"
[fieldSize]="fieldSize">
@@ -0,0 +1,15 @@
@import "../../@theme/styles/themes";
form.ng-touched input.ng-invalid {
border-color: nb-theme(color-danger-default);
}
:host {
flex: auto;
display: contents;
}
input:read-only {
background-color: transparent;
box-shadow: none;
border-color: nb-theme(border-primary-color-1);
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CurrencyInputComponent } from './currency-input.component';
describe('CurrencyInputComponent', () => {
let component: CurrencyInputComponent;
let fixture: ComponentFixture<CurrencyInputComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CurrencyInputComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CurrencyInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,331 @@
import { Component, Input, Output, EventEmitter, forwardRef, ViewChild, ElementRef, Renderer2 } from '@angular/core';
import { formatCurrency } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms';
import { NbTooltipDirective, NbTrigger } from '@nebular/theme';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { StringUtils } from '../../utilities/string-utils';
@Component({
selector: 'rbj-currency-input',
templateUrl: './currency-input.component.html',
styleUrls: ['./currency-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CurrencyInputComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CurrencyInputComponent),
multi: true,
},
]
})
export class CurrencyInputComponent implements ControlValueAccessor, Validator {
@ViewChild('currencyInput', { static: true }) input: ElementRef;
@ViewChild(NbTooltipDirective) tooltip: NbTooltipDirective;
private destroy$: Subject<void> = new Subject<void>();
private _ngModel: number;
private _reverseStatus: boolean = false;
private _disableStatus: boolean = false;
private _initialized: boolean;
private _lastBlurValue: number;
realTimeUpdate: boolean
status = '';
displayValue: string;
readOnly: boolean = false;
required: boolean = false;
leftToRight: boolean = false;
@Input() id?= "";
@Input() name = "";
onChange = (value: number) => { };
onTouched = () => { };
@Input() min = -999999999.99;
@Input() max = 999999999.99;
@Input() currencyStep = '0.01';
@Input() class = '';
/**
* A calculated best value should be for this field, if got mismatch will have hover message
*/
@Input() valueShouldBe: number = null;
/**
* The hover message format for mismatch, Default:`The value should be {1}`, `{1}` will be replace with formatted value
*/
@Input() valueShouldBeWarningText: string = "The value should be {1}";
/**
* The hover message color default:warning | empty, primary, info, success, warning, danger
*/
@Input() valueShouldBeWarningStatus = 'warning';
@Input() zeroStatus = '';
@Input() zeroExpression = '';
@Input() nullExpression = '';
/**
* Positive value status color default:empty | empty, primary, info, success, warning, danger
*/
@Input() positiveStatus = '';
/**
* Negative value status color default:danger | primary, info, success, warning, danger
*/
@Input() negativeStatus = 'danger';
@Input() currencyCode = 'USD';
@Input() nullValue = 0;
@Input() disabled = false;
@Input() precision = 2;
@Input() fieldSize = 'medium';
@Input()
public set reverseStatus(value) {
this._reverseStatus = typeof value !== 'undefined' && value !== false;
this.setStatusColor();
}
@Input()
public set disableStatus(value) {
this._disableStatus = typeof value !== 'undefined' && value !== false;
this.setStatusColor();
}
@Input("readonly")
public set input_readOnly(value) {
this.readOnly = typeof value !== "undefined" && value !== false;
}
@Input("required")
public set input_required(value) {
this.required = typeof value !== "undefined" && value !== false;
}
@Input("LTR")
public set input_leftToRight(value) {
this.leftToRight = typeof value !== "undefined" && value !== false;
}
@Input("realTimeUpdate")
public set input_realTimeUpdate(value) {
this.realTimeUpdate = typeof value !== "undefined" && value !== false;
}
@Input("tooltips") tooltipContent: string;
@Output() change = new EventEmitter<number>();
@Output() focus = new EventEmitter();
@Output() blur = new EventEmitter<number>();
constructor(
private elementRef: ElementRef,
private renderer: Renderer2) {
}
validate(control: AbstractControl): ValidationErrors {
if (this.required && (this.value == null || this.value == 0)) {
return { "currency": "" };
}
return null;
}
registerOnValidatorChange?(fn: () => void): void {
}
//#region Implements
writeValue(value: string): void {
if (value) {
this.value = Number(value);
} else {
this.value = this.nullValue;
}
this._lastBlurValue = this.value;
//this.onChange(this.value);
this.setDisplayValue(this.value);
this.setStatusColor();
}
registerOnChange(fn: (value: number) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
ngAfterViewInit() {
this.tooltip.nbTooltipShowStateChange
.pipe(takeUntil(this.destroy$))
.subscribe(isShown => {
if (!this.tooltipContent && isShown) {
this.tooltip.hide();
}
})
this.renderer.removeAttribute(this.elementRef.nativeElement, 'id')
}
ngOnInit() {
// if (this.readOnly) {
// this.positiveStatus = '';
// this.negativeStatus = '';
// }
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
//#endregion
public get value(): number {
return this._ngModel;
}
public set value(v: number) {
if (v) v = Number(v.toFixed(this.precision));
if (v < this.min) {
v = this.min;
} else if (v > this.max) {
v = this.max;
}
if (this._initialized) {
if (v != this._ngModel) {
this._ngModel = v;
this.onChange(this.value);
}
} else {
this._initialized = true;
this._ngModel = v;
}
}
onBlur() {
this.setDisplayValue(this.value);
if (this.value != this._lastBlurValue) {
this.change.emit(this.value);
this.onChange(this.value);
this._lastBlurValue = this.value;
this.onTouched();
this.blur.emit(this.value);
this.setStatusColor();
}
}
setDisplayValue(v: number) {
if (null === v) {
this.displayValue = this.nullExpression;
} else if (['', 0, undefined, NaN].includes(v)) {
this.displayValue = this.zeroExpression;
} else {
this.displayValue = formatCurrency(v, "en", "", this.currencyCode, `0.${this.precision}`);
}
//to avoid input 0 twice will not update empty to ui
this.input.nativeElement.value = this.displayValue;
}
currencyInputChanged(value: string) {
if (this.displayValue != value) {
if (this.realTimeUpdate) {
this.value = Number(value.replace('..', '.').replace(/[$,]/g, ''));
if (false == [0, null, undefined, NaN].includes(this.value)) {
let focus = this.input.nativeElement.selectionStart;
let lengthBeforeCursor = this.input.nativeElement.value.substring(0, focus).replace(',', '').length;
this.setDisplayValue(this.value);
let formattedValue = this.input.nativeElement.value;
let commaCount = 0;
for (let i = 0; i < lengthBeforeCursor; i++) {
const c = formattedValue[i];
if (c == ',') commaCount++;
}
this.setCursorPosition(lengthBeforeCursor + commaCount);
} else {
//this.setDisplayValue(this.value);
}
} else {
this.value = StringUtils.isNullOrWhitespace(value) ? this.nullValue : Number(value.replace(/[$,]/g, ''));
}
}
}
setCursorPosition(position) {
let input = this.input.nativeElement;
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(position, position);
} else if (input.createTextRange) {
var range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', position);
range.moveStart('character', position);
range.select();
}
}
setStatusColor() {
if (this._disableStatus) {
this.status = "";
return;
}
if (this._ngModel > 0) this.status = this._reverseStatus ? this.negativeStatus : this.positiveStatus;
else if (['', 0, null, undefined, NaN].includes(this._ngModel)) {
this.status = this.zeroStatus;
}
else {
this.status = this._reverseStatus ? this.positiveStatus : this.negativeStatus;
}
if (this.valueShouldBe != null) {
let shouldBe = formatCurrency(this.valueShouldBe, "en", "", this.currencyCode);
if (this.displayValue != (this.valueShouldBe == 0 ? this.zeroExpression : shouldBe)) {
this.status = this.valueShouldBeWarningStatus;
this.tooltipContent = this.valueShouldBeWarningText.replace('{1}', shouldBe);
} else {
this.tooltipContent = '';
}
}
}
get alignmentClass(): string {
return this.leftToRight ? "text-left" : "text-right";
}
public onKeyDown(e: KeyboardEvent): void {
if ([46, 8, 9, 27, 13, 110, 190].indexOf(e.keyCode) !== -1 ||
// Allow: Ctrl+A
(e.keyCode === 65 && (e.ctrlKey || e.metaKey)) ||
// Allow: Ctrl+C
(e.keyCode === 67 && (e.ctrlKey || e.metaKey)) ||
// Allow: Ctrl+V
(e.keyCode === 86 && (e.ctrlKey || e.metaKey)) ||
// Allow: Ctrl+X
(e.keyCode === 88 && (e.ctrlKey || e.metaKey)) ||
// Allow: page up, page down, home, end, left, right
(e.keyCode >= 33 && e.keyCode <= 39)) {
// let it happen, don't do anything
return;
}
let regMatch = e.key.match(/[\d.,-]/g);
if (regMatch == null) {
//e.stopPropagation()
e.preventDefault();
}
}
}
@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CurrencyInputComponent } from './currency-input.component';
import { FormsModule } from '@angular/forms';
import { NbInputModule, NbTooltipModule } from '@nebular/theme';
import { MaskDirectiveModule } from '../../directives/mask-directive/mask-directive.module';
@NgModule({
declarations: [CurrencyInputComponent],
imports: [
CommonModule,
FormsModule,
NbInputModule,
NbTooltipModule,
MaskDirectiveModule
],
exports: [CurrencyInputComponent]
})
export class CurrencyInputModule { }
@@ -0,0 +1,48 @@
<nb-card class="rbjCalendar">
<nb-card-body>
<dx-calendar [showTodayButton]="true" (onValueChanged)="onChange($event.value)">
</dx-calendar>
</nb-card-body>
<!-- <nb-card-body class="cardBody rbjCalendar px-0" [ngSwitch]="mode">
<ng-container *ngSwitchCase="ViewMode.DATE">
<nb-calendar-pageable-navigation *ngSwitchCase="ViewMode.DATE" [date]="visibleDate" (next)="nextMonth()"
(prev)="prevMonth()" class="rbjCalendar pb-1" (changeMode)="setViewMode(ViewMode.YEAR)">
</nb-calendar-pageable-navigation>
<nb-calendar-day-picker [cellComponent]="dayCellComponent" [min]="min" [max]="max" [filter]="filter"
[visibleDate]="visibleDate" [size]="size" [date]="date" class="rbjCalendar" (dateChange)="onChange($event)">
</nb-calendar-day-picker>
</ng-container>
<ng-container *ngSwitchCase="ViewMode.YEAR">
<nb-calendar-pageable-navigation [date]="visibleDate" (next)="nextYears()" (prev)="prevYears()"
(changeMode)="setViewMode(ViewMode.DATE)" class="rbjCalendar pb-1">
</nb-calendar-pageable-navigation>
<nb-calendar-year-picker [cellComponent]="yearCellComponent" [date]="date" [min]="min" [max]="max"
[filter]="filter" [size]="size" [year]="visibleDate"
(yearChange)="setVisibleDate($event); setViewMode(ViewMode.MONTH)">
</nb-calendar-year-picker>
</ng-container>
<ng-container *ngSwitchCase="ViewMode.MONTH">
<nb-calendar-navigation [date]="visibleDate" (changeMode)="setViewMode(ViewMode.DATE)" class="rbjCalendar pb-1">
</nb-calendar-navigation>
<nb-calendar-month-picker [cellComponent]="monthCellComponent" [min]="min" [max]="max" [filter]="filter"
[size]="size" [month]="visibleDate" (monthChange)="setVisibleDate($event); setViewMode(ViewMode.DATE)">
</nb-calendar-month-picker>
</ng-container>
</nb-card-body> -->
</nb-card>
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CalendarViewComponent } from './calendar-view.component';
describe('CalendarViewComponent', () => {
let component: CalendarViewComponent;
let fixture: ComponentFixture<CalendarViewComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CalendarViewComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CalendarViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,183 @@
import { Component, HostBinding, HostListener, Input, OnInit, Renderer2, Type } from '@angular/core';
import { NbCalendarCell, NbCalendarRange, NbCalendarRangeYearCellComponent, NbCalendarSize, NbCalendarViewMode, NbCalendarViewModeValues, NbCalendarYearModelService, NbDateService, NbPositionedContainerComponent, NbRenderableContainer } from '@nebular/theme';
import { Subject } from 'rxjs';
@Component({
selector: 'ngx-calendar-view',
templateUrl: './calendar-view.component.html',
styleUrls: ['./calendar-view.component.scss']
})
export class CalendarViewComponent extends NbPositionedContainerComponent implements NbRenderableContainer {
/**
* Defines active view for calendar.
* */
@Input('startView') mode: NbCalendarViewMode = NbCalendarViewMode.DATE;
static ngAcceptInputType_activeViewMode: NbCalendarViewModeValues;
/**
* Determines whether we should show calendar navigation or not.
* */
@Input()
@HostBinding('class.has-navigation')
showNavigation: boolean = true;
date: Date;
visibleDate: Date;
ViewMode = NbCalendarViewMode;
max: Date;
min: Date;
size: NbCalendarSize = NbCalendarSize.MEDIUM;
context: contextSetting;
private listener: any;
private destroy$: Subject<void> = new Subject<void>();
constructor(
protected dateService: NbDateService<Date>,
private renderer: Renderer2,
protected yearModelService: NbCalendarYearModelService<Date>,
) {
super();
}
renderContent() {
}
filter: (Date) => boolean;
private hasParentClass(child, classname) {
if (child.className && child.className.split(' ').indexOf(classname) >= 0) return true;
try {
//Throws TypeError if child doesn't have parent any more
return child.parentNode && this.hasParentClass(child.parentNode, classname);
} catch (TypeError) {
return false;
}
}
ngOnInit() {
this.date = this.context.date;
this.visibleDate = this.date ? this.date : this.dateService.today();
setTimeout(() => {
this.listener = this.renderer.listen('window', 'click', (e: Event) => {
//
let el = (e.target as HTMLElement);
if (
this.hasParentClass(el, 'rbjCalendar')
// el.className.indexOf('rbjCalendar') > -1 ||
// el.tagName.indexOf('NB-CALENDAR') > -1
) {
this.context.focus();
} else {
this.context.hide();
}
})
}, 50);
}
ngOnDestroy() {
if (this.listener) { this.listener(); }
this.destroy$.next();
this.destroy$.complete();
}
setViewMode(mode: NbCalendarViewMode) {
this.mode = mode;
}
setVisibleDate(date: Date) {
}
prevMonth() {
this.changeVisibleMonth(-1);
}
nextMonth() {
this.changeVisibleMonth(1);
}
prevYear() {
this.changeVisibleYear(-1);
}
nextYear() {
this.changeVisibleYear(1);
}
prevYears() {
this.changeVisibleYears(-1);
}
nextYears() {
this.changeVisibleYears(1);
}
navigateNext() {
switch (this.mode) {
case NbCalendarViewMode.DATE:
return this.nextMonth();
case NbCalendarViewMode.MONTH:
return this.nextYear();
case NbCalendarViewMode.YEAR:
return this.nextYears();
}
}
onChangeViewMode() {
if (this.mode === NbCalendarViewMode.DATE) {
return this.setViewMode(NbCalendarViewMode.YEAR);
}
this.setViewMode(NbCalendarViewMode.DATE);
}
private changeVisibleMonth(direction: number) {
this.visibleDate = this.dateService.addMonth(this.visibleDate, direction);
}
private changeVisibleYear(direction: number) {
this.visibleDate = this.dateService.addYear(this.visibleDate, direction);
}
private changeVisibleYears(direction: number) {
this.visibleDate = this.dateService.addYear(this.visibleDate, direction * this.yearModelService.getYearsInView());
}
navigateToday() {
this.visibleDate = this.dateService.today();
this.context.dateChange(this.dateService.today());
}
onChange(date: Date) {
this.context.dateChange(date);
}
@HostListener('focus')
focus() {
this.context.focus();
}
dayCellComponent: Type<NbCalendarCell<Date, NbCalendarRange<Date>>>
/**
* Custom month cell component. Have to implement `NbCalendarCell` interface.
* */
monthCellComponent: Type<NbCalendarCell<Date, NbCalendarRange<Date>>>;
/**
* Custom year cell component. Have to implement `NbCalendarCell` interface.
* */
yearCellComponent: Type<NbCalendarCell<Date, NbCalendarRange<Date>>> = NbCalendarRangeYearCellComponent;
}
export interface contextSetting {
date: Date,
hide: () => {},
focus: () => {},
dateChange: (d: Date) => {};
}
@@ -0,0 +1,21 @@
<div class="input-group" #datePickerContainer>
<input #inputBox nbInput fullWidth [id]="id" name='{{name}}' class='rbj-form-control {{inputClass}}'
[disabled]="_disabled||showUnlockBtn" [mask]="maskExpression" placeholder='{{placeholder}}'
[attr.maxlength]="maskExpression.length" [required]="_required" (blur)="onBlur()">
<div class="input-group-append" *ngIf="!(_disabled||showUnlockBtn)" nbTooltip='Open Calendar' nbTooltipTrigger='hover'
nbTooltipPlacement='top' nbTooltipStatus='primary'>
<button #calendarBtn nbButton hero type="button" size="small" (click)="toggleCalendar()" class="editBtn">
<nb-icon pack="eva" icon="calendar-outline"></nb-icon>
</button>
</div>
<div class="input-group-append" *ngIf="showUnlockBtn">
<button #dropButton nbButton hero type="button" class="editBtn" size="small" (click)="unlockField()">
<nb-icon pack="eva" icon="edit-outline"></nb-icon>
</button>
</div>
<div></div>
</div>
<!-- <nb-datepicker #datePicker [date]="value" (dateChange)="pickDate($event)" [min]="min" [max]="max"></nb-datepicker> -->
@@ -0,0 +1,13 @@
@import "../../@theme/styles/themes";
form.ng-touched input.ng-invalid {
border-color: nb-theme(color-danger-default);
}
:host {
display: block;
}
.editBtn {
width: 29px !important;
padding: 0 !important;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DateInputComponent } from './date-input.component';
describe('DateInputComponent', () => {
let component: DateInputComponent;
let fixture: ComponentFixture<DateInputComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ DateInputComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DateInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,375 @@
import { Component, OnInit, ChangeDetectionStrategy, forwardRef, ViewChild, ElementRef, Output, Input, EventEmitter, Renderer2, ChangeDetectorRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, Validator, AbstractControl, ValidationErrors } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { ApplyMaskService } from '../../services/apply-mask.service';
import { ForceFocusMsgDirective } from '../../directives/force-focus-msg/force-focus-msg.directive';
import { NbAdjustment, NbDatepickerComponent, NbDynamicOverlay, NbDynamicOverlayHandler, NbPosition, NbTrigger } from '@nebular/theme';
import { MsgBoxService } from '../../services/msg-box.service';
import { first } from 'rxjs/operators';
import { StringUtils } from '../../utilities/string-utils';
import { CalendarViewComponent } from './calendar-view/calendar-view.component';
import { Subject } from 'rxjs';
import { DateUtilsService } from '../../services/date-utils.service';
import { ADIcon } from '../alert-dlg/alert-dlg.model';
const DATE_FORMAT = 'MM/dd/yyyy'
@Component({
selector: 'rbj-date-input',
templateUrl: './date-input.component.html',
styleUrls: ['./date-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DateInputComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DateInputComponent),
multi: true,
},
NbDynamicOverlayHandler,
NbDynamicOverlay
]
})
export class DateInputComponent implements OnInit, Validator {
@ViewChild('datePickerContainer', { static: true }) datePickerContainer: ElementRef;
@ViewChild('inputBox', { static: true }) input: ElementRef;
@ViewChild(ForceFocusMsgDirective) popover: ForceFocusMsgDirective;
private _value: Date;
private _lastBlurValue: Date;
private dynamicOverlay: NbDynamicOverlay;
private _calendarIsShown: boolean = false;
private _passwordTag: string = null;
_readOnly: boolean;
//this is for password protected field
passwordSecured: boolean = false;
onChange = (value: Date) => { };
onTouched = () => { };
name: string;
invalid: boolean = false;
@Input() id?: string = "";
@Input() placeholder: string;
@Input() inputClass: string;
@Input() size: string = 'medium';
@Input() min: Date;
@Input() max: Date;
@Input() maskExpression: string = '00/00/0000';
_disabled = false;
_required = false;
@Input()
public set readonly(value) {
this._readOnly = typeof value !== 'undefined' && value !== false;
}
@Input()
public set required(value) {
this._required = typeof value !== 'undefined' && value !== false;
}
skipHoliday: boolean
@Input("skipHoliday")
public set input_skipHoliday(value) {
this.skipHoliday = typeof value !== "undefined" && value !== false;
}
@Input()
public set passwordTag(value) {
this._passwordTag = value;
this.passwordSecured = true;
}
public get passwordTag() {
return this._passwordTag;
}
public get showUnlockBtn(): boolean {
return !this.readonly && this.passwordSecured;
}
@Output() dateChange = new EventEmitter<Date>();
@Output() focus = new EventEmitter();
constructor(
public datepipe: DatePipe,
private applyMaskService: ApplyMaskService,
private cdRef: ChangeDetectorRef,
private msgBoxService: MsgBoxService,
private renderer: Renderer2,
private el: ElementRef,
private dynamicOverlayHandler: NbDynamicOverlayHandler,
private dateUtilsService: DateUtilsService,
) {
}
ngAfterViewInit() {
this.renderer.removeAttribute(this.el.nativeElement, 'id');
setTimeout(() => {
this.dynamicOverlay = this.configureDynamicOverlay().build();
}, 400);
}
writeValue(value: Date): void {
if (value) {
this.value = value;
this.displayValue = this.dateUtilsService.format(this.value, DATE_FORMAT)
}
else if (value !== undefined) {
this.value = null;
this.displayValue = '';
}
this._lastBlurValue = this.value;
}
registerOnChange(fn: (value: Date) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this._disabled = isDisabled;
}
//#endregion
ngOnInit() {
this.dynamicOverlayHandler
.host(this.input)
.componentType(CalendarViewComponent);
}
public get displayValue(): string {
return this.input.nativeElement.value;
}
public set displayValue(v: string) {
this.input.nativeElement.value = v;
}
public get value(): Date {
return this._value;
}
public set value(v: Date) {
var changedByLimited = false;
if (v) {
//Check max and min
if (v < this.min) {
v = this.min;
changedByLimited = true;
} else if (v > this.max) {
v = this.max;
changedByLimited = true;
}
}
if (false == this.dateUtilsService.isSameDate(this._lastBlurValue, v)) {
this._value = v;
this.onChange(this.value);
}
//this.datePicker.hide();
if (changedByLimited) {
this.displayValue = this.dateUtilsService.format(v, DATE_FORMAT);
}
}
toggleCalendar() {
this._calendarIsShown = !this._calendarIsShown;
if (this._calendarIsShown) {
this.dynamicOverlay = this.configureDynamicOverlay().rebuild();
}
this.dynamicOverlay.toggle();
// if (this.datePicker.isShown) {
// this.datePicker.hide();
// } else {
// this.datePicker.show();
// }
}
setDisplayFormat() {
this.displayValue = this.applyMaskService
.applyMask(this.displayValue, this.maskExpression)
//this.validate();
}
validate(control: AbstractControl): ValidationErrors {
if (this._required && !this.value && this.displayValue.length == 0) {
this.invalid = true;
}
else {
this.invalid = !this.applyMaskService.validate(this.displayValue, this.maskExpression);
}
if (this.invalid && this.input.nativeElement.parentElement.className.indexOf('ng-touched') > -1) {
if (this.popover) this.popover.show();
return { key: 'Date', }
}
if (this.popover) this.popover.hide();
return null;
}
// triggerChange(wipeOutInvalidDate = false) {
// if (this.displayValue.length == this.maskExpression.length) {
// var newValue = new Date(this.displayValue);
// let isValid = this.dateUtilsService.isValidDate(newValue);
// if (
// this.value == null ||
// false === this.dateUtilsService.isSameDate(newValue, this.value)
// ) {
// this.value = newValue;
// }
// }
// else if (this.displayValue.length == 0 || wipeOutInvalidDate) {
// this.clearDate();
// }
// if (this.value != this._lastEditValue) {
// this._lastEditValue = this.value;
// this.onChange(this.value);
// this.dateChange.emit(this.value);
// }
// }
pickDate(date: Date) {
if (!this._disabled && !this._readOnly) {
this.displayValue = this.dateUtilsService.format(date, DATE_FORMAT);
this.onBlur();
}
this.hide();
this.input.nativeElement.focus();
this.dynamicOverlay = this.configureDynamicOverlay().rebuild();
}
unlockField() {
this.msgBoxService.getAuthPermission(this._passwordTag).pipe(first()).subscribe(result => {
if (result) {
this.passwordSecured = false;
setTimeout(() => {
this.setCursorPosition(0);
}, 100);
}
});
}
setCursorPosition(position) {
let input = this.input.nativeElement;
if (input.setSelectionRange) {
input.focus();
input.setSelectionRange(position, position);
}
else if (input.createTextRange) {
var range = input.createTextRange();
range.collapse(true);
range.moveEnd('character', position);
range.moveStart('character', position);
range.select();
}
}
private destroy$: Subject<void> = new Subject<void>();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.dynamicOverlayHandler.destroy();
}
private configureDynamicOverlay() {
//.el[0].scrollTop = (i - MAX_VISIBLE_ITEMS / 2) * VISIBLE_ITEM_HEIGHT;
var windowHeight = window.innerHeight;
//include input box height
//var itemHeight = (visibleItemsCount + 1) * VISIBLE_ITEM_HEIGHT;
var itemHeight = 405;
//this.input.nativeElement.offsetHeight
//var y = (this.input.nativeElement.offsetHeight + itemHeight) > windowHeight ? (windowHeight - itemHeight) : this.input.nativeElement.offsetHeight;
var y = this.input.nativeElement ? this.input.nativeElement.getBoundingClientRect().y : 0;
let menuOnBottom = (y + itemHeight) < windowHeight;
return this.dynamicOverlayHandler
.position(menuOnBottom ? NbPosition.BOTTOM_START : NbPosition.TOP_START)
.trigger(NbTrigger.NOOP)
.offset(menuOnBottom ? 2 : -11)
.adjustment(NbAdjustment.NOOP)
.context({
position: menuOnBottom ? NbPosition.BOTTOM : NbPosition.TOP,
date: this.value,
class: 'text-left',
width: this.input.nativeElement.parentElement.clientWidth,
hide: () => { this.hide(); },
focus: () => { this.isFocused = true; },
dateChange: (date) => { this.pickDate(date); }
});
}
isFocused: boolean = false;
hide() {
this._calendarIsShown = false;
if (this.dynamicOverlay && !this.cdRef['destroyed']) {
this.dynamicOverlay.hide();
this.cdRef.detectChanges();
}
}
onBlur() {
this.onTouched();
if (false == this._calendarIsShown) {
//For quick input `07/09/21` => `07/09/2021`
if (this.displayValue.match(/^\d{2}\/\d{2}\/\d{2}$/g)) {
this.displayValue = this.displayValue.slice(0, 6) + '20' + this.displayValue.slice(6, 8);
} else if (this.displayValue.match(/^\d{2}\/\d{2}$/g)) {
this.displayValue = this.displayValue.slice(0, 6) + '20' + this.displayValue.slice(6, 8);
}
}
if (this.displayValue) {
var newValue = new Date(this.displayValue);
let isValid = this.displayValue.length == this.maskExpression.length && this.dateUtilsService.isValidDate(newValue);
if (isValid) {
//This is for 02/30/2022 detection, new Date('02/30/2022') will parse it as 03/02/2022
isValid = this.displayValue == this.dateUtilsService.format(newValue, DATE_FORMAT);
}
if (isValid) {
this.value = newValue;
} else {
this.msgBoxService.show('Invalid Date Input', {
text: `<b>${this.displayValue}</b> is not a valid date!`,
icon: ADIcon.WARNING
});
this.clearDate();
}
} else {
this.clearDate();
}
if (false == this.dateUtilsService.isSameDate(this._lastBlurValue, this.value)) {
this._lastBlurValue = this.value;
this.dateChange.emit(this.value);
}
}
private clearDate() {
this.value = null;
this.displayValue = '';
}
}
@@ -0,0 +1,35 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DateInputComponent } from './date-input.component';
import { NbInputModule, NbDatepickerModule, NbIconModule, NbButtonModule, NbCardModule, NbCalendarKitModule, NbTooltipModule } from '@nebular/theme';
import { RegexValidatorModule } from '../../directives/regex-validator/regex-validator.module';
import { FormsModule } from '@angular/forms';
import { ForceFocusMsgModule } from '../../directives/force-focus-msg/force-focus-msg.module';
import { CalendarViewComponent } from './calendar-view/calendar-view.component';
import { MaskDirectiveModule } from '../../directives/mask-directive/mask-directive.module';
import { DxCalendarModule } from 'devextreme-angular';
@NgModule({
declarations: [DateInputComponent, CalendarViewComponent],
imports: [
CommonModule,
FormsModule,
NbInputModule,
NbDatepickerModule,
NbIconModule,
NbButtonModule,
RegexValidatorModule,
NbIconModule,
NbButtonModule,
NbCardModule,
NbCalendarKitModule,
NbTooltipModule,
ForceFocusMsgModule,
MaskDirectiveModule,
DxCalendarModule
],
exports: [
DateInputComponent
]
})
export class DateInputModule { }
@@ -0,0 +1,16 @@
<div class="input-group div{{uuid}}" style="flex-wrap: nowrap;">
<input #inputBox class="rbj-form-control form-control {{inputClass}}" nbInput title="{{tooltip}}" autocomplete="off"
[(ngModel)]="text" [ngClass]="{'dropdownInput': !hideButton,'editable':!mustMatch,'px-2':compact}"
[readonly]="readonly||!allowSearch" [placeholder]="placeholder" [mask]="maskExpression" [disabled]="disabled"
[required]="required" (click)="toggle();clickSelectAllText()" (focus)="onTextFocus()"
(keydown)="onKeyDown($event)" (focusout)="onTextFocusout()" (blur)="onTextFieldBlur()" [id]="id"
tabindex="{{tabindex}}" type="text">
<div class="input-group-append" [ngClass]="{'d-none': hideButton}">
<button #dropButton nbButton hero class="btn dropdownBtn rbj-form-control" type="button" id="{{uuid}}"
size="small" (click)="toggle()" (focus)="onFocus()" (focusout)="focusout()" [disabled]="readonly||disabled">
<nb-icon pack="eva" icon="chevron-down-outline" [@openClose]="menuIsOpen ? 'open' : 'closed'">
</nb-icon>
</button>
</div>
</div>
@@ -0,0 +1,33 @@
@import "../../@theme/styles/themes";
form.ng-touched input.ng-invalid {
border-color: nb-theme(color-danger-default);
}
.dropdownBtn {
width: 25px !important;
padding: 0 !important;
}
[nbInput]:disabled {
background-color: white !important;
}
input {
cursor: pointer;
text-overflow: ellipsis;
.dropdownInput {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
min-width: 25px;
height: auto;
}
.editable {
cursor: text;
}
&:read-only:not(.editable) {
cursor: pointer;
}
&.upperFirstLetter {
text-transform: capitalize;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DropDownListComponent } from './drop-down-list.component';
describe('DropDownListComponent', () => {
let component: DropDownListComponent;
let fixture: ComponentFixture<DropDownListComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ DropDownListComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DropDownListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,699 @@
import { Component, Input, Output, EventEmitter, forwardRef, ViewChild, ElementRef, ChangeDetectionStrategy, ChangeDetectorRef, Renderer2, HostListener, ViewChildren, QueryList } from '@angular/core';
import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator } from '@angular/forms';
import { DropDownOption } from '../../entity/dropDownOption';
import { NbContextMenuDirective, NbDynamicOverlay, NbDynamicOverlayHandler, NbTrigger, NbAdjustment, NbPosition, NbTooltipDirective } from '@nebular/theme';
import { filter, map, takeUntil } from 'rxjs/operators';
import { trigger, state, transition, animate, style } from '@angular/animations';
import { Subject } from 'rxjs';
import { DropDownMenuComponent } from './drop-down-menu/drop-down-menu.component';
import { UuidUtils } from '../../utilities/uuid-utils';
import { DropDownListService } from './drop-down-list.service';
import { StringUtils } from '../../utilities/string-utils';
import { DebounceTimer } from '../../utilities/timer-utils';
import { RbjTooltipService } from '../../services/rbj-tooltip.service';
import { TextareaUtils } from '../../utilities/textarea-utils';
const MAX_VISIBLE_ITEMS = 8;
const VISIBLE_ITEM_HEIGHT = 40;
const TEXT_CLEAR = "Clear The Field";
@Component({
selector: 'rbj-drop-down',
templateUrl: './drop-down-list.component.html',
styleUrls: ['./drop-down-list.component.scss'],
animations: [
trigger('openClose', [
// ...
state('open', style({
transform: 'rotate(180deg)'
})),
state('closed', style({
transform: 'rotate(0deg)'
})),
transition('* => *', [
animate(100)
]),
]),
],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DropDownListComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DropDownListComponent),
multi: true,
},
NbDynamicOverlayHandler,
NbDynamicOverlay
]
})
export class DropDownListComponent implements ControlValueAccessor, Validator {
@ViewChild('inputBox', { static: true }) input: ElementRef;
@ViewChild('dropButton', { static: true }) button: ElementRef;
@ViewChild(NbContextMenuDirective, { static: true }) contextMenu: NbContextMenuDirective;
refreshMenuDebounceTimer = new DebounceTimer(20, () => { if (this.menuIsOpen) { this.setUpDropdownList(); } });
constructor(
private dropDownListService: DropDownListService,
private cdRef: ChangeDetectorRef,
private dynamicOverlayHandler: NbDynamicOverlayHandler,
private elementRef: ElementRef,
private renderer: Renderer2,
private rbjTooltipService: RbjTooltipService,
) {
this.tabindex = elementRef.nativeElement.getAttribute('tabindex');
}
private destroy$: Subject<void> = new Subject<void>();
private _initialized: boolean;
private _viewInitialized: boolean = false;
private _value: any;
private _text: string;
private _textTyping: boolean = false;
private dynamicOverlay: NbDynamicOverlay;
private isFocused: boolean = false;
private _propDisplay: string = 'value1';
status = '';
displayValue: string;
selectedItem: DropDownOption;
firstMatchOption: DropDownOption;
uuid = 'rbjDropDown';
items: DropDownOption[] = [];
readonly: boolean = false;
required: boolean = false;
mustMatch: boolean = true;
@Input() allowSearch: boolean = true;
compact: boolean
menuIsOpen: boolean = false;
selectedIndex: number = -1;
tabindex: string = null;
focusIndex: number = -1;
usingFocusIndex: boolean = false;
public get tooltip(): string {
return this.selectedItem ? this.selectedItem[this.propItemDisplay] || '' : '';
}
public get value(): any {
return this._value;
}
public set value(v: any) {
if (this._initialized) {
if (v != this._value) {
this._value = v;
this.onChange(this.value);
}
}
else {
this._initialized = true;
this._value = v;
}
this.keyValueChangedByBinding();
}
@Input()
public get text(): string {
return this._text;
}
public set text(v: string) {
this.input.nativeElement.value = v;
if (v != this._text) {
this._text = v;
this.textChange.emit(this._text);
}
}
@Input() public set source(v: DropDownOption[]) {
if (this._source != v) {
this._source = v;
this.refreshMenuDebounceTimer.resetTimer();
//if (this.menuIsOpen) { this.setUpDropdownList(); }
this.keyValueChangedByBinding();
}
}
private _source: DropDownOption[];
public get source(): DropDownOption[] {
return this._source;
}
public get availableOptions(): DropDownOption[] {
if (this.source) {
return this.source.filter(o => this.itemIsVisible(o) && !o[this.propDisabled]);
}
return [];
}
@Input() name?: string = ''
@Input() id?: string = ''
@Input() hideButton: boolean = false;
@Input() inputClass = '';
public get propDisplay(): string {
return this._propDisplay;
}
/**
* Display text of dropdown will displaying this property of dropdown item, defaults as `value1`, will set `propItemDisplay` also, if it's default.
*/
@Input() public set propDisplay(v: string) {
this._propDisplay = v;
if (this.propItemDisplay == 'value1') this.propItemDisplay = v;
}
/**
* Display text of dropdown context menu will displaying this property of dropdown item, support HTML code, defaults as `value1`
*/
@Input() propItemDisplay = 'value1';
/**
* The `key` property name of dropdown item, defaults as `key`
*/
@Input() propKey = 'key';
/**
* The `disabled` property name of dropdown item, defaults as `disabled`
*/
@Input() propDisabled = 'disabled';
/**
* The `visible` property name of dropdown item, defaults as `visible`
*/
@Input() propVisible = 'visible';
@Input() matchWith = 'key';
@Input() disabled = false;
@Input() placeholder = '';
@Input() maskExpression = null;
@Input() clearOption = false;
/**
* Tooltip message for no matching found, default as "$inputText Not Matched", `$inputText` will replace with user input text
*/
@Input() inputNotMatchedTooltip = "$inputText does not exist";
@HostListener('focus')
focusHandler() {
this.focus();
}
@Input("mustMatch")
public set input_mustMatch(value) {
this.mustMatch = typeof value !== "undefined" && value !== false;
}
@Input("required")
public set input_required(value) {
this.required = typeof value !== "undefined" && value !== false;
}
@Input("readonly")
public set input_readonly(value) {
this.readonly = typeof value !== "undefined" && value !== false;
}
@Input("compact")
public set input_compact(value) {
this.compact = typeof value !== "undefined" && value !== false;
}
@Input("clearOption")
public set input_clearOption(value) {
this.clearOption = typeof value !== "undefined" && value !== false;
}
@Output() selectedChange = new EventEmitter<any>();
@Output() selectedChangeFrom = new EventEmitter<{ previousValue: any, currentValue: any }>();
@Output() menuItemSelected = new EventEmitter<any>();
@Output() textChange = new EventEmitter<string>();
@Output() blur: EventEmitter<any> = new EventEmitter();
@Output() inputTextNotMatched = new EventEmitter<string>();
/**
* Triggering when dropdown option been change by user, when subscribe this will not trigger selectedChange any more
*/
@Output() beforeChange = new EventEmitter<any>();
onChange = (value: any) => { };
onTouched = () => { };
writeValue(value: any): void {
this.value = value;
}
registerOnChange(fn: (value: any) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
validate(control: AbstractControl): ValidationErrors {
if (this.required && (this.value == null)) {
return { "dropdown": "" };
}
return null;
}
registerOnValidatorChange?(fn: () => void): void {
}
ngOnInit() {
this.uuid += UuidUtils.generate();
this.dropDownListService.onItemSelect
.pipe(
takeUntil(this.destroy$),
filter(({ tag }) => tag === this.uuid),
map(({ item }) => item),
)
.subscribe(item => {
this.selectByMenu(item);
});
this.dynamicOverlayHandler
.host(this.input)
.componentType(DropDownMenuComponent);
}
ngAfterViewInit() {
this._viewInitialized = true;
this.setUpDropdownList();
this.dynamicOverlay = this.configureDynamicOverlay().build();
this.renderer.removeAttribute(this.elementRef.nativeElement, 'id')
}
ngOnChanges() {
//this.dynamicOverlay = this.configureDynamicOverlay().rebuild();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.dynamicOverlayHandler.destroy();
}
private itemIsVisible(item: any) {
if (item[this.propVisible] == undefined) {
item[this.propVisible] = true;
}
return true === item[this.propVisible];
}
focusout() {
this.isFocused = false;
}
onFocus() {
this.isFocused = true;
}
onTextFocus() {
this.isFocused = true;
this._textTyping = true;
}
onTextFocusout() {
this._textTyping = false;
this.focusout();
}
focus() {
this.input.nativeElement.focus();
}
/**
* Remove `ng-invalid` class from input field, only calling this method for data been changed in background but UI doesn't refresh.
*/
private removeInvalidClassFormInput() {
this.renderer.removeClass(this.input.nativeElement, 'ng-invalid');
}
updateTextFromValue() {
if (this.clearOption && StringUtils.isNullOrWhitespace(this.value)) {
this.text = '';
this.input.nativeElement.value = "";
}
else if (this.source) {
let item: DropDownOption = null;
if (null == this.value && this.mustMatch) {
item = this.availableOptions.find(o => o.default);
}
else {
item = this.availableOptions.find(o => o[this.propKey] == this.value);
}
this.selectedIndex = item ? this.source.indexOf(item) : -1;
this._text = item ? item[this.propDisplay] : this.value;
if (this.input) {
this.input.nativeElement.value = this._text;
this.removeInvalidClassFormInput();
}
}
else {
this.text = '';
}
this.cdRef.detectChanges();
}
clickSelectAllText() {
if (this.allowSearch) {
this.input.nativeElement.select();
}
}
onTextFieldBlur() {
//set timeout for avoiding menu been hided before click event triggered
// setTimeout(() => {
this.updateSelectedItemByText();
this.onTouched();
this.blur.next(this.text);
// }, 200);
}
private setUpDropdownList() {
this.items = [];
if (this.source && this.source.length > 0) {
if (this.clearOption) {
this.items = [new DropDownOption('', '')];
this.items[0][this.propKey] = "";
this.items[0][this.propDisplay] = "";
this.items[0][this.propItemDisplay] = TEXT_CLEAR;
}
if (this.menuIsOpen && this.allowSearch && this.input.nativeElement) {
let filter = this.input.nativeElement.value.toLowerCase();
this.items = this.items.concat(
this.availableOptions.filter(s => s[this.propItemDisplay].toLowerCase().indexOf(filter) > -1
));
//unique by this.propItemDisplay
this.items = Array.from(new Map(this.items.map(item =>
[item[this.propItemDisplay], item])).values());
if (this.items.length > 0) {
this.firstMatchOption = this.availableOptions.find(s => s[this.propKey] == this.items[0][this.propKey]);
this.firstMatchOption = this.items[this.focusIndex];
}
else {
this.firstMatchOption = null;
}
}
else {
this.items = this.items.concat(this.source)
}
if (this.selectedItem) {
this.focusIndex = this.items.indexOf(this.selectedItem)
this.focusIndex = this.focusIndex > -1 ? this.focusIndex : 0;
}
this.usingFocusIndex = false;
}
if (this._viewInitialized) {
this.dynamicOverlay = this.configureDynamicOverlay()
.rebuild();
}
}
private configureDynamicOverlay() {
let visibleItemsCount = this.items.filter(i => i.visible).length;
visibleItemsCount = visibleItemsCount < MAX_VISIBLE_ITEMS ? visibleItemsCount : MAX_VISIBLE_ITEMS;
var windowHeight = window.innerHeight;
//include input box height
var itemHeight = (visibleItemsCount + 1) * VISIBLE_ITEM_HEIGHT;
var y = this.input.nativeElement ? this.input.nativeElement.getBoundingClientRect().y : 0;
let menuOnBottom = (y + itemHeight) < windowHeight;
return this.dynamicOverlayHandler
.position(menuOnBottom ? NbPosition.BOTTOM_END : NbPosition.TOP_END)
.trigger(NbTrigger.NOOP)
.offset(menuOnBottom ? 2 : -40)
.adjustment(NbAdjustment.NOOP)
.context({
position: menuOnBottom ? NbPosition.BOTTOM : NbPosition.TOP,
items: this.items,
tag: this.uuid,
class: 'text-left',
width: this.input.nativeElement.parentElement.clientWidth,
propDisplay: this.propItemDisplay,
propDisabled: this.propDisabled,
focusIndex: this.focusIndex,
hide: () => { this.hide(); },
focus: () => { this.isFocused = true; }
});
}
private updateSelectedItemInMenu() {
this.items.forEach(item => {
item.selected = item[this.propKey] == this.value;
});
}
private show() {
this.menuIsOpen = true;
this.dynamicOverlay.show();
this.cdRef.detectChanges();
}
private hide() {
this.menuIsOpen = false;
if (this.dynamicOverlay && !this.cdRef['destroyed']) {
this.dynamicOverlay.hide();
this.cdRef.detectChanges();
}
}
toggle(selectAllText: boolean = true) {
if (!this.disabled && !this.readonly) {
if (false == this.menuIsOpen) {
this.setUpDropdownList();
}
this.dynamicOverlay.toggle();
if (this.dynamicOverlay.isAttached) {
this.updateSelectedItemInMenu();
this.scrollingMenuByIndex(this.selectedIndex);
this.menuIsOpen = true;
if (selectAllText) {
this.clickSelectAllText();
}
}
else {
this.menuIsOpen = false;
}
}
}
private scrollingMenuByIndex(i: number) {
if (this.dynamicOverlay && this.dynamicOverlay.isAttached) {
if (this.items.length > MAX_VISIBLE_ITEMS) {
const el = document.getElementsByClassName(this.uuid);
el[0].scrollTop = (i - MAX_VISIBLE_ITEMS / 2) * VISIBLE_ITEM_HEIGHT;
}
}
}
//Triggered when user clicks option from dropdown menu
private selectByMenu(item: DropDownOption) {
if (this.beforeChange.observers.length == 0) {
this.menuItemSelected.emit(item[this.propKey]);
if (this.value != item[this.propKey]) {
const previousValue = this.value;
this.value = item[this.propKey]
this._text = item[this.propDisplay] == TEXT_CLEAR ? '' : item[this.propDisplay];
//this line is for triggering html input field validating without triggering whole textChange process
this.removeInvalidClassFormInput();
this.selectedIndex = this.items.indexOf(item);
this.blur.next(this.text);
this.textChange.next(this.text);
this.onChange(this.value);
this.selectedChange.emit(this.value);
this.selectedChangeFrom.emit({ previousValue, currentValue: this.value });
if (this.hideButton) {
this.input.nativeElement.focus();
}
else {
this.input.nativeElement.focus();
this.clickSelectAllText();
}
this.onTouched();
this.updateSelectedItemInMenu();
}
}
else {
this.beforeChange.emit(item[this.propKey]);
}
}
//Key value changed from binding, will update the displaying text and selected item in menu.
private keyValueChangedByBinding() {
let defaultOption = null;
if (this.source) {
let comparison = this._value;
this.selectedItem = this.availableOptions.find(o => o[this.propKey] == comparison);
defaultOption = this.availableOptions.find(o => o.default);
}
if (!this.selectedItem) {
if (this.mustMatch) {
if (defaultOption) {
this._value = defaultOption[this.propKey];
this.selectedItem = defaultOption;
}
else {
this._value = null;
this.selectedItem = new DropDownOption('', '');
}
}
this.onChange(this.value);
}
//this.text = this.selectedItem[this.propDisplay];
this.updateTextFromValue();
}
private updateSelectedItemByText() {
this._text = this.input.nativeElement.value;
let originSelectedItem = this.selectedItem;
if (!this.readonly && this.source && this.allowSearch) {
var newValue = null;
var inputText = StringUtils.toUpperString(this.text);
var foundByInput = this.availableOptions.find(o => StringUtils.toUpperString(o[this.propDisplay]) == inputText);
//Find again with Key Value
if (!foundByInput) {
foundByInput = this.availableOptions.find(o => StringUtils.toUpperString(o[this.matchWith]) == inputText);
}
if (this.usingFocusIndex || (!foundByInput && this.menuIsOpen && this.items.length > 0)) {
foundByInput = this.items[this.focusIndex];
//this.firstMatchOption = null;
}
if (!foundByInput) {
if (this.mustMatch) {
//Reset text to current value if mustMatch is true and no result
this.textInputNoMatched(this.text);
inputText = this.selectedItem[this.propDisplay];
}
else {
inputText = this.text;
newValue = inputText;
//Set inputText as option to selected item
this.selectedItem = new DropDownOption(newValue, inputText);
this.selectedIndex = -1;
}
}
else {
//Have matched option with text input
this.selectedItem = foundByInput;
this.selectedIndex = this.source.indexOf(this.selectedItem);
newValue = this.selectedItem[this.propKey];
inputText = this.selectedItem[this.propDisplay];
}
if ((newValue != null || false == this.mustMatch) && newValue != this.value) {
if (this.beforeChange.observers.length == 0) {
let previousValue = this.value;
this.value = newValue;
this.selectedChange.emit(this.value);
this.selectedChangeFrom.emit({ previousValue, currentValue: this.value });
}
else {
this.beforeChange.emit(newValue);
}
}
if (this.beforeChange.observers.length == 0) {
if (!this.clearOption || inputText != TEXT_CLEAR) {
this.text = inputText;
}
}
else {
this.selectedItem = originSelectedItem;
this.text = this.selectedItem[this.propDisplay];
}
}
}
private textInputNoMatched(inputValue: string) {
this.inputTextNotMatched.next(this.text);
if (false == StringUtils.isNullOrWhitespace(this.text)) {
let tempMsg = this.inputNotMatchedTooltip.replace("$inputText", inputValue.toUpperCase());
//this.tooltips.first.content = `"${inputValue}" does not exist`;
this.rbjTooltipService.show(this.input, tempMsg, this.uuid, 3, NbPosition.TOP_START);
}
}
public onKeyDown(e: KeyboardEvent): void {
switch (e.key) {
case 'Enter':
this.updateSelectedItemByText();
this.hide();
return;
break;
case 'ArrowDown':
case 'ArrowUp':
if (this.menuIsOpen) {
if (e.key == "ArrowUp") {
this.focusIndex = this.focusIndex > 0 ? this.focusIndex - 1 : this.focusIndex;
}
else {
this.focusIndex = this.focusIndex == this.items.length - 1 ? this.focusIndex : this.focusIndex + 1
}
this.dropDownListService.setFocusIndex(this.uuid, this.focusIndex);
this.scrollingMenuByIndex(this.focusIndex);
this.usingFocusIndex = true;
}
else {
this.toggle();
}
return;
break;
case 'Tab':
this.isFocused = false;
//use time out to let tab out support select the last index user choose from menu
setTimeout(() => {
this.hide();
}, 100);
return;
break;
default:
break;
}
if (TextareaUtils.isEditingKeyPress(e)) {
if (this.allowSearch) {
if (this.isFocused && false == this.menuIsOpen) {
this.toggle(false);
}
}
this.refreshMenuDebounceTimer.resetTimer();
}
}
}
@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DropDownListComponent } from './drop-down-list.component';
import { FormsModule } from '@angular/forms';
import { NbInputModule, NbSelectModule, NbMenuModule, NbContextMenuModule, NbButtonModule, NbIconModule, NbCardModule, NbTooltipModule } from '@nebular/theme';
import { MaskDirectiveModule } from '../../directives/mask-directive/mask-directive.module';
import { DropDownMenuComponent } from './drop-down-menu/drop-down-menu.component';
@NgModule({
declarations: [
DropDownListComponent,
DropDownMenuComponent,
],
imports: [
CommonModule,
FormsModule,
NbInputModule,
NbSelectModule,
NbMenuModule,
NbContextMenuModule,
NbButtonModule,
NbIconModule,
NbCardModule,
NbTooltipModule,
MaskDirectiveModule
],
exports: [
DropDownListComponent,
]
})
export class DropDownListModule { }
@@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { DropDownListService } from './drop-down-list.service';
describe('DropDownListService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: DropDownListService = TestBed.get(DropDownListService);
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { DropDownOption } from '../../entity/dropDownOption';
import { DropDownBag } from './drop-down-menu/drop-down-menu.component';
@Injectable({
providedIn: 'root'
})
export class DropDownListService {
constructor() { }
onItemSelect = new Subject<DropDownBag>();
focusIndexChange = new Subject<DropDownBag>();
selectItem(tag: string, item: DropDownOption) {
this.onItemSelect.next({ tag: tag, item: item } as DropDownBag)
}
setFocusIndex(tag: string, i: number) {
this.focusIndexChange.next(
{ tag: tag, focusIndex: i } as DropDownBag);
}
}
@@ -0,0 +1,15 @@
<!-- <nb-menu class="{{context.tag}} {{context.class}} context-menu" [items]="context.items" [tag]="context.tag"
[style.min-width.px]="clientWidth"></nb-menu> -->
<nb-card class=" {{context.class}} context-menu rbjDropdownMenu" [style.min-width.px]="clientWidth">
<nb-card-body class="p-1 {{context.tag}} rbjDropdownMenu">
<ul class="rbjDropdownMenu">
<ng-container *ngFor="let item of items;let i=index">
<li *ngIf="item.visible" [innerHTML]="item[context.propDisplay]" class="rbjDropdownMenu rbjDropdownMenuItem"
[ngClass]="{'selected': item.selected,'focus':context.focusIndex==i,
'disabled':item[context.propDisabled]}" (click)="choose(item)"></li>
</ng-container>
</ul>
</nb-card-body>
</nb-card>
@@ -0,0 +1,87 @@
@import "../../../@theme/styles/themes";
@include nb-install-component() {
.context-menu {
background-color: nb-theme(background-basic-color-1);
box-shadow: nb-theme(shadow);
}
ul {
display: block;
list-style-type: none;
padding: 0;
margin: 0;
li {
font-size: 0.9375rem;
font-weight: 600;
line-height: 1.5rem;
padding: 0.4375rem 1rem;
cursor: pointer;
&.focus {
background-color: nb-theme(color-basic-transparent-300);
color: nb-theme(text-basic-color); //#222b45
outline: none;
}
&:hover {
background-color: nb-theme(color-basic-transparent-200);
color: nb-theme(text-basic-color);
}
&.selected {
background-color: nb-theme(color-primary-500);
color: nb-theme(text-control-color);
}
&:hover.selected {
background-color: nb-theme(color-primary-400);
color: nb-theme(text-control-color);
}
&.disabled {
color: nb-theme(text-disabled-color) !important;
}
}
}
}
:host {
border: 0 solid transparent;
border-radius: 0.25rem;
//box-shadow: 0 0.5rem 1rem 0 rgba(44, 51, 73, 0.1);
color: #192038;
font-family: Open Sans, sans-serif;
font-size: 0.9375rem;
font-weight: 400;
line-height: 1.25rem;
margin-bottom: 1.875rem;
scrollbar-face-color: #e4e9f2;
scrollbar-track-color: #f7f9fc;
.context-menu {
-webkit-animation-name: fadeFromTop; /* Chrome, Safari, Opera */
-webkit-animation-duration: 0.25s; /* Chrome, Safari, Opera */
animation-name: fadeFromTop;
animation-duration: 0.25s;
max-height: 300px;
overflow-y: auto !important;
}
}
/* Add animation (Chrome, Safari, Opera) */
@-webkit-keyframes fadeFromTop {
from {
max-height: 0px;
}
to {
max-height: 300px;
}
}
/* Add animation (Standard syntax) */
@keyframes fadeFromTop {
from {
max-height: 0px;
}
to {
max-height: 300px;
}
}
// .rbjDropdownMenuItem:empty::before{
// content: "\200b"; /* unicode zero width space character */
// }
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DropDownMenuComponent } from './drop-down-menu.component';
describe('DropDownMenuComponent', () => {
let component: DropDownMenuComponent;
let fixture: ComponentFixture<DropDownMenuComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ DropDownMenuComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DropDownMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,103 @@
import { Component, OnInit, Input, HostListener, Renderer2 } from '@angular/core';
import { NbPositionedContainerComponent, NbRenderableContainer, NbMenuItem } from '@nebular/theme';
import { Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { DropDownOption } from '../../../entity/dropDownOption';
import { DropDownListService } from '../drop-down-list.service';
@Component({
selector: 'ngx-drop-down-menu',
templateUrl: './drop-down-menu.component.html',
styleUrls: ['./drop-down-menu.component.scss']
})
export class DropDownMenuComponent extends NbPositionedContainerComponent implements NbRenderableContainer {
// [style.width.px]="clientWidth"
private _context: menuSetting;
private _indexSubscription: Subscription;
clientWidth: number = 0;
items: DropDownOption[] = [];
private listener: any;
constructor(private dropDownListService: DropDownListService,
private renderer: Renderer2) {
super();
}
ngOnInit() {
setTimeout(() => {
this.listener = this.renderer.listen('window', 'click', (e: Event) => {
//
var element = (e.target as HTMLElement);
if (typeof element.className === 'string' && element.className.indexOf('rbjDropdownMenu') > -1) {
this.context.focus();
} else {
this.context.hide();
this.listener();
}
})
}, 50);
this._indexSubscription = this.dropDownListService.focusIndexChange
.pipe(
filter(({ tag }) => tag === this._context.tag),
map(({ focusIndex }) => focusIndex),
)
.subscribe(i => {
this._context.focusIndex = i;
})
}
ngOnDestroy() {
if (this.listener) { this.listener(); }
if (this._indexSubscription) {
this._indexSubscription.unsubscribe();
}
}
public get context(): menuSetting {
return this._context;
}
@Input() public set context(v: menuSetting) {
if (this._context != v) {
this._context = v;
this.clientWidth = document.getElementsByClassName(`div${this._context.tag}`)[0].clientWidth;
this.items = v.items;
}
}
@HostListener('focus')
focus() {
this.context.focus();
}
renderContent() {
}
choose(item: DropDownOption) {
if (!item[this.context.propDisabled]) {
this.dropDownListService.selectItem(this.context.tag, item);
this.context.hide();
}
}
}
export interface menuSetting {
items: DropDownOption[],
tag?: string,
class?: string,
propDisplay: string,
propDisabled: string,
focusIndex: number
hide: () => {},
focus: () => {}
}
export interface DropDownBag {
tag: string,
item: DropDownOption,
focusIndex?: number
}
@@ -0,0 +1,3 @@
<input #inputBox="ngModel" name="emailInput{{uuid}}" [id]="id" class="{{class}}" type="email" nbInput fullWidth
[(ngModel)]="value" [regexValidator]="'[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,3}'" [invalidMsg]="'Invalid Email format'"
[readonly]="readOnly" [inputLimitation]="100" [placeholder]="placeholder">
@@ -0,0 +1,4 @@
:host {
flex: auto;
display: contents;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EmailInputComponent } from './email-input.component';
describe('EmailInputComponent', () => {
let component: EmailInputComponent;
let fixture: ComponentFixture<EmailInputComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ EmailInputComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(EmailInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,74 @@
import { Component, OnInit, ChangeDetectionStrategy, forwardRef, Input, Output, EventEmitter, ViewChild, ElementRef, Renderer2 } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlContainer, NgForm, DefaultValueAccessor } from '@angular/forms';
import { UuidUtils } from '../../utilities/uuid-utils';
@Component({
selector: 'rbj-email-input',
templateUrl: './email-input.component.html',
styleUrls: ['./email-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EmailInputComponent),
multi: true
}
],
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }]
})
export class EmailInputComponent implements OnInit {
private _value: string;
disabledState: boolean = false;
readOnly: boolean = false;
@Input()
public get value(): string {
return this._value;
}
onChange = (value: string) => { };
onTouched = () => { };
uuid: string = UuidUtils.generate();
@Input() id?= "";
@Input() placeholder: string;
@Input() class: string;
@Input() size: string = 'medium';
public set value(v: string) {
if (this._value != v) {
this._value = v;
this.onChange(v)
}
}
@Input("readonly")
public set input_readOnly(value) {
this.readOnly = typeof value !== "undefined" && value !== false;
}
@ViewChild('inputBox', { static: true }) input: ElementRef;
constructor(
private renderer: Renderer2,
private el: ElementRef) { }
ngAfterViewInit() {
this.renderer.removeAttribute(this.el.nativeElement, 'id')
}
writeValue(value: any): void {
this.value = value;
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabledState = isDisabled;
}
ngOnInit() {
}
}
@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EmailInputComponent } from './email-input.component';
import { NbInputModule } from '@nebular/theme';
import { RegexValidatorModule } from '../../directives/regex-validator/regex-validator.module';
import { FormsModule } from '@angular/forms';
import { RbjTooltipModule } from '../../directives/rbj-tooltip/rbj-tooltip.module';
@NgModule({
declarations: [EmailInputComponent],
imports: [
CommonModule,
FormsModule,
NbInputModule,
RegexValidatorModule,
RbjTooltipModule
],
exports: [EmailInputComponent]
})
export class EmailInputModule { }
@@ -0,0 +1,9 @@
<nb-card>
<nb-card-header>{{ title }}</nb-card-header>
<nb-card-footer>
<div class="buttons-row">
<button nbButton hero status="danger" size="small" (click)="delete(true)">Delete</button>
<button nbButton hero status="primary" size="small" (click)="cancel()">Cancel</button>
</div>
</nb-card-footer>
</nb-card>
@@ -0,0 +1,25 @@
@import './../../../@theme/styles/themes';
@include nb-install-component() {
.buttons-row {
margin: -0.5rem;
}
button[nbButton] {
margin: 0.5rem;
}
.action-icon {
@include nb-ltr(margin-right, 0.5rem);
@include nb-rtl(margin-left, 0.5rem);
}
.actions-card {
height: 8rem;
}
nb-card {
max-width: 600px;
max-height: 500px;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ConfirmDialogComponent } from './confirm-dialog.component';
describe('ConfirmDialogComponent', () => {
let component: ConfirmDialogComponent;
let fixture: ComponentFixture<ConfirmDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ConfirmDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ConfirmDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,24 @@
import { Component, Input, Output } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
@Component({
selector: 'ngx-confirm-dialog',
templateUrl: './confirm-dialog.component.html',
styleUrls: ['./confirm-dialog.component.scss']
})
export class ConfirmDialogComponent {
@Input() title: string;
constructor(
protected ref: NbDialogRef<ConfirmDialogComponent>,
) { }
cancel() {
this.ref.close();
}
delete(confirm) {
this.ref.close(confirm);
}
}
@@ -0,0 +1,3 @@
<p>
custom-columns-dlg works!
</p>
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CustomColumnsDlgComponent } from './custom-columns-dlg.component';
describe('CustomColumnsDlgComponent', () => {
let component: CustomColumnsDlgComponent;
let fixture: ComponentFixture<CustomColumnsDlgComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ CustomColumnsDlgComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CustomColumnsDlgComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'rbj-custom-columns-dlg',
templateUrl: './custom-columns-dlg.component.html',
styleUrls: ['./custom-columns-dlg.component.scss']
})
export class CustomColumnsDlgComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}
@@ -0,0 +1,18 @@
<div class="fancyGroupMenuContainer">
<ng-content></ng-content>
<div class="fancyGroupMenu" [ngClass]="{'dropHover': draggingRow}">
<!-- <li class="list-group-item active">Cras justo odio</li> (mouseenter)="onGroupListMouseEnter(group,$event)"
(mouseleave)="onGroupListMouseLeave()"-->
<div class="menu-item" *ngFor="let group of groups" (click)="clickGroup(group)"
(mouseup)="onGroupListMouseUp(group,$event)"
[ngClass]="{'active': filteringGroupKey==group.groupKey,'hasData':group.rows.length>0,'altColor':usingAltColor}"
rbjFileDragDrop="{{settings.enableFileDrop}}" (onFileDropped)="fileDroppedInGroup(group,$event)">
{{group.groupValue}}
<div class="dx-tag-remove-button" (click)="removeGroup(group)"
*ngIf="group.uiAllowDeleteWhenEmpty && (!group.rows|| group.rows.length==0)"></div>
</div>
</div>
</div>
@@ -0,0 +1,91 @@
.fancyGroupMenuContainer {
width: inherit;
position: sticky;
top: 0px;
}
.dx-tag-remove-button {
top: 0;
right: 10px;
}
::-webkit-scrollbar {
width: 0.3rem !important;
}
.fancyGroupMenu {
position: relative;
cursor: pointer;
display: flex;
flex-direction: column;
padding-left: 0;
margin-bottom: 0;
width: 220px;
max-height: 67vh;
overflow-y: auto;
padding-bottom: 1px;
&::before {
bottom: 0;
//background-color: rgba(0, 0, 0, 0.2);
//background-color: var(--palette-black-alpha-20, rgba(0, 0, 0, 0.2));
content: "";
left: 0;
position: absolute;
top: 0;
width: 3px;
height: inherit;
}
.menu-item {
display: block;
padding: 0.75rem 1.25rem;
margin-bottom: -1px;
position: relative;
cursor: pointer;
&:hover {
//background-color: rgba(3, 169, 244, 0.08);
background-color: rgba(240, 91, 65, 0.12);
&.altColor {
background-color: rgba(0, 0, 0, 0.04);
}
}
&.hasData {
color: rgba(240, 91, 65, 1);
&.altColor {
color: rgba(60, 186, 178, 1);
}
}
&.active {
border-radius: 2px;
//color: rgba(0, 90, 158, 1);
//color: var(--communication-foreground, rgba(0, 90, 158, 1));
//color: #03a9f4;
color: rgba(255, 255, 255, 1) !important;
background-color: rgba(240, 91, 65, 1);
font-weight: 600;
transition:
color 80ms cubic-bezier(0.165, 0.84, 0.44, 1),
background 80ms linear;
&::before {
//background-color: #0078d4;
//background-color: var(--communication-background, #0078d4);
content: "";
left: 0px;
top: 0px;
position: absolute;
width: 3px;
height: 100%;
}
&.altColor {
background-color: rgba(60, 186, 178, 1);
}
}
}
&.dropHover .menu-item:hover {
border-color: rgba(240, 91, 65, 1);
&.altColor {
border-color: rgba(60, 186, 178, 1);
}
border: nb-theme(border-basic-color-5);
border-style: dashed;
border-width: medium;
cursor: copy;
}
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FancyGroupMenuComponent } from './fancy-group-menu.component';
describe('FancyGroupMenuComponent', () => {
let component: FancyGroupMenuComponent<any>;
let fixture: ComponentFixture<FancyGroupMenuComponent<any>>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FancyGroupMenuComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FancyGroupMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,95 @@
import { ChangeDetectionStrategy, Component, inject, Injector, Input, NgZone, Output } from '@angular/core';
import { FancyGroup } from '../fancy-row-column.model';
import { Subject } from 'rxjs';
import { FancySettings } from '../fancy-settings.model';
import { DebounceTimer } from '../../../utilities/timer-utils';
import { DropFileEvent } from '../../../shared/file-drag-drop/file-drag-drop.directive';
@Component({
selector: 'rbj-fancy-group-menu',
templateUrl: './fancy-group-menu.component.html',
styleUrl: './fancy-group-menu.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FancyGroupMenuComponent<T> {
injector = inject(Injector);
ngZone = inject(NgZone);
@Input() usingAltColor = false;
deletingGroup = false;
private clearDroppingOnGroupKeyTimer = new DebounceTimer(100, () => {
this._droppingOnGroupKey = null;
this.droppingOnGroupKeyChange.next(null);
})
@Input() groups: FancyGroup[];
@Input() settings: FancySettings;
private _draggingRow: boolean;
public get draggingRow(): boolean {
return this._draggingRow;
}
@Input() public set draggingRow(v: boolean) {
if (this._draggingRow != v) {
this._draggingRow = v;
this.draggingRowChange.next(v);
}
}
@Output() draggingRowChange = new Subject<boolean>()
@Output() itemDropInGroup = new Subject<string>();
private _droppingOnGroupKey: string;
public get droppingOnGroupKey(): string {
return this._droppingOnGroupKey;
}
@Input() public set droppingOnGroupKey(v: string) {
if (this._droppingOnGroupKey != v) {
this._droppingOnGroupKey = v;
this.droppingOnGroupKeyChange.next(this.droppingOnGroupKey);
}
}
@Input() filteringGroupKey: string;
@Output() droppingOnGroupKeyChange = new Subject<string>();
@Output() onFileDroppedInGroup = new Subject<FancyGroup<T>>();
@Output() clickOnGroup = new Subject<FancyGroup<T>>();
@Output() clickOnDeleteGroup = new Subject<FancyGroup<T>>();
constructor() {
}
public onGroupListMouseUp(group: FancyGroup<T>, $event: MouseEvent) {
if (!this.deletingGroup) {
this.droppingOnGroupKey = group.groupKey;
if (this.draggingRow) {
this.itemDropInGroup.next(group.groupKey);
//this.draggingRow = false;
}
setTimeout(() => {
this.draggingRow = false;
this.droppingOnGroupKey = null;
}, 500);
}
}
fileDroppedInGroup(group: FancyGroup<T>, event: DropFileEvent) {
if (group) {
group.uiFileDropList = event.files;
this.onFileDroppedInGroup.next(group);
}
}
clickGroup(group: FancyGroup<T>) {
if (!this.deletingGroup) {
this.clickOnGroup.next(group);
}
}
removeGroup(group: FancyGroup<T>) {
this.deletingGroup = true;
setTimeout(() => {
this.deletingGroup = false;
}, 50);
this.clickOnDeleteGroup.next(group);
}
}
@@ -0,0 +1,185 @@
import { SafeHtml } from "@angular/platform-browser";
import { DropDownOption } from "../../entity/dropDownOption";
import { TemplateRef } from "@angular/core";
export enum FtEditableType {
NONE = 'none',
READONLY = 'readOnly',
EDITABLE = 'editable',
}
export enum FtTrigger {
NOOP = 'noop',
DOUBLE_CLICK = 'doubleClick',
}
export declare type FtTagType = 'info' | 'primary' | 'danger' | 'warning' | 'success' | 'secondary' | 'light' | 'dark';
export declare type FtColType = 'text' | 'date' | 'dateTime' | 'dropdown' | 'number' | 'checkbox' | 'checkall' | 'currency' | 'tag' | 'template';
export declare type FtFilterMode = 'startsWith' | 'contains' | 'allkeywords';
export class FancyColumn<T = any> {
public name: string;
public title: string;
/**
* Col data type, Defaults to text, available types:
* text, date, dateTime, dropdown, number, checkbox, checkall, currency, tag
*/
public type?: FtColType = 'text'; // 'text', 'date', 'dropdown' etc. Defaults to 'text'
public dropdownSource?: DropDownOption[];
public class?: string; // apply this class to the cells
//public sort?: boolean;
/**
* Sort by this column, available types:'asc', 'desc'
*/
public sortDirection?: string;
public filter?: any;
/**
* Col filter mode, Defaults to contains, available types:
* startsWith, contains, allkeyswords
*/
public filterMode?: FtFilterMode = 'contains';
/**
* Col filter case sensitive setting, Defaults to `true`
*/
public filterIgnoreCase?: boolean = true;
public filterPreparedValue?: boolean = false;
public dateRange?: any;
public group?: boolean;
/**
* Col visible setting, Defaults to `true`
*/
public visible?: boolean = true;
/**
* Disable column hover tooltip, Defaults to `false`
*/
public noToolTip?: boolean = false;
public editableType?: FtEditableType = FtEditableType.NONE;
public editingFieldIndex?: number;
public editingFieldWidth?: number;
public footerClass?: string; // apply this class to the cells in the footer
public valuePrepareFunction?: (value: T) => string | SafeHtml | number | TemplateRef<any>;
/**
* `(optional)` Preparation function for tooltip, if not exist will using same value with col value
*/
public toolTipPrepareFunction?: (value: T) => string;
public groupKeyPrepareFunction?: (value: T) => string;
public sortFunction?: (a: T, b: T) => number;
public filterFunction?: (value: FancyDisplayRow<T>, input) => boolean;
public tagsPrepareFunction?: (value: T) => FancyTag[];
/**
* apply the width px to column 0 for auto width
*/
public widthPx?: number;
/**
* apply the width rem to column 0 for auto width, will overwrite `widthPx`
*/
public widthRem?: number;
/**
* apply the width % to column 0 for auto width, will overwrite `widthRem` and `widthPx`
*/
public widthPct?: number;
/**
* maxWidth for text wrapping, calculated by `widthPct`, do NOT use this for now!
*/
public maxWidth?: string;
public initFocusFilterInput?: boolean = false;
/**
* the tagging field name for `checkall` column type to track checked order
*/
public taggingTarget?: string = null;
constructor(config: Partial<FancyColumn>) {
Object.assign(this, config);
}
/**
* When `allowDragAndDrop` is enabled, then this column is draggable, default as `true`
*/
public draggable?: boolean = true;
/**
* Set `td colspan`, currently only works in footer
*/
public colspan?: number = 1;
uiCheckAll?: boolean = false;
uiWidth?: string;
}
export class FancyTag {
/**
* status could be `info` , `primary` , `danger` , `warning` , `success`
*/
constructor(text: string, status: FtTagType, actionTag: string = null, tooltip = null) {
this.text = text;
this.status = status;
this.tooltip = tooltip || text;
this.actionTag = actionTag;
}
text: string;
tooltip: string;
status: FtTagType;
actionTag: string;
}
export class FancyDisplayRow<T = any> {
index: number;
seq: number;
/**
* Sequence number for UI only
*/
uiSeq: number;
isGroupHeader: boolean;
groupKey?: any;
colHtmlTexts: any;
colTooltips: any;
rowDatum: T;
visible: boolean;
/**
* this is for custom groups to store row id for many to many relationships.
*/
customRowId: string;
/**
* this only has value when file drop to the row
*/
uiFileDropList: FileList | File[];
}
export class FancyGroup<T = any> {
seq: number;
/**
* Origin value type of groupKey
*/
groupKey: any;
/**
* Formatted string value of groupValue
*/
groupValue: any;
rows: FancyDisplayRow<T>[];
expanded: boolean;
/**
* this only has value when file drop to the Group
*/
uiFileDropList: FileList | File[];
uiAllowDeleteWhenEmpty: boolean;
}
export class FancyColumnCustomSetting {
id: string
name: string
seq: number
visible: boolean
groupBy: boolean
widthPx: number
}
@@ -0,0 +1,22 @@
import { FancyDisplayRow, FancyGroup } from "./fancy-row-column.model";
export class FancySelection<T> {
index: number;
focusedCustomRowId: string;
focusedRow: T;
selectedRows: T[];
}
export class FancyEditorInstance<T = any> {
index: number;
object: T;
}
export class FancyDragDrop<T = any> {
previousIndex: number;
currentIndex: number;
focusRow: FancyDisplayRow<T>;
previousRow?: FancyDisplayRow<T>;
nextRow?: FancyDisplayRow<T>;
previousGroup?: FancyGroup<T>;
currentGroup?: FancyGroup<T>;
}
@@ -0,0 +1,188 @@
import { Observable, Subject } from 'rxjs';
import { FancyColumn, FancyGroup, FtTrigger } from './fancy-row-column.model';
import { FancyEditorInstance, FancySelection } from './fancy-selection.model';
import { EventEmitter } from '@angular/core';
import { ContextMenuItem } from '../../directives/right-click-menu/context-menu-item.model';
export const isFancyGroup = <T>(o: T | FancyGroup<T>): o is FancyGroup<T> => {
return o && o.hasOwnProperty("groupKey") && typeof (o as FancyGroup<T>).groupKey === "string";
}
export const isNotFancyGroup = <T>(o: T | FancyGroup<T>): o is T => {
return false == (o && o.hasOwnProperty("groupKey") && typeof (o as FancyGroup<T>).groupKey === "string");
}
export class FancySettings<T = any> {
/**
* Set width to 100%
*/
public autoWidth?: boolean = true;
/**
* Displaying titles of each column, default as `true`
*/
public showHeader?: boolean = true;
/**
* Displaying footer, default as `false`
*/
public showFooter?: boolean = false;
/**
* Enable sticky footer, default as `false`
*/
public stickyFooter?: boolean = false;
/**
* Displaying filter of each column, default as `true`
*/
public showFilters?: boolean = true;
/**
* Displaying group side menu when group by a column, default as `false`
*/
public showGroupsMenu?: boolean = false;
/**
* Allow user selecting row and hight light it, default as `true`
*/
public allowSelection?: boolean = true;
/**
* Allow user drag and drop row, default as `false`
*/
public allowDragAndDrop?: boolean = false;
/**
* Allow user drag and drop row, default as `false`
*/
public allowDrag?: boolean = false;
/**
* When allow user drag and drop row, using drag icon instead of drag all row, default as `false`
*/
public usingDragIcon?: boolean = false;
/**
* Allow user selecting multi rows, default as `false`
*/
public multiselect?: boolean = false;
/**
* Allow user resizing columns, default as `true`
*/
public resizingColumn?: boolean = true;
/**
* Allow user dropping files, default as `false`
*/
public enableFileDrop?: boolean = false;
private _columns?: FancyColumn<T>[];
public get columns(): FancyColumn<T>[] {
return this._columns;
}
public set columns(v: FancyColumn<T>[]) {
if (v) {
for (let i = 0; i < v.length; i++) {
v[i] = new FancyColumn<T>({ ...v[i] });
}
}
this._columns = v;
}
// public groups: FancyGroup[];
public contextMenuItems?: ContextMenuItem<T>[];
public onContextMenuOpening?: (datum: T | FancyGroup<T>, rightClickMenuItems: ContextMenuItem<T>[], selection: FancySelection<T>, event: MouseEvent) => void;
public editorTrigger?: FtTrigger = FtTrigger.NOOP;
public groupMissing?: string = 'N/A';
/**
* Using custom groups object list
*/
public customGroups?: any[] = null;
/**
* The group key field name of custom groups
*/
public customGroupKeyFieldName?: string = 'id';
/**
* The group value field name of custom groups
*/
public customGroupValueFieldName?: string = 'name';
/**
* The group seq field name of custom groups
*/
public customGroupSeqFieldName?: string = 'seq';
/**
* The group rows field name of custom groups
*/
public customGroupRowsFieldName?: string = '';
/**
* Group sort rule, available types:'asc', 'desc'
*/
public groupSortDirection?: string = 'asc';
public groupSortFunction?: (a: FancyGroup<T>, b: FancyGroup<T>) => number;
/**
* For generating Group Row Description
*/
public groupSeqPrepareFunction?: (groupKey: any) => number;
/**
* For generating Group Row Description
*/
public groupValuePrepareFunction?: (groupKey: any, groupValues: T[]) => string;
/**
* Display group without data
*/
public displayEmptyGroup?: boolean = false;
/**
* initial rows count for infinite scrolling, default as 50
*/
public initialRows?: number = 50;
/**
* append rows count for infinite scrolling, default as 10
*/
public appendRowsOnScroll?: number = 10;
/**
* Smooth scroll duration when using scrollToItem. Set to 0 to disable smooth scrolling.
*/
public smoothScrollDurationMs?: number = 0;
/**
* adding validation for build-in editor, return `Observable<string>` error message, if valid return `null`
*/
public addingValidation?: (obj: FancyEditorInstance<T>) => Observable<string> | string;
/**
* editing validation for build-in editor, return `Observable<string>` error message, if valid return `null`
*/
public editingValidation?: (obj: FancyEditorInstance<T>) => Observable<string> | string;
/**
* deleting validation for build-in editor, return `Observable<string>` error message, if valid return `null`
*/
public deletingValidation?: (obj: FancyEditorInstance<T>) => Observable<string> | string;
/**
* the call back buttons for easy table picker
*/
public headerCallbackButtons?: ContextMenuItem<T>[];
/**
* Reload subject must be assigned from component setting
*/
public reloadSubject?: Subject<void> = null;
/**
* Reassign custom group and reload table,Reload subject must be assigned from component setting
*/
public reloadSettingSubject?: Subject<FancySettings<T>> = null;
/**
* The tagged row target field values, it's for `checkall` column type
*/
public taggedValues?: any[] = [];
/**
* The tagged row custom row data, it's for `checkall` column type
*/
public taggedRowData?: T[] = [];
public dialogCssClass?: string = null;
constructor(config: Partial<FancySettings<T>>) {
Object.assign(this, config);
}
}
@@ -0,0 +1,239 @@
<div class="tableContainer tableContainer{{tableId}} row no-gutters" infinite-scroll [scrollWindow]="false"
(scrolled)="onInfinityScrollDown()" (scrolledUp)="onInfinityScrollUp()" [nbSpinner]="processing"
nbSpinnerStatus="success" nbSpinnerSize="giant"
[ngClass]="{'colResizing': colDragResizing,'overflow-x': !settings.autoWidth}"
rbjFileDragDrop="{{settings.enableFileDrop}}" (onFileDropped)="fileDropped(null,$event)">
<button class="btnClearAllFilter" nbButton hero status="danger" size="small" (click)="clearAllFilters()"
*ngIf="anyFiltered">
<nb-icon pack="eva" icon="sync-outline" class="m-0"></nb-icon><span class="clearTooltip">Clear All Filters</span>
</button>
<div class="d-flex">
<div *ngIf="settings.showGroupsMenu && anyGroups">
<rbj-fancy-group-menu [settings]="settings" [groups]="groups" [(draggingRow)]="draggingRow"
[(droppingOnGroupKey)]="droppingOnGroupKey" [filteringGroupKey]="filteringGroupKey"
(itemDropInGroup)="itemDropInGroup.emit($event)" (clickOnGroup)="filterGroupKey($event)"
(clickOnDeleteGroup)="onClickOnDeleteGroup.emit($event)"
(onFileDroppedInGroup)="onFileDroppedInGroup.emit($event)" [usingAltColor]="usingAltColor">
<ng-content select="[fancy-group-menu-header]"></ng-content>
</rbj-fancy-group-menu>
</div>
<div>
<table #table class="fancy" (keydown)="keyHandler($event)" tabindex="0" (focus)="focusLastAccessFilter()"
[ngClass]="{'w-100': settings && settings.autoWidth}">
<colgroup>
<col *ngIf="anyGroups" span="1" style="width: 25px;">
<ng-container *ngFor="let column of visibleColumns">
<col span="1" [style.width]="column.uiWidth">
</ng-container>
</colgroup>
<thead *ngIf="settings.showHeader">
<tr>
<th *ngIf="anyGroups"></th>
<th id="{{tableId}}col-{{i}}" *ngFor="let column of visibleColumns;let i=index">
<div (click)="setSort(column)" title="{{column.title}}">
{{column.title}}
<nb-icon pack="eva" *ngIf="sortColumn && sortColumn.name ==column.name"
[icon]="sortColumn.sortDirection === 'asc' ? 'chevron-down' : 'chevron-up'">
</nb-icon>
</div>
<span #dragHandleRight *ngIf="settings.resizingColumn" cdkDragLockAxis="x" class="dragHandle right"
cdkDrag (cdkDragStarted)="colResizeDragStart(column,$event)"
(cdkDragMoved)="colResizeDragResize(column,$event)" (cdkDragEnded)="colResizeEnd(column,$event)"></span>
</th>
</tr>
<tr *ngIf="settings.showFilters">
<th *ngIf="anyGroups" class="text-center px-0">
<button nbButton ghost size="small" (click)="toggleExpandAll()" class="px-0"
[nbTooltip]="anyExpanded ? 'Collapse All' : 'Expand All'">
<nb-icon pack="eva" [icon]="anyExpanded ? 'arrowhead-up' : 'arrowhead-down'"></nb-icon>
</button>
</th>
<th *ngFor="let column of visibleColumns">
<ng-container [ngSwitch]="column.type">
<ng-container *ngSwitchCase="'date'">
<input type="text" nbInput fullWidth size="tiny" [placeholder]="column.title"
[(ngModel)]="column.dateRange" [nbDatepicker]="filterDatePicker"
(ngModelChange)="filteringDebounceTimer.resetTimer()" [initFocus]="column.initFocusFilterInput"
(focus)="setFocusedFilterInput(null)">
<nb-rangepicker #filterDatePicker>
</nb-rangepicker>
</ng-container>
<ng-container *ngSwitchCase="'dateTime'">
<input type="text" nbInput fullWidth size="tiny" [placeholder]="column.title"
[(ngModel)]="column.dateRange" [nbDatepicker]="filterDatePicker"
(ngModelChange)="filteringDebounceTimer.resetTimer()" [initFocus]="column.initFocusFilterInput"
(focus)="setFocusedFilterInput(null)">
<nb-rangepicker #filterDatePicker>
</nb-rangepicker>
</ng-container>
<ng-container *ngSwitchCase="'checkall'">
<div align="center">
<!-- <kendo-checkbox #checkAllBox class="ftCheck" name="{{column.name}}-uiCheckAll"
[(ngModel)]="column.uiCheckAll" size="large"
(ngModelChange)="checkboxCheckAll(column, $event)"></kendo-checkbox> -->
<input #checkAllBox class="ftCheck" name="{{column.name}}-uiCheckAll"
[(ngModel)]="column.uiCheckAll" (ngModelChange)="checkboxCheckAll(column, $event)" type='checkbox'
kendoCheckBox [indeterminate]="column.uiCheckAll==null" size="medium" />
</div>
</ng-container>
<ng-container *ngSwitchCase="'checkbox'">
<nb-select fullWidth [ngModel]="checkboxGetFilter(column.filter)"
(ngModelChange)="checkboxSetFilter(column.name, $event)" [initFocus]="column.initFocusFilterInput"
(focus)="setFocusedFilterInput($event)">
<nb-option [value]="0">All</nb-option>
<nb-option [value]="1">Disabled</nb-option>
<nb-option [value]="2">Enabled</nb-option>
</nb-select>
</ng-container>
<ng-container *ngSwitchDefault>
<input type="text" nbInput fullWidth size="tiny" [placeholder]="column.title"
[(ngModel)]="column.filter" [initFocus]="column.initFocusFilterInput"
(ngModelChange)="filteringDebounceTimer.resetTimer()" (focus)="setFocusedFilterInput($event)">
</ng-container>
</ng-container>
</th>
</tr>
</thead>
<tbody RightClickMenu [RightClickMenuItems]="settings.contextMenuItems" [RightClickValue]="this"
[onContextMenuOpening]="onContextMenuOpening" cdkDropList (cdkDropListDropped)="drop($event)"
[cdkDropListDisabled]="!settings.allowDragAndDrop && !settings.allowDrag">
<ng-container *ngFor="let row of visibleData;let i = index; trackBy:trackByUiSeq">
<!-- Collapsable group row -->
<tr *ngIf="anyGroups&&row.isGroupHeader" (click)="showHideGroup(row.groupKey)"
class="grouprow {{this.tableId}}GroupRow-{{row.index}}" cdkDrag
rbjFileDragDrop="{{settings.enableFileDrop}}" (onFileDropped)="fileDropped(row,$event)" cdkDragDisabled
attr.groupKey='{{row.groupKey}}' [id]="tableId + row.groupKey +row.index">
<td [colSpan]="visibleColumnCount">
<nb-icon pack="eva" [icon]="expandedGroups(row.groupKey) ? 'chevron-down' : 'chevron-up'"></nb-icon>
<span [innerHTML]="getGroupValue(row.groupKey)"></span>
</td>
</tr>
<tr *ngIf="!row.isGroupHeader && row.visible" [id]="tableId + row.index"
class="{{this.tableId}}Row-{{row.index}}"
[ngClass]="{'selected': (selectedRows && selectedRows.has(row)),'focused': (firstSelectedRow && row.uiSeq == firstSelectedRow.uiSeq),'groupedItem': anyGroups }"
(mousedown)="onRowSelect(row.index,$event)" cdkDrag cdkDragLockAxis="y" (cdkDragStarted)="startDragging()"
(mouseup)="onRowMouseUp(row,$event)" rbjFileDragDrop="{{settings.enableFileDrop}}"
(onFileDropped)="fileDropped(row,$event)">
<td *ngIf="anyGroups">
<nb-icon *ngIf="settings.usingDragIcon" pack="eva" icon="move-outline" class="g-cursor-grab label"
cdkDragHandle></nb-icon>
</td> <!-- Blank cell for the collapse arrow in the group row -->
<ng-container *ngFor="let column of visibleColumns;">
<ng-container [ngSwitch]="column.type">
<td *ngSwitchCase="'checkbox'" class="checkbox text-center" [style.width]="column.uiWidth">
<div align="center">
<nb-checkbox class="ftCheck" [(ngModel)]="row.rowDatum[column.name]"
disabled="true"></nb-checkbox>
</div>
</td>
<td *ngSwitchCase="'checkall'" class="checkbox text-center" [style.width]="column.uiWidth"
(mousedown)="$event.stopPropagation()">
<div class="d-flex">
<ng-container
*ngIf="column.taggingTarget && settings.taggedValues.includes(row.rowDatum[column.taggingTarget])">
<span class="badge badge-warning mr-1 ftCheckBadge"
(click)="toggleCheckBox(column,row,false)">{{(settings.taggedValues.indexOf(row.rowDatum[column.taggingTarget])+1)}}</span>
</ng-container>
<kendo-checkbox #checkAllBox class="ftCheck mx-auto"
name="{{column.name}}-uiCheckAll-{{row.index}}" [(ngModel)]="row.rowDatum[column.name]"
(ngModelChange)="toggleCheckBox(column,row,$event)"></kendo-checkbox>
</div>
</td>
<td *ngSwitchCase="'tag'" [style.width]="column.uiWidth" (mousedown)="onCellMouseDown(column,$event)"
(dblclick)="triggerDoubleClick(row)">
<div title="{{row.colTooltips[column.name]}}">
<label *ngFor="let tag of row.colHtmlTexts[column.name]"
[ngClass]="{'g-cursor-pointer': !!tag.actionTag}" class="badge mr-1 badge-{{tag.status}} "
(click)="tagOnClick(row,tag.actionTag)" [innerHTML]="tag.text"></label>
</div>
</td>
<td *ngSwitchCase="'template'" [style.width]="column.uiWidth" (mousedown)="onCellMouseDown(column,$event)"
(dblclick)="triggerDoubleClick(row)" title="{{row.colTooltips[column.name]}}" class="p-0">
<ng-container *ngTemplateOutlet="row.colHtmlTexts[column.name]; context: {rowData: row.rowDatum, columnName: column.name};"></ng-container>
</td>
<td *ngSwitchDefault [class]="column.class ? column.class : ''" (dblclick)="triggerDoubleClick(row)"
[style.width]="column.uiWidth" [style.maxWidth]="column.uiWidth"
(mousedown)="onCellMouseDown(column,$event)">
<div [innerHTML]="row.colHtmlTexts[column.name]" title="{{row.colTooltips[column.name]}}"></div>
</td>
</ng-container>
</ng-container>
</tr>
</ng-container>
<tr *ngIf="!visibleData || visibleData.length==0" rbjFileDragDrop="{{settings.enableFileDrop}}"
(onFileDropped)="fileDropped(null,$event)">
<td [attr.colspan]="visibleColumnCount">
<nb-alert status="info" class="mt-3">
<div class="d-flex align-items-center">
<div class="ml-2 d-inline-block">
<nb-icon icon="info-outline" pack="eva" class="g-font-size-40"></nb-icon>
</div>
<div class="ml-1 d-inline-block">
<strong *ngIf="!anyFiltered" class="g-font-size-16" [innerHtml]="noDataMessage">
</strong>
<strong *ngIf="anyFiltered" class="g-font-size-16">
No Match Found
</strong>
<p *ngIf="anyFiltered" style="color: whitesmoke;" class="mb-0">
<span style="font-weight: bold;">Tip:</span>
Please try another keyword.
</p>
</div>
</div>
</nb-alert>
</td>
</tr>
</tbody>
<tfoot *ngIf="settings.showFooter" [class.stickyFooter]="settings.stickyFooter">
<tr *ngFor="let datum of footerDisplayData">
<td *ngIf="anyGroups"></td> <!-- Blank cell for the collapse arrow in the group row -->
<ng-container *ngFor="let column of footerSettings.columns">
<td *ngIf="column.visible" [innerHTML]="datum.colHtmlTexts[column.name]"
[class]="column.footerClass ? column.footerClass : ''"
[colSpan]="column.colspan ? column.colspan : 1"
></td>
</ng-container>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="d-flex" style="width: 100%;">
<div class="rect demo-dark" style="flex: 0 0 220px;" *ngIf="settings.showGroupsMenu && anyGroups">
</div>
<div style="flex: 1;">
</div>
</div>
</div>
<!-- <div class="bottomShadow" *ngIf="hasScrollBar"></div> -->
<!-- <pager *ngIf="itemsPerPage > 0" [page]="page" (pageChange)="gotoPage($event)" [pages]="pages" [maxPage]="maxPage">
</pager> -->
@@ -0,0 +1,244 @@
@import "../../@theme/styles/themes";
@include nb-install-component() {
table.fancy {
table-layout: fixed;
border-collapse: separate !important;
border: 1px solid nb-theme(border-basic-color-5);
box-shadow: nb-theme(shadow);
border-top: 0px;
user-select: none;
border-spacing: 0;
table tbody,
table thead {
display: block;
}
th:first-child,
td:first-child {
/* Apply a left border on the first <td> or <th> in a row */
border-left: 1px solid nb-theme(border-basic-color-5);
}
td {
//display: block;
text-overflow: ellipsis;
//border: 1px solid nb-theme(border-basic-color-5);
padding: 5px 0.5rem; //0.5rem;
background: transparent;
border-bottom: 1px solid nb-theme(border-basic-color-5);
border-right: 1px solid nb-theme(border-basic-color-5);
}
tr.grouprow > td {
padding: 0.5rem;
}
tr.groupedItem > td {
padding: 0 0.5rem;
&.checkbox {
padding: 0.3rem 0.5rem;
}
}
td > div {
//width: 50px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
tr {
height: nb-theme(text-paragraph-font-size);
white-space: nowrap;
overflow: hidden;
}
tr.grouprow {
color: nb-theme(text-alternate-color);
background: nb-theme(color-primary-300) !important;
cursor: pointer;
user-select: none;
}
tr.collapsed {
display: none;
}
tbody {
tr:hover {
background: nb-theme(background-basic-color-4);
}
tr.selected {
color: nb-theme(text-alternate-color);
background: nb-theme(color-info-300);
}
tr.focused {
// border: 2px dotted nb-theme(color-info-900);
border: 2px dotted nb-theme(text-basic-color);
td {
border-top: 2px dotted nb-theme(text-basic-color);
border-bottom: 2px dotted nb-theme(text-basic-color);
}
td:first-child {
border-left: 2px dotted nb-theme(text-basic-color);
}
td:last-child {
border-right: 2px dotted nb-theme(text-basic-color);
}
}
}
tbody tr:nth-child(even):not(:hover):not(.selected) {
background: nb-theme(background-basic-color-1);
}
tbody tr:nth-child(odd):not(:hover):not(.selected) {
background: nb-theme(background-basic-color-2);
}
thead {
th {
background: white;
position: sticky;
top: 0;
//top: -16px;
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.4);
height: 2.4rem;
padding: 0 5px;
background: nb-theme(background-basic-color-1);
border-bottom: 1px solid nb-theme(border-basic-color-5);
border-right: 1px solid nb-theme(border-basic-color-5);
z-index: 100;
}
tr:nth-child(1) th {
border-top: 2px solid nb-theme(border-basic-color-5);
cursor: pointer;
user-select: none;
}
tr:nth-child(2) th {
top: calc(2.4rem);
height: 3rem;
}
}
tfoot.stickyFooter {
td {
position: sticky;
bottom: 0;
box-shadow: 0 -2px 2px -1px rgba(0, 0, 0, 0.4);
height: 2.4rem;
padding: 0 5px;
background: nb-theme(background-basic-color-1);
border-top: 1px solid nb-theme(border-basic-color-5);
border-right: 1px solid nb-theme(border-basic-color-5);
z-index: 100;
}
tr:nth-child(1) td {
border-bottom: 2px solid nb-theme(border-basic-color-5);
}
}
}
}
table.fancy.overflow-x {
table-layout: auto;
}
// .searchclear {
// position: absolute;
// right: 5px;
// top: 0;
// height: 14px;
// margin: auto;
// font-size: 14px;
// cursor: pointer;
// }
::ng-deep nb-checkbox.ftCheck {
width: 20px;
display: block;
> .label {
cursor: pointer;
}
}
.ftCheckBadge {
height: 17px;
margin-top: 3px;
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 4px;
box-shadow:
0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.tableContainer {
height: 100%;
max-height: 100%; //calc(80vh - 20px);
overflow-y: auto;
overflow-x: hidden;
&.colResizing {
cursor: ew-resize !important;
}
}
.tableContainer.overflow-x {
overflow-x: auto;
}
:host {
//height: 100%;
//display: block;
//overflow: hidden;
}
.bottomShadow {
box-shadow: 0 2px 1rem 10px rgba(44, 51, 73, 0.25);
position: relative;
bottom: 4px;
width: 98%;
left: 30px;
}
.dragHandle {
position: absolute;
//background-color: cornflowerblue;
z-index: 2;
transform: translate(0, 0) !important;
}
.dragHandle.right {
right: 0px;
width: 2px;
height: 100%;
cursor: ew-resize;
top: 0;
}
.btnClearAllFilter {
/* Add animation */
-webkit-animation-name: fadeFromTop; /* Chrome, Safari, Opera */
-webkit-animation-duration: 0.5s; /* Chrome, Safari, Opera */
animation-name: fadeFromTop;
animation-duration: 0.5s;
position: absolute;
right: 28px;
margin-top: 41px;
z-index: 1011;
.clearTooltip {
-webkit-transition: width 0.5s ease-in-out;
-moz-transition: width 0.5s ease-in-out;
-o-transition: width 0.5s ease-in-out;
transition: width 0.5s ease-in-out;
width: 0;
overflow: hidden;
}
&:hover .clearTooltip {
padding-left: 6px;
width: 110px;
}
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FancyTableComponent } from './fancy-table.component';
describe('FancyTableComponent', () => {
let component: FancyTableComponent;
let fixture: ComponentFixture<FancyTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ FancyTableComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FancyTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,75 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FancyTableComponent } from './fancy-table.component';
import { FormsModule } from '@angular/forms';
import {
NbButtonModule,
NbDatepickerModule,
NbCalendarRangeModule,
NbCardModule,
NbCheckboxModule,
NbDialogModule,
NbIconModule,
NbInputModule,
NbSelectModule,
NbTooltipModule,
NbAlertModule,
NbSpinnerModule,
NbActionsModule,
} from '@nebular/theme';
import { PagerModule } from '../pager/pager.module';
import { CurrencyInputModule } from '../currency-input/currency-input.module';
import { DropDownListModule } from '../drop-down-list/drop-down-list.module';
import { DateInputModule } from '../date-input/date-input.module';
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { TablePickerComponent } from './table-picker/table-picker.component';
import { CustomColumnsDlgComponent } from './custom-columns-dlg/custom-columns-dlg.component';
import { FileDragDropModule } from '../../shared/file-drag-drop/file-drag-drop.module';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { FancyGroupMenuComponent } from './fancy-group-menu/fancy-group-menu.component';
import { EmailInputModule } from '../email-input/email-input.module';
import { PhoneInputModule } from '../phone-input/phone-input.module';
import { RightClickMenuModule } from '../../directives/right-click-menu/right-click-menu.module';
import { InitFocusModule } from '../../directives/init-focus/init-focus.module';
const components = [
FancyTableComponent,
ConfirmDialogComponent,
]
@NgModule({
declarations: [...components, TablePickerComponent, CustomColumnsDlgComponent, FancyGroupMenuComponent],
imports: [
CommonModule,
FormsModule,
NbButtonModule,
NbCalendarRangeModule,
NbCardModule,
NbCheckboxModule,
NbDatepickerModule,
NbDialogModule,
NbIconModule,
NbInputModule,
NbSelectModule,
NbTooltipModule,
NbAlertModule,
NbSpinnerModule,
NbActionsModule,
InfiniteScrollModule,
CurrencyInputModule,
DateInputModule,
DragDropModule,
DropDownListModule,
PagerModule,
RightClickMenuModule,
InitFocusModule,
FileDragDropModule,
PhoneInputModule,
EmailInputModule,
InputsModule
],
exports: [...components]
})
export class FancyTableModule { }
@@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { SeqCalcService } from './seq-calc.service';
describe('SeqCalcService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: SeqCalcService = TestBed.get(SeqCalcService);
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,162 @@
import { Inject, Injectable, Optional } from '@angular/core';
import { FancyDragDrop } from './fancy-selection.model';
import { ISeqBusinessEntity } from '../../models/base.model';
import { moveItemInArray } from '@angular/cdk/drag-drop';
import { UpdateSeqViewModel } from '../../shared/models/common.model';
import { NumberUtils } from '../../utilities/number-utils';
export const SEQ_INTERVAL = 'SEQ_INTERVAL_DEFAULT';
export const DEFAULT_SEQ_INTERVAL = 1000;
@Injectable({
providedIn: 'root'
})
export class SeqCalcService {
private SEQ_INTERVAL_REGULAR_AMOUNT = 10; // Regular interval amount for seq calculation
private baseInterval: number;
constructor(
@Optional() @Inject(SEQ_INTERVAL) baseInterval: number | null,
) {
// use DEFAULT when token missing
this.baseInterval = baseInterval ?? DEFAULT_SEQ_INTERVAL;
}
calculateAndAssignSeq(event: FancyDragDrop<ISeqBusinessEntity>,
allData: ISeqBusinessEntity[], updateSeqFunc: (model: UpdateSeqViewModel) => void) {
let movedRows = [event.focusRow.rowDatum] as ISeqBusinessEntity[];
this.calculateAndAssignSeqs(event, allData, movedRows, updateSeqFunc);
}
calculateAndAssignSeqs(event: FancyDragDrop<ISeqBusinessEntity>,
allData: ISeqBusinessEntity[],
movedRows: ISeqBusinessEntity[], updateSeqFunc: (model: UpdateSeqViewModel) => void) {
let intervalCount = this.baseInterval;
let insertSeq = 0;
//allData = allData.sort((a, b) => a.seq - b.seq); // Ensure allData is sorted by seq before processing
//Find the start seq, and interval amount for moving
if (event.previousRow && event.nextRow) {
console.log('previousRowSeq', event.previousRow.rowDatum.seq, event.nextRow.rowDatum.seq);
if (movedRows.length > 1) {
insertSeq = (event.previousRow.rowDatum.seq + event.nextRow.rowDatum.seq) / (movedRows.length + 1);
intervalCount = insertSeq + (insertSeq % 2);
} else {
insertSeq = (event.previousRow.rowDatum.seq + event.nextRow.rowDatum.seq) / 2;
}
} else if (event.previousRow) {
insertSeq = event.previousRow.rowDatum.seq + this.baseInterval;
}
else if (event.nextRow) {
intervalCount = event.nextRow.rowDatum.seq / movedRows.length;
intervalCount += intervalCount % 2;
}
let updateDocuments = [] as ISeqBusinessEntity[];
// if (this.tableSettings.taggedRowData.length > 0) {
// movedRows = this.tableSettings.taggedRowData as DocumentGroupTemplate[];
// }
//moveItemInArray(allData, event.previousIndex, event.currentIndex);
let needToUpdateAllRows = false;
let lastSeq = null;
for (let i = 0; i < movedRows.length; i++) {
const row = movedRows[i];
row.seq = Math.round(insertSeq + intervalCount * i);
if (lastSeq != null && (row.seq - lastSeq) === 0) {
needToUpdateAllRows = true;
break;
}
lastSeq = row.seq;
allData.find(d => d.id == row.id).seq = row.seq;
if (allData.filter(d => d.seq == row.seq).length > 1) {
needToUpdateAllRows = true;
}
updateDocuments.push(row);
console.log('newSeq', row.seq);
}
allData = allData.sort((a, b) => a.seq - b.seq); // Ensure allData is sorted by seq after processing
if (needToUpdateAllRows) {
this.resetAllSeq(allData, updateSeqFunc, allData[0].seq);
} else {
updateSeqFunc(
{ ids: updateDocuments.map(d => d.id), seqs: updateDocuments.map(d => d.seq) } as UpdateSeqViewModel
);
}
}
resetAllSeq(allData: ISeqBusinessEntity[], updateSeqFunc: (model: UpdateSeqViewModel) => void, startSeq: number = 0) {
let seq = startSeq;
for (let i = 0; i < allData.length; i++) {
const document = allData[i];
document.seq = seq;
seq += this.baseInterval;
}
if (updateSeqFunc) {
updateSeqFunc(
{ ids: allData.map(d => d.id), seqs: allData.map(d => d.seq) } as UpdateSeqViewModel
);
}
}
calculateNewSeq(
allData: ISeqBusinessEntity[],
focusedDatum: ISeqBusinessEntity,
insertAfter: boolean,
updateSeqFunc: (model: UpdateSeqViewModel) => void
): number {
if (!Array.isArray(allData) || !focusedDatum) {
throw new Error("Invalid arguments.");
}
const base = this.baseInterval;
const idx = allData.indexOf(focusedDatum);
if (idx === -1) throw new Error("focusedDatum is not in allData.");
// Helper: resequence using the current array order
const resequence = () => {
for (let i = 0; i < allData.length; i++) {
allData[i].seq = (i + 1) * base;
}
};
let newSeq = 0;
if (insertAfter) {
// Append at end
if (idx === allData.length - 1) {
newSeq = focusedDatum.seq + base;
} else {
const next = allData[idx + 1];
const gap = next.seq - focusedDatum.seq;
if (gap >= 2) {
newSeq = NumberUtils.Mid(focusedDatum.seq, next.seq);
} else {
// No integer room; normalize then take the middle
//resequence();
this.resetAllSeq(allData, updateSeqFunc, allData[0].seq);
newSeq = NumberUtils.Mid(allData[idx].seq, allData[idx + 1].seq);
}
}
} else {
// Insert before
if (idx === 0) {
newSeq = focusedDatum.seq - base;
} else {
const prev = allData[idx - 1];
const gap = focusedDatum.seq - prev.seq;
if (gap >= 2) {
newSeq = NumberUtils.Mid(prev.seq, focusedDatum.seq);
} else {
//resequence();
this.resetAllSeq(allData, updateSeqFunc, allData[0].seq);
newSeq = NumberUtils.Mid(allData[idx - 1].seq, allData[idx].seq);
}
}
}
return newSeq;
}
}
@@ -0,0 +1,22 @@
<nb-card class="{{cardCssClass}}">
<nb-card-header *ngIf="title">
{{title}}
<nb-actions size="small" fullWidth class="float-right" *ngIf="settings.headerCallbackButtons">
<nb-action icon="{{callbackBtn.icon}}" title="{{callbackBtn.title}}" (click)="callbackBtn.callback( datum,null)"
*ngFor="let callbackBtn of settings.headerCallbackButtons">
</nb-action>
</nb-actions>
</nb-card-header>
<nb-card-body>
<fancy-table [data]="data" [settings]="settings" (rowSelected)="selectRow($event)"
(doubleClick)="doubleClickRow($event)"></fancy-table>
</nb-card-body>
<nb-card-footer>
<div class="buttons-row">
<button nbButton hero status="danger" size="small" class="float-right" (click)="close()">Cancel</button>
<button nbButton hero status="primary" size="small" class="float-right mr-3" (click)="submit()"
*ngIf="displaySubmitBtn">Submit</button>
</div>
</nb-card-footer>
</nb-card>
@@ -0,0 +1,8 @@
.nbCard {
max-width: 1080px;
max-height: 99vh;
height: calc(70vh - 20px);
}
.tableContainer {
height: calc(50vh - 20px);
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { TablePickerComponent } from './table-picker.component';
describe('TablePickerComponent', () => {
let component: TablePickerComponent;
let fixture: ComponentFixture<TablePickerComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ TablePickerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TablePickerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,92 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { ArrayUtils } from '../../../utilities/array-utils';
import { FancyColumn } from '../fancy-row-column.model';
import { FancySelection } from '../fancy-selection.model';
import { FancySettings } from '../fancy-settings.model';
import { FancyTableComponent } from '../fancy-table.component';
export const TABLE_PICKER_COL_CHECK_NAME = 'uiChecked';
const COL_ID = 'id';
@Component({
selector: 'ngx-table-picker',
templateUrl: './table-picker.component.html',
styleUrls: ['./table-picker.component.scss']
})
export class TablePickerComponent implements OnInit {
@ViewChild(FancyTableComponent) fancyTable: FancyTableComponent<any>;
title: string;
data: any[];
selectedIds: string[];
settings: FancySettings;
displaySubmitBtn: boolean;
datum: any;
cardCssClass: string = 'nbCard';
constructor(
private dlgRef: NbDialogRef<TablePickerComponent>
) { }
ngOnInit() {
if (this.settings.multiselect && !this.settings.columns.find(c => c.name == TABLE_PICKER_COL_CHECK_NAME)) {
this.settings.columns.unshift(
new FancyColumn({ name: TABLE_PICKER_COL_CHECK_NAME, type: 'checkall', widthPx: 20 })
);
}
if (this.settings.dialogCssClass) {
this.cardCssClass = this.settings.dialogCssClass;
}
for (let i = 0; i < this.data.length; i++) {
const element = this.data[i];
element[TABLE_PICKER_COL_CHECK_NAME] = this.selectedIds.includes(element[COL_ID]);
}
setTimeout(() => {
if (!this.settings.multiselect) {
this.fancyTable.searchRowAndFocus(r => r[COL_ID] == this.selectedIds[0]);
}
}, 200);
}
submit() {
if (this.settings.multiselect) {
this.removeCheckboxCol();
this.dlgRef.close(this.data.filter(d => d[TABLE_PICKER_COL_CHECK_NAME]));
} else {
//let selectedId = this.fancyTable.getSelected().focusedRow[COL_ID];
this.dlgRef.close(this.fancyTable.getSelected().focusedRow);
}
}
close() {
this.removeCheckboxCol();
this.dlgRef.close(null);
}
removeCheckboxCol() {
if (this.settings.multiselect) {
this.settings.multiselect = false;
this.settings.columns.splice(0, 1);
}
}
doubleClickRow(select: FancySelection<any>) {
if (this.displaySubmitBtn) {
if (!this.settings.multiselect) {
this.dlgRef.close(select.focusedRow);
} else {
this.data[select.index][TABLE_PICKER_COL_CHECK_NAME] = !this.data[select.index][TABLE_PICKER_COL_CHECK_NAME];
}
} else {
let editContextMenuItem = this.settings.contextMenuItems.find(c => c.id == 'edit');
if (editContextMenuItem) {
editContextMenuItem.callback(select.focusedRow, null);
}
}
}
selectRow(select: FancySelection<any>) {
if (select) {
this.datum = select.focusedRow;
}
}
}
@@ -0,0 +1,9 @@
<div
*ngIf="isLoading"
[nbSpinner]="isLoading"
nbSpinnerSize="giant"
nbSpinnerStatus="success"
style="height: 50vh;"
class="row"
></div>
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { LoadingSpinnerComponent } from './loading-spinner.component';
describe('LoadingSpinnerComponent', () => {
let component: LoadingSpinnerComponent;
let fixture: ComponentFixture<LoadingSpinnerComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoadingSpinnerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoadingSpinnerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'loading-spinner',
templateUrl: './loading-spinner.component.html',
styleUrls: ['./loading-spinner.component.scss']
})
export class LoadingSpinnerComponent {
@Input() public isLoading: boolean = false;
constructor() { }
}
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NbSpinnerModule } from '@nebular/theme';
import { LoadingSpinnerComponent } from './loading-spinner.component';
const components = [LoadingSpinnerComponent,];
@NgModule({
declarations: [...components],
imports: [
CommonModule,
NbSpinnerModule,
],
exports: [...components],
})
export class LoadingSpinnerModule { }
@@ -0,0 +1,48 @@
<div *ngIf="pages.length > 0">
<nav aria-label="Page navigation">
<ul class="pagination " [ngClass]="{'justify-content-center': alignJustify}">
<li class="page-item" [nbTooltip]="'First ' + prefix">
<button type="submit" nbButton outline size="small" [disabled]="page === 1" class="rounded-0 border-right-0"
(click)="gotoPage(1)">
<nb-icon pack="eva" icon="arrowhead-left"></nb-icon>
</button>
</li>
<li class="page-item" [nbTooltip]="'Previous ' + prefix">
<button type="submit" nbButton outline size="small" [disabled]="page === 1" class="rounded-0 border-right-0"
(click)="gotoPage(page - 1)">
<nb-icon pack="eva" icon="arrow-ios-back"></nb-icon>
</button>
</li>
<li *ngFor="let p of pages.slice(pagesStart, pagesEnd)" class="page-item">
<button type="submit" nbButton outline class="rounded-0 border-right-0" *ngIf="p !== page" size="small"
(click)="gotoPage(p)" nbTooltip={{getPageToolTip(p)}}>
{{p}}
</button>
<button type="submit" nbButton class="rounded-0 border-right-0" *ngIf="p === page" size="small"
(click)="gotoPage(p)" nbTooltip={{getPageToolTip(p)}}>
{{p}}
</button>
</li>
<li class="page-item" nbTooltip={{getNewPageToolTip()}} *ngIf="page!=lastPage||page == maxPage">
<button type="submit" nbButton outline size="small" class="rounded-0" [disabled]="page == maxPage"
(click)="gotoPage(page + 1)">
<!-- <nb-icon pack="eva" [icon]="page >= pages[pages.length - 1] ? 'plus' : 'arrow-ios-forward'"></nb-icon> -->
<nb-icon pack="eva" [icon]="'arrow-ios-forward'"></nb-icon>
</button>
</li>
<li class="page-item" nbTooltip={{getNewPageToolTip()}} *ngIf="page<maxPage && page==lastPage">
<button type="submit" nbButton outline size="small" class="rounded-0" (click)="addNewPage()">
<nb-icon pack="eva" [icon]="'plus'"></nb-icon>
</button>
</li>
<li class="page-item" [nbTooltip]="'Last ' + prefix">
<button type="submit" nbButton outline size="small" [disabled]="page >= pages[pages.length - 1]"
class="rounded-0 border-left-0" (click)="gotoPage(pages[pages.length - 1])">
<nb-icon pack="eva" icon="arrowhead-right"></nb-icon>
</button>
</li>
</ul>
</nav>
</div>
@@ -0,0 +1,12 @@
nb-icon {
// margin-top: -2px !important;
// margin-bottom: -2px !important;
margin: -2px -6px !important;
}
ul {
margin-top: 0;
margin-bottom: 0px;
}
.justify-content-center {
margin-bottom: 0 !important;
}
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PagerComponent } from './pager.component';
describe('PagerComponent', () => {
let component: PagerComponent;
let fixture: ComponentFixture<PagerComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ PagerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PagerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,98 @@
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { NumberUtils } from '../../utilities/number-utils';
@Component({
selector: 'pager',
templateUrl: './pager.component.html',
styleUrls: ['./pager.component.scss']
})
export class PagerComponent implements OnChanges {
@Input() public alignJustify: boolean = true;
@Input() public page: number;
@Input() public pages: number[];
@Input() public toolTips: string[];
@Input() public minPage: number = 1;
@Input() public maxPage: number = Number.POSITIVE_INFINITY; // default no limit
@Input() public prefix: string = "Page";
@Input() public numberOfPages: number = 5; // max number of pages to show at a time between "Previous" and "Next" buttons
@Output() public pageChange = new EventEmitter<number>();
@Output() public addingNewPage = new EventEmitter<number>();
pagesStart: number;
pagesEnd: number;
constructor() { }
public get lastPage(): number {
return this.pages[this.pages.length - 1];
}
ngOnChanges(change: SimpleChanges) {
if (false
|| (change.pages && change.pages.currentValue !== change.pages.previousValue)
|| (change.maxPage && change.maxPage.currentValue !== change.maxPage.previousValue)
) {
if (this.pages.length > 0 && (this.page > Math.min(this.pages.length + 1, this.maxPage) || this.page < this.minPage)) {
this.page = NumberUtils.Clamp(this.page, this.minPage, Math.min(this.pages.length + 1, this.maxPage));
this.gotoPage(this.page);
// this.calculatePages();
}
}
if (false
|| (change.page && change.page.currentValue !== change.page.previousValue)
|| (change.maxPage && change.maxPage.currentValue !== change.maxPage.previousValue)
) {
if (this.pages.length > 0) {
this.page = NumberUtils.Clamp(this.page, this.minPage, Math.min(this.pages.length + 1, this.maxPage));
//
this.calculatePages();
}
}
}
gotoPage(p: number) {
//
this.pageChange.emit(p);
}
getPageToolTip(page: number): string {
if (this.toolTips && this.toolTips.length > (page - 1)) {
return this.toolTips[(page - 1)];
}
return `Page ${page}`;
}
getNewPageToolTip(): string {
return this.page === this.pages[this.pages.length - 1] ? `New ${this.prefix}` : `Next ${this.prefix}`;
}
calculatePages() {
//
if (this.pages.length > 0) {
if (!this.pages.includes(this.page)) {
this.pages.push(this.page);
this.pages = this.pages.sort(NumberUtils.SortFunction);
}
let currentPage = this.page;
let lastPage = this.pages[this.pages.length - 1];
let pagesAfter = Math.floor(this.numberOfPages / 2);
let pagesBefore = this.numberOfPages - pagesAfter;
if (currentPage - pagesBefore < 0) {
pagesAfter += pagesBefore - currentPage;
}
if (currentPage + pagesAfter > lastPage) {
pagesBefore += currentPage + pagesAfter - lastPage;
}
this.pagesStart = Math.max(0, currentPage - pagesBefore);
this.pagesEnd = Math.min(lastPage, currentPage + pagesAfter);
}
}
addNewPage() {
if (this.page < this.maxPage)
this.addingNewPage.emit(this.page + 1);
}
}
+21
View File
@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PagerComponent } from './pager.component';
import { NbIconModule, NbButtonModule, NbTooltipModule } from '@nebular/theme';
const components = [
PagerComponent,
]
@NgModule({
declarations: [...components],
imports: [
CommonModule,
NbButtonModule,
NbIconModule,
NbTooltipModule,
],
exports: [...components],
})
export class PagerModule { }
@@ -0,0 +1,11 @@
<div class="input-group">
<input #password nbInput fullWidth name='passwordInput{{uuid}}' [type]="fieldTextType ? 'text' : 'password'"
(blur)="onBlur()" [(ngModel)]="text" />
<div class="input-group-append">
<button nbButton hero type="button" tabindex="-1" size="small" status="primary"
nbTooltip="{{ fieldTextType ? 'Hide password' : 'Show password' }}" nbTooltipStatus="primary"
(click)="toggleFieldTextType()" [disabled]="disabledState">
<nb-icon [icon]="fieldTextType ? 'eye-outline' : 'eye-off-outline'" pack="eva"></nb-icon>
</button>
</div>
</div>
@@ -0,0 +1,19 @@
:host {
flex: auto;
display: contents;
}
.input-group {
display: flex;
align-items: stretch;
width: 100%;
input {
flex: 1;
min-width: 0;
}
.input-group-append {
flex-shrink: 0;
}
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { PasswordInputComponent } from './password-input.component';
import { PasswordInputModule } from './password-input.module';
describe('PasswordInputComponent', () => {
let component: PasswordInputComponent;
let fixture: ComponentFixture<PasswordInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsModule, PasswordInputModule],
}).compileComponents();
fixture = TestBed.createComponent(PasswordInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,78 @@
import {
ChangeDetectorRef,
Component,
EventEmitter,
forwardRef,
Input,
Output,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ControlContainer, NgForm } from '@angular/forms';
import { UuidUtils } from '../../utilities/uuid-utils';
@Component({
selector: 'rbj-password-input',
templateUrl: './password-input.component.html',
styleUrl: './password-input.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => PasswordInputComponent),
multi: true,
},
],
viewProviders: [{ provide: ControlContainer, useExisting: NgForm }],
})
export class PasswordInputComponent implements ControlValueAccessor {
private _value = '';
disabledState = false;
uuid: string = UuidUtils.generate();
/**
*
*/
constructor(
private cdRef: ChangeDetectorRef,
) {
}
@Input() id = '';
@Input() placeholder = 'Passphrase';
text: string = '';
onChange = (value: string) => { };
onTouched = () => { };
@Output() blur = new EventEmitter<string>();
fieldTextType = false;
toggleFieldTextType(): void {
this.fieldTextType = !this.fieldTextType;
}
writeValue(value: string): void {
if (value != this.lastBlurValue) {
this.text = value;
this.lastBlurValue = value;
}
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabledState = isDisabled;
}
lastBlurValue: string = '';
onBlur() {
if (this.lastBlurValue != this.text) {
this.lastBlurValue = this.text;
this.blur.emit(this.text);
}
}
}
@@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NbInputModule, NbButtonModule, NbIconModule, NbTooltipModule } from '@nebular/theme';
import { PasswordInputComponent } from './password-input.component';
@NgModule({
declarations: [PasswordInputComponent],
imports: [
CommonModule,
FormsModule,
NbInputModule,
NbButtonModule,
NbIconModule,
NbTooltipModule,
],
exports: [PasswordInputComponent],
})
export class PasswordInputModule {}
@@ -0,0 +1,4 @@
<input #inputBox="ngModel" nbInput fullWidth [id]="id" name='phoneInput{{uuid}}' class='{{class}}'
placeholder='{{placeholder}}' [disabled]="_disabled" [readonly]="readonly" [(ngModel)]="value" validate
[mask]="allowExtension?(maskExpression+extensionMask):maskExpression" [invalidMsg]="'Invalid Phone/Fax format'"
(blur)="onBlur()">

Some files were not shown because too many files have changed in this diff Show More