WIP
This commit is contained in:
@@ -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() {
|
||||
// }
|
||||
|
||||
// }
|
||||
@@ -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>
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user