aboutsummaryrefslogtreecommitdiffstats
path: root/sdc-workflow-designer-ui/src
diff options
context:
space:
mode:
authorYuanHu <yuan.hu1@zte.com.cn>2018-03-27 17:33:22 +0800
committerYuanHu <yuan.hu1@zte.com.cn>2018-03-27 17:33:22 +0800
commit8261a4ea8091c27b61ac581a852e2e18283b3cdd (patch)
treea2ca109f7600e9e0cbe73eb9139ffe6284be1159 /sdc-workflow-designer-ui/src
parent573f32b362f4639928485d66feb1c0721109716b (diff)
Include paletx components
Include paletx components to WF Designer UI. Issue-ID: SDC-1130,SDC-1131 Change-Id: Iad06b2dde8fc98d03a0e3633e829b686d75cafd0 Signed-off-by: YuanHu <yuan.hu1@zte.com.cn>
Diffstat (limited to 'sdc-workflow-designer-ui/src')
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/numberedFixLen.pipe.ts27
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.html134
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.less434
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.ts1712
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.module.ts27
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.html14
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.ts162
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover-config.ts13
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover.ts175
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/time.ts51
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker-config.ts19
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.less163
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.ts558
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/popup.ts58
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/positioning.ts153
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/triggers.ts62
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/util.ts39
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts16
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts9
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts4
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts109
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts103
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts114
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts82
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less125
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts21
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts597
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts54
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/index.ts8
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv4-validator.directive.ts24
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv6-validator.directive.ts24
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/max-validator.directive.ts49
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/min-validator.directive.ts49
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip-address.component.ts170
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip.component.ts189
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.component.ts765
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.html69
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.less423
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.module.ts31
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-text-input/validate-on-blur.directive.ts18
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts11
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts13
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less241
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts12
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts485
-rw-r--r--sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts176
46 files changed, 7792 insertions, 0 deletions
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/numberedFixLen.pipe.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/numberedFixLen.pipe.ts
new file mode 100644
index 00000000..9d26b16f
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/numberedFixLen.pipe.ts
@@ -0,0 +1,27 @@
+/**
+ * numberFixedLen.pipe
+ */
+
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'numberFixedLen'
+})
+export class NumberFixedLenPipe implements PipeTransform {
+ transform(num: number, len: number): any {
+ let numberInt = Math.floor(num);
+ let length = Math.floor(len);
+
+ if (num === null || isNaN(numberInt) || isNaN(length)) {
+ return num;
+ }
+
+ let numString = numberInt.toString();
+
+ while (numString.length < length) {
+ numString = '0' + numString;
+ }
+
+ return numString;
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.html b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.html
new file mode 100644
index 00000000..8e4102c2
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.html
@@ -0,0 +1,134 @@
+<div
+ class="owl-dateTime owl-widget"
+ [ngClass]="{'owl-dateTime-inline': inline}"
+ [ngStyle]="style"
+ #container>
+ <div *ngIf="!inline && customTemp.children.length == 0" class="owl-dateTime-inputWrapper" (mouseout)="Mouseout($event)" (mouseover)="Mouseover($event)">
+ <input *ngIf="!supportKeyboardInput" type="text" [class]="inputStyleClass"
+ [ngClass]="{
+ 'owl-dateTime-input owl-inputtext owl-state-default': true
+ }"
+ [ngStyle]="inputStyle"
+ [attr.placeholder]="placeHolder"
+ [attr.tabindex]="tabIndex" [attr.id]="inputId"
+ [attr.required]="required"
+ [disabled]="disabled"
+ [value]="formattedValue"
+ (focus)="onInputFocus($event)" (blur)="onInputBlur($event)" (click)="onInputClick($event)" readonly>
+ <input *ngIf="supportKeyboardInput" type="text" [class]="inputStyleClass"
+ [ngClass]="{
+ 'owl-dateTime-input owl-inputtext owl-state-default': true
+ }"
+ [ngStyle]="inputStyle"
+ [attr.placeholder]="placeHolder"
+ [attr.tabindex]="tabIndex" [attr.id]="inputId"
+ (change)="onInputChange($event)"
+ [attr.required]="required"
+ [value]="formattedValue"
+ (focus)="onInputFocus($event)" (blur)="onInputBlur($event)" (click)="onInputClick($event)">
+ <i class="ict ict-close owl-icon" style="margin-right: 5px;" [hidden]="(!value)||(disabled)||(!mouseIn)||(!canClear)" (click)="clearValue($event)"></i>
+ <i class="fa fa-calendar owl-icon" style="margin-right: 5px;" [hidden]="value&&(!disabled)" (click)="onInputClick($event)" ></i>
+ </div>
+ <!-- Workaround of ng-content default content (angular issue #12530) -->
+ <div [ngClass]="{'owl-dateTime-customTemp': customTemp.children.length !== 0}" #customTemp (click)="onInputClick($event)">
+ <ng-content></ng-content>
+ </div>
+ <div class="owl-dateTime-dialog owl-state-default owl-corner-all"
+ [ngStyle]="{'display': inline ? 'inline-block' : null}"
+ [@fadeInOut]="dialogVisible? 'visible' : (!inline? 'hidden': null)"
+ (click)="onDialogClick($event)" #dialog>
+ <div *ngIf="showHeader" class="owl-dateTime-dialogHeader owl-corner-top">
+ <span *ngIf="value; else elseBlock">{{formattedValue}}</span>
+ <ng-template #elseBlock><span>{{placeHolder}}</span></ng-template>
+ </div>
+ <div *ngIf="type ==='both' || type === 'calendar'" class="owl-calendar-wrapper owl-corner-all owl-padding">
+ <div class="owl-calendar-control owl-cal-header">
+ <div class="owl-calendar-controlNav">
+ <span class="fa fa-angle-left" style="padding: 8px;margin-left:12px; font-size: 20px;"
+ (click)="prevNav($event)"></span>
+ </div>
+ <div class="owl-calendar-controlContent">
+ <span class="month-control" (click)="changeDialogType(2)">{{pickerMonth}}</span>
+ <span class="year-control" (click)="changeDialogType(3)">{{pickerYear}}</span>
+ </div>
+ <div class="owl-calendar-controlNav">
+ <span class="fa fa-angle-right" style="padding: 8px; font-size: 20px;" (click)="nextNav($event)"></span>
+ </div>
+ </div>
+ <div class="owl-calendar" [hidden]="dialogType !== 1">
+ <table class="owl-calendar-day">
+ <thead class="owl-cal-header">
+ <tr class="owl-weekdays" style="height:40px">
+ <th *ngFor="let weekDay of calendarWeekdays" class="owl-weekday" scope="col">
+ <span>{{weekDay}}</span>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr class="owl-days" *ngFor="let week of calendarDays">
+ <td *ngFor="let d of week" class="owl-day" style="padding: 5px;"
+ [ngClass]="{
+ 'owl-calendar-invalid': !isValidDay(d.date),
+ 'owl-calendar-outFocus': d.otherMonth,
+ 'owl-calendar-hidden': d.hide,
+ 'owl-day-today': d.today
+ }" (click)="selectDate($event, d.date)">
+ <div style="height:32px;" class="owl-day day" [ngClass]=" {'owl-calendar-selected': isSelectedDay(d.date), 'owl-calendar-invalid': !isValidDay(d.date)}">
+ <a style="line-height:32px;">{{d.num}}</a>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="owl-calendar" [hidden]="dialogType !== 2">
+ <table class="owl-calendar-month">
+ <tbody>
+ <tr class="owl-months" *ngFor="let months of calendarMonths; let i = index">
+ <td *ngFor="let month of months; let j = index" class="owl-month"
+ (click)="selectMonth(i*3 + j)">
+ <div class="owl-month" [ngClass]="{'owl-calendar-div-selected': isCurrentMonth(i*3 + j),'owl-calendar-month-part':true,'owl-calendar-month-selected': isCurrentMonth(i*3 + j)}">
+ <a>{{month}}</a>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="owl-calendar" [hidden]="dialogType !== 3">
+ <table class="owl-calendar-year">
+ <tbody>
+ <tr class="owl-years" *ngFor="let years of calendarYears">
+ <td class="owl-year" *ngFor="let year of years"
+ (click)="selectYear(+year)">
+ <div [ngClass]="{'owl-calendar-year-part':true,'owl-calendar-year-selected': isCurrentYear(year)}">
+ <a>{{year}}</a>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <!--
+ <div class="owl-calendar-yearArrow left" style="left: 12px;
+ font-size: 20px;
+ margin-top: -116px;" (click)="generateYearList('prev')">
+ <i style="padding:8px" class="fa fa-angle-left"></i>
+ </div>
+ <div class="owl-calendar-yearArrow right" style="left: 261px;
+ font-size: 20px;
+ margin-top: -116px;" (click)="generateYearList('next')">
+ <i style="padding:8px" class="fa fa-angle-right"></i>
+ </div>
+ -->
+ </div>
+ </div>
+ <div *ngIf="type ==='both' || type === 'timer'" [hidden]="dialogType !== 1" >
+ <div style="height: 35px; padding: 8px;margin-bottom: 20px;">
+ <oes-timepickerr #timepicker [max]="_max" [min]="_min" class="time-picker" (TimerChange)="TimerChange($event)" [showSecondsTimer]="showSeconds" [(ngModel)]="mtime" [size]="'small'" [seconds]="seconds"></oes-timepickerr>
+ <div class="confirm-btn-div" style=" float: right; margin-top: -35px;">
+ <button class="plx-btn plx-btn-primary plx-btn-xs" (click)="confirm()" type="button"> {{locale.confirm}} </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.less b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.less
new file mode 100644
index 00000000..8e50660b
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.less
@@ -0,0 +1,434 @@
+@import "../../assets/components/themes/default/theme.less";
+@import "../../assets/components/themes/common/plx-input.less";
+@import "../../assets/components/themes/common/plx-button.less";
+
+.owl-dateTime {
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ font-family: @font-family;
+ font-size: @font-size;
+ background: @component-bg;
+ color: @text-color;
+}
+
+.owl-dateTime input {
+ .plx-input;
+}
+
+.owl-dateTime input:-ms-input-placeholder {
+ color: @unselected-text-color !important;
+}
+.owl-dateTime input::-webkit-input-placeholder {
+ color: @unselected-text-color !important;
+}
+
+.owl-dateTime-input {
+ width: 100%;
+ padding-right: 1.5em; }
+
+.owl-dateTime-cancel {
+ position: absolute;
+ top: 50%;
+ right: .1em;
+ border-radius: 50%;
+ transform: translateY(-50%);
+ cursor: pointer;
+ color: inherit; }
+
+.owl-dateTime-inputWrapper {
+ position: relative; }
+
+.owl-dateTime-customTemp {
+ display: inline-block;
+ position: relative; }
+
+.owl-dateTime-dialog {
+ padding: 0px;
+ position: absolute; }
+
+.owl-dateTime-dialogHeader {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%; }
+
+.owl-calendar-wrapper,
+.owl-timer-wrapper {
+ position: relative;
+ width: 100%;
+ padding: .2em .5em; }
+
+.owl-calendar-control {
+ display: flex;
+ justify-content: space-around;
+ width: 100%;
+ height: 2em; }
+ .owl-calendar-control .owl-calendar-controlNav {
+ position: relative;
+ cursor: pointer;
+ width: 12.5%; }
+ .owl-calendar-control .owl-calendar-controlContent {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 75%;
+ height: 100%; }
+
+.owl-calendar {
+ position: relative;
+ min-height: 13.7em; }
+ .owl-calendar table {
+ width: 100%;
+ border-collapse: collapse; }
+ .owl-calendar tbody td {
+ position: relative;
+ text-align: center; }
+ .owl-calendar tbody td a {
+ display: block;
+ width: 100%;
+ height: 100%;
+ text-decoration: none;
+ color: inherit;
+ font-size:12px;
+ }
+ .owl-calendar .owl-calendar-yearArrow {
+ position: absolute;
+ top: 50%;
+ width: 1.5em;
+ height: 1.5em;
+ transform: translateY(-50%);
+ cursor: pointer; }
+ .owl-calendar .owl-calendar-yearArrow.left {
+ left: -.5em; }
+ .owl-calendar .owl-calendar-yearArrow.right {
+ right: -.5em; }
+
+.owl-timer-wrapper {
+ position: relative;
+ display: flex;
+ justify-content: center; }
+ .owl-timer-wrapper .owl-timer {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 25%;
+ height: 100%; }
+ .owl-timer-wrapper .owl-timer-control {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 30%;
+ width: 100%;
+ cursor: pointer; }
+ .owl-timer-wrapper .owl-timer-control .icon:before {
+ margin: 0; }
+ .owl-timer-wrapper .owl-timer-input {
+ width: 60%;
+ height: 100%; }
+
+/*# sourceMappingURL=picker.component.css.map */
+.font-face {
+ font-weight: normal;
+ font-style: normal; }
+
+[class^="paletx-datepicker-icon-"]:before, [class*="paletx-datepicker-icon-"]:before {
+ font-family: "fontello";
+ font-style: normal;
+ font-weight: normal;
+ speak: none;
+ display: inline-block;
+ text-decoration: inherit;
+ width: 1em;
+ margin-right: .2em;
+ text-align: center;
+ /* opacity: .8; */
+ /* For safety - reset parent styles, that can break glyph codes*/
+ font-variant: normal;
+ text-transform: none;
+ /* fix buttons height, for twitter bootstrap */
+ line-height: 1em;
+ /* Animation center compensation - margins should be symmetric */
+ /* remove if not needed */
+ margin-left: .2em;
+ /* you can be more comfortable with increased icons size */
+ /* font-size: 120%; */
+ /* Font smoothing. That was taken from TWBS */
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ /* Uncomment for 3D effect */
+ /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ }
+
+.paletx-datepicker-icon-cancel:before {
+ content: '\e802'; }
+
+/* '' */
+.paletx-datepicker-icon-up-open:before {
+ content: '\e805'; }
+
+/* '' */
+.paletx-datepicker-icon-down-open:before {
+ content: '\e80b'; }
+
+/* '' */
+.paletx-datepicker-icon-left-open:before {
+ content: '\e817'; }
+
+/* '' */
+.paletx-datepicker-icon-right-open:before {
+ content: '\e818'; }
+
+/* '' */
+.owl-widget,
+.owl-widget * {
+ box-sizing: border-box; }
+
+.owl-widget {
+ font-size: 1em; }
+.owl-padding{
+ padding: 0px;
+}
+.owl-corner-all {
+ border-radius: 3px; }
+
+.owl-corner-top {
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px; }
+
+.owl-state-default {
+ border: 1px solid @border-color-base;
+ background: @component-bg;
+ color: @text-color; }
+
+.owl-inputtext {
+ margin: 0;
+ outline: medium none;
+ transition: .2s; }
+
+
+ .owl-dateTime.owl-dateTime-inline {
+ width: auto; }
+ .owl-dateTime.owl-dateTime-inline .owl-dateTime-dialog {
+ position: relative;
+ z-index: auto; }
+
+.owl-dateTime-dialog {
+ width: 300px;
+ user-select: none;
+ z-index: 99999; }
+
+.owl-dateTime-dialogHeader {
+ height: 2.5em;
+ padding: .25em;
+ background-color: @component-bg;
+ overflow-y: auto; }
+
+.owl-calendar-control .owl-calendar-controlNav .nav-prev,
+.owl-calendar-control .owl-calendar-controlNav .nav-next {
+ position: absolute;
+ top: 50%;
+ right: auto;
+ bottom: auto;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.owl-cal-header{
+ background: @selected-bg-color;
+ //color: @form-label;
+ height: 35px;
+ //width: 105%;
+ //margin-left: -7px;
+}
+ .owl-calendar-control .owl-calendar-controlNav .nav-prev:before,
+ .owl-calendar-control .owl-calendar-controlNav .nav-next:before {
+ //content: "";
+ border-top: .5em solid transparent;
+ border-bottom: .5em solid transparent;
+ border-right: 0.75em solid #000000;
+ width: 0;
+ height: 0;
+ display: block;
+ margin: 0 auto; }
+.owl-calendar-control .owl-calendar-controlNav .nav-next:before {
+ border-right: 0;
+ border-left: 0.75em solid #000000; }
+.owl-calendar-control .owl-calendar-controlContent .month-control,
+.owl-calendar-control .owl-calendar-controlContent .year-control {
+ color: @unselected-text-color;
+ display: inline-block;
+ cursor: pointer;
+ transition: transform 200ms ease; }
+ .owl-calendar-control .owl-calendar-controlContent .month-control:hover,
+ .owl-calendar-control .owl-calendar-controlContent .year-control:hover {
+ // transform: scale(1.2); }
+ color: @guide-color; }
+.owl-calendar-control .owl-calendar-controlContent .month-control {
+ font-size: @font-size-title-group;
+ margin-right: .8rem;
+}
+.owl-calendar-control .owl-calendar-controlContent .year-control {
+ font-size: @font-size-title-group;
+}
+
+.owl-calendar tbody td.owl-calendar-selected {
+ background-color: @guide-color;
+ color: @component-bg }
+.owl-calendar tbody td.owl-calendar-invalid {
+ color: @disabled-text-color }
+.owl-calendar tbody td.owl-calendar-outFocus {
+ color: @unselected-text-color; }
+.owl-calendar tbody td.owl-calendar-hidden {
+ visibility: hidden; }
+ /**
+.owl-calendar tbody td:not(.owl-calendar-selected):not(.owl-calendar-invalid):hover {
+ background-color: @hover-bg-color;
+ color: @shadow-color }
+**/
+.owl-years td.owl-year,
+.owl-years td.owl-month,
+.owl-months td.owl-year,
+.owl-months td.owl-month {
+ font-size: 1.2em;
+ height: 2.5em;
+ width: 33.33%;
+ line-height: 2.5em;
+ border-radius: 60px;
+ }
+
+.owl-weekdays th.owl-weekday {
+ height: 1em;
+ line-height: 2em;
+ text-align: center;
+ font-weight: normal;
+ font-size: @font-size;
+ /**color: @unselected-text-color; **/
+ }
+
+.owl-days td.owl-day {
+ border-radius: 30px;
+ height: 2em;
+ width: calc(100% / 7);
+ line-height: 2em; }
+ .owl-days td.owl-day.owl-day-today:before {
+ content: '';
+ display: block;
+ position: absolute;
+ right: 2px;
+ top: 2px;
+ color: @primary-color;
+ border-top: 0.5em solid @primary-color-hover;
+ border-left: .5em solid transparent;
+ }
+
+.owl-timer-wrapper {
+ height: 5.4em;
+ background-color: @shadow-color; }
+ .owl-timer-wrapper .owl-timer-text {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 40%;
+ font-size: 1.5em; }
+ .owl-timer-wrapper .owl-meridian-btn {
+ font-size: .8em;
+ color: @guide-color;
+ background-image: none;
+ background-color: transparent;
+ border-color: @guide-color; }
+ .owl-timer-wrapper .owl-meridian-btn:hover {
+ color: @scene-textcolor;
+ background-color: @guide-color;
+ border-color: @guide-color; }
+
+.owl-timer-divider {
+ display: inline-block;
+ align-self: flex-end;
+ position: absolute;
+ width: .6em;
+ height: 100%;
+ left: -.3em; }
+ .owl-timer-divider .owl-timer-dot {
+ display: block;
+ width: .3em;
+ height: .3em;
+ position: absolute;
+ left: 50%;
+ border-radius: 50%;
+ transform: translateX(-50%); }
+ .owl-timer-divider .owl-timer-dot.dot-top {
+ top: 38%; }
+ .owl-timer-divider .owl-timer-dot.dot-bottom {
+ bottom: 38%; }
+.owl-icon{
+ position: absolute;
+ top: 50%;
+ right: .1em;
+ border-radius: 50%;
+ -webkit-transform: translateY(-50%);
+ transform: translateY(-50%);
+ cursor: pointer;
+ color: @fonticon-color;
+}
+
+.oes-time-control{
+ color: @text-color !important;
+}
+.owl-calendar-selected {
+ background-color: @guide-color;
+ color: #fff;
+ border-radius: 50%;
+}
+.owl-calendar tbody td div.day:not(.owl-calendar-selected):not(.owl-calendar-invalid):hover {
+ background-color: @hover-bg-color;
+ color:#000;
+ border-radius: 50%; }
+.oes-time-control{
+ font-size: @font-size;
+}
+.owl-calendar-year-part{
+ width: 42px;
+ margin-left: 30px;
+ text-align: center;
+}
+.owl-calendar-year-part:hover{
+ background-color: @hover-bg-color;
+ color:#000;
+ border-radius: 50%;
+}
+.owl-calendar-year-selected{
+ background-color: @guide-color;
+ color: #fff;
+ border-radius: 50%;
+}
+.owl-calendar-year-selected:hover{
+ background-color: @guide-color;
+ color: #fff;
+ border-radius: 50%;
+}
+.owl-calendar-month-part{
+ width: 42px;
+ margin-left: 30px;
+ text-align: center;
+}
+.owl-calendar-month-part:hover{
+ background-color: @hover-bg-color;
+ color:#000;
+ border-radius: 50%;
+}
+.owl-calendar-month-selected{
+ background-color: @guide-color;
+ color: #fff;
+ border-radius: 50%;
+}
+.owl-calendar-month-selected:hover{
+ background-color: @guide-color;
+ color: #fff;
+ border-radius: 50%;
+}
+
+/*# sourceMappingURL=picker.css.map */
+
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.ts
new file mode 100644
index 00000000..493e0cb2
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.ts
@@ -0,0 +1,1712 @@
+/**
+ * picker.component
+ */
+
+import {
+ AfterViewInit,
+ Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output, Renderer2,
+ ViewChild
+} from '@angular/core';
+import { animate, state, style, transition, trigger } from '@angular/animations';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import {
+ parse,
+ isValid,
+ startOfMonth,
+ getDate,
+ getDay,
+ addDays,
+ addMonths,
+ isSameDay,
+ isSameMonth,
+ isToday,
+ getMonth,
+ setMonth,
+ getYear,
+ addYears,
+ differenceInCalendarDays,
+ setYear,
+ getHours,
+ setHours,
+ getMinutes,
+ setMinutes,
+ getSeconds,
+ setSeconds,
+ isBefore,
+ isAfter,
+ compareAsc,
+ startOfDay,
+ format,
+ endOfDay,
+} from 'date-fns';
+import { NumberFixedLenPipe } from './numberedFixLen.pipe';
+
+export interface LocaleSettings {
+ firstDayOfWeek?: number;
+ dayNames: string[];
+ dayNamesShort: string[];
+ monthNames: string[];
+ monthNamesShort: string[];
+ dateFns: any;
+ confirm: string;
+}
+
+export enum DialogType {
+ Time,
+ Date,
+ Month,
+ Year,
+}
+
+export const DATETIMEPICKER_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => DateTimePickerComponent),
+ multi: true
+};
+
+@Component({
+ selector: 'plx-datepicker',
+ templateUrl: './picker.component.html',
+ styleUrls: ['./picker.component.less'],
+ providers: [NumberFixedLenPipe, DATETIMEPICKER_VALUE_ACCESSOR],
+ animations: [
+ trigger('fadeInOut', [
+ state('hidden', style({
+ opacity: 0,
+ display: 'none'
+ })),
+ state('visible', style({
+ opacity: 1,
+ display: 'block'
+ })),
+ transition('visible => hidden', animate('200ms ease-in')),
+ transition('hidden => visible', animate('400ms ease-out'))
+ ])
+ ],
+})
+
+export class DateTimePickerComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor {
+
+ @ViewChild('timepicker') public timepicker;
+
+ /**
+ * Type of the value to write back to ngModel
+ * @default 'date' -- Javascript Date type
+ * @type {'string' | 'date'}
+ * */
+ @Input() dataType: 'string' | 'date' = 'date';
+
+ /**
+ * Format of the date
+ * @default 'y/MM/dd'
+ * @type {String}
+ * */
+ @Input() dateFormat: string = 'YYYY-MM-DD HH:mm';
+ /**
+ * When present, it specifies that the component should be disabled
+ * @default false
+ * @type {Boolean}
+ * */
+ @Input() disabled: boolean;
+ /**
+ * @default false
+ * @type {Boolean}
+ * */
+ @Input() supportKeyboardInput: boolean = false;
+ /**
+ * Array with dates that should be disabled (not selectable)
+ * @default null
+ * @type {Date[]}
+ * */
+ @Input() disabledDates: Date[] = [];
+
+ /**
+ * Array with weekday numbers that should be disabled (not selectable)
+ * @default null
+ * @type {number[]}
+ * */
+ @Input() disabledDays: number[];
+
+ /**
+ * When enabled, displays the calendar as inline
+ * @default false -- popup mode
+ * @type {boolean}
+ * */
+ @Input() inline: boolean;
+
+ /**
+ * Identifier of the focus input to match a label defined for the component
+ * @default null
+ * @type {String}
+ * */
+ @Input() inputId: string;
+
+ /**
+ * Inline style of the picker text input
+ * @default null
+ * @type {any}
+ * */
+ @Input() inputStyle: any;
+
+ /**
+ * Style class of the picker text input
+ * @default null
+ * @type {String}
+ * */
+ @Input() inputStyleClass: string;
+
+ /**
+ * Maximum number of selectable dates in multiple mode
+ * @default null
+ * @type {number}
+ * */
+ @Input() maxDateCount: number;
+
+ /**
+ * The minimum selectable date time
+ * @default null
+ * @type {Date | string}
+ * */
+ private _max: Date;
+ @Input()
+ get max() {
+ return this._max;
+ }
+
+ set max(val: Date | string) {
+ this._max = this.parseToDate(val);
+ }
+
+ @Input()
+ get maxDate() {
+ return this._max;
+ }
+
+ set maxDate(val: Date | string) {
+ this._max = this.parseToDate(val);
+ }
+
+ /**
+ * The maximum selectable date time
+ * @default null
+ * @type {Date | string }
+ * */
+ private _min: Date;
+ @Input()
+ get min() {
+ return this._min;
+ }
+
+ set min(val: Date | string) {
+ this._min = this.parseToDate(val);
+ }
+ @Input()
+ get minDate() {
+ return this._min;
+ }
+
+ set minDate(val: Date | string) {
+ this._min = this.parseToDate(val);
+ }
+
+ @Input()
+ get dateValue() {
+ return this.value;
+ }
+
+ set dateValue(val: Date | string) {
+ let newvalue = this.parseToDate(val);
+ if(newvalue!==undefined)
+ {
+ this.updateModel(newvalue);
+ this.updateCalendar(newvalue);
+ this.updateTimer(newvalue);
+ this.updateFormattedValue();
+ }
+ }
+
+
+ /**
+ * Picker input placeholder value
+ * @default
+ * @type {String}
+ * */
+ @Input() placeHolder: string = 'yyyy-mm-dd hh:mm';
+
+ /**
+ * When present, it specifies that an input field must be filled out before submitting the form
+ * @default false
+ * @type {Boolean}
+ * */
+ @Input() required: boolean;
+
+ /**
+ * Defines the quantity of the selection
+ * 'single' -- allow only a date value to be selected
+ * 'multiple' -- allow multiple date value to be selected
+ * 'range' -- allow to select an range ot date values
+ * @default 'single'
+ * @type {string}
+ * */
+ @Input() selectionMode: 'single' | 'multiple' | 'range' = 'single';
+
+ /**
+ * Whether to show the picker dialog header
+ * @default false
+ * @type {Boolean}
+ * */
+ @Input() showHeader: boolean;
+
+ @Input() canClear: boolean = true;
+
+ /**
+ * Whether to show the second's timer
+ * @default false
+ * @type {Boolean}
+ * */
+ @Input() showSeconds: boolean;
+
+ /**
+ * Inline style of the element
+ * @default null
+ * @type {any}
+ * */
+ @Input() style: any;
+
+ /**
+ * Style class of the element
+ * @default null
+ * @type {String}
+ * */
+ @Input() styleClass: string;
+
+ /**
+ * Index of the element in tabbing order
+ * @default null
+ * @type {Number}
+ * */
+ @Input() tabIndex: number;
+
+ /**
+ * Set the type of the dateTime picker
+ * 'both' -- show both calendar and timer
+ * 'calendar' -- show only calendar
+ * 'timer' -- show only timer
+ * @default 'both'
+ * @type {'both' | 'calendar' | 'timer'}
+ * */
+ @Input() type: 'both' | 'calendar' | 'timer' = 'calendar';
+
+ //附加方法
+ @Input()
+ set timeOnly(value: boolean) {
+ if (value) {
+ this.type = 'timer';
+ }
+ else {
+ this.type = "both";
+ }
+ }
+
+ @Input()
+ set showTime(value: boolean) {
+ if (value) {
+ this.type = 'both';
+ }
+ else {
+ this.type = "calendar";
+ }
+ }
+
+ /**
+ * An object having regional configuration properties for the dateTimePicker
+ * */
+ @Input()
+ get locale(): any {
+ return this._locale;
+ }
+ set locale(val: any) {
+ if (val !== undefined) {
+ this._locale = val;
+ this._userlocale = true;
+ }
+ }
+
+ @Input() localePrefab: 'Zh' | 'En' = 'En';
+ /**
+ * Determine the hour format (12 or 24)
+ * @default '24'
+ * @type {'24'| '12'}
+ * */
+ @Input() hourFormat: '12' | '24' = '24';
+
+
+ /**
+ * When it is set to false, only show current month's days in calendar
+ * @default true
+ * @type {boolean}
+ * */
+ @Input() showOtherMonths: boolean = true;
+
+ /**
+ * Callback to invoke when dropdown gets focus.
+ * */
+ @Output() onFocus: any = new EventEmitter<any>();
+
+ /**
+ * Callback to invoke when dropdown gets focus.
+ * */
+ @Output() onConfirm: any = new EventEmitter<any>();
+
+ /**
+ * Callback to invoke when dropdown loses focus.
+ * */
+ @Output() onBlur: any = new EventEmitter<any>();
+
+ /**
+ * Callback to invoke when a invalid date is selected.
+ * */
+ @Output() onInvalid: any = new EventEmitter<any>();
+
+
+
+ @ViewChild('container') containerElm: ElementRef;
+ @ViewChild('textInput') textInputElm: ElementRef;
+ @ViewChild('dialog') dialogElm: ElementRef;
+
+ public calendarDays: Array<any[]>;
+ public calendarWeekdays: string[];
+ public calendarMonths: Array<string[]>;
+ public calendarYears: Array<string[]> = [];
+ public dialogType: DialogType = DialogType.Date;
+ public dialogVisible: boolean;
+ public focus: boolean;
+ public formattedValue: string = '';
+ public value: any;
+ public pickerMoment: Date;
+ public pickerMonth: string;
+ public pickerYear: string;
+
+ public hourValue: number;
+ public minValue: number;
+ public secValue: number;
+ public meridianValue: string = 'AM';
+ private _userlocale: boolean = false;
+ private _locale: LocaleSettings = {
+ firstDayOfWeek: 0,
+ dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ dayNamesShort: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
+ //dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ dateFns: null,
+ confirm: 'OK'
+ };
+ private _localeEn: LocaleSettings = {
+ firstDayOfWeek: 0,
+ dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ dayNamesShort: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
+ monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ dateFns: null,
+ confirm: 'OK'
+ };
+ private _localeZh: LocaleSettings = {
+ firstDayOfWeek: 0,
+ dayNames: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
+ dayNamesShort: ['日', '一', '二', '三', '四', '五', '六'],
+ monthNames: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
+ monthNamesShort: ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
+ dateFns: null,
+ confirm: '确定'
+ };
+ private dialogClick: boolean;
+ private documentClickListener: Function;
+ private valueIndex: number = 0;
+ private onModelChange: Function = () => {
+ //
+ }
+ private onModelTouched: Function = () => {
+ //
+ }
+
+ constructor(private renderer: Renderer2,
+ private numFixedLenPipe: NumberFixedLenPipe) {
+ }
+ private updateDate(newvalue : Date)
+ {
+ if(newvalue!==undefined&&newvalue!==null)
+ {
+ if(this.min)
+ {
+ newvalue = this._min.getTime()<=newvalue.getTime()?newvalue:new Date(this._min);
+ }
+ if(this.max)
+ {
+ newvalue = this._max.getTime()>=newvalue.getTime()?newvalue:new Date(this._max);
+ }
+ this.updateModel(newvalue);
+ this.updateCalendar(newvalue);
+ this.updateTimer(newvalue);
+ this.updateFormattedValue();
+ return;
+ }
+ }
+ public onInputChange(event:any): void {
+ let newvalue = this.parseToDate(event.target.value);
+ if(newvalue!==undefined&&newvalue!==null)
+ {
+ if(this.min)
+ {
+ newvalue = this._min.getTime()<=newvalue.getTime()?newvalue:new Date(this._min);
+ }
+ if(this.max)
+ {
+ newvalue = this._max.getTime()>=newvalue.getTime()?newvalue:new Date(this._max);
+ }
+ this.updateModel(newvalue);
+ this.updateCalendar(newvalue);
+ this.updateTimer(newvalue);
+ this.updateFormattedValue();
+ return;
+ }
+ this.updateModel(null);
+ this.updateCalendar(null);
+ this.updateTimer(null);
+ this.updateFormattedValue();
+ }
+ public ngOnInit(): void {
+
+ if ((!this._userlocale) || this.locale === null && this.locale === undefined) {
+ switch (this.localePrefab) {
+
+ case 'Zh': {
+ this._locale = this._localeZh;
+ break;
+ }
+ case 'En': {
+ this._locale = this._localeEn;
+ break;
+ }
+ default:
+ {
+ this._locale = this._localeEn;
+ break;
+ }
+ }
+ }
+ this.pickerMoment = new Date();
+
+ if (this.type === 'both' || this.type === 'calendar') {
+ this.generateWeekDays();
+ this.generateMonthList();
+ }
+ this.updateTimer(this.value);
+ }
+
+ public ngAfterViewInit(): void {
+ this.updateCalendar(this.value);
+ this.updateTimer(this.value);
+ }
+
+ public ngOnDestroy(): void {
+ this.unbindDocumentClickListener();
+ }
+
+ public writeValue(obj: any): void {
+
+ if (obj instanceof Array) {
+ this.value = [];
+ for (let o of obj) {
+ let v = this.parseToDate(o);
+ this.value.push(v);
+ }
+ this.updateCalendar(this.value[0]);
+ this.updateTimer(this.value[0]);
+ } else {
+ this.value = this.parseToDate(obj);
+ this.updateCalendar(this.value);
+ this.updateTimer(this.value);
+ }
+ this.updateFormattedValue();
+ }
+
+ public registerOnChange(fn: any): void {
+ this.onModelChange = fn;
+ }
+
+ public registerOnTouched(fn: any): void {
+ this.onModelTouched = fn;
+ }
+
+ public setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+
+
+ private initflag = true;
+ /**
+ * Handle click event on the text input
+ * @param {any} event
+ * @return {void}
+ * */
+ public onInputClick(event: any): void {
+
+ if (this.timepicker !== undefined && this.initflag) {
+ this.initflag = false;
+ if (this.value !== undefined && this.value !== null) {
+ this.timepicker.updateHour(this.value.getHours());
+ this.timepicker.updateMinute(this.value.getMinutes());
+ this.timepicker.updateSecond(this.value.getSeconds());
+ }
+ else {
+ this.timepicker.updateHour(0);
+ this.timepicker.updateMinute(0);
+ this.timepicker.updateSecond(0);
+ this.updateModel(null);
+ this.updateFormattedValue();
+ }
+ }
+ if (this.disabled) {
+ event.preventDefault();
+ return;
+ }
+
+ this.dialogClick = true;
+ if (!this.dialogVisible) {
+ this.show();
+ }
+ event.preventDefault();
+ return;
+ }
+
+ /**
+ * Set the element on focus
+ * @param {any} event
+ * @return {void}
+ * */
+ public onInputFocus(event: any): void {
+ this.focus = true;
+ this.onFocus.emit(event);
+ event.preventDefault();
+ return;
+ }
+
+ /**
+ * Set the element on blur
+ * @param {any} event
+ * @return {void}
+ * */
+ public onInputBlur(event: any): void {
+ this.focus = false;
+ this.onModelTouched();
+ this.onBlur.emit(event);
+ event.preventDefault();
+ return;
+ }
+
+ /**
+ * Handle click event on the dialog
+ * @param {any} event
+ * @return {void}
+ * */
+ public onDialogClick(event: any): void {
+ this.dialogClick = true;
+ }
+
+ /**
+ * Go to previous month
+ * @param {any} event
+ * @return {void}
+ * */
+ public prevMonth(event: any): void {
+
+ if (this.disabled) {
+ event.preventDefault();
+ return;
+ }
+
+ this.pickerMoment = addMonths(this.pickerMoment, -1);
+ this.generateCalendar();
+ if(this.value!==undefined&&this.value!==null)
+ {
+ let nowvalue = new Date(this.value);
+ nowvalue.setMonth(this.pickerMoment.getMonth());
+ this.updateDate(nowvalue);
+ }
+ event.preventDefault();
+ return;
+ }
+
+ /**
+ * Go to next month
+ * @param {any} event
+ * @return {void}
+ * */
+ public nextMonth(event: any): void {
+
+ if (this.disabled) {
+ event.preventDefault();
+ return;
+ }
+
+ this.pickerMoment = addMonths(this.pickerMoment, 1);
+ this.generateCalendar();
+ if(this.value!==undefined&&this.value!==null)
+ {
+ let nowvalue = new Date(this.value);
+ nowvalue.setMonth(this.pickerMoment.getMonth());
+ this.updateDate(nowvalue);
+ }
+ event.preventDefault();
+ return;
+ }
+
+ /**
+ * Select a date
+ * @param {any} event
+ * @param {Date} date
+ * @return {void}
+ * */
+ public selectDate(event: any, date: Date): void {
+
+ if (this.disabled || !date) {
+ event.preventDefault();
+ return;
+ }
+
+ let temp: Date;
+ // check if the selected date is valid
+ if (this.isValidValue(date)) {
+ temp = date;
+ } else {
+ if (isSameDay(date, this._min)) {
+ temp = new Date(this._min);
+ } else if (isSameDay(date, this._max)) {
+ temp = new Date(this._max);
+ } else {
+ this.onInvalid.emit({ originalEvent: event, value: date });
+ return;
+ }
+ }
+ if (this.minValue !== undefined) {
+ temp.setMinutes(this.minValue);
+ }
+ if (this.secValue !== undefined) {
+ temp.setSeconds(this.secValue);
+ }
+ if (this.hourValue !== undefined) {
+ temp.setHours(this.hourValue);
+ }
+ let selected;
+ if (this.isSingleSelection()) {
+ if (!isSameDay(this.value, temp)) {
+ selected = temp;
+ }
+ } else if (this.isRangeSelection()) {
+ if (this.value && this.value.length) {
+ let startDate = this.value[0];
+ let endDate = this.value[1];
+
+ if (!endDate && temp.getTime() > startDate.getTime()) {
+ endDate = temp;
+ this.valueIndex = 1;
+ } else {
+ startDate = temp;
+ endDate = null;
+ this.valueIndex = 0;
+ }
+ selected = [startDate, endDate];
+ } else {
+ selected = [temp, null];
+ this.valueIndex = 0;
+ }
+ } else if (this.isMultiSelection()) {
+
+ // check if it exceeds the maxDateCount limit
+ if (this.maxDateCount && this.value &&
+ this.value.length && this.value.length >= this.maxDateCount) {
+ this.onInvalid.emit({ originalEvent: event, value: 'Exceed max date count.' });
+ return;
+ }
+
+ if (this.isSelectedDay(temp)) {
+ selected = this.value.filter((d: Date) => {
+ return !isSameDay(d, temp);
+ });
+ } else {
+ selected = this.value ? [...this.value, temp] : [temp];
+ this.valueIndex = selected.length - 1;
+ }
+ }
+ if (selected) {
+ this.updateModel(selected);
+ if (this.value instanceof Array) {
+ this.updateCalendar(this.value[this.valueIndex]);
+ this.updateTimer(this.value[this.valueIndex]);
+ } else {
+ this.updateCalendar(this.value);
+ this.updateTimer(this.value);
+ }
+ this.updateFormattedValue();
+ }
+ }
+
+ /**
+ * Set a pickerMoment's month
+ * @param {Number} monthNum
+ * @return {void}
+ * */
+ public selectMonth(monthNum: number): void {
+ this.pickerMoment = setMonth(this.pickerMoment, monthNum);
+ this.generateCalendar();
+ if(this.value!==undefined&&this.value!==null)
+ {
+ let nowvalue = new Date(this.value);
+ nowvalue.setMonth(monthNum);
+ this.updateDate(nowvalue);
+ }
+ this.changeDialogType(DialogType.Month);
+ }
+
+ /**
+ * Set a pickerMoment's year
+ * @param {Number} yearNum
+ * @return {void}
+ * */
+ public selectYear(yearNum: number): void {
+ this.pickerMoment = setYear(this.pickerMoment, yearNum);
+ this.generateCalendar();
+ if(this.value!==undefined&&this.value!==null)
+ {
+ let nowvalue = new Date(this.value);
+ nowvalue.setFullYear(yearNum);
+ this.updateDate(nowvalue);
+ }
+ this.changeDialogType(DialogType.Year);
+ }
+
+ /**
+ * Set the selected moment's meridian
+ * @param {any} event
+ * @return {void}
+ * */
+ public toggleMeridian(event: any): void {
+
+ let value = this.value ? (this.value.length ? this.value[this.valueIndex] : this.value) : null;
+
+ if (this.disabled) {
+ event.preventDefault();
+ return;
+ }
+
+ if (!value) {
+ this.meridianValue = this.meridianValue === 'AM' ? 'PM' : 'AM';
+ return;
+ }
+
+ let hours = getHours(value);
+ if (this.meridianValue === 'AM') {
+ hours += 12;
+ } else if (this.meridianValue === 'PM') {
+ hours -= 12;
+ }
+
+ let selectedTime = setHours(value, hours);
+ this.setSelectedTime(selectedTime);
+ event.preventDefault();
+ return;
+ }
+
+ /**
+ * Set the selected moment's hour
+ * @param {any} event
+ * @param {'increase' | 'decrease' | number} val
+ * 'increase' -- increase hour value by 1
+ * 'decrease' -- decrease hour value by 1
+ * number -- set hour value to val
+ * @param {HTMLInputElement} input -- optional
+ * @return {boolean}
+ * */
+ public setHours(event: any, val: 'increase' | 'decrease' | number, input?: HTMLInputElement): boolean {
+
+ let value;
+ if (this.value) {
+ if (this.value.length) {
+ value = this.value[this.valueIndex];
+ } else {
+ value = this.value;
+ }
+ } else {
+ if (this.type === 'timer') {
+ value = new Date();
+ } else {
+ value = null;
+ }
+ }
+
+ if (this.disabled || !value) {
+ event.preventDefault();
+ return false;
+ }
+
+ let hours = getHours(value);
+ if (val === 'increase') {
+ hours += 1;
+ } else if (val === 'decrease') {
+ hours -= 1;
+ } else {
+ hours = val;
+ }
+
+ if (hours > 23) {
+ hours = 0;
+ } else if (hours < 0) {
+ hours = 23;
+ }
+
+ let selectedTime = setHours(value, hours);
+ let done = this.setSelectedTime(selectedTime);
+
+ // Focus the input and select its value when model updated
+ if (input) {
+ setTimeout(() => {
+ input.focus();
+ }, 0);
+ }
+
+ event.preventDefault();
+ return done;
+ }
+
+ /**
+ * Set the selected moment's minute
+ * @param {any} event
+ * @param {'increase' | 'decrease' | number} val
+ * 'increase' -- increase minute value by 1
+ * 'decrease' -- decrease minute value by 1
+ * number -- set minute value to val
+ * @param {HTMLInputElement} input -- optional
+ * @return {boolean}
+ * */
+ public setMinutes(event: any, val: 'increase' | 'decrease' | number, input?: HTMLInputElement): boolean {
+
+ let value;
+ if (this.value) {
+ if (this.value.length) {
+ value = this.value[this.valueIndex];
+ } else {
+ value = this.value;
+ }
+ } else {
+ if (this.type === 'timer') {
+ value = new Date();
+ } else {
+ value = null;
+ }
+ }
+
+ if (this.disabled || !value) {
+ event.preventDefault();
+ return false;
+ }
+
+ let minutes = getMinutes(value);
+ if (val === 'increase') {
+ minutes += 1;
+ } else if (val === 'decrease') {
+ minutes -= 1;
+ } else {
+ minutes = val;
+ }
+
+ if (minutes > 59) {
+ minutes = 0;
+ } else if (minutes < 0) {
+ minutes = 59;
+ }
+
+ let selectedTime = setMinutes(value, minutes);
+ let done = this.setSelectedTime(selectedTime);
+
+ // Focus the input and select its value when model updated
+ if (input) {
+ setTimeout(() => {
+ input.focus();
+ }, 0);
+ }
+
+ event.preventDefault();
+ return done;
+ }
+
+ /**
+ * Set the selected moment's second
+ * @param {any} event
+ * @param {'increase' | 'decrease' | number} val
+ * 'increase' -- increase second value by 1
+ * 'decrease' -- decrease second value by 1
+ * number -- set second value to val
+ * @param {HTMLInputElement} input -- optional
+ * @return {boolean}
+ * */
+ public setSeconds(event: any, val: 'increase' | 'decrease' | number, input?: HTMLInputElement): boolean {
+
+ let value;
+ if (this.value) {
+ if (this.value.length) {
+ value = this.value[this.valueIndex];
+ } else {
+ value = this.value;
+ }
+ } else {
+ if (this.type === 'timer') {
+ value = new Date();
+ } else {
+ value = null;
+ }
+ }
+
+ if (this.disabled || !value) {
+ event.preventDefault();
+ return false;
+ }
+
+ let seconds = getSeconds(value);
+ if (val === 'increase') {
+ seconds = this.secValue + 1;
+ } else if (val === 'decrease') {
+ seconds = this.secValue - 1;
+ } else {
+ seconds = val;
+ }
+
+ if (seconds > 59) {
+ seconds = 0;
+ } else if (seconds < 0) {
+ seconds = 59;
+ }
+
+ let selectedTime = setSeconds(value, seconds);
+ let done = this.setSelectedTime(selectedTime);
+
+ // Focus the input and select its value when model updated
+ if (input) {
+ setTimeout(() => {
+ input.focus();
+ }, 0);
+ }
+
+ event.preventDefault();
+ return done;
+ }
+
+ /**
+ * Check if the date is selected
+ * @param {Date} date
+ * @return {Boolean}
+ * */
+ public isSelectedDay(date: Date): boolean {
+ if (this.isSingleSelection()) {
+ return this.value && isSameDay(this.value, date);
+ } else if (this.isRangeSelection() && this.value && this.value.length) {
+ if (this.value[1]) {
+ return (isSameDay(this.value[0], date) || isSameDay(this.value[1], date) ||
+ this.isDayBetween(this.value[0], this.value[1], date)) && this.isValidDay(date);
+ } else {
+ return isSameDay(this.value[0], date);
+ }
+ } else if (this.isMultiSelection() && this.value && this.value.length) {
+ let selected;
+ for (let d of this.value) {
+ selected = isSameDay(d, date);
+ if (selected) {
+ break;
+ }
+ }
+ return selected;
+ }
+ return false;
+ }
+
+ /**
+ * Check if a day is between two specific days
+ * @param {Date} start
+ * @param {Date} end
+ * @param {Date} day
+ * @return {boolean}
+ * */
+ public isDayBetween(start: Date, end: Date, day: Date): boolean {
+ if (start && end) {
+ return isAfter(day, start) && isBefore(day, end);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Check if the calendar day is a valid day
+ * @param {Date} date
+ * @return {Boolean}
+ * */
+ public isValidDay(date: Date): boolean {
+ let isValid = true;
+ if (this.disabledDates && this.disabledDates.length) {
+ for (let disabledDate of this.disabledDates) {
+ if (isSameDay(disabledDate, date)) {
+ return false;
+ }
+ }
+ }
+
+ if (isValid && this.disabledDays && this.disabledDays.length) {
+ let weekdayNum = getDay(date);
+ isValid = this.disabledDays.indexOf(weekdayNum) === -1;
+ }
+
+ if (isValid && this.min) {
+ isValid = isValid && !isBefore(date, startOfDay(this.min));
+ }
+
+ if (isValid && this.max) {
+ isValid = isValid && !isAfter(date, endOfDay(this.max));
+ }
+ return isValid;
+ }
+
+ /**
+ * Check if the month is current pickerMoment's month
+ * @param {Number} monthNum
+ * @return {Boolean}
+ * */
+ public isCurrentMonth(monthNum: number): boolean {
+ return getMonth(this.pickerMoment) === monthNum;
+ }
+
+ /**
+ * Check if the year is current pickerMoment's year
+ * @param {Number} yearNum
+ * @return {Boolean}
+ * */
+ public isCurrentYear(yearNum: any): boolean {
+ return getYear(this.pickerMoment) === yearNum||(getYear(this.pickerMoment)+"") === yearNum;
+ }
+
+ /**
+ * Change the dialog type
+ * @param {DialogType} type
+ * @return {void}
+ * */
+ public changeDialogType(type: DialogType): void {
+ if (this.dialogType === type) {
+ this.dialogType = DialogType.Date;
+ return;
+ } else {
+ this.dialogType = type;
+ }
+
+ if (this.dialogType === DialogType.Year) {
+ this.generateYearList();
+ }
+ }
+
+ /**
+ * Handle blur event on timer input
+ * @param {any} event
+ * @param {HTMLInputElement} input
+ * @param {string} type
+ * @param {number} modelValue
+ * @return {void}
+ * */
+ public onTimerInputBlur(event: any, input: HTMLInputElement, type: string, modelValue: number): void {
+ let val = +input.value;
+
+ if (this.disabled || val === modelValue) {
+ event.preventDefault();
+ return;
+ }
+
+ let done;
+ if (!isNaN(val)) {
+ switch (type) {
+ case 'hours':
+ if (this.hourFormat === '24' &&
+ val >= 0 && val <= 23) {
+ done = this.setHours(event, val);
+ } else if (this.hourFormat === '12'
+ && val >= 1 && val <= 12) {
+ if (this.meridianValue === 'AM' && val === 12) {
+ val = 0;
+ } else if (this.meridianValue === 'PM' && val < 12) {
+ val = val + 12;
+ }
+ done = this.setHours(event, val);
+ }
+ break;
+ case 'minutes':
+ if (val >= 0 && val <= 59) {
+ done = this.setMinutes(event, val);
+ }
+ break;
+ case 'seconds':
+ if (val >= 0 && val <= 59) {
+ done = this.setSeconds(event, val);
+ }
+ break;
+ }
+ }
+
+ if (!done) {
+ input.value = this.numFixedLenPipe.transform(modelValue, 2);
+ input.focus();
+ return;
+ }
+ event.preventDefault();
+ return;
+ }
+
+ /**
+ * Set value to null
+ * @param {any} event
+ * @return {void}
+ * */
+ public clearValue(event: any): void {
+ this.dialogClick = true;
+ this.updateModel(null);
+ this.updateTimer(this.value);
+ if (this.timepicker!==undefined) {
+ this.timepicker.settime(undefined);
+ }
+ this.updateFormattedValue();
+ if(this.value!==null)
+ {
+ event.date=new Date(this.value);
+ }
+ this.onConfirm.emit(event);
+ event.preventDefault();
+ }
+
+ /**
+ * Show the dialog
+ * @return {void}
+ * */
+ private show(): void {
+ this.alignDialog();
+ this.dialogVisible = true;
+ this.dialogType = DialogType.Date;
+ this.bindDocumentClickListener();
+ return;
+ }
+ private nextNav(event : any):void {
+ if( this.dialogType===DialogType.Date|| this.dialogType===DialogType.Month)
+ {
+ this.nextMonth(event);
+ }
+ else if(this.dialogType===DialogType.Year){
+ this.generateYearList('next');
+ }
+ }
+ private prevNav(event : any):void {
+ if( this.dialogType===DialogType.Date|| this.dialogType===DialogType.Month)
+ {
+ this.prevMonth(event);
+ }
+ else if(this.dialogType===DialogType.Year){
+ this.generateYearList('prev');
+ }
+ }
+ /**
+ * Hide the dialog
+ * @return {void}
+ * */
+ private hide(): void {
+ this.dialogVisible = false;
+ this.timepicker ? this.timepicker.closeProp() : 0;
+ this.unbindDocumentClickListener();
+ if(this.value!==null)
+ {
+ event["date"]=new Date(this.value);
+ }
+ this.onConfirm.emit(event);
+ return;
+ }
+
+ /**
+ * Set the dialog position
+ * @return {void}
+ * */
+ private alignDialog(): void {
+ let element = this.dialogElm.nativeElement;
+ let target = this.containerElm.nativeElement;
+ let elementDimensions = element.offsetParent ? {
+ width: element.offsetWidth,
+ height: element.offsetHeight
+ } : this.getHiddenElementDimensions(element);
+ let targetHeight = target.offsetHeight;
+ let targetWidth = target.offsetWidth;
+ let targetOffset = target.getBoundingClientRect();
+ let viewport = this.getViewport();
+ let top, left;
+
+ if ((targetOffset.top + targetHeight + elementDimensions.height) > viewport.height) {
+ top = -1 * (elementDimensions.height);
+ if (targetOffset.top + top < 0) {
+ top = 0;
+ }
+ } else {
+ top = targetHeight;
+ }
+
+
+ if ((targetOffset.left + elementDimensions.width) > viewport.width) {
+ left = targetWidth - elementDimensions.width;
+ } else {
+ left = 0;
+ }
+
+ element.style.top = top + 'px';
+ element.style.left = left + 'px';
+ }
+
+ /**
+ * Bind click event on document
+ * @return {void}
+ * */
+ private bindDocumentClickListener(): void {
+ let firstClick = true;
+ if (!this.documentClickListener) {
+ this.documentClickListener = this.renderer.listen('document', 'click', () => {
+ if (!firstClick && !this.dialogClick) {
+ this.hide();
+ }
+
+ firstClick = false;
+ this.dialogClick = false;
+ });
+ }
+ return;
+ }
+
+ /**
+ * Unbind click event on document
+ * @return {void}
+ * */
+ private unbindDocumentClickListener(): void {
+ if (this.documentClickListener) {
+ this.documentClickListener();
+ this.documentClickListener = null;
+ }
+ return;
+ }
+
+ /**
+ * Parse a object to Date object
+ * @param {any} val
+ * @return {Date}
+ * */
+ private parseToDate(val: any): Date {
+ if (!val) {
+ return;
+ }
+
+ let parsedVal;
+ if (typeof val === 'string') {
+ parsedVal = parse(val);
+ } else {
+ parsedVal = val;
+ }
+
+ return isValid(parsedVal) ? parsedVal : null;
+ }
+
+ /**
+ * Generate the calendar days array
+ * @return {void}
+ * */
+ private generateCalendar(): void {
+
+ if (!this.pickerMoment) {
+ return;
+ }
+
+ this.calendarDays = [];
+ let startDateOfMonth = startOfMonth(this.pickerMoment);
+ let startWeekdayOfMonth = getDay(startDateOfMonth);
+
+ let dayDiff = 0 - (startWeekdayOfMonth + (7 - this.locale.firstDayOfWeek)) % 7;
+
+ for (let i = 1; i < 7; i++) {
+ let week = [];
+ for (let j = 0; j < 7; j++) {
+ let date = addDays(startDateOfMonth, dayDiff);
+ let inOtherMonth = !isSameMonth(date, this.pickerMoment);
+ week.push({
+ date,
+ num: getDate(date),
+ today: isToday(date),
+ otherMonth: inOtherMonth,
+ hide: !this.showOtherMonths && inOtherMonth,
+ });
+ dayDiff += 1;
+ }
+ this.calendarDays.push(week);
+ }
+
+ this.pickerMonth = this.locale.monthNames[getMonth(this.pickerMoment)];
+ this.pickerYear = getYear(this.pickerMoment).toString();
+ }
+
+ /**
+ * Generate the calendar weekdays array
+ * */
+ private generateWeekDays(): void {
+ this.calendarWeekdays = [];
+ let dayIndex = this.locale.firstDayOfWeek;
+ for (let i = 0; i < 7; i++) {
+ this.calendarWeekdays.push(this.locale.dayNamesShort[dayIndex]);
+ dayIndex = (dayIndex === 6) ? 0 : ++dayIndex;
+ }
+ }
+
+ /**
+ * Generate the calendar month array
+ * @return {void}
+ * */
+ private generateMonthList(): void {
+ this.calendarMonths = [];
+ let monthIndex = 0;
+ for (let i = 0; i < 4; i++) {
+ let months = [];
+ for (let j = 0; j < 3; j++) {
+ months.push(this.locale.monthNamesShort[monthIndex]);
+ monthIndex += 1;
+ }
+ this.calendarMonths.push(months);
+ }
+ }
+
+ /**
+ * Generate the calendar year array
+ * @return {void}
+ * */
+ public generateYearList(dir?: string): void {
+
+ if (!this.pickerMoment) {
+ return;
+ }
+ let start;
+
+ if (dir === 'prev') {
+ start = +this.calendarYears[0][0] - 12;
+ if(start<0)
+ {
+ start=0;
+ }
+ } else if (dir === 'next') {
+ start = +this.calendarYears[3][2] + 1;
+ } else {
+ start = getYear(addYears(this.pickerMoment, -4));
+ }
+
+ for (let i = 0; i < 4; i++) {
+ let years = [];
+ for (let j = 0; j < 3; j++) {
+ let year = (start + i * 3 + j).toString();
+ years.push(year);
+ }
+ this.calendarYears[i] = years;
+ }
+ return;
+ }
+
+ /**
+ * Update the calendar
+ * @param {Date} value
+ * @return {void}
+ * */
+ private updateCalendar(value: Date): void {
+
+ // if the dateTime picker is only the timer,
+ // no need to update the update Calendar.
+ if (this.type === 'timer') {
+ return;
+ }
+
+ if (value && (!this.calendarDays || !isSameMonth(value, this.pickerMoment))) {
+ this.pickerMoment = setMonth(this.pickerMoment, getMonth(value));
+ this.pickerMoment = setYear(this.pickerMoment, getYear(value));
+ this.generateCalendar();
+ } else if (!value && !this.calendarDays) {
+ this.generateCalendar();
+ }
+ return;
+ }
+
+ /**
+ * Update the timer
+ * @param {Date} value
+ * @return {boolean}
+ * */
+ private updateTimer(value: Date): boolean {
+
+ // if the dateTime picker is only the calendar,
+ // no need to update the timer
+ if (this.type === 'calendar') {
+ return false;
+ }
+
+ if (!value) {
+ this.hourValue = null;
+ this.minValue = null;
+ this.secValue = null;
+ this.mtime.hour = 0;
+ this.mtime.minute = 0;
+ this.mtime.second = 0;
+ return true;
+ }
+ this.mtime.hour = value.getHours();
+ this.mtime.minute = value.getMinutes();
+ this.mtime.second = value.getSeconds();;
+
+ let time = value;
+ let hours = getHours(time);
+ if (this.hourFormat === '12') {
+ if (hours < 12 && hours > 0) {
+ this.hourValue = hours;
+ this.meridianValue = 'AM';
+ } else if (hours > 12) {
+ this.hourValue = hours - 12;
+ this.meridianValue = 'PM';
+ } else if (hours === 12) {
+ this.hourValue = 12;
+ this.meridianValue = 'PM';
+ } else if (hours === 0) {
+ this.hourValue = 12;
+ this.meridianValue = 'AM';
+ }
+ } else if (this.hourFormat === '24') {
+ this.hourValue = hours;
+ }
+
+ this.minValue = getMinutes(time);
+ this.secValue = getSeconds(time);
+ if(this.value!==undefined&&this.timepicker!==undefined)
+ {
+ this.timepicker.settime(new Date(this.value));
+ }
+ return true;
+ }
+
+ /**
+ * Update ngModel
+ * @param {Date} value
+ * @return {Boolean}
+ * */
+ private updateModel(value: Date | Date[]): boolean {
+ this.value = value;
+ if (this.dataType === 'date') {
+ this.onModelChange(this.value);
+ } else if (this.dataType === 'string') {
+ if (this.value && this.value.length) {
+ let formatted = [];
+ for (let v of this.value) {
+ if (v) {
+ formatted.push(format(v, this.dateFormat, { locale: this.locale.dateFns }));
+ } else {
+ formatted.push(null);
+ }
+ }
+ this.onModelChange(formatted);
+ } else {
+ this.onModelChange(format(this.value, this.dateFormat, { locale: this.locale.dateFns }));
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Update variable formattedValue
+ * @return {void}
+ * */
+ private updateFormattedValue(): void {
+ let formattedValue = '';
+
+ if (this.value) {
+ if (this.isSingleSelection()) {
+ formattedValue = format(this.value, this.dateFormat, { locale: this.locale.dateFns });
+ } else if (this.isRangeSelection()) {
+ let startDate = this.value[0];
+ let endDate = this.value[1];
+
+ formattedValue = format(startDate, this.dateFormat, { locale: this.locale.dateFns });
+
+ if (endDate) {
+ formattedValue += ' - ' + format(endDate, this.dateFormat, { locale: this.locale.dateFns });
+ } else {
+ formattedValue += ' - ' + this.dateFormat;
+ }
+ } else if (this.isMultiSelection()) {
+ for (let i = 0; i < this.value.length; i++) {
+ let dateAsString = format(this.value[i], this.dateFormat, { locale: this.locale.dateFns });
+ formattedValue += dateAsString;
+ if (i !== (this.value.length - 1)) {
+ formattedValue += ', ';
+ }
+ }
+ }
+ }
+
+ this.formattedValue = formattedValue;
+
+ return;
+ }
+
+ /**
+ * Set the time
+ * @param {Date} val
+ * @return {boolean}
+ * */
+ public setSelectedTime(val: Date): boolean {
+ let done;
+ if (this.isValidValue(val)) {
+ if (this.value instanceof Array) {
+ this.value[this.valueIndex] = val;
+ done = this.updateModel(this.value);
+ done = done && this.updateTimer(this.value[this.valueIndex]);
+ } else {
+ done = this.updateModel(val);
+ done = done && this.updateTimer(this.value);
+ }
+ this.updateFormattedValue();
+ } else {
+ this.onInvalid.emit({ originalEvent: event, value: val });
+ done = false;
+ }
+ return done;
+ }
+
+ private isValidValue(value: Date): boolean {
+ let isValid = true;
+
+ if (this.disabledDates && this.disabledDates.length) {
+ for (let disabledDate of this.disabledDates) {
+ if (isSameDay(disabledDate, value)) {
+ return false;
+ }
+ }
+ }
+
+ if (isValid && this.disabledDays && this.disabledDays.length) {
+ let weekdayNum = getDay(value);
+ isValid = this.disabledDays.indexOf(weekdayNum) === -1;
+ }
+
+ if (isValid && this.min) {
+ isValid = isValid && !isBefore(value, this.min);
+ }
+
+ if (isValid && this.max) {
+ isValid = isValid && !isAfter(value, this.max);
+ }
+
+ return isValid;
+ }
+
+ /**
+ * Check if the selection mode is 'single'
+ * @return {boolean}
+ * */
+ private isSingleSelection(): boolean {
+ return this.selectionMode === 'single';
+ }
+
+ /**
+ * Check if the selection mode is 'range'
+ * @return {boolean}
+ * */
+ private isRangeSelection(): boolean {
+ return this.selectionMode === 'range';
+ }
+
+ /**
+ * Check if the selection mode is 'multiple'
+ * @return {boolean}
+ * */
+ private isMultiSelection(): boolean {
+ return this.selectionMode === 'multiple';
+ }
+
+ private getHiddenElementDimensions(element: any): any {
+ let dimensions: any = {};
+ element.style.visibility = 'hidden';
+ element.style.display = 'block';
+ dimensions.width = element.offsetWidth;
+ dimensions.height = element.offsetHeight;
+ element.style.display = 'none';
+ element.style.visibility = 'visible';
+
+ return dimensions;
+ }
+
+ private getViewport(): any {
+ let win = window,
+ d = document,
+ e = d.documentElement,
+ g = d.getElementsByTagName('body')[0],
+ w = win.innerWidth || e.clientWidth || g.clientWidth,
+ h = win.innerHeight || e.clientHeight || g.clientHeight;
+
+ return { width: w, height: h };
+ }
+ public confirm() {
+ this.hide();
+ }
+ public seconds = false;
+ public mtime: any = { hour: 0, minute: 0, second: 0 };
+ public TimerChange(time: any) {
+ let value;
+ if (this.value) {
+ if (this.value.length) {
+ value = this.value[this.valueIndex];
+ } else {
+ value = this.value;
+ }
+ } else {
+ if (this.type === 'timer') {
+ value = new Date();
+ } else {
+ value = new Date();
+ }
+ }
+
+ if (this.disabled || !value) {
+ event.preventDefault();
+ return false;
+ }
+
+ let minute = time.minute;
+ let hour = time.hour;
+ let second = time.second;
+ this.minValue = minute;
+ this.hourValue = hour;
+ this.secValue = second;
+ let selectedTime = setMinutes(value, minute);
+ selectedTime = setHours(selectedTime, hour);
+ selectedTime = setSeconds(selectedTime, second);
+ let done = this.setSelectedTime(selectedTime);
+
+ // Focus the input and select its value when model updated
+
+ event.preventDefault();
+ return done;
+ }
+ private mouseIn :boolean = false;
+ private Mouseout(event:any)
+ {
+ this.mouseIn = false;
+ }
+ private Mouseover(event:any)
+ {
+ this.mouseIn = true;
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.module.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.module.ts
new file mode 100644
index 00000000..0511ad71
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.module.ts
@@ -0,0 +1,27 @@
+/**
+ * picker.module
+ */
+
+import { NgModule } from '@angular/core';
+
+import { DateTimePickerComponent } from './picker.component';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { NumberFixedLenPipe } from './numberedFixLen.pipe';
+import { NgbTimepickerr } from './timepicker';
+import { OesDaterangePopover, OesDaterangePopoverWindow } from './popover';
+import { OesDaterangePopoverConfig } from './popover-config';
+import { NgbTimepickerConfig } from './timepicker-config';
+import { PlxDateRangePickerComponent } from './pickerrange.component'
+export {DateTimePickerComponent} from './picker.component';
+
+@NgModule({
+ imports: [CommonModule, FormsModule],
+ exports: [DateTimePickerComponent, NgbTimepickerr, OesDaterangePopover,PlxDateRangePickerComponent],
+ declarations: [DateTimePickerComponent, NumberFixedLenPipe, NgbTimepickerr, OesDaterangePopoverWindow, OesDaterangePopover,PlxDateRangePickerComponent],
+ providers: [OesDaterangePopoverConfig, NgbTimepickerConfig, OesDaterangePopoverConfig],
+ entryComponents: [DateTimePickerComponent, OesDaterangePopoverWindow]
+})
+export class PlxDatePickerModule {
+}
+
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.html b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.html
new file mode 100644
index 00000000..2b1986fe
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.html
@@ -0,0 +1,14 @@
+<div style="width:100%;">
+<div class="datepickboxleft" >
+ <plx-datepicker [canClear]="canClear" [supportKeyboardInput]="supportKeyboardInput" [disabled]="disabled" [(ngModel)]="startDate" [showTime]="showTime" [showSeconds]="showSeconds" [timeOnly]="timeOnly" [dateFormat]="dateFormat" [locale]="locale" [minDate]="startMinDate" [maxDate]="_startMaxDate" (onConfirm)="EvonStartDateClosed($event)"
+ placeHolder="{{placeHolderStartDate}}"></plx-datepicker>
+</div>
+<div class="datepickboxto" >
+{{locale.to}}
+</div>
+<div class="datepickboxright" >
+ <plx-datepicker [canClear]="canClear" [supportKeyboardInput]="supportKeyboardInput" [disabled]="disabled" [(ngModel)]="endDate" [showTime]="showTime" [showSeconds]="showSeconds" [timeOnly]="timeOnly" [dateFormat]="dateFormat" [locale]="locale" [minDate]="_endMinDate" [maxDate]="endMaxDate" (onConfirm)="EvonEndDateClosed($event)"
+ placeHolder="{{placeHolderEndDate}}"></plx-datepicker>
+</div>
+<br/>
+</div> \ No newline at end of file
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.ts
new file mode 100644
index 00000000..a84e0987
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.ts
@@ -0,0 +1,162 @@
+/**
+ * picker.component
+ */
+
+import {
+ AfterViewInit,
+ Component, ElementRef, EventEmitter, forwardRef, Input, OnDestroy, OnInit, Output, Renderer2,
+ ViewChild
+} from '@angular/core';
+import {animate, state, style, transition, trigger} from '@angular/animations';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+
+export interface LocaleSettings {
+ firstDayOfWeek?: number;
+ dayNames: string[];
+ dayNamesShort: string[];
+ monthNames: string[];
+ monthNamesShort: string[];
+ dateFns: any;
+}
+
+export enum DialogType {
+ Time,
+ Date,
+ Month,
+ Year,
+}
+
+@Component({
+ selector: 'plx-daterange-picker',
+ templateUrl: './pickerrange.component.html',
+ styleUrls: ['./pickerrange.component.css'],
+ providers: [],
+})
+
+export class PlxDateRangePickerComponent {
+ /*
+disabled boolean false 设置为true时input框不能输入
+minDate Date null 最小可选日期
+maxDate Date null 最大可选日期
+showTime boolean false 设置为true时显示时间选择器
+showSeconds boolean false 时间选择器显示秒
+timeOnly boolean false 设置为true时只显示时间选择器
+dateFormat string YYYY-MM-DD HH:mm 设置时间选择模式
+locale Object null 设置国际化对象,请参考国际化例子。
+改变组件时间*/
+
+ @Input() disabled : boolean = false;
+ @Input() showTime : boolean = false;
+ @Input() showSeconds : boolean = false;
+ @Input() timeOnly : boolean = false;
+ @Input() dateFormat : string = "YYYY-MM-DD HH:mm";
+ @Input() placeHolderStartDate : string = "";
+ @Input() placeHolderEndDate : string = "";
+ @Input() locale : any ={
+ firstDayOfWeek: 0,
+ dayNames: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+ dayNamesShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+ monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+ monthNamesShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+ dateFns: null,
+ confirm:'OK',
+ to:"to"
+ };
+ @Input() startDate : Date;
+ @Input() endDate : Date;
+ @Input() canClear: boolean = true;
+ @Input() startMinDate:Date;
+ @Input() endMaxDate:Date;
+ /**
+ * @default false
+ * @type {Boolean}
+ * */
+ @Input() supportKeyboardInput: boolean = false;
+ _startSetMaxDate:Date;
+ _startMaxDate:Date;
+ @Input()
+ set startMaxDate( date:Date)
+ {
+ this._startSetMaxDate=date;
+ this.BuildstartMaxDate();
+ }
+ _endSetMinDate:Date;
+ _endMinDate:Date;
+ @Input()
+ set endMinDate( date:Date)
+ {
+ this._endSetMinDate=date;
+ this.BuildendMinDate();
+ }
+ BuildstartMaxDate()
+ {
+ if(this._startSetMaxDate===undefined)
+ {
+ this._startMaxDate=this.endDate
+ return;
+ }
+ if(this.endDate!==undefined)
+ {
+ this._startMaxDate= this.endDate<this._startSetMaxDate?this.endDate:this._startSetMaxDate;
+ return;
+ }
+ this._startMaxDate=this._startSetMaxDate;
+ }
+ BuildendMinDate()
+ {
+ if(this._endSetMinDate===undefined)
+ {
+ this._endMinDate=this.startDate
+ return;
+ }
+ if(this.startDate!==undefined)
+ {
+ this._endMinDate= this.startDate>this._endSetMinDate?this.startDate:this._endSetMinDate;
+ return;
+ }
+ this._endMinDate=this._endSetMinDate;
+ }
+
+ @Output()
+ onStartDateClosed: EventEmitter<any> = new EventEmitter<any>();
+ @Output()
+ onEndDateClosed: EventEmitter<any> = new EventEmitter<any>();
+
+ EvonStartDateClosed(event : any)
+ {
+ this.BuildendMinDate();
+ if(this.startDate!==null)
+ {
+ event.date=new Date(this.startDate);
+ }
+ this.onStartDateClosed.emit(event);
+ event.preventDefault();
+ let dd= this;
+ return;
+ }
+
+
+ EvonEndDateClosed (event : any)
+ {
+
+ this.BuildstartMaxDate()
+ if(this.endDate!==null)
+ {
+ event.date=new Date(this.endDate);
+ }
+ this.onEndDateClosed.emit(event);
+ event.preventDefault();
+ let dd= this;
+ return;
+ }
+
+
+ public navigateTo (startDate: Date, endDate: Date)
+ {
+ this.startDate=startDate;
+ this.endDate = endDate;
+ }
+
+
+
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover-config.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover-config.ts
new file mode 100644
index 00000000..5ac773c5
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover-config.ts
@@ -0,0 +1,13 @@
+import { Injectable } from '@angular/core';
+
+/**
+ * Configuration service for the OesDaterangePopover directive.
+ * You can inject this service, typically in your root component, and customize the values of its properties in
+ * order to provide default values for all the popovers used in the application.
+ */
+@Injectable()
+export class OesDaterangePopoverConfig {
+ public placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
+ public triggers = 'click';
+ public container: string;
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover.ts
new file mode 100644
index 00000000..3d054120
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover.ts
@@ -0,0 +1,175 @@
+import {
+ Component,
+ Directive,
+ Input,
+ Output,
+ EventEmitter,
+ ChangeDetectionStrategy,
+ OnInit,
+ OnDestroy,
+ Injector,
+ Renderer,
+ ComponentRef,
+ ElementRef,
+ TemplateRef,
+ ViewContainerRef,
+ ComponentFactoryResolver,
+ NgZone
+} from '@angular/core';
+
+import { listenToTriggers } from './util/triggers';
+import { positionElements } from './util/positioning';
+import { PopupService } from './util/popup';
+import { OesDaterangePopoverConfig } from './popover-config';
+
+let nextId = 0;
+
+@Component({
+ selector: 'ngb-popover-window',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: { '[class]': '"popover show popover-" + placement', 'role': 'tooltip', '[id]': 'id' },
+ styles: [`
+
+ .popover-title,.popover-content{
+ background-color: #fff;
+ }
+ .popover-custom{
+ padding:9px 5px !important;
+ }
+
+
+ `],
+ template: `
+ <h3 class="popover-title">{{title}}</h3><div class="popover-content popover-custom"><ng-content></ng-content></div>
+ `
+})
+export class OesDaterangePopoverWindow {
+ @Input() public placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
+ @Input() public title: string;
+ @Input() public id: string;
+}
+
+/**
+ * A lightweight, extensible directive for fancy oes-popover creation.
+ */
+@Directive({ selector: '[oesDaterangePopover]', exportAs: 'oesDaterangePopover' })
+export class OesDaterangePopover implements OnInit, OnDestroy {
+ /**
+ * Content to be displayed as oes-popover.
+ */
+ @Input() public oesDaterangePopover: string | TemplateRef<any>;
+ /**
+ * Title of a oes-popover.
+ */
+ @Input() public popoverTitle: string;
+ /**
+ * Placement of a oes-popover. Accepts: "top", "bottom", "left", "right"
+ */
+ @Input() public placement: 'top' | 'bottom' | 'left' | 'right';
+ /**
+ * Specifies events that should trigger. Supports a space separated list of event names.
+ */
+ @Input() public triggers: string;
+ /**
+ * A selector specifying the element the oes-popover should be appended to.
+ * Currently only supports "body".
+ */
+ @Input() public container: string;
+ /**
+ * Emits an event when the oes-popover is shown
+ */
+ @Output() public shown = new EventEmitter();
+ /**
+ * Emits an event when the oes-popover is hidden
+ */
+ @Output() public hidden = new EventEmitter();
+
+ private _OesDaterangePopoverWindowId = `ngb-popover-${nextId++}`;
+ private _popupService: PopupService<OesDaterangePopoverWindow>;
+ private _windowRef: ComponentRef<OesDaterangePopoverWindow>;
+ private _unregisterListenersFn;
+ private _zoneSubscription: any;
+
+ constructor(
+ private _elementRef: ElementRef, private _renderer: Renderer, injector: Injector,
+ componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: OesDaterangePopoverConfig,
+ ngZone: NgZone) {
+ this.placement = config.placement;
+ this.triggers = config.triggers;
+ this.container = config.container;
+ this._popupService = new PopupService<OesDaterangePopoverWindow>(
+ OesDaterangePopoverWindow, injector, viewContainerRef, _renderer, componentFactoryResolver);
+
+ this._zoneSubscription = ngZone.onStable.subscribe(() => {
+ if (this._windowRef) {
+ positionElements(
+ this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement,
+ this.container === 'body');
+ }
+ });
+ }
+
+ /**
+ * Opens an element’s oes-popover. This is considered a “manual” triggering of the oes-popover.
+ * The context is an optional value to be injected into the oes-popover template when it is created.
+ */
+ public open(context?: any) {
+ if (!this._windowRef) {
+ this._windowRef = this._popupService.open(this.oesDaterangePopover, context);
+ this._windowRef.instance.placement = this.placement;
+ this._windowRef.instance.title = this.popoverTitle;
+ this._windowRef.instance.id = this._OesDaterangePopoverWindowId;
+
+ this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', this._OesDaterangePopoverWindowId);
+
+ if (this.container === 'body') {
+ window.document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement);
+ }
+
+ // we need to manually invoke change detection since events registered via
+ // Renderer::listen() are not picked up by change detection with the OnPush strategy
+ this._windowRef.changeDetectorRef.markForCheck();
+ this.shown.emit();
+ }
+ }
+
+ /**
+ * Closes an element’s oes-popover. This is considered a “manual” triggering of the oes-popover.
+ */
+ public close(): void {
+ if (this._windowRef) {
+ this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', null);
+ this._popupService.close();
+ this._windowRef = null;
+ this.hidden.emit();
+ }
+ }
+
+ /**
+ * Toggles an element’s oes-popover. This is considered a “manual” triggering of the oes-popover.
+ */
+ public toggle(): void {
+ if (this._windowRef) {
+ this.close();
+ } else {
+ this.open();
+ }
+ }
+
+ /**
+ * Returns whether or not the oes-popover is currently being shown
+ */
+ public isOpen(): boolean { return this._windowRef !== null; }
+
+ public ngOnInit() {
+ this._unregisterListenersFn = listenToTriggers(
+ this._renderer, this._elementRef.nativeElement, this.triggers, this.open.bind(this), this.close.bind(this),
+ this.toggle.bind(this));
+ }
+
+ public ngOnDestroy() {
+ this.close();
+ this._unregisterListenersFn();
+ this._zoneSubscription.unsubscribe();
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/time.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/time.ts
new file mode 100644
index 00000000..ab31a498
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/time.ts
@@ -0,0 +1,51 @@
+import { isNumber, toInteger } from './util/util';
+
+export class NgbTime {
+ public hour: number;
+ public minute: number;
+ public second: number;
+
+ constructor(hour?: number, minute?: number, second?: number) {
+ this.hour = toInteger(hour);
+ this.minute = toInteger(minute);
+ this.second = toInteger(second);
+ }
+
+ public changeHour(step = 1) { this.updateHour((isNaN(this.hour) ? 0 : this.hour) + step); }
+
+ public updateHour(hour: number) {
+ if (isNumber(hour)) {
+ this.hour = (hour < 0 ? 24 + hour : hour) % 24;
+ } else {
+ this.hour = NaN;
+ }
+ }
+
+ public changeMinute(step = 1) { this.updateMinute((isNaN(this.minute) ? 0 : this.minute) + step); }
+
+ public updateMinute(minute: number) {
+ if (isNumber(minute)) {
+ this.minute = minute % 60 < 0 ? 60 + minute % 60 : minute % 60;
+ this.changeHour(Math.floor(minute / 60));
+ } else {
+ this.minute = NaN;
+ }
+ }
+
+ public changeSecond(step = 1) { this.updateSecond((isNaN(this.second) ? 0 : this.second) + step); }
+
+ public updateSecond(second: number) {
+ if (isNumber(second)) {
+ this.second = second < 0 ? 60 + second % 60 : second % 60;
+ this.changeMinute(Math.floor(second / 60));
+ } else {
+ this.second = NaN;
+ }
+ }
+
+ public isValid(checkSecs = true) {
+ return isNumber(this.hour) && isNumber(this.minute) && (checkSecs ? isNumber(this.second) : true);
+ }
+
+ public toString() { return `${this.hour || 0}:${this.minute || 0}:${this.second || 0}`; }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker-config.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker-config.ts
new file mode 100644
index 00000000..8b752866
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker-config.ts
@@ -0,0 +1,19 @@
+import { Injectable } from '@angular/core';
+
+/**
+ * Configuration service for the NgbTimepicker component.
+ * You can inject this service, typically in your root component, and customize the values of its properties in
+ * order to provide default values for all the timepickers used in the application.
+ */
+@Injectable()
+export class NgbTimepickerConfig {
+ public meridian = false;
+ public spinners = true;
+ public seconds = false;
+ public hourStep = 1;
+ public minuteStep = 1;
+ public secondStep = 1;
+ public disabled = false;
+ public readonlyInputs = false;
+ public size: 'small' | 'medium' | 'large' = 'medium';
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.less b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.less
new file mode 100644
index 00000000..60acfa6b
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.less
@@ -0,0 +1,163 @@
+@import "../../assets/components/themes/default/theme.less";
+@import "../../assets/components/themes/common/plx-input.less";
+@import "../../assets/components/themes/common/plx-button.less";
+.oes-time-table .chevron::before {
+ border-style: solid;
+ border-width: 0.29em 0.29em 0 0;
+ content: '';
+ display: inline-block;
+ height: 0.69em;
+ left: 0.05em;
+ position: relative;
+ top: 0.15em;
+ transform: rotate(-45deg);
+ -webkit-transform: rotate(-45deg);
+ -ms-transform: rotate(-45deg);
+ vertical-align: middle;
+ width: 0.71em;
+}
+
+.oes-time-table .chevron.bottom:before {
+ top: -.3em;
+ -webkit-transform: rotate(135deg);
+ -ms-transform: rotate(135deg);
+ transform: rotate(135deg);
+}
+
+.oes-time-table .btn-link {
+ border: none!important;
+ cursor: pointer;
+ outline: 0;
+ display: block;
+}
+
+.oes-time-table .btn-link.disabled {
+ cursor: not-allowed;
+ opacity: .65;
+}
+
+.oes-time-control {
+ text-align: center;
+}
+
+.datapicker-form-control {
+ width: auto !important;
+ display: inline-block;
+}
+
+.oes-time-table .ict-stretch{
+
+ font-size: 8px;
+}
+
+.oes-time-table .ict-shrink{
+ font-size: 8px;
+}
+.time-pick-bk{
+ background-color: #fff;
+}
+
+.btn-link:focus, .btn-link:hover{
+ text-decoration: none;
+}
+.oes-time-control{
+ border: 0;
+ width: 30px !important;
+ padding: 3px 0;
+ margin: 0;
+ font-size: @font-size;
+}
+
+.oes-time-control:hover{
+ background-color: #e6e6e6;
+ color:#000;
+ cursor: pointer;
+}
+
+
+.oes-time-control-foucs-bk{
+ background-color: #00abff !important;
+ color:#fff!important;
+
+}
+
+.oes-time-separator{
+ margin: 0 -5px;
+}
+.oes-time-group,.oes-time-group:hover{
+
+ border-bottom: 1px solid #ccc;
+ border-left: 1px solid #ccc;
+ border-top: 1px solid #ccc;
+ border-radius: 0.2em;
+ }
+ .oes-time-btns,.oes-time-btns:hover{
+
+ border-bottom: 1px solid #ccc;
+ border-right: 1px solid #ccc;
+ border-top: 1px solid #ccc;
+ border-radius: 0.2em;
+ padding: 0 0 7px 0 !important;
+
+ }
+
+ .oes-time-btns-wrapper {
+ margin-top:-3px;
+ transform:scale(0.6,0.6);
+ }
+
+ .i18nTimeDes,.i18nTimeDes:hover{
+
+ padding: 0 5px 0px 0;
+
+ }
+
+ .oes-time-btn{
+
+ height: 5px;
+ }
+
+
+ .oes-time-table{
+ margin-bottom: 10px;
+ }
+
+.hour-table{
+
+ font-size:12px;
+}
+
+.hour-table td{
+
+ padding: 5px;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ cursor: pointer;
+}
+.oes-time-btn-shrink{
+ position: relative;
+ top:-5px;
+ left:0px;
+ color:#CCC;
+}
+
+.oes-time-btn-stretch{
+ position: relative;
+ left:0px;
+ color:#CCC;
+}
+.owl-calendar-timer-invalid{
+ color: #acacac;
+}
+.owl-calendar-timer-selected{
+ background-color: #00abff;
+ color: #FFFFFF;
+ border-radius: 1.2em;
+}
+.hour-table td:not(.owl-calendar-timer-selected):not(.owl-calendar-timer-invalid):hover {
+ background-color: #ebf6fd;
+ color: #000000;
+ border-radius: 1.2em;
+}
+
+
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.ts
new file mode 100644
index 00000000..45dd7a4a
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.ts
@@ -0,0 +1,558 @@
+import { Component, Input, Output, forwardRef, OnChanges, EventEmitter, SimpleChanges, ViewChild } from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+import { isNumber, padNumber, toInteger, isDefined } from './util/util';
+import { NgbTime } from './time';
+import { NgbTimepickerConfig } from './timepicker-config';
+
+const NGB_TIMEPICKER_VALUE_ACCESSOR = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => NgbTimepickerr),
+ multi: true
+};
+
+/**
+ * A lightweight & configurable timepicker directive.
+ */
+@Component({
+ selector: 'oes-timepickerr',
+ styleUrls: ['./timepicker.less'],
+ template: `
+ <template #popContentHour>
+
+ <table class="hour-table">
+ <tbody>
+ <tr><td (click)="selectHour(hour,$event)" *ngFor="let hour of hours1 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedHour(hour), 'owl-calendar-timer-invalid': !isValidHour(hour)}">{{hour}}</td></tr>
+ <tr><td (click)="selectHour(hour,$event)" *ngFor="let hour of hours2 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedHour(hour), 'owl-calendar-timer-invalid': !isValidHour(hour)}">{{hour}}</td></tr>
+ <tr><td (click)="selectHour(hour,$event)" *ngFor="let hour of hours3 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedHour(hour), 'owl-calendar-timer-invalid': !isValidHour(hour)}">{{hour}}</td></tr>
+
+ </tbody>
+ </table>
+
+ </template>
+
+ <template #popContentMin>
+
+ <table class="hour-table">
+ <tbody>
+ <tr><td (click)="selectMin(minuter,$event)" *ngFor="let minuter of minute1 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedMin(minuter), 'owl-calendar-timer-invalid': !isValidMin(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectMin(minuter,$event)" *ngFor="let minuter of minute2 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedMin(minuter), 'owl-calendar-timer-invalid': !isValidMin(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectMin(minuter,$event)" *ngFor="let minuter of minute3 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedMin(minuter), 'owl-calendar-timer-invalid': !isValidMin(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectMin(minuter,$event)" *ngFor="let minuter of minute4 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedMin(minuter), 'owl-calendar-timer-invalid': !isValidMin(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectMin(minuter,$event)" *ngFor="let minuter of minute5 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedMin(minuter), 'owl-calendar-timer-invalid': !isValidMin(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectMin(minuter,$event)" *ngFor="let minuter of minute6 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedMin(minuter), 'owl-calendar-timer-invalid': !isValidMin(minuter)}">{{minuter}}</td></tr>
+
+ </tbody>
+ </table>
+
+ </template>
+
+ <template #popContentSecond>
+ <table class="hour-table">
+ <tbody>
+ <tr><td (click)="selectSecond(minuter,$event)" *ngFor="let minuter of minute1 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedSec(minuter), 'owl-calendar-timer-invalid': !isValidSec(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectSecond(minuter,$event)" *ngFor="let minuter of minute2 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedSec(minuter), 'owl-calendar-timer-invalid': !isValidSec(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectSecond(minuter,$event)" *ngFor="let minuter of minute3 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedSec(minuter), 'owl-calendar-timer-invalid': !isValidSec(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectSecond(minuter,$event)" *ngFor="let minuter of minute4 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedSec(minuter), 'owl-calendar-timer-invalid': !isValidSec(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectSecond(minuter,$event)" *ngFor="let minuter of minute5 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedSec(minuter), 'owl-calendar-timer-invalid': !isValidSec(minuter)}">{{minuter}}</td></tr>
+ <tr><td (click)="selectSecond(minuter,$event)" *ngFor="let minuter of minute6 " [ngClass]=" {'owl-calendar-timer-selected': isSelectedSec(minuter), 'owl-calendar-timer-invalid': !isValidSec(minuter)}">{{minuter}}</td></tr>
+ </tbody>
+ </table>
+ </template>
+ <table class="oes-time-table">
+ <tr>
+ <td class="i18nTimeDes">
+ {{i18nTimeDes}}
+ </td>
+ <td class="oes-time-group">
+ <input placement="top" style="padding-left:1px;padding-right:1px;border: 0; width: 30px !important;padding: 3px 0; margin: 0; font-size: 12px;"
+ [oesDaterangePopover]="popContentHour" #propHour="oesDaterangePopover"
+ #hourItem type="text" (focus)="selectItem('hour')"
+ [ngClass]="{'oes-time-control-foucs-bk': currSelectedItem === 'hour'}"
+ class="form-control datapicker-form-control form-control-sm oes-time-control " maxlength="2" size="2" placeholder="HH"
+ [value]="formatHour(model?.hour)" (change)="updateHour($event.target.value)"
+ [readonly]="readonlyInputs" [disabled]="disabled">
+ <span class="oes-time-separator">&nbsp;:&nbsp;</span>
+ <input
+ [oesDaterangePopover]="popContentMin" #propMin="oesDaterangePopover"
+ #minuteItem type="text"
+ (focus)="selectItem('minute')" style="padding-left:1px;padding-right:1px;border: 0; width: 30px !important;padding: 3px 0; margin: 0; font-size: 12px;"
+ [ngClass]="{'oes-time-control-foucs-bk': currSelectedItem === 'minute'}"
+ class="form-control datapicker-form-control form-control-sm oes-time-control" maxlength="2" size="2" placeholder="MM"
+ [value]="formatMinSec(model?.minute)" (change)="updateMinute($event.target.value)"
+ [readonly]="readonlyInputs" [disabled]="disabled">
+ <span *ngIf="showSecondsTimer" class="oes-time-separator">&nbsp;:&nbsp;</span>
+ <input *ngIf="showSecondsTimer" style="padding-left:1px;padding-right:1px;border: 0; width: 30px !important;padding: 3px 0; margin: 0; font-size: 12px;"
+ [oesDaterangePopover]="popContentSecond" #propSecond="oesDaterangePopover"
+ #secondItem type="text"
+ (focus)="selectItem('second')"
+ [ngClass]="{'oes-time-control-foucs-bk': currSelectedItem === 'second'}"
+ class="form-control datapicker-form-control form-control-sm oes-time-control" maxlength="2" size="2" placeholder="SS"
+ [value]="formatMinSec(model?.second)" (change)="updateSecond($event.target.value)"
+ [readonly]="readonlyInputs" [disabled]="disabled">
+ </td>
+
+ <td class="text-center oes-time-btns">
+ <div class="oes-time-btns-wrapper">
+ <button type="button" class="btn-link btn-sm oes-time-btn oes-time-btn-shrink " (click)="changeTime(hourStep)"
+ [disabled]="disabled" [class.disabled]="disabled">
+ <span class="ict-shrink"></span>
+ </button>
+ <button type="button" class="btn-link btn-sm oes-time-btn oes-time-btn-stretch" (click)="changeTime(-hourStep)"
+ [disabled]="disabled" [class.disabled]="disabled">
+ <span class="ict-stretch"></span>
+ </button>
+ </div>
+ </td>
+ </tr>
+ </table>
+ `,
+ providers: [NGB_TIMEPICKER_VALUE_ACCESSOR]
+})
+export class NgbTimepickerr implements ControlValueAccessor,
+ OnChanges {
+ public disabled: boolean;
+ public model: NgbTime;
+ public datemodel: Date;
+ @Output() TimerChange = new EventEmitter<NgbTime>();
+ /**
+ * Whether to display 12H or 24H mode.
+ */
+ @Input() public meridian: boolean;
+
+ /**
+ * Whether to display the spinners above and below the inputs.
+ */
+ @Input() public spinners: boolean;
+
+ /**
+ * Whether to display seconds input.
+ */
+ @Input() public seconds: boolean;
+
+ /**
+ * Number of hours to increase or decrease when using a button.
+ */
+ @Input() public hourStep: number;
+
+ /**
+ * Number of minutes to increase or decrease when using a button.
+ */
+ @Input() public minuteStep: number;
+
+ /**
+ * Number of seconds to increase or decrease when using a button.
+ */
+ @Input() public secondStep: number;
+
+ /**
+ * To make timepicker readonly
+ */
+ @Input() public readonlyInputs: boolean;
+
+ /**
+ * To set the size of the inputs and button
+ */
+ @Input() public size: 'small' | 'medium' | 'large';
+
+
+
+ private _max: Date;
+ @Input()
+ get max() {
+ return this._max;
+ }
+
+ set max(val: Date) {
+ this._max = val;
+ }
+ private _min: Date;
+ @Input()
+ get min() {
+ return this._min;
+ }
+
+ set min(val: Date) {
+ this._min = val;
+ }
+
+ /**
+ * Whether to show the second's timer
+ * @default false
+ * @type {Boolean}
+ * */
+ @Input() showSecondsTimer: boolean;
+ /**
+ * datePicker的国际化描述
+ */
+ @Input() public i18nTimeDes: string;
+
+ @ViewChild('hourItem') public hourItem;
+
+ @ViewChild('minuteItem') public minuteItem;
+ @ViewChild('secondItem') public secondItem;
+
+ @ViewChild('propHour') public propHour;
+
+ @ViewChild('propMin') public propMin;
+ @ViewChild('propSecond') public propSecond;
+
+ public currSelectedItem: 'hour' | 'minute' | 'second';
+
+ public hours1 = ['00', '01', '02', '03', '04', '05', '06', '07'];
+
+ public hours2 = ['08', '09', '10', '11', '12', '13', '14', '15'];
+
+ public hours3 = ['16', '17', '18', '19', '20', '21', '22', '23'];
+
+ public minute1 = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09'];
+
+ public minute2 = ['10', '11', '12', '13', '14', '15', '16', '17', '18', '19'];
+
+ public minute3 = ['20', '21', '22', '23', '24', '25', '26', '27', '28', '29'];
+
+ public minute4 = ['30', '31', '32', '33', '34', '35', '36', '37', '38', '39'];
+
+ public minute5 = ['40', '41', '42', '43', '44', '45', '46', '47', '48', '49'];
+
+ public minute6 = ['50', '51', '52', '53', '54', '55', '56', '57', '58', '59'];
+
+ constructor(config: NgbTimepickerConfig) {
+ this.meridian = config.meridian;
+ this.spinners = config.spinners;
+ this.seconds = config.seconds;
+ this.hourStep = config.hourStep;
+ this.minuteStep = config.minuteStep;
+ this.secondStep = config.secondStep;
+ this.disabled = config.disabled;
+ this.readonlyInputs = config.readonlyInputs;
+ this.size = config.size;
+ }
+
+ public onChange = (_: any) => {
+ // TO DO
+ }
+ public onTouched = () => {
+ // TO DO
+ }
+ public settime(date : Date)
+ {
+ if(date!=null&&date!==undefined)
+ {
+ if(this._max!==undefined&&this._max.getTime()<date.getTime())
+ {
+ date.setHours(this._max.getHours());
+ date.setMinutes(this._max.getMinutes());
+ date.setSeconds(this._max.getSeconds());
+ this.TimerChange.emit(new NgbTime(date.getHours(),date.getMinutes(),date.getSeconds()));
+ }
+ if(this._min!==undefined&&this._min.getTime()>date.getTime())
+ {
+ date.setHours(this._min.getHours());
+ date.setMinutes(this._min.getMinutes());
+ date.setSeconds(this._min.getSeconds());
+ this.TimerChange.emit(new NgbTime(date.getHours(),date.getMinutes(),date.getSeconds()));
+ }
+ }
+ if(date!==null&&date!==undefined)
+ {
+ let temptime = new NgbTime(date.getHours(),date.getMinutes(),date.getSeconds())
+ this.model = temptime;
+ this.datemodel = date;
+ }
+ else
+ {
+ let temptime = new NgbTime(0,0,0)
+ this.model = temptime;
+ this.datemodel = date;
+ }
+
+ }
+ public selectHour(hour: string, event) {
+ if(!this.isValidHour(parseInt(hour)))
+ {
+ return;
+ }
+ this.model.hour = parseInt(hour);
+ this.propHour.close();
+ this.propagateModelChange();
+ event.stopPropagation();
+ }
+
+ public selectMin(minute: string, event) {
+ if(!this.isValidMin(parseInt(minute)))
+ {
+ return;
+ }
+ this.model.minute = parseInt(minute);
+ this.propMin.close();
+ this.propagateModelChange();
+
+ event.stopPropagation();
+ }
+ public selectSecond(second: string, event) {
+ if(!this.isValidSec(parseInt(second)))
+ {
+ return;
+ }
+ this.model.second = parseInt(second);
+ this.propSecond.close();
+ this.propagateModelChange();
+
+ event.stopPropagation();
+ }
+
+ /**
+ * ###描述
+ * 单击小时或者分钟选项时触发的事件
+ *
+ *
+ * */
+
+ public selectItem(item: 'hour' | 'minute' | 'second') {
+
+ // 切换选中项
+ this.currSelectedItem = item;
+
+ if (item === 'hour') {
+
+ this.propMin?this.propMin.close():0;
+ this.propSecond?this.propSecond.close():0;
+ } else if (item === 'minute') {
+ this.propHour?this.propHour.close():0;
+ this.propSecond?this.propSecond.close():0;
+ } else if (item === 'second') {
+ this.propHour?this.propHour.close():0;
+ this.propMin?this.propMin.close():0;
+ }
+
+ this.minuteItem.nativeElement.blur();
+ this.hourItem.nativeElement.blur();
+
+ this.secondItem?this.secondItem.nativeElement.blur():0;
+
+ // 弹出时间选择列表
+ }
+
+ public changeTime(stepTime) {
+
+ if (this.currSelectedItem === 'hour') { // 如果当前选中的是小时
+
+ this.changeHour(stepTime);
+
+ } else if (this.currSelectedItem === 'minute') {
+
+ this.changeMinute(stepTime);
+ } else if (this.currSelectedItem === 'second') {
+
+ this.changeSecond(stepTime);
+ }
+
+ }
+
+
+ public writeValue(value) {
+ this.model = value ? new NgbTime(value.hour, value.minute, value.second) : new NgbTime();
+ if (!this.seconds && (!value || !isNumber(value.second))) {
+ this.model.second = 0;
+ }
+ }
+
+ public registerOnChange(fn: (value: any) => any): void { this.onChange = fn; }
+
+ public registerOnTouched(fn: () => any): void { this.onTouched = fn; }
+
+ public setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; }
+
+ public changeHour(step: number) {
+ let newDate = new Date(this.datemodel.getTime());
+ newDate.setHours(newDate.getHours()+step);
+ if(!this.isValidDate(newDate))
+ {
+ return;
+ }
+ this.model.changeHour(step);
+ this.propagateModelChange();
+ }
+
+ public changeMinute(step: number) {
+ let newDate = new Date(this.datemodel.getTime());
+ newDate.setMinutes(newDate.getMinutes()+step);
+ if(!this.isValidDate(newDate))
+ {
+ return;
+ }
+ this.model.changeMinute(step);
+ this.propagateModelChange();
+ }
+
+ public changeSecond(step: number) {
+ let newDate = new Date(this.datemodel.getTime());
+ newDate.setSeconds(newDate.getSeconds()+step);
+ if(!this.isValidDate(newDate))
+ {
+ return;
+ }
+ this.model.changeSecond(step);
+ this.propagateModelChange();
+ }
+
+ public updateHour(newVal: string) {
+ this.model.updateHour(toInteger(newVal));
+ this.propagateModelChange();
+ }
+
+ public updateMinute(newVal: string) {
+ this.model.updateMinute(toInteger(newVal));
+ this.propagateModelChange();
+ }
+
+ public updateSecond(newVal: string) {
+ this.model.updateSecond(toInteger(newVal));
+ this.propagateModelChange();
+ }
+
+ public toggleMeridian() {
+ if (this.meridian) {
+ this.changeHour(12);
+ }
+ }
+
+ public formatHour(value: number) {
+ if (isNumber(value)) {
+ if (this.meridian) {
+ return padNumber(value % 12 === 0 ? 12 : value % 12);
+ } else {
+ return padNumber(value % 24);
+ }
+ } else {
+ return padNumber(NaN);
+ }
+ }
+
+ public formatMinSec(value: number) { return padNumber(value); }
+
+ public setFormControlSize() { return { 'form-control-sm': this.size === 'small', 'form-control-lg': this.size === 'large' }; }
+
+ public setButtonSize() { return { 'btn-sm': this.size === 'small', 'btn-lg': this.size === 'large' }; }
+
+
+ public ngOnChanges(changes: SimpleChanges): void {
+ if (changes['seconds'] && !this.seconds && this.model && !isNumber(this.model.second)) {
+ this.model.second = 0;
+ this.propagateModelChange(false);
+ }
+ }
+
+ private propagateModelChange(touched = true) {
+ this.TimerChange.emit(this.model);
+ if (touched) {
+ this.onTouched();
+ }
+ if (this.model.isValid(this.seconds)) {
+ this.onChange({ hour: this.model.hour, minute: this.model.minute, second: this.model.second });
+ } else {
+ this.onChange(null);
+ }
+ }
+ public closeProp()
+ {
+
+ if(this.propSecond!==undefined)
+ {
+ this.propSecond.close();
+ }
+ if(this.propMin!==undefined)
+ {
+ this.propMin.close();
+ }
+ if(this.propHour!==undefined)
+ {
+ this.propHour.close();
+ }
+ }
+ private isValidDate(date: Date)
+ {
+ let isValid = true;
+ if (isValid && this._min!==undefined&&this._min!==null) {
+ isValid = date.getTime()>=this._min.getTime();
+ }
+ if (isValid && this._max!==undefined&&this._max!==null) {
+ isValid = date.getTime()<=this._max.getTime();
+ }
+ return isValid;
+ }
+ private isSelectedMin(strvalue:any): boolean {
+ let value = parseInt(strvalue);
+ if(this.model!==null&&this.model!==undefined)
+ {
+ return this.model.minute === value;
+ }
+ else
+ {
+ return false;
+ }
+}
+ private isValidMin(strvalue:any): boolean {
+ let value = parseInt(strvalue);
+ let nowdate = new Date();
+ if(this.datemodel===undefined||this.datemodel===null)
+ {
+ }
+ else
+ {
+ nowdate = new Date(this.datemodel);
+ }
+ nowdate.setMinutes(value);
+ return this.isValidDate(nowdate);
+}
+private isSelectedSec(strvalue:any): boolean {
+ let value = parseInt(strvalue);
+ if(this.model!==null&&this.model!==undefined)
+ {
+ return this.model.second === value;
+ }
+ else
+ {
+ return false;
+ }
+}
+private isValidSec(strvalue:any): boolean {
+ let value = parseInt(strvalue);
+ let nowdate = new Date();
+ if(this.datemodel===undefined||this.datemodel===null)
+ {
+ }
+ else
+ {
+ nowdate = new Date(this.datemodel);
+ }
+ nowdate.setSeconds(value);
+ return this.isValidDate(nowdate);
+}
+private isSelectedHour(strvalue:any): boolean {
+ let value = parseInt(strvalue);
+ if(this.model!==null&&this.model!==undefined)
+ {
+ return this.model.hour === value;
+ }
+ else
+ {
+ return false;
+ }
+}
+private isValidHour(strvalue:any): boolean {
+ debugger;
+ let value = parseInt(strvalue);
+ let nowdate = new Date();
+ if(this.datemodel===undefined||this.datemodel===null)
+ {
+ }
+ else
+ {
+ nowdate = new Date(this.datemodel);
+ }
+ nowdate.setHours(value);
+ return this.isValidDate(nowdate);
+}
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/popup.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/popup.ts
new file mode 100644
index 00000000..56c26d62
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/popup.ts
@@ -0,0 +1,58 @@
+import {
+ Injector,
+ TemplateRef,
+ ViewRef,
+ ViewContainerRef,
+ Renderer,
+ ComponentRef,
+ ComponentFactory,
+ ComponentFactoryResolver
+} from '@angular/core';
+
+export class ContentRef {
+ constructor(public nodes: any[], public viewRef?: ViewRef, public componentRef?: ComponentRef<any>) {}
+}
+
+export class PopupService<T> {
+ private _windowFactory: ComponentFactory<T>;
+ private _windowRef: ComponentRef<T>;
+ private _contentRef: ContentRef;
+
+ constructor(
+ type: any, private _injector: Injector, private _viewContainerRef: ViewContainerRef, private _renderer: Renderer,
+ componentFactoryResolver: ComponentFactoryResolver) {
+ this._windowFactory = componentFactoryResolver.resolveComponentFactory<T>(type);
+ }
+
+ public open(content?: string | TemplateRef<any>, context?: any): ComponentRef<T> {
+ if (!this._windowRef) {
+ this._contentRef = this._getContentRef(content, context);
+ this._windowRef =
+ this._viewContainerRef.createComponent(this._windowFactory, 0, this._injector, this._contentRef.nodes);
+ }
+ return this._windowRef;
+ }
+
+ public close() {
+ if (this._windowRef) {
+ this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._windowRef.hostView));
+ this._windowRef = null;
+
+ if (this._contentRef.viewRef) {
+ this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._contentRef.viewRef));
+ this._contentRef = null;
+ }
+ }
+ }
+
+ private _getContentRef(content: string | TemplateRef<any>, context?: any): ContentRef {
+ if (!content) {
+ return new ContentRef([]);
+ } else if (content instanceof TemplateRef) {
+ const viewRef = this._viewContainerRef.createEmbeddedView(<TemplateRef<T>>content, context);
+ return new ContentRef([viewRef.rootNodes], viewRef);
+ } else {
+ return new ContentRef([[this._renderer.createText(null, `${content}`)]]);
+ }
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/positioning.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/positioning.ts
new file mode 100644
index 00000000..ed9005c1
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/positioning.ts
@@ -0,0 +1,153 @@
+// previous version:
+// https://github.com/angular-ui/bootstrap/blob/07c31d0731f7cb068a1932b8e01d2312b796b4ec/src/position/position.js
+export class Positioning {
+ private getStyle(element: HTMLElement, prop: string): string { return window.getComputedStyle(element)[prop]; }
+
+ private isStaticPositioned(element: HTMLElement): boolean {
+ return (this.getStyle(element, 'position') || 'static') === 'static';
+ }
+
+ private offsetParent(element: HTMLElement): HTMLElement {
+ let offsetParentEl = <HTMLElement>element.offsetParent || document.documentElement;
+
+ while (offsetParentEl && offsetParentEl !== document.documentElement && this.isStaticPositioned(offsetParentEl)) {
+ offsetParentEl = <HTMLElement>offsetParentEl.offsetParent;
+ }
+
+ return offsetParentEl || document.documentElement;
+ }
+
+ public position(element: HTMLElement, round = true): ClientRect {
+ let elPosition: ClientRect;
+ let parentOffset: ClientRect = {width: 0, height: 0, top: 0, bottom: 0, left: 0, right: 0};
+
+ if (this.getStyle(element, 'position') === 'fixed') {
+ elPosition = element.getBoundingClientRect();
+ } else {
+ const offsetParentEl = this.offsetParent(element);
+
+ elPosition = this.offset(element, false);
+
+ if (offsetParentEl !== document.documentElement) {
+ parentOffset = this.offset(offsetParentEl, false);
+ }
+
+ parentOffset.top += offsetParentEl.clientTop;
+ parentOffset.left += offsetParentEl.clientLeft;
+ }
+
+ elPosition.top -= parentOffset.top;
+ elPosition.bottom -= parentOffset.top;
+ elPosition.left -= parentOffset.left;
+ elPosition.right -= parentOffset.left;
+
+ if (round) {
+ elPosition.top = Math.round(elPosition.top);
+ elPosition.bottom = Math.round(elPosition.bottom);
+ elPosition.left = Math.round(elPosition.left);
+ elPosition.right = Math.round(elPosition.right);
+ }
+
+ return elPosition;
+ }
+
+ public offset(element: HTMLElement, round = true): ClientRect {
+ const elBcr = element.getBoundingClientRect();
+ const viewportOffset = {
+ top: window.pageYOffset - document.documentElement.clientTop,
+ left: window.pageXOffset - document.documentElement.clientLeft
+ };
+
+ let elOffset = {
+ height: elBcr.height || element.offsetHeight,
+ width: elBcr.width || element.offsetWidth,
+ top: elBcr.top + viewportOffset.top,
+ bottom: elBcr.bottom + viewportOffset.top,
+ left: elBcr.left + viewportOffset.left,
+ right: elBcr.right + viewportOffset.left
+ };
+
+ if (round) {
+ elOffset.height = Math.round(elOffset.height);
+ elOffset.width = Math.round(elOffset.width);
+ elOffset.top = Math.round(elOffset.top);
+ elOffset.bottom = Math.round(elOffset.bottom);
+ elOffset.left = Math.round(elOffset.left);
+ elOffset.right = Math.round(elOffset.right);
+ }
+
+ return elOffset;
+ }
+
+ public positionElements(hostElement: HTMLElement, targetElement: HTMLElement, placement: string, appendToBody?: boolean):
+ ClientRect {
+ const hostElPosition = appendToBody ? this.offset(hostElement, false) : this.position(hostElement, false);
+ const shiftWidth: any = {
+ left: hostElPosition.left,
+ left2: (hostElPosition.left - 85),
+ center: hostElPosition.left + hostElPosition.width / 2 - targetElement.offsetWidth / 2,
+ right: hostElPosition.left + hostElPosition.width
+ };
+ const shiftHeight: any = {
+ top: hostElPosition.top,
+ center: hostElPosition.top + hostElPosition.height / 2 - targetElement.offsetHeight / 2,
+ bottom: hostElPosition.top + hostElPosition.height
+ };
+ const targetElBCR = targetElement.getBoundingClientRect();
+ const placementPrimary = placement.split('-')[0] || 'top';
+ const placementSecondary = placement.split('-')[1] || 'center';
+
+ let targetElPosition: ClientRect = {
+ height: targetElBCR.height || targetElement.offsetHeight,
+ width: targetElBCR.width || targetElement.offsetWidth,
+ top: 0,
+ bottom: targetElBCR.height || targetElement.offsetHeight,
+ left: 0,
+ right: targetElBCR.width || targetElement.offsetWidth
+ };
+
+ switch (placementPrimary) {
+ case 'top':
+ targetElPosition.top = hostElPosition.top - targetElement.offsetHeight;
+ targetElPosition.bottom += hostElPosition.top - targetElement.offsetHeight;
+ targetElPosition.left = shiftWidth[placementSecondary];
+ targetElPosition.right += shiftWidth[placementSecondary];
+ break;
+ case 'bottom':
+ targetElPosition.top = shiftHeight[placementPrimary];
+ targetElPosition.bottom += shiftHeight[placementPrimary];
+ targetElPosition.left = shiftWidth[placementSecondary];
+ targetElPosition.right += shiftWidth[placementSecondary];
+ break;
+ case 'left':
+ targetElPosition.top = shiftHeight[placementSecondary];
+ targetElPosition.bottom += shiftHeight[placementSecondary];
+ targetElPosition.left = hostElPosition.left - targetElement.offsetWidth;
+ targetElPosition.right += hostElPosition.left - targetElement.offsetWidth;
+ break;
+ case 'right':
+ targetElPosition.top = shiftHeight[placementSecondary];
+ targetElPosition.bottom += shiftHeight[placementSecondary];
+ targetElPosition.left = shiftWidth[placementPrimary];
+ targetElPosition.right += shiftWidth[placementPrimary];
+ break;
+
+ }
+
+ targetElPosition.top = Math.round(targetElPosition.top);
+ targetElPosition.bottom = Math.round(targetElPosition.bottom);
+ targetElPosition.left = Math.round(targetElPosition.left);
+ targetElPosition.right = Math.round(targetElPosition.right);
+
+ return targetElPosition;
+ }
+}
+
+const positionService = new Positioning();
+export function positionElements(
+ hostElement: HTMLElement, targetElement: HTMLElement, placement: string, appendToBody?: boolean): void {
+ const pos = positionService.positionElements(hostElement, targetElement, placement, appendToBody);
+
+ targetElement.style.top = `${pos.top}px`;
+ targetElement.style.left = `${pos.left}px`;
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/triggers.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/triggers.ts
new file mode 100644
index 00000000..8197de5b
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/triggers.ts
@@ -0,0 +1,62 @@
+export class Trigger {
+ constructor(public open: string, public close?: string) {
+ if (!close) {
+ this.close = open;
+ }
+ }
+
+ public isManual() { return this.open === 'manual' || this.close === 'manual'; }
+}
+
+const DEFAULT_ALIASES = {
+ hover: ['mouseenter', 'mouseleave']
+};
+
+export function parseTriggers(triggers: string, aliases = DEFAULT_ALIASES): Trigger[] {
+ const trimmedTriggers = (triggers || '').trim();
+
+ if (trimmedTriggers.length === 0) {
+ return [];
+ }
+
+ const parsedTriggers = trimmedTriggers.split(/\s+/).map(trigger => trigger.split(':')).map((triggerPair) => {
+ let alias = aliases[triggerPair[0]] || triggerPair;
+ return new Trigger(alias[0], alias[1]);
+ });
+
+ const manualTriggers = parsedTriggers.filter(triggerPair => triggerPair.isManual());
+
+ if (manualTriggers.length > 1) {
+ throw 'Triggers parse error: only one manual trigger is allowed';
+ }
+
+ if (manualTriggers.length === 1 && parsedTriggers.length > 1) {
+ throw 'Triggers parse error: manual trigger can\'t be mixed with other triggers';
+ }
+
+ return parsedTriggers;
+}
+
+const noopFn = () => {
+ // TO DO
+};
+
+export function listenToTriggers(renderer: any, nativeElement: any, triggers: string, openFn, closeFn, toggleFn) {
+ const parsedTriggers = parseTriggers(triggers);
+ const listeners = [];
+
+ if (parsedTriggers.length === 1 && parsedTriggers[0].isManual()) {
+ return noopFn;
+ }
+
+ parsedTriggers.forEach((trigger: Trigger) => {
+ if (trigger.open === trigger.close) {
+ listeners.push(renderer.listen(nativeElement, trigger.open, toggleFn));
+ } else {
+ listeners.push(
+ renderer.listen(nativeElement, trigger.open, openFn), renderer.listen(nativeElement, trigger.close, closeFn));
+ }
+ });
+
+ return () => { listeners.forEach(unsubscribeFn => unsubscribeFn()); };
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/util.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/util.ts
new file mode 100644
index 00000000..fcabe960
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/util.ts
@@ -0,0 +1,39 @@
+export function toInteger(value: any): number {
+ return parseInt(`${value}`, 10);
+}
+
+export function toString(value: any): string {
+ return (value !== undefined && value !== null) ? `${value}` : '';
+}
+
+export function getValueInRange(value: number, max: number, min = 0): number {
+ return Math.max(Math.min(value, max), min);
+}
+
+export function isString(value: any): boolean {
+ return typeof value === 'string';
+}
+
+export function isNumber(value: any): boolean {
+ return !isNaN(toInteger(value));
+}
+
+export function isInteger(value: any): boolean {
+ return typeof value === 'number' && isFinite(value) && Math.floor(value) === value;
+}
+
+export function isDefined(value: any): boolean {
+ return value !== undefined && value !== null;
+}
+
+export function padNumber(value: number) {
+ if (isNumber(value)) {
+ return value > 9? `${value}`.slice(-2):'0' + `${value}`.slice(-2);
+ } else {
+ return '';
+ }
+}
+
+export function regExpEscape(text) {
+ return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts
new file mode 100644
index 00000000..887b66e4
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts
@@ -0,0 +1,16 @@
+import {TestBed} from '@angular/core/testing';
+import {PlxModalBackdrop} from './modal-backdrop';
+
+describe('plx-modal-backdrop', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({declarations: [PlxModalBackdrop]});
+ });
+
+ it('should render backdrop with required CSS classes', () => {
+ const fixture = TestBed.createComponent(PlxModalBackdrop);
+
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveCssClass('modal-backdrop');
+ });
+});
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts
new file mode 100644
index 00000000..07e2ff84
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts
@@ -0,0 +1,9 @@
+import {Component} from '@angular/core';
+
+@Component({
+ selector: 'plx-modal-backdrop',
+ template: '',
+ host: {'class': 'modal-backdrop fade show'}
+})
+export class PlxModalBackdrop {
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts
new file mode 100644
index 00000000..08395852
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts
@@ -0,0 +1,4 @@
+export enum ModalDismissReasons {
+ BACKDROP_CLICK,
+ ESC
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts
new file mode 100644
index 00000000..061dc70e
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts
@@ -0,0 +1,109 @@
+import {Injectable, ComponentRef} from '@angular/core';
+import {PlxModalBackdrop} from './modal-backdrop';
+import {PlxModalWindow} from './modal-window';
+import {ContentRef} from '../util/popup';
+
+/**
+ * A reference to an active (currently opened) modal. Instances of this class
+ * can be injected into components passed as modal content.
+ */
+@Injectable()
+export class PlxActiveModal {
+ /**
+ * Can be used to close a modal, passing an optional result.
+ */
+ public close(result?: any): void {
+ // TO DO
+ }
+
+ /**
+ * Can be used to dismiss a modal, passing an optional reason.
+ */
+ public dismiss(reason?: any): void {
+ // TO DO
+ }
+}
+
+/**
+ * A reference to a newly opened modal.
+ */
+@Injectable()
+export class PlxModalRef {
+ private _resolve: (result?: any) => void;
+ private _reject: (reason?: any) => void;
+
+ /**
+ * The instance of component used as modal's content.
+ * Undefined when a TemplateRef is used as modal's content.
+ */
+ get componentInstance(): any {
+ if (this._contentRef.componentRef) {
+ return this._contentRef.componentRef.instance;
+ }
+ }
+
+ // only needed to keep TS1.8 compatibility
+ set componentInstance(instance: any) {
+ // TO DO
+ }
+
+ /**
+ * A promise that is resolved when a modal is closed and rejected when a modal is dismissed.
+ */
+ public result: Promise<any>;
+
+ constructor(private _windowCmptRef: ComponentRef<PlxModalWindow>, private _contentRef: ContentRef,
+ private _backdropCmptRef?: ComponentRef<PlxModalBackdrop>) {
+ _windowCmptRef.instance.dismissEvent.subscribe((reason: any) => {
+ this.dismiss(reason);
+ });
+
+ this.result = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ this.result.then(null, () => {
+ // TO DO
+ });
+ }
+
+ /**
+ * Can be used to close a modal, passing an optional result.
+ */
+ public close(result?: any): void {
+ if (this._windowCmptRef) {
+ this._resolve(result);
+ this._removeModalElements();
+ }
+ }
+
+ /**
+ * Can be used to dismiss a modal, passing an optional reason.
+ */
+ public dismiss(reason?: any): void {
+ if (this._windowCmptRef) {
+ this._reject(reason);
+ this._removeModalElements();
+ }
+ }
+
+ private _removeModalElements() {
+ const windowNativeEl = this._windowCmptRef.location.nativeElement;
+ windowNativeEl.parentNode.removeChild(windowNativeEl);
+ this._windowCmptRef.destroy();
+
+ if (this._backdropCmptRef) {
+ const backdropNativeEl = this._backdropCmptRef.location.nativeElement;
+ backdropNativeEl.parentNode.removeChild(backdropNativeEl);
+ this._backdropCmptRef.destroy();
+ }
+
+ if (this._contentRef && this._contentRef.viewRef) {
+ this._contentRef.viewRef.destroy();
+ }
+
+ this._windowCmptRef = null;
+ this._backdropCmptRef = null;
+ this._contentRef = null;
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts
new file mode 100644
index 00000000..37f5b171
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts
@@ -0,0 +1,103 @@
+import {
+ ApplicationRef,
+ Injectable,
+ Injector,
+ ReflectiveInjector,
+ ComponentFactory,
+ ComponentFactoryResolver,
+ ComponentRef,
+ TemplateRef
+} from '@angular/core';
+
+import {ContentRef} from '../util/popup';
+import {isDefined, isString} from '../util/util';
+
+import {PlxModalBackdrop} from './modal-backdrop';
+import {PlxModalWindow} from './modal-window';
+import {PlxActiveModal, PlxModalRef} from './modal-ref';
+
+@Injectable()
+export class PlxModalStack {
+ private _backdropFactory: ComponentFactory<PlxModalBackdrop>;
+ private _windowFactory: ComponentFactory<PlxModalWindow>;
+
+ constructor(private _applicationRef: ApplicationRef, private _injector: Injector,
+ private _componentFactoryResolver: ComponentFactoryResolver) {
+ this._backdropFactory = _componentFactoryResolver.resolveComponentFactory(PlxModalBackdrop);
+ this._windowFactory = _componentFactoryResolver.resolveComponentFactory(PlxModalWindow);
+ }
+
+ public open(moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any, options): PlxModalRef {
+ const containerSelector = options.container || 'body';
+ const containerEl = document.querySelector(containerSelector);// 默认获取到body的DOM
+
+ if (!containerEl) {
+ throw new Error(`The specified modal container "${containerSelector}" was not found in the DOM.`);
+ }
+
+ const activeModal = new PlxActiveModal();
+ const contentRef = this._getContentRef(moduleCFR, contentInjector, content, activeModal);
+
+ let windowCmptRef: ComponentRef<PlxModalWindow>;
+ let backdropCmptRef: ComponentRef<PlxModalBackdrop>;
+ let ngbModalRef: PlxModalRef;
+
+
+ if (options.backdrop !== false) {
+ backdropCmptRef = this._backdropFactory.create(this._injector);
+ this._applicationRef.attachView(backdropCmptRef.hostView);
+ containerEl.appendChild(backdropCmptRef.location.nativeElement);
+ }
+ windowCmptRef = this._windowFactory.create(this._injector, contentRef.nodes);
+
+ /**
+ * Attaches a view so that it will be dirty checked.
+ * The view will be automatically detached when it is destroyed.
+ * This will throw if the view is already attached to a ViewContainer.
+ */
+ this._applicationRef.attachView(windowCmptRef.hostView);
+
+ containerEl.appendChild(windowCmptRef.location.nativeElement);
+
+ ngbModalRef = new PlxModalRef(windowCmptRef, contentRef, backdropCmptRef);
+
+ activeModal.close = (result: any) => {
+ ngbModalRef.close(result);
+ };
+ activeModal.dismiss = (reason: any) => {
+ ngbModalRef.dismiss(reason);
+ };
+
+ this._applyWindowOptions(windowCmptRef.instance, options);
+
+ return ngbModalRef;
+ }
+
+ private _applyWindowOptions(windowInstance: PlxModalWindow, options: Object): void {
+ ['backdrop', 'keyboard', 'size', 'windowClass'].forEach((optionName: string) => {
+ if (isDefined(options[optionName])) {
+ windowInstance[optionName] = options[optionName];
+ }
+ });
+ }
+
+ private _getContentRef(moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any,
+ context: PlxActiveModal): ContentRef {
+ if (!content) {
+ return new ContentRef([]);
+ } else if (content instanceof TemplateRef) {
+ const viewRef = content.createEmbeddedView(context);
+ this._applicationRef.attachView(viewRef);
+ return new ContentRef([viewRef.rootNodes], viewRef);
+ } else if (isString(content)) {
+ return new ContentRef([[document.createTextNode(`${content}`)]]);
+ } else {
+ const contentCmptFactory = moduleCFR.resolveComponentFactory(content);
+ const modalContentInjector =
+ ReflectiveInjector.resolveAndCreate([{provide: PlxActiveModal, useValue: context}], contentInjector);
+ const componentRef = contentCmptFactory.create(modalContentInjector);
+ this._applicationRef.attachView(componentRef.hostView);
+ return new ContentRef([[componentRef.location.nativeElement]], componentRef.hostView, componentRef);
+ }
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts
new file mode 100644
index 00000000..5767bfee
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts
@@ -0,0 +1,114 @@
+import {TestBed, ComponentFixture} from '@angular/core/testing';
+
+import {PlxModalWindow} from './modal-window';
+import {ModalDismissReasons} from './modal-dismiss-reasons';
+
+describe('plx-modal-dialog', () => {
+
+ let fixture: ComponentFixture<PlxModalWindow>;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({declarations: [PlxModalWindow]});
+ fixture = TestBed.createComponent(PlxModalWindow);
+ });
+
+ describe('basic rendering functionality', () => {
+
+ it('should render default modal window', () => {
+ fixture.detectChanges();
+
+ const modalEl: Element = fixture.nativeElement;
+ const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog');
+
+ expect(modalEl).toHaveCssClass('modal');
+ expect(dialogEl).toHaveCssClass('modal-dialog');
+ });
+
+ it('should render default modal window with a specified size', () => {
+ fixture.componentInstance.size = 'sm';
+ fixture.detectChanges();
+
+ const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog');
+ expect(dialogEl).toHaveCssClass('modal-dialog');
+ expect(dialogEl).toHaveCssClass('modal-sm');
+ });
+
+ it('should render default modal window with a specified class', () => {
+ fixture.componentInstance.windowClass = 'custom-class';
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveCssClass('custom-class');
+ });
+
+ it('aria attributes', () => {
+ fixture.detectChanges();
+ const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog');
+
+ expect(fixture.nativeElement.getAttribute('role')).toBe('dialog');
+ expect(dialogEl.getAttribute('role')).toBe('document');
+ });
+ });
+
+ describe('dismiss', () => {
+
+ it('should dismiss on backdrop click by default', (done) => {
+ fixture.detectChanges();
+
+ fixture.componentInstance.dismissEvent.subscribe(($event) => {
+ expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK);
+ done();
+ });
+
+ fixture.nativeElement.click();
+ });
+
+ it('should not dismiss on modal content click when there is active backdrop', (done) => {
+ fixture.detectChanges();
+ fixture.componentInstance.dismissEvent.subscribe(
+ () => {
+ done.fail(new Error('Should not trigger dismiss event'));
+ });
+
+ fixture.nativeElement.querySelector('.modal-content').click();
+ setTimeout(done, 200);
+ });
+
+ it('should ignore backdrop clicks when there is no backdrop', (done) => {
+ fixture.componentInstance.backdrop = false;
+ fixture.detectChanges();
+
+ fixture.componentInstance.dismissEvent.subscribe(($event) => {
+ expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK);
+ done.fail(new Error('Should not trigger dismiss event'));
+ });
+
+ fixture.nativeElement.querySelector('.modal-dialog').click();
+ setTimeout(done, 200);
+ });
+
+ it('should ignore backdrop clicks when backdrop is "static"', (done) => {
+ fixture.componentInstance.backdrop = 'static';
+ fixture.detectChanges();
+
+ fixture.componentInstance.dismissEvent.subscribe(($event) => {
+ expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK);
+ done.fail(new Error('Should not trigger dismiss event'));
+ });
+
+ fixture.nativeElement.querySelector('.modal-dialog').click();
+ setTimeout(done, 200);
+ });
+
+ it('should dismiss on esc press by default', (done) => {
+ fixture.detectChanges();
+
+ fixture.componentInstance.dismissEvent.subscribe(($event) => {
+ expect($event).toBe(ModalDismissReasons.ESC);
+ done();
+ });
+
+ fixture.debugElement.triggerEventHandler('keyup.esc', {});
+ });
+ });
+
+});
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts
new file mode 100644
index 00000000..eda5b39f
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts
@@ -0,0 +1,82 @@
+import {
+ Component,
+ Output,
+ EventEmitter,
+ Input,
+ ElementRef,
+ Renderer,
+ OnInit,
+ AfterViewInit,
+ OnDestroy, ViewEncapsulation
+} from '@angular/core';
+
+import {ModalDismissReasons} from './modal-dismiss-reasons';
+
+@Component({
+ selector: 'plx-modal-window',
+ host: {
+ '[class]': '"modal plx-modal fade show" + (windowClass ? " " + windowClass : "")',
+ 'role': 'dialog',
+ 'tabindex': '-1',
+ 'style': 'display: block;',
+ '(keyup.esc)': 'escKey($event)',
+ '(click)': 'backdropClick($event)'
+ },
+ template: `
+ <div [class]="'modal-dialog' + (size ? ' modal-' + size : '')" role="document">
+ <div class="modal-content"><ng-content></ng-content></div>
+ </div>
+ `,
+ styleUrls: ['modal.less'],
+ encapsulation: ViewEncapsulation.None
+})
+export class PlxModalWindow implements OnInit, AfterViewInit, OnDestroy {
+ private _elWithFocus: Element; // element that is focused prior to modal opening
+
+ @Input() public backdrop: boolean | string = true;
+ @Input() public keyboard = true;
+ @Input() public size: string;
+ @Input() public windowClass: string;
+
+ @Output('dismiss') public dismissEvent = new EventEmitter();
+
+ constructor(private _elRef: ElementRef, private _renderer: Renderer) {
+ }
+
+ public backdropClick($event): void {
+ if (this.backdrop === true && this._elRef.nativeElement === $event.target) {
+ this.dismiss(ModalDismissReasons.BACKDROP_CLICK);
+ }
+ }
+
+ public escKey($event): void {
+ if (this.keyboard && !$event.defaultPrevented) {
+ this.dismiss(ModalDismissReasons.ESC);
+ }
+ }
+
+ public dismiss(reason): void {
+ this.dismissEvent.emit(reason);
+ }
+
+ public ngOnInit() {
+ this._elWithFocus = document.activeElement;
+ this._renderer.setElementClass(document.body, 'modal-open', true);
+ }
+
+ public ngAfterViewInit() {
+ if (!this._elRef.nativeElement.contains(document.activeElement)) {
+ this._renderer.invokeElementMethod(this._elRef.nativeElement, 'focus', []);
+ }
+ }
+
+ public ngOnDestroy() {
+ if (this._elWithFocus && document.body.contains(this._elWithFocus)) {
+ this._renderer.invokeElementMethod(this._elWithFocus, 'focus', []);
+ } else {
+ this._renderer.invokeElementMethod(document.body, 'focus', []);
+ }
+ this._elWithFocus = null;
+ this._renderer.setElementClass(document.body, 'modal-open', false);
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less
new file mode 100644
index 00000000..c17a7fd1
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less
@@ -0,0 +1,125 @@
+@import "../../assets/components/themes/default/theme.less";
+
+plx-modal-window {
+ .modal {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ display: none;
+ outline: 0;
+ z-index: 10000;
+ }
+ .modal-dialog {
+ position: relative;
+ max-width: 600px;
+ margin: 30px auto;
+ &.modal-sm {
+ max-width: 600px;
+ }
+ &.modal-lg {
+ max-width: 1000px;
+ }
+ }
+ .modal-content {
+ position: relative;
+ display: flex;
+ -webkit-box-orient: vertical;
+ -webkit-box-direction: normal;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ background-color: @component-bg;
+ background-clip: padding-box;
+ border-radius: @radius;
+ box-shadow: 0 5px 15px @shadow-color;
+ outline: 0;
+ .modal-header {
+ border-bottom: 0;
+ display: flex;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ -webkit-box-pack: justify;
+ -ms-flex-pack: justify;
+ justify-content: space-between;
+ padding: 15px;
+ }
+ .modal-body {
+ .form-group:last-child, form:last-child {
+ margin-bottom: 0;
+ }
+ }
+ .modal-footer {
+ display: block;
+ border-top: 0;
+ margin-top: 0;
+ padding: 0 15px 15px 15px;
+ }
+ .modal-title {
+ font-size: @font-size-title-level1;
+ margin-bottom: 0;
+ line-height: 1.5;
+ }
+ .modal-btn {
+ text-align: center;
+ font-size: 0;
+ }
+ }
+ .close {
+ color: @fonticon-color;
+ font-size: @font-size-title-level2;
+ text-shadow: none;
+ width: 24px;
+ height: 24px;
+ background: @scene-textcolor;
+ border-radius: 20px;
+ padding-bottom: 2px;
+ outline: none;
+ &:hover {
+ color: @fonticon-color;
+ background: @fonticon-bg-color-hover;
+ }
+ }
+ .alert-modal {
+ &.row {
+ margin-left: 100px;
+ margin-bottom: 30px;
+ text-align: left;
+ .tip-img {
+ display: inline-block;
+ width: 52px;
+ height: 52px;
+ border-radius: 50px;
+ font-size: 45px;
+ text-align: center;
+ line-height: 1;
+ margin-top: -5px;
+ margin-right: 15px;
+ &::before {
+ content: "!";
+ }
+ }
+ .tip-info {
+ width: 300px;
+ .alert-title {
+ font-size: @font-size-title-level2;
+ color: @title-text-color;
+ }
+ .alert-result {
+ margin-top: 5px;
+ font-size: @font-size;
+ color: @unselected-text-color;
+ }
+ }
+ .warning {
+ border: 3px solid @warning-color;
+ color: @warning-color;
+ }
+ .error {
+ border: 3px solid @error-color;
+ color: @error-color;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts
new file mode 100644
index 00000000..67fdb478
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts
@@ -0,0 +1,21 @@
+import {NgModule, ModuleWithProviders} from '@angular/core';
+
+import {PlxModalBackdrop} from './modal-backdrop';
+import {PlxModalWindow} from './modal-window';
+import {PlxModalStack} from './modal-stack';
+import {PlxModal} from './modal';
+
+export {PlxModal, PlxModalOptions} from './modal';
+export {PlxModalRef, PlxActiveModal} from './modal-ref';
+export {ModalDismissReasons} from './modal-dismiss-reasons';
+
+@NgModule({
+ declarations: [PlxModalBackdrop, PlxModalWindow],
+ entryComponents: [PlxModalBackdrop, PlxModalWindow],
+ providers: [PlxModal]
+})
+export class PlxModalModule {
+ public static forRoot(): ModuleWithProviders {
+ return {ngModule: PlxModalModule, providers: [PlxModal, PlxModalStack]};
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts
new file mode 100644
index 00000000..a99c86b1
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts
@@ -0,0 +1,597 @@
+import {Component, Injectable, ViewChild, OnDestroy, NgModule, getDebugNode, DebugElement} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {TestBed, ComponentFixture} from '@angular/core/testing';
+
+import {PlxModalModule, PlxModal, PlxActiveModal, PlxModalRef} from './modal.module';
+
+const NOOP = () => {
+};
+
+@Injectable()
+class SpyService {
+ called = false;
+}
+
+describe('plx-modal', () => {
+
+ let fixture: ComponentFixture<TestComponent>;
+
+ beforeEach(() => {
+ jasmine.addMatchers({
+ toHaveModal: function (util, customEqualityTests) {
+ return {
+ compare: function (actual, content?, selector?) {
+ const allModalsContent = document.querySelector(selector || 'body').querySelectorAll('.modal-content');
+ let pass = true;
+ let errMsg;
+
+ if (!content) {
+ pass = allModalsContent.length > 0;
+ errMsg = 'at least one modal open but found none';
+ } else if (Array.isArray(content)) {
+ pass = allModalsContent.length === content.length;
+ errMsg = `${content.length} modals open but found ${allModalsContent.length}`;
+ } else {
+ pass = allModalsContent.length === 1 && allModalsContent[0].textContent.trim() === content;
+ errMsg = `exactly one modal open but found ${allModalsContent.length}`;
+ }
+
+ return {pass: pass, message: `Expected ${actual.outerHTML} to have ${errMsg}`};
+ },
+ negativeCompare: function (actual) {
+ const allOpenModals = actual.querySelectorAll('plx-modal-window');
+
+ return {
+ pass: allOpenModals.length === 0,
+ message: `Expected ${actual.outerHTML} not to have any modals open but found ${allOpenModals.length}`
+ };
+ }
+ };
+ }
+ });
+
+ jasmine.addMatchers({
+ toHaveBackdrop: function (util, customEqualityTests) {
+ return {
+ compare: function (actual) {
+ return {
+ pass: document.querySelectorAll('plx-modal-backdrop').length === 1,
+ message: `Expected ${actual.outerHTML} to have exactly one backdrop element`
+ };
+ },
+ negativeCompare: function (actual) {
+ const allOpenModals = document.querySelectorAll('plx-modal-backdrop');
+
+ return {
+ pass: allOpenModals.length === 0,
+ message: `Expected ${actual.outerHTML} not to have any backdrop elements`
+ };
+ }
+ };
+ }
+ });
+ });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({imports: [OesModalTestModule]});
+ fixture = TestBed.createComponent(TestComponent);
+ });
+
+ afterEach(() => {
+ // detect left-over modals and close them or report errors when can't
+
+ const remainingModalWindows = document.querySelectorAll('plx-modal-window');
+ if (remainingModalWindows.length) {
+ fail(`${remainingModalWindows.length} modal windows were left in the DOM.`);
+ }
+
+ const remainingModalBackdrops = document.querySelectorAll('plx-modal-backdrop');
+ if (remainingModalBackdrops.length) {
+ fail(`${remainingModalBackdrops.length} modal backdrops were left in the DOM.`);
+ }
+ });
+
+ describe('basic functionality', () => {
+
+ it('should open and close modal with default options', () => {
+ const modalInstance = fixture.componentInstance.open('foo');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should open and close modal from a TemplateRef content', () => {
+ const modalInstance = fixture.componentInstance.openTpl();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Hello, World!');
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should properly destroy TemplateRef content', () => {
+ const spyService = fixture.debugElement.injector.get(SpyService);
+ const modalInstance = fixture.componentInstance.openDestroyableTpl();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Some content');
+ expect(spyService.called).toBeFalsy();
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(spyService.called).toBeTruthy();
+ });
+
+ it('should open and close modal from a component type', () => {
+ const spyService = fixture.debugElement.injector.get(SpyService);
+ const modalInstance = fixture.componentInstance.openCmpt(DestroyableCmpt);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Some content');
+ expect(spyService.called).toBeFalsy();
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(spyService.called).toBeTruthy();
+ });
+
+ it('should inject active modal ref when component is used as content', () => {
+ fixture.componentInstance.openCmpt(WithActiveModalCmpt);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Close');
+
+ (<HTMLElement>document.querySelector('button.closeFromInside')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should expose component used as modal content', () => {
+ const modalInstance = fixture.componentInstance.openCmpt(WithActiveModalCmpt);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('Close');
+ expect(modalInstance.componentInstance instanceof WithActiveModalCmpt).toBeTruthy();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should open and close modal from inside', () => {
+ fixture.componentInstance.openTplClose();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#close')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should open and dismiss modal from inside', () => {
+ fixture.componentInstance.openTplDismiss().result.catch(NOOP);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#dismiss')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should resolve result promise on close', () => {
+ let resolvedResult;
+ fixture.componentInstance.openTplClose().result.then((result) => resolvedResult = result);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#close')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ fixture.whenStable().then(() => {
+ expect(resolvedResult).toBe('myResult');
+ });
+ });
+
+ it('should reject result promise on dismiss', () => {
+ let rejectReason;
+ fixture.componentInstance.openTplDismiss().result.catch((reason) => rejectReason = reason);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#dismiss')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ fixture.whenStable().then(() => {
+ expect(rejectReason).toBe('myReason');
+ });
+ });
+
+ it('should add / remove "modal-open" class to body when modal is open', () => {
+ const modalRef = fixture.componentInstance.open('bar');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+ expect(document.body).toHaveCssClass('modal-open');
+
+ modalRef.close('bar result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(document.body).not.toHaveCssClass('modal-open');
+ });
+
+ it('should not throw when close called multiple times', () => {
+ const modalInstance = fixture.componentInstance.open('foo');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ modalInstance.close('some result');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not throw when dismiss called multiple times', () => {
+ const modalRef = fixture.componentInstance.open('foo');
+ modalRef.result.catch(NOOP);
+
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ modalRef.dismiss('some reason');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+
+ modalRef.dismiss('some reason');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+ });
+
+ describe('backdrop options', () => {
+
+ it('should have backdrop by default', () => {
+ const modalInstance = fixture.componentInstance.open('foo');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(fixture.nativeElement).toHaveBackdrop();
+
+ modalInstance.close('some reason');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+ });
+
+ it('should open and close modal without backdrop', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {backdrop: false});
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+
+ modalInstance.close('some reason');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+ });
+
+ it('should open and close modal without backdrop from template content', () => {
+ const modalInstance = fixture.componentInstance.openTpl({backdrop: false});
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('Hello, World!');
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+
+ modalInstance.close('some reason');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+ });
+
+ it('should dismiss on backdrop click', () => {
+ fixture.componentInstance.open('foo').result.catch(NOOP);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(fixture.nativeElement).toHaveBackdrop();
+
+ (<HTMLElement>document.querySelector('plx-modal-window')).click();
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(fixture.nativeElement).not.toHaveBackdrop();
+ });
+
+ it('should not dismiss on "static" backdrop click', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {backdrop: 'static'});
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(fixture.nativeElement).toHaveBackdrop();
+
+ (<HTMLElement>document.querySelector('plx-modal-window')).click();
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveModal();
+ expect(fixture.nativeElement).toHaveBackdrop();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not dismiss on clicks outside content where there is no backdrop', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {backdrop: false});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ (<HTMLElement>document.querySelector('plx-modal-window')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not dismiss on clicks that result in detached elements', () => {
+ const modalInstance = fixture.componentInstance.openTplIf({});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ (<HTMLElement>document.querySelector('button#if')).click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+ });
+
+ describe('container options', () => {
+
+ it('should attach window and backdrop elements to the specified container', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {container: '#testContainer'});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo', '#testContainer');
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should throw when the specified container element doesnt exist', () => {
+ const brokenSelector = '#notInTheDOM';
+ expect(() => {
+ fixture.componentInstance.open('foo', {container: brokenSelector});
+ }).toThrowError(`The specified modal container "${brokenSelector}" was not found in the DOM.`);
+ });
+ });
+
+ describe('keyboard options', () => {
+
+ it('should dismiss modals on ESC by default', () => {
+ fixture.componentInstance.open('foo').result.catch(NOOP);
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ (<DebugElement>getDebugNode(document.querySelector('plx-modal-window'))).triggerEventHandler('keyup.esc', {});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not dismiss modals on ESC when keyboard option is false', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {keyboard: false});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ (<DebugElement>getDebugNode(document.querySelector('plx-modal-window'))).triggerEventHandler('keyup.esc', {});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ it('should not dismiss modals on ESC when default is prevented', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {keyboard: true});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+
+ (<DebugElement>getDebugNode(document.querySelector('plx-modal-window'))).triggerEventHandler('keyup.esc', {
+ defaultPrevented: true
+ });
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal();
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+ });
+
+ describe('size options', () => {
+
+ it('should render modals with specified size', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {size: 'sm'});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(document.querySelector('.modal-dialog')).toHaveCssClass('modal-sm');
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ });
+
+ describe('custom class options', () => {
+
+ it('should render modals with the correct custom classes', () => {
+ const modalInstance = fixture.componentInstance.open('foo', {windowClass: 'bar'});
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(document.querySelector('plx-modal-window')).toHaveCssClass('bar');
+
+ modalInstance.close();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).not.toHaveModal();
+ });
+
+ });
+
+ describe('focus management', () => {
+
+ it('should focus modal window and return focus to previously focused element', () => {
+ fixture.detectChanges();
+ const openButtonEl = fixture.nativeElement.querySelector('button#open');
+
+ openButtonEl.focus();
+ openButtonEl.click();
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('from button');
+ expect(document.activeElement).toBe(document.querySelector('plx-modal-window'));
+
+ fixture.componentInstance.close();
+ expect(fixture.nativeElement).not.toHaveModal();
+ expect(document.activeElement).toBe(openButtonEl);
+ });
+
+
+ it('should return focus to body if no element focused prior to modal opening', () => {
+ const modalInstance = fixture.componentInstance.open('foo');
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveModal('foo');
+ expect(document.activeElement).toBe(document.querySelector('plx-modal-window'));
+
+ modalInstance.close('ok!');
+ expect(document.activeElement).toBe(document.body);
+ });
+ });
+
+ describe('window element ordering', () => {
+ it('should place newer windows on top of older ones', () => {
+ const modalInstance1 = fixture.componentInstance.open('foo', {windowClass: 'window-1'});
+ fixture.detectChanges();
+
+ const modalInstance2 = fixture.componentInstance.open('bar', {windowClass: 'window-2'});
+ fixture.detectChanges();
+
+ let windows = document.querySelectorAll('plx-modal-window');
+ expect(windows.length).toBe(2);
+ expect(windows[0]).toHaveCssClass('window-1');
+ expect(windows[1]).toHaveCssClass('window-2');
+
+ modalInstance2.close();
+ modalInstance1.close();
+ fixture.detectChanges();
+ });
+ });
+});
+
+@Component({selector: 'destroyable-cmpt', template: 'Some content'})
+export class DestroyableCmpt implements OnDestroy {
+ constructor(private _spyService: SpyService) {
+ }
+
+ ngOnDestroy(): void {
+ this._spyService.called = true;
+ }
+}
+
+@Component(
+ {selector: 'modal-content-cmpt', template: '<button class="closeFromInside" (click)="close()">Close</button>'})
+export class WithActiveModalCmpt {
+ constructor(public activeModal: PlxActiveModal) {
+ }
+
+ close() {
+ this.activeModal.close('from inside');
+ }
+}
+
+@Component({
+ selector: 'test-cmpt',
+ template: `
+ <div id="testContainer"></div>
+ <template #content>Hello, {{name}}!</template>
+ <template #destroyableContent><destroyable-cmpt></destroyable-cmpt></template>
+ <template #contentWithClose let-close="close"><button id="close" (click)="close('myResult')">Close me</button></template>
+ <template #contentWithDismiss let-dismiss="dismiss"><button id="dismiss" (click)="dismiss('myReason')">Dismiss me</button></template>
+ <template #contentWithIf>
+ <template [ngIf]="show">
+ <button id="if" (click)="show = false">Click me</button>
+ </template>
+ </template>
+ <button id="open" (click)="open('from button')">Open</button>
+ `
+})
+class TestComponent {
+ name = 'World';
+ openedModal: PlxModalRef;
+ show = true;
+ @ViewChild('content') tplContent;
+ @ViewChild('destroyableContent') tplDestroyableContent;
+ @ViewChild('contentWithClose') tplContentWithClose;
+ @ViewChild('contentWithDismiss') tplContentWithDismiss;
+ @ViewChild('contentWithIf') tplContentWithIf;
+
+ constructor(private modalService: PlxModal) {
+ }
+
+ open(content: string, options?: Object) {
+ this.openedModal = this.modalService.open(content, options);
+ return this.openedModal;
+ }
+
+ close() {
+ if (this.openedModal) {
+ this.openedModal.close('ok');
+ }
+ }
+
+ openTpl(options?: Object) {
+ return this.modalService.open(this.tplContent, options);
+ }
+
+ openCmpt(cmptType: any, options?: Object) {
+ return this.modalService.open(cmptType, options);
+ }
+
+ openDestroyableTpl(options?: Object) {
+ return this.modalService.open(this.tplDestroyableContent, options);
+ }
+
+ openTplClose(options?: Object) {
+ return this.modalService.open(this.tplContentWithClose, options);
+ }
+
+ openTplDismiss(options?: Object) {
+ return this.modalService.open(this.tplContentWithDismiss, options);
+ }
+
+ openTplIf(options?: Object) {
+ return this.modalService.open(this.tplContentWithIf, options);
+ }
+}
+
+@NgModule({
+ declarations: [TestComponent, DestroyableCmpt, WithActiveModalCmpt],
+ exports: [TestComponent, DestroyableCmpt],
+ imports: [CommonModule, PlxModalModule.forRoot()],
+ entryComponents: [DestroyableCmpt, WithActiveModalCmpt],
+ providers: [SpyService]
+})
+class OesModalTestModule {
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts
new file mode 100644
index 00000000..5935eee6
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts
@@ -0,0 +1,54 @@
+import {Injectable, Injector, ComponentFactoryResolver} from '@angular/core';
+import {PlxModalStack} from './modal-stack';
+import {PlxModalRef} from './modal-ref';
+
+/**
+ * Represent options available when opening new modal windows.
+ */
+export interface PlxModalOptions {
+ /**
+ * Whether a backdrop element should be created for a given modal (true by default).
+ * Alternatively, specify 'static' for a backdrop which doesn't close the modal on click.
+ */
+ backdrop?: boolean | 'static';
+
+ /**
+ * An element to which to attach newly opened modal windows.
+ */
+ container?: string;
+
+ /**
+ * Whether to close the modal when escape key is pressed (true by default).
+ */
+ keyboard?: boolean;
+
+ /**
+ * Size of a new modal window.
+ */
+ size?: 'sm' | 'lg';
+
+ /**
+ * Custom class to append to the modal window
+ */
+ windowClass?: string;
+}
+
+/**
+ * A service to open modal windows. Creating a modal is straightforward: create a template and pass it as an argument to
+ * the "open" method!
+ */
+@Injectable()
+export class PlxModal {
+ constructor(private _moduleCFR: ComponentFactoryResolver, private _injector: Injector, private _modalStack: PlxModalStack) {
+ }
+
+ /**
+ * Opens a new modal window with the specified content and using supplied options. Content can be provided
+ * as a TemplateRef or a component type. If you pass a component type as content than instances of those
+ * components can be injected with an instance of the PlxActiveModal class. You can use methods on the
+ * PlxActiveModal class to close / dismiss modals from "inside" of a component.
+ */
+ public open(content: any, options: PlxModalOptions = {}): PlxModalRef {
+ return this._modalStack.open(this._moduleCFR, this._injector, content, options);
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/index.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/index.ts
new file mode 100644
index 00000000..c677a944
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/index.ts
@@ -0,0 +1,8 @@
+export * from './text-input.component';
+export * from './text-input.module';
+export * from './ipv4-validator.directive';
+export * from './ipv6-validator.directive';
+export * from './max-validator.directive';
+export * from './min-validator.directive';
+export * from './text-input-ip.component';
+export * from './text-input-ip-address.component';
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv4-validator.directive.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv4-validator.directive.ts
new file mode 100644
index 00000000..312ea5f3
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv4-validator.directive.ts
@@ -0,0 +1,24 @@
+import {Directive, forwardRef} from '@angular/core';
+import {AbstractControl, NG_VALIDATORS, Validators} from '@angular/forms';
+
+@Directive({
+ selector: '[ipv4][ngModel],[ipv4][formControl],[ipv4][formControlName]',
+ providers: [{
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => Ipv4ValidatorDirective),
+ multi: true
+ }],
+})
+
+export class Ipv4ValidatorDirective {
+ validate(c: AbstractControl) {
+ if (Validators.required(c) !== undefined &&
+ Validators.required(c) !== null) {
+ return null;
+ }
+ const ipv4Reg =
+ /^((25[0-5]|2[0-4]\d|[0-1]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[0-1]?\d\d?)$/;
+ let regex = new RegExp(ipv4Reg);
+ return regex.test(c.value) ? null : {'ipv4': true};
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv6-validator.directive.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv6-validator.directive.ts
new file mode 100644
index 00000000..45182036
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv6-validator.directive.ts
@@ -0,0 +1,24 @@
+import {Directive, forwardRef} from '@angular/core';
+import {AbstractControl, NG_VALIDATORS, Validators} from '@angular/forms';
+
+@Directive({
+ selector: '[ipv6][ngModel],[ipv6][formControl],[ipv6][formControlName]',
+ providers: [{
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => Ipv6ValidatorDirective),
+ multi: true
+ }],
+})
+
+export class Ipv6ValidatorDirective {
+ validate(c: AbstractControl) {
+ if (Validators.required(c) !== undefined &&
+ Validators.required(c) !== null) {
+ return null;
+ }
+ const ipv6Reg =
+ /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
+ let regex = new RegExp(ipv6Reg);
+ return regex.test(c.value) ? null : {'ipv6': true};
+ }4
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/max-validator.directive.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/max-validator.directive.ts
new file mode 100644
index 00000000..143dccc6
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/max-validator.directive.ts
@@ -0,0 +1,49 @@
+import {AfterViewInit, Directive, ElementRef, forwardRef, Input} from '@angular/core';
+import {AbstractControl, NG_VALIDATORS, ValidatorFn, Validators} from '@angular/forms';
+
+import {NumberWrapperParseFloat} from '../core/number-wrapper-parse';
+
+@Directive({
+ selector: '[max][ngModel],[max][formControl],[max][formControlName]',
+ providers: [{
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => MaxValidatorDirective),
+ multi: true
+ }],
+})
+
+export class MaxValidatorDirective implements AfterViewInit {
+ private _validator: ValidatorFn;
+ private inputElement: any;
+ constructor(elementRef: ElementRef) {
+ this.inputElement = elementRef;
+ }
+ ngAfterViewInit() {
+ this.inputElement = this.inputElement.nativeElement.querySelector('input');
+ if (this.inputElement && this.inputElement.querySelector('input')) {
+ this._validator = max(NumberWrapperParseFloat(
+ this.inputElement.querySelector('input').getAttribute('max')));
+ }
+ }
+ @Input()
+ set max(maxValue: string) {
+ this._validator = max(NumberWrapperParseFloat(maxValue));
+ }
+
+ validate(c: AbstractControl): {[key: string]: any} {
+ return this._validator(c);
+ }
+}
+
+function max(maxvalue: number): ValidatorFn {
+ return (control: AbstractControl): {[key: string]: any} => {
+ if (Validators.required(control) !== undefined &&
+ Validators.required(control) !== null) {
+ return null;
+ }
+ let v: Number = Number(control.value);
+ return v > maxvalue ?
+ {'max': {'requiredValue': maxvalue, 'actualValue': v}} :
+ null;
+ };
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/min-validator.directive.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/min-validator.directive.ts
new file mode 100644
index 00000000..260a93ed
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/min-validator.directive.ts
@@ -0,0 +1,49 @@
+import {AfterViewInit, Directive, ElementRef, forwardRef, Input} from '@angular/core';
+import {AbstractControl, NG_VALIDATORS, ValidatorFn, Validators} from '@angular/forms';
+
+import {NumberWrapperParseFloat} from '../core/number-wrapper-parse';
+
+@Directive({
+ selector: '[min][ngModel],[min][formControl],[min][formControlName]',
+ providers: [{
+ provide: NG_VALIDATORS,
+ useExisting: forwardRef(() => MinValidatorDirective),
+ multi: true
+ }],
+})
+
+export class MinValidatorDirective implements AfterViewInit {
+ private _validator: ValidatorFn;
+ private inputElement: any;
+ constructor(elementRef: ElementRef) {
+ this.inputElement = elementRef;
+ }
+ ngAfterViewInit() {
+ this.inputElement = this.inputElement.nativeElement.querySelector('input');
+ if (this.inputElement && this.inputElement.querySelector('input')) {
+ this._validator = min(NumberWrapperParseFloat(
+ this.inputElement.querySelector('input').getAttribute('min')));
+ }
+ }
+ @Input()
+ set min(minValue: string) {
+ this._validator = min(NumberWrapperParseFloat(minValue));
+ }
+
+ validate(c: AbstractControl): {[key: string]: any} {
+ return this._validator(c);
+ }
+}
+
+function min(minvalue: number): ValidatorFn {
+ return (control: AbstractControl): {[key: string]: any} => {
+ if (Validators.required(control) !== undefined &&
+ Validators.required(control) !== null) {
+ return null;
+ }
+ let v: Number = Number(control.value);
+ return v < minvalue ?
+ {'min': {'requiredValue': minvalue, 'actualValue': v}} :
+ null;
+ };
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip-address.component.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip-address.component.ts
new file mode 100644
index 00000000..501d2326
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip-address.component.ts
@@ -0,0 +1,170 @@
+import {Component, EventEmitter, forwardRef, Input, OnInit, Output} from '@angular/core';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {BooleanFieldValue} from '../core/boolean-field-value';
+
+const noop = () => {};
+
+export const PX_TEXT_INPUT_IP_ADDRESS_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => PlxTextInputIpAddressComponent),
+ multi: true
+};
+@Component({
+ selector: 'plx-text-input-ip-address',
+ template: `
+ <div>
+ <plx-text-input #textInputIpAddress type="text" [(ngModel)]="ipValue"
+ [width]="'280px'" [numberShowSpinner]="false" minlength="1"
+ (keyup)="keyUp($event)" (paste)="paste($event)" class="{{sizeClass}}"></plx-text-input>
+ <div class="plx-text-input-error">{{errMsg}}</div>
+ </div>
+ `,
+ styleUrls: ['text-input.less'],
+ host: {'style': 'display: inline-block;'},
+ providers: [PX_TEXT_INPUT_IP_ADDRESS_CONTROL_VALUE_ACCESSOR]
+})
+
+export class PlxTextInputIpAddressComponent implements OnInit, ControlValueAccessor {
+ @Input() lang: string = 'zh'; //zh|en
+ @Input() size: string; //空代表普通尺寸,sm代表小尺寸
+ @Input() ipAddressCheckTip: string = ''; //
+
+ @Input() @BooleanFieldValue() required: boolean = false;
+
+ @Input() public ipValue: string;
+ @Output() public ipValueChange: EventEmitter<any> = new EventEmitter<any>();
+
+ @Input() public ipValueFlg : boolean;
+ @Output() public ipValueFlgChange: EventEmitter<any> = new EventEmitter<any>();
+
+ private isNull : boolean = true;
+
+ /** Callback registered via registerOnTouched (ControlValueAccessor) */
+ private _onTouchedCallback: () => void = noop;
+ /** Callback registered via registerOnChange (ControlValueAccessor) */
+ private _onChangeCallback: (_: any) => void = noop;
+
+ public errMsgs = {
+ 'zh': {
+ 'empty': '此项不能为空',
+ 'invalidate': 'IP格式不对',
+ 'range': '请输入正确的IPV4地址或IPV6地址',
+ 'range-IPV4': '请输入正确的IPV4',
+ 'range-IPV6': '请输入正确的IPV6'
+ },
+ 'en': {
+ 'empty': 'IP can not be empty',
+ 'invalidate': 'IP format is incorrect',
+ 'range': 'IP range is IPV4 or IPV6',
+ 'range-IPV4': 'IP range is IPV4',
+ 'range-IPV6': 'IP range is IPV6'
+ }
+ };
+ public errMsg: string;
+ public sizeClass: string;
+
+ constructor() {
+ }
+
+ ngOnInit(): void {
+ if (this.size === 'sm') {
+ this.sizeClass = 'plx-input-sm';
+ }
+ this.isNull = this.ipValueFlg;
+ if(this.repIPStr(this.ipValue) === ''&& !this.ipValueFlg){
+ this.ipValueFlg = false;
+ this.emitValue();
+ }
+ }
+
+ public keyUp(event: any): void {
+ this.setValueToOutside(this.validate());
+ this.emitValue();
+ }
+
+ public paste(event: any): void{
+ setTimeout(() => {
+ this.ipValue = event.target.value;
+ this.setValueToOutside(this.validate());
+ this.emitValue();
+ }, 0);
+ }
+
+ private emitValue(){
+ this.ipValueChange.emit(this.ipValue);
+ this.ipValueFlgChange.emit(this.ipValueFlg);
+ }
+
+ private setValueToOutside(validateFlg: boolean): void {
+ this.ipValueFlg = validateFlg;
+ let value;
+ if (validateFlg) {
+ if (this.ipValue) {
+ value = this.ipValue;
+ }
+ if(this.ipValue === "" && !this.isNull){
+ this.ipValueFlg = false;
+ }
+ } else {
+ value = false;
+ }
+ this._onChangeCallback(value);
+ }
+
+ writeValue(value: any): void {
+ //
+ this.errMsg = '';
+ this.ipValue = value;
+ if (value) {
+ this.validate();
+ }
+ }
+
+ registerOnChange(fn: any) {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any) {
+ this._onTouchedCallback = fn;
+ }
+
+ public validate(): boolean {
+ this.errMsg = '';
+ if (this.required) {
+ if (!this.ipValue) {
+ this.errMsg = this.errMsgs[this.lang]['empty'];
+ return false;
+ }
+ }
+ if ((this.ipValue) && (!this.ipValue)) {
+ this.errMsg = this.errMsgs[this.lang]['invalidate'];
+ return false;
+ }
+ let blackStr = this.repIPStr(this.ipValue);
+ if(this.ipAddressCheckTip === ''){
+ if(this.ipValue !== '' && blackStr === ''){
+ this.errMsg = this.errMsgs[this.lang]['range'];
+ return false;
+ }
+ }else{
+ if(this.ipValue !== '' && this.ipAddressCheckTip !== blackStr) {
+ this.errMsg = this.errMsgs[this.lang]['range-'+ this.ipAddressCheckTip];
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private repIPStr(value: any): string {
+ let blackStr = '';
+ var regip4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
+ if (regip4.test(value)) {
+ return "IPV4";
+ }
+ var regip6 = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
+ if (regip6.test(value)) {
+ return "IPV6";
+ }
+ return blackStr;
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip.component.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip.component.ts
new file mode 100644
index 00000000..7c9d616d
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip.component.ts
@@ -0,0 +1,189 @@
+import {Component, forwardRef, Input, OnInit} from '@angular/core';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+
+import {BooleanFieldValue} from '../core/boolean-field-value';
+
+const noop = () => {};
+
+export const PX_TEXT_INPUT_IP_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => PlxTextInputIpComponent),
+ multi: true
+};
+
+@Component({
+ selector: 'plx-text-input-ip',
+ template: `
+ <div>
+ <plx-text-input #textInputIp1 type="number" [(ngModel)]="ipValue1"
+ [width]="'45px'" [numberShowSpinner]="false" maxLength="3" minlength="1"
+ (keyup)="keyup($event, null, textInputIp2)" (change)="change($event)"
+ [ngClass]="{'plx-input-sm': size==='sm', 'plx-text-input-ip-invalid': errMsg}"></plx-text-input>
+ <span class="plx-text-input-ip-dot">.</span>
+ <plx-text-input #textInputIp2 type="number" [(ngModel)]="ipValue2"
+ [width]="'45px'" [numberShowSpinner]="false" maxLength="3" minlength="1"
+ (keyup)="keyup($event, textInputIp1, textInputIp3)" (change)="change($event)"
+ [ngClass]="{'plx-input-sm': size==='sm', 'plx-text-input-ip-invalid': errMsg}"></plx-text-input>
+ <span class="plx-text-input-ip-dot">.</span>
+ <plx-text-input #textInputIp3 type="number" [(ngModel)]="ipValue3"
+ [width]="'45px'" [numberShowSpinner]="false" maxLength="3" minlength="1"
+ (keyup)="keyup($event, textInputIp2, textInputIp4)" (change)="change($event)"
+ [ngClass]="{'plx-input-sm': size==='sm', 'plx-text-input-ip-invalid': errMsg}"></plx-text-input>
+ <span class="plx-text-input-ip-dot">.</span>
+ <plx-text-input #textInputIp4 type="number" [(ngModel)]="ipValue4"
+ [width]="'45px'" [numberShowSpinner]="false" maxLength="3" minlength="1"
+ (keyup)="keyup($event, textInputIp3, null)" (change)="change($event)"
+ [ngClass]="{'plx-input-sm': size==='sm', 'plx-text-input-ip-invalid': errMsg}"></plx-text-input>
+ <div class="plx-text-input-error">{{errMsg}}</div>
+ </div>
+ `,
+ styleUrls: ['text-input.less'],
+ host: {'style': 'display: inline-block;'},
+ providers: [PX_TEXT_INPUT_IP_CONTROL_VALUE_ACCESSOR]
+})
+
+export class PlxTextInputIpComponent implements OnInit, ControlValueAccessor {
+ @Input() lang: string = 'zh'; //zh|en
+ @Input() size: string; //空代表普通尺寸,sm代表小尺寸
+ @Input() @BooleanFieldValue() required: boolean = false;
+ /** Callback registered via registerOnTouched (ControlValueAccessor) */
+ private _onTouchedCallback: () => void = noop;
+ /** Callback registered via registerOnChange (ControlValueAccessor) */
+ private _onChangeCallback: (_: any) => void = noop;
+ public ipValue1: number;
+ public ipValue2: number;
+ public ipValue3: number;
+ public ipValue4: number;
+ public errMsgs = {
+ 'zh': {
+ 'empty': 'IP不能为空',
+ 'invalidate': '非法IP',
+ 'range': 'IP范围[0.0.0.0]~[255.255.255.255]'
+ },
+ 'en': {
+ 'empty': 'IP can not be empty',
+ 'invalidate': 'Invalid IP',
+ 'range': 'IP range is [0.0.0.0]~[255.255.255.255]'
+ }
+ };
+ public errMsg: string;
+
+ constructor() {
+ }
+
+ ngOnInit(): void {
+ }
+
+ public change(event: any) :void {
+ event.target.value = this.repNumber(event.target.value);
+ this.setValueToOutside(this.validate());
+ }
+
+ public keyup(event: any, frontElement: any, backElement: any): void {
+ event.target.value = this.repNumber(event.target.value);
+ if (((event.keyCode === 13 || event.keyCode === 110 || event.keyCode === 190)
+ && event.target.value.length !== 0)
+ || event.target.value.length === 3) { //enter:13,dot:110、190
+ if (event.keyCode !== 9) { //tab:9
+ if (backElement) {
+ backElement.autoFocus = true;
+ backElement.inputViewChild.nativeElement.select();
+ } else {
+ if (event.keyCode !== 110 && event.keyCode !== 190) {
+ event.target.blur();
+ }
+ }
+ }
+ }
+
+ if (event.target.value.length === 0 // backspace:8 delete:46
+ && (event.keyCode === 8 || event.keyCode === 46)) {
+ if (frontElement) {
+ frontElement.autoFocus = true;
+ frontElement.inputViewChild.nativeElement.select();
+ }
+ }
+
+ this.setValueToOutside(this.validate());
+ }
+
+ private setValueToOutside(validateFlg: boolean): void {
+ let value;
+ if (validateFlg) {
+ if (this.ipValue1 && this.ipValue2 && this.ipValue3 && this.ipValue4) {
+ value = this.ipValue1 + '.'
+ + this.ipValue2 + '.'+ this.ipValue3 + '.'+ this.ipValue4;
+ }
+ } else {
+ value = false;
+ }
+ this._onChangeCallback(value);
+ }
+
+ writeValue(value: any): void {
+ //
+ this.errMsg = '';
+ if (value) {
+ if (this.isIPStr(value)) {
+ let ipArr = value.split('.');
+ this.ipValue1 = ipArr[0];
+ this.ipValue2 = ipArr[1];
+ this.ipValue3 = ipArr[2];
+ this.ipValue4 = ipArr[3];
+ } else {
+ this.errMsg = this.errMsgs[this.lang]['invalidate'] + ' : ' + value;
+ }
+ }
+ }
+
+ registerOnChange(fn: any) {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any) {
+ this._onTouchedCallback = fn;
+ }
+
+ public validate(): boolean {
+ this.errMsg = '';
+ if (this.required) {
+ if (!this.ipValue1 && !this.ipValue2 && !this.ipValue3 && !this.ipValue4) {
+ this.errMsg = this.errMsgs[this.lang]['empty'];
+ return false;
+ }
+ }
+ if ((this.ipValue1 || this.ipValue2 || this.ipValue3 || this.ipValue4)
+ && (!this.ipValue1 || !this.ipValue2 || !this.ipValue3 || !this.ipValue4)) {
+ this.errMsg = this.errMsgs[this.lang]['invalidate'];
+ return false;
+ }
+ if ((this.ipValue1 && (this.ipValue1 < 0 || this.ipValue1 > 255))
+ || (this.ipValue2 && (this.ipValue2 < 0 || this.ipValue2 > 255))
+ || (this.ipValue3 && (this.ipValue3 < 0 || this.ipValue3 > 255))
+ || (this.ipValue4 && (this.ipValue4 < 0 || this.ipValue4 > 255))) {
+ this.errMsg = this.errMsgs[this.lang]['range'];
+ return false;
+ }
+
+ return true;
+ }
+
+ private repNumber(value: any): number {
+ var reg = /^[\d]+$/g;
+ if (!reg.test(value)) {
+ let txt = value;
+ txt.replace(/[^0-9]+/, function (char, index, val) { //匹配第一次非数字字符
+ value = val.replace(/\D/g, ""); //将非数字字符替换成""
+ })
+ }
+ return value;
+ }
+
+ private isIPStr(value: any): boolean {
+ var regip4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;
+ if (regip4.test(value)) {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.component.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.component.ts
new file mode 100644
index 00000000..9b5a01e9
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.component.ts
@@ -0,0 +1,765 @@
+import {AfterContentInit, Component, ElementRef, EventEmitter, forwardRef, HostBinding, HostListener, Input, OnInit, Output, Renderer2, ViewChild} from '@angular/core';
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
+import {Observable} from 'rxjs/Observable';
+
+import {BooleanFieldValue} from '../core/boolean-field-value';
+import {NumberWrapperParseFloat} from '../core/number-wrapper-parse';
+import {UUID} from '../core/uuid';
+
+const noop = () => {};
+
+export const PX_TEXT_INPUT_CONTROL_VALUE_ACCESSOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => PlxTextInputComponent),
+ multi: true
+};
+
+@Component({
+ selector: 'plx-text-input',
+ templateUrl: 'text-input.html',
+ styleUrls: ['text-input.less'],
+ host: {'style': 'display: inline-block;'},
+ providers: [PX_TEXT_INPUT_CONTROL_VALUE_ACCESSOR]
+})
+
+export class PlxTextInputComponent implements ControlValueAccessor, OnInit,
+ AfterContentInit {
+ private _focused: boolean = false;
+ private _value: any = '';
+ /** Callback registered via registerOnTouched (ControlValueAccessor) */
+ private _onTouchedCallback: () => void = noop;
+ /** Callback registered via registerOnChange (ControlValueAccessor) */
+ private _onChangeCallback: (_: any) => void = noop;
+
+ /** Readonly properties. */
+ get empty() {
+ return this._value === null || this._value === '';
+ }
+ get inputId(): string {
+ return `${this.id}`;
+ }
+ get isShowHintLabel() {
+ return this._focused && this.hintLabel !== null;
+ }
+ get inputType() {
+ return this.type === 'number' ? 'text' : this.type;
+ }
+
+ @Input() id: string = `plx-input-${UUID.UUID()}`;
+ @Input() name: string = null;
+ @Input() hintLabel: string = null;
+ @Input() lang: string = 'zh';
+ @Input() @BooleanFieldValue() disabled: boolean = false;
+
+ @Input() numberShowSpinner = true;
+ @Input() max: string|number = null;
+ @Input() maxLength: number = 64;
+ @Input() min: string|number = null;
+ @Input() minLength: number = null;
+ @Input() placeholder: string = '';
+ @Input() @BooleanFieldValue() readOnly: boolean = false;
+ @Input() @BooleanFieldValue() required: boolean = false;
+ @Input() @BooleanFieldValue() notShowOption: boolean = true;
+ @Input() type: string = 'text';
+ @Input() tabIndex: number = null;
+ @Input() pattern: string = null;
+
+ @Input() @BooleanFieldValue() shortInput: boolean = false;
+ @Input() unit: string = null;
+ @Input() unitOptions: string[] = null;
+ @Input() prefix: string = null;
+ @Input() suffixList: string[] = null;
+
+ // @Input() precision: number = 0;
+ @Input() step: number = 1;
+ @Input() width: string = '400px';
+ @Input() unitWidth: string = '45px';
+ @Input() unitOptionWidth: string = '84px';
+ @Input() prefixWidth: string = '70px';
+ @Input() historyList: string[];
+
+ @Input() prefixOptions: string[] = [];
+ @Input() prefixOptionWidth: string = '84px';
+
+ @Input() @BooleanFieldValue() passwordSwitch: boolean = false;
+
+ @ViewChild('input') inputViewChild: ElementRef;
+ @ViewChild('inputOutter') pxTextInputElement: ElementRef;
+
+ @HostBinding('class.input-invalid') selectClass: boolean = true;
+
+ isDisabledUp: boolean = false;
+ isDisabledDown: boolean = false;
+ displayDataList: string[];
+ currentPrecision: number = 0;
+ keyPattern: RegExp = /[0-9\-]/;
+ langPattern: RegExp =
+ /[a-zA-Z]|[\u4e00-\u9fa5]|[\uff08\uff09\u300a\u300b\u2014\u2014\uff1a\uff1b\uff0c\u201c\u201d\u2018\u2019\+\=\{\}\u3001\u3002\u3010\u3011\<\>\uff01\uff1f\/\|]/g;
+ timer: any;
+ optionalLabel: string = null;
+ hasSelection = false;
+ _precision: number = 0;
+ displayValue: any;
+
+ isOpenDataList: boolean = false;
+ dataListClicked: boolean = false;
+ isOpenSuffixList: boolean = false;
+ suffixListClicked: boolean = false;
+
+ showUnit: string;
+ isShowUnitOption: boolean = false;
+ unitOptionClicked: boolean = false;
+
+ prefixOptionClicked: boolean = false;
+ isShowPrefixOption = false;
+ showPrefix: string;
+ showPassword: boolean = false;
+ tooltipText: string;
+ tooltipTexts = {
+ 'zh': {
+ 'true': '隐藏',
+ 'false': '显示',
+ },
+ 'en': {
+ 'true': 'hidden',
+ 'false': 'show',
+ }
+ };
+ isPwdSwithHover: boolean = false;
+ isPwdSwithClick: boolean = false;
+
+ _autoFocus: boolean = false;
+ @Input()
+ set autoFocus(value: boolean) {
+ this._autoFocus = value;
+
+ const that = this;
+ if (this._autoFocus) {
+ setTimeout(() => {
+ that.inputViewChild.nativeElement.focus();
+ }, 0);
+ }
+ }
+ get autoFocus() {
+ return this._autoFocus;
+ }
+
+ @Input()
+ set precision(value: string) {
+ this._precision = parseInt(value);
+ }
+ get precision() {
+ return this._value;
+ }
+
+ get inputWidth() {
+ if (this.prefixOptions && this.prefixOptions.length > 0) {
+ if (this.unitOptions && this.unitOptions.length > 0) {
+ return `calc(${this.width} - ${this.prefixOptionWidth} - ${this.unitOptionWidth})`;
+ } else if (this.unit !== null) {
+ return `calc(${this.width} - ${this.prefixOptionWidth} - ${this.unitWidth})`;
+ } else {
+ return `calc(${this.width} - ${this.prefixOptionWidth})`;
+ }
+ }
+
+ if (this.unit !== null && this.prefix !== null) {
+ return `calc(${this.width} - ${this.unitWidth} - ${this.prefixWidth})`;
+ }
+
+ if (this.unit !== null) {
+ return `calc(${this.width} - ${this.unitWidth})`;
+ }
+
+ if (!!this.unitOptions && this.unitOptions.length !== 0 &&
+ this.prefix !== null) {
+ return `calc(${this.width} - ${this.unitOptionWidth} - ${
+ this.prefixWidth
+ })`;
+ }
+
+ if (!!this.unitOptions && this.unitOptions.length !== 0) {
+ return `calc(${this.width} - ${this.unitOptionWidth})`;
+ }
+ if (this.prefix !== null) {
+ return `calc(${this.width} - ${this.prefixWidth})`;
+ }
+ return this.width;
+ }
+
+
+ get hasUnit() {
+ return this.unit !== null;
+ }
+ get hasUnitOption() {
+ return this.showUnit !== undefined;
+ }
+ get hasPrefix() {
+ return this.prefix !== null;
+ }
+ get hasPrefixOption() {
+ return this.prefixOptions && this.prefixOptions.length > 0;
+ }
+ get isFocus() {
+ return this._focused;
+ }
+
+ private _blurEmitter: EventEmitter<FocusEvent> =
+ new EventEmitter<FocusEvent>();
+ private _focusEmitter: EventEmitter<FocusEvent> =
+ new EventEmitter<FocusEvent>();
+ private click = new EventEmitter<any>();
+ private unitChange = new EventEmitter<any>();
+ @Output() public prefixChange = new EventEmitter<any>();
+
+ @Output('blur')
+ get onBlur(): Observable<FocusEvent> {
+ return this._blurEmitter.asObservable();
+ }
+
+ @Output('focus')
+ get onFocus(): Observable<FocusEvent> {
+ return this._focusEmitter.asObservable();
+ }
+
+ @HostListener('focus')
+ onHostFocus() {
+ this.renderer.addClass(this.el.nativeElement, 'input-focus');
+ this.renderer.removeClass(this.el.nativeElement, 'input-blur');
+ }
+
+ @HostListener('blur')
+ onHostBlur() {
+ this.renderer.addClass(this.el.nativeElement, 'input-blur');
+ this.renderer.removeClass(this.el.nativeElement, 'input-focus');
+ }
+
+ @Input()
+ set value(v: any) {
+ v = this.filterZhChar(v);
+ v = this._convertValueForInputType(v);
+ if (v !== this._value) {
+ this._value = v;
+ if (this.type === 'number') {
+ if (this._value === '') {
+ this._onChangeCallback(null);
+ } else if (isNaN(this._value)) {
+ this._onChangeCallback(this._value);
+ } else {
+ this._onChangeCallback(NumberWrapperParseFloat(this._value));
+ }
+ } else {
+ this._onChangeCallback(this._value);
+ }
+ }
+ }
+ get value(): any {
+ return this._value;
+ }
+
+ constructor(
+ private el: ElementRef, private renderer: Renderer2) {}
+
+ ngOnInit() {
+ if (this.shortInput) {
+ this.width = '120px';
+ this.unitWidth = '40px';
+ this.prefixWidth = '40px';
+ }
+ this.translateLabel();
+
+ if (!!this.unitOptions && this.unitOptions.length !== 0) {
+ this.showUnit = this.unitOptions[0];
+ }
+
+ if (!!this.prefixOptions && this.prefixOptions.length !== 0) {
+ this.showPrefix = this.prefixOptions[0];
+ }
+ }
+
+ ngAfterContentInit() {
+ if (this.pxTextInputElement) {
+ Array.from(this.pxTextInputElement.nativeElement.childNodes)
+ .forEach((node: HTMLElement) => {
+ if (node.nodeType === 3) {
+ this.pxTextInputElement.nativeElement.removeChild(node);
+ }
+ });
+ }
+ }
+ private translateLabel() {
+ if (this.lang === 'zh') {
+ this.optionalLabel = '(可选)';
+ } else {
+ this.optionalLabel = '(Optional)';
+ }
+ }
+
+ _handleFocus(event: FocusEvent) {
+ this._focused = true;
+ this._focusEmitter.emit(event);
+ }
+
+ _handleBlur(event: FocusEvent) {
+ this._focused = false;
+ this._onTouchedCallback();
+ this._blurEmitter.emit(event);
+ }
+
+ _checkValueLimit(value: any) {
+ if (this.type === 'number') {
+ if ((value === '' || value === undefined || value === null) &&
+ !this.required) {
+ return '';
+ } else if (
+ this.min !== null &&
+ NumberWrapperParseFloat(value) < NumberWrapperParseFloat(this.min)) {
+ return this.min;
+ } else if (
+ this.max !== null &&
+ NumberWrapperParseFloat(value) > NumberWrapperParseFloat(this.max)) {
+ return this.max;
+ } else {
+ return value;
+ }
+ }
+ return value;
+ }
+
+ _checkValue() {
+ this.value = this._checkValueLimit(this.value);
+ this.displayValue = this.value;
+ }
+
+ _handleChange(event: Event) {
+ this.value = (<HTMLInputElement>event.target).value;
+ this._onTouchedCallback();
+ }
+
+ openDataList() {
+ this.dataListClicked = true;
+ if (this.historyList) {
+ if (this.value) {
+ this.filterOption(this.value);
+ } else {
+ this.displayDataList = this.historyList;
+ if (!this.isOpenDataList) {
+ this.isOpenDataList = true;
+ }
+ }
+ }
+ }
+
+ _handleClick(event: Event) {
+ if (this.isShowUnitOption) {
+ this.isShowUnitOption = false;
+ }
+ this.click.emit(event);
+
+ if (this.historyList) {
+ this.openDataList();
+ }
+ }
+
+ _handleSelect(event: Event) { // 输入框文本被选中时处理
+ if (!this.hasSelection) {
+ this.hasSelection = true;
+ }
+ }
+ deleteSelection() { // 删除选中文本,
+ document.getSelection().deleteFromDocument();
+ this.hasSelection = false;
+ }
+
+ _onWindowClick(event: Event) {
+ if (this.historyList) {
+ if (!this.dataListClicked) {
+ this.isOpenDataList = false;
+ }
+ this.dataListClicked = false;
+ }
+
+ if (this.suffixList) {
+ if (!this.suffixListClicked) {
+ this.isOpenSuffixList = false;
+ }
+ this.suffixListClicked = false;
+ }
+
+ if (this.unitOptions) {
+ if (!this.unitOptionClicked) {
+ this.isShowUnitOption = false;
+ }
+ this.unitOptionClicked = false;
+ }
+
+ if (this.prefixOptions) {
+ if (!this.prefixOptionClicked) {
+ this.isShowPrefixOption = false;
+ }
+ this.prefixOptionClicked = false;
+ }
+ }
+
+ _showPrefixOption(event: Event) {
+ this.isShowPrefixOption = !this.isShowPrefixOption;
+ this.prefixOptionClicked = true;
+ }
+
+ _showUnitOption(event: Event) {
+ this.isShowUnitOption = !this.isShowUnitOption;
+ this.unitOptionClicked = true;
+ }
+
+ _setUnit(unitValue: string) {
+ this.unitOptionClicked = true;
+ this.showUnit = unitValue;
+ this.unitChange.emit(unitValue);
+ this.isShowUnitOption = false;
+ }
+
+ _setPrefix(value: string) {
+ if (value !== this.showPrefix) {
+ this.showPrefix = value;
+ this.prefixChange.emit(value);
+ }
+ this.prefixOptionClicked = true;
+ this.isShowPrefixOption = false;
+ }
+
+ filterOption(value: any) {
+ this.displayDataList = [];
+ this.displayDataList = this.historyList.filter((data: string) => {
+ return data.toLowerCase().indexOf(value.toLowerCase()) > -1;
+ });
+
+ this.isOpenDataList = this.displayDataList.length !== 0;
+ }
+
+ concatValueAndSuffix(value: string) {
+ const that = this;
+ that.displayDataList = [];
+ let mark = '@';
+ if (value === '') {
+ that.displayDataList = [];
+ } else if (value.trim().toLowerCase().indexOf(mark) === -1) {
+ that.displayDataList.push(value);
+ that.suffixList.map((item: string) => {
+ let tempValue = value + mark + item;
+ that.displayDataList.push(tempValue);
+ });
+ } else {
+ that.suffixList.map((item: string) => {
+ let tempValue = value.split(mark)[0] + mark + item;
+ that.displayDataList.push(tempValue);
+ });
+ that.displayDataList = that.displayDataList.filter((item: string) => {
+ return item.trim().toLowerCase().indexOf(value) > -1;
+ });
+ }
+
+ that.isOpenSuffixList = that.displayDataList.length !== 0;
+ }
+
+ _handleInput(event: any) {
+ let inputValue = event.target.value.trim().toLowerCase();
+
+ if (this.historyList) {
+ this.filterOption(inputValue);
+ }
+
+ if (this.suffixList) {
+ this.concatValueAndSuffix(inputValue);
+ }
+ }
+
+ chooseInputData(data: any) {
+ this.displayValue = data;
+ this.value = data;
+
+ this.isOpenDataList = false;
+ this.dataListClicked = true;
+ this.isOpenSuffixList = false;
+ this.suffixListClicked = true;
+ }
+
+ writeValue(value: any) {
+ this.displayValue = this._checkValueLimit(value);
+ this._value = this.displayValue;
+ }
+
+ registerOnChange(fn: any) {
+ this._onChangeCallback = fn;
+ }
+
+ registerOnTouched(fn: any) {
+ this._onTouchedCallback = fn;
+ }
+
+ private getConvertValue(v: any) {
+ this.currentPrecision = v.toString().split('.')[1].length;
+ if (this.currentPrecision === 0) { // 输入小数点,但小数位数为0时
+ return v;
+ }
+ if (this.currentPrecision <
+ this._precision) { // 输入小数点,且小数位数不为0
+ return this.toFixed(v, this.currentPrecision);
+ } else {
+ return this.toFixed(v, this._precision);
+ }
+ }
+
+ private filterZhChar(v: any) {
+ if (this.type === 'number') {
+ let reg = /[^0-9.-]/;
+ while (reg.test(v)) {
+ v = v.replace(reg, '');
+ }
+ }
+ return v;
+ }
+
+ private _convertValueForInputType(v: any): any {
+ switch (this.type) {
+ case 'number': {
+ if (v === '' || v === '-') {
+ return v;
+ }
+
+ if (v.toString().indexOf('.') === -1) { // 整数
+ return this.toFixed(v, 0);
+ } else {
+ return this.getConvertValue(v);
+ }
+ }
+ default:
+ return v;
+ }
+ }
+
+ private toFixed(value: number, precision: number) {
+ return Number(value).toFixed(precision);
+ }
+
+ repeat(interval: number, dir: number) {
+ let i = interval || 500;
+
+ this.clearTimer();
+ this.timer = setTimeout(() => {
+ this.repeat(40, dir);
+ }, i);
+
+ this.spin(dir);
+ }
+
+ clearTimer() {
+ if (this.timer) {
+ clearInterval(this.timer);
+ }
+ }
+
+ spin(dir: number) {
+ let step = this.step * dir;
+ let currentValue = this._convertValueForInputType(this.value) || 0;
+
+ this.value = Number(currentValue) + step;
+
+ if (this.maxLength !== null &&
+ this.value.toString().length > this.maxLength) {
+ this.value = currentValue;
+ }
+
+ if (this.min !== null && this.value <= NumberWrapperParseFloat(this.min)) {
+ this.value = this.min;
+ this.isDisabledDown = true;
+ }
+
+ if (this.max !== null && this.value >= NumberWrapperParseFloat(this.max)) {
+ this.value = this.max;
+ this.isDisabledUp = true;
+ }
+ this.displayValue = this.value;
+ this._onChangeCallback(NumberWrapperParseFloat(this.value));
+ }
+
+ onUpButtonMousedown(event: Event, input: HTMLInputElement) {
+ if (!this.disabled && this.type === 'number') {
+ input.focus();
+ this.repeat(null, 1);
+ event.preventDefault();
+ }
+ }
+
+ onUpButtonMouseup(event: Event) {
+ if (!this.disabled) {
+ this.clearTimer();
+ }
+ }
+
+ onUpButtonMouseleave(event: Event) {
+ if (!this.disabled) {
+ this.clearTimer();
+ }
+ }
+
+ onDownButtonMousedown(event: Event, input: HTMLInputElement) {
+ if (!this.disabled && this.type === 'number') {
+ input.focus();
+ this.repeat(null, -1);
+ event.preventDefault();
+ }
+ }
+
+ onDownButtonMouseup(event: Event) {
+ if (!this.disabled) {
+ this.clearTimer();
+ }
+ }
+
+ onDownButtonMouseleave(event: Event) {
+ if (!this.disabled) {
+ this.clearTimer();
+ }
+ }
+
+ onInputKeydown(event: KeyboardEvent) {
+ if (this.type === 'number') {
+ if (event.which === 229) {
+ event.preventDefault();
+ return;
+ } else if (event.which === 38) {
+ this.spin(1);
+ event.preventDefault();
+ } else if (event.which === 40) {
+ this.spin(-1);
+ event.preventDefault();
+ }
+ }
+ }
+ onInputKeyPress(event: KeyboardEvent) {
+ let inputChar = String.fromCharCode(event.charCode);
+ if (this.type === 'number') {
+ if (event.which === 8) {
+ return;
+ }
+
+ // if (!this.isValueLimit()) {
+ // this.handleSelection(event);
+ // }
+
+ if (inputChar === '-' && this.min !== null &&
+ NumberWrapperParseFloat(this.min) >= 0) {
+ event.preventDefault();
+ return;
+ }
+ if (this.isIllegalNumberInputChar(event) ||
+ this.isIllegalIntergerInput(inputChar)) {
+ event.preventDefault();
+ return;
+ }
+ if (this.isIllegalFloatInput(
+ inputChar)) { // 当该函数返回true时,执行两种情景
+ this.handleSelection(event);
+ }
+ if (this.hasSelection) { // 文本被选中,执行文本替换
+ this.deleteSelection();
+ }
+ }
+ }
+
+ private handleSelection(event: any) {
+ if (this.hasSelection) { // 文本被选中,执行文本替换
+ this.deleteSelection();
+ } else { // 无选中文本,阻止非法输入
+ event.preventDefault();
+ }
+ }
+ // private isValueLimit() {
+ // if (this.min !== null && NumberWrapperParseFloat(this.value) !== 0 &&
+ // this.value <= NumberWrapperParseFloat(this.min)) {
+ // return false;
+ // }
+ // if (this.max !== null && NumberWrapperParseFloat(this.value) !== 0 &&
+ // this.value >= NumberWrapperParseFloat(this.max)) {
+ // return false;
+ // }
+ // return true;
+ // }
+
+ private isIllegalNumberInputChar(event: KeyboardEvent) {
+ /* 8:backspace, 46:. */
+ return !this.keyPattern.test(String.fromCharCode(event.charCode)) &&
+ event.which !== 46 && event.which !== 0;
+ }
+
+ private isIllegalIntergerInput(inputChar: string) {
+ return this._precision === 0 &&
+ (inputChar === '.' ||
+ (this._value && this._value && this._value.toString().length > 0 &&
+ inputChar === '-'));
+ }
+
+ private isIllegalFloatInput(inputChar: string) {
+ return this._precision > 0 && this._value &&
+ ((this._value.toString().length > 0 && inputChar === '-') ||
+ ((this._value.toString() === '' ||
+ this._value.toString().indexOf('.') > 0) &&
+ inputChar === '.') ||
+ (this._value.toString().indexOf('.') > 0 &&
+ this._value.toString().split('.')[1].length === this._precision));
+ }
+
+ onInput(event: Event, inputValue: string) {
+ this.value = inputValue;
+ }
+
+ //处理鼠标经过上下箭头时,样式设置
+ isEmptyValue() {
+ if (this.value === undefined || this.value === null || this.value === '') {
+ return true;
+ }
+ return false;
+ }
+
+ isDisabledUpCaret() {
+ if (this.isEmptyValue()) {
+ return true;
+ } else if (
+ this.max !== null &&
+ (NumberWrapperParseFloat(this.value) >=
+ NumberWrapperParseFloat(this.max))) {
+ return true;
+ }
+ return false;
+ }
+
+ isDisabledDownCaret() {
+ if (this.isEmptyValue()) {
+ return true;
+ } else if (
+ this.min !== null &&
+ (NumberWrapperParseFloat(this.value) <=
+ NumberWrapperParseFloat(this.min))) {
+ return true;
+ }
+ return false;
+ }
+
+ _handleMouseEnterUp() {
+ this.isDisabledUp = this.isDisabledUpCaret();
+ }
+
+ _handleMouseEnterDown() {
+ this.isDisabledDown = this.isDisabledDownCaret();
+ }
+
+ public switch(): void {
+ this.showPassword = !this.showPassword;
+ this.showPassword?this.inputViewChild.nativeElement.type =
+ 'text':this.inputViewChild.nativeElement.type = 'password';
+ }
+
+ private setPasswordTooltip(): void {
+ this.tooltipTexts[this.lang][this.showPassword.toString()]
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.html b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.html
new file mode 100644
index 00000000..9065badd
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.html
@@ -0,0 +1,69 @@
+<div #inputOutter class="text-input"
+ [class.text-input-with-hint]="isShowHintLabel"
+ [class.short-text-input]="shortInput"
+ [class.text-input-with-unit]="hasUnit"
+ [class.text-input-with-unitOption]="hasUnitOption"
+ [class.text-input-with-prefix]="hasPrefix"
+ [class.text-input-with-prefixOption]="hasPrefixOption"
+ [class.text-input-with-passwordSwith]="passwordSwitch"
+ [class.input-right-border]="isShowUnitOption"
+ [class.input-left-border]="isShowPrefixOption"
+ [class.input-right-border-pwdswith-hover]="isPwdSwithHover"
+ [class.input-right-border-pwdswith-click]="isPwdSwithClick"
+ (window:click)="_onWindowClick($event)">
+ <div #prefixOpt *ngIf="hasPrefixOption" [class.prefix-focus]="isShowPrefixOption" class="text-input-prefix-option" (click)="_showPrefixOption($event)">{{showPrefix}}<span class="toggle"></span></div>
+ <div *ngIf="isShowPrefixOption" class="plx-text-input-prefix-group">
+ <li *ngFor="let option of prefixOptions" (click)="_setPrefix(option)" [ngClass]="{'group-selected':showPrefix===option}">{{option}}</li>
+ </div>
+ <div *ngIf="hasPrefix" class="text-input-prefix" [style.width]="prefixWidth">{{prefix}}</div>
+ <span [class.input-span-focus]="isFocus ||isOpenDataList" class="input-span">
+ <input #input
+ class="plx-input"
+ [disabled]="disabled"
+ [id]="inputId"
+ [name]="name"
+ [attr.max]="max"
+ [attr.maxlength]="maxLength"
+ [attr.min]="min"
+ [attr.minlength]="minLength"
+ [attr.tabindex]="tabIndex"
+ [attr.pattern]="pattern"
+ [placeholder]="placeholder"
+ [readonly]="readOnly"
+ [required]="required"
+ [type]="inputType"
+ [(ngModel)]="displayValue"
+ (click)="_handleClick($event)"
+ (focus)="_handleFocus($event)"
+ (blur)="_handleBlur($event);_checkValue()"
+ (input)="_handleInput($event)"
+ (change)="_handleChange($event)"
+ (select)="_handleSelect($event)"
+ (keydown)="onInputKeydown($event)" (keyup)="onInput($event,input.value)"
+ (keypress)="onInputKeyPress($event)" [style.width]="inputWidth" validateOnBlur/>
+ <a class="input-spinner-up" [style.cursor]="isDisabledUp ? 'not-allowed' : 'pointer'" *ngIf="type === 'number' && numberShowSpinner" (mouseenter)="_handleMouseEnterUp()"
+ (mouseleave)="onUpButtonMouseleave($event)" (mousedown)="onUpButtonMousedown($event,input)"
+ (mouseup)="onUpButtonMouseup($event)">
+ <span class="caret-up" [class.caret-up-hover]="!isDisabledUp"></span>
+ </a>
+ <a class="input-spinner-down" [style.cursor]="isDisabledDown ? 'not-allowed':'pointer'" *ngIf="type === 'number' && numberShowSpinner" (mouseenter)="_handleMouseEnterDown()"
+ (mouseleave)="onDownButtonMouseleave($event)" (mousedown)="onDownButtonMousedown($event,input)"
+ (mouseup)="onDownButtonMouseup($event)">
+ <span class="caret-down" [class.caret-down-hover]="!isDisabledDown"></span>
+ </a>
+ </span><div *ngIf="hasUnit" class="text-input-unit" [style.width]="unitWidth">{{unit}}</div>
+
+ <div *ngIf="hasUnitOption" [class.unit-focus]="isShowUnitOption" class="text-input-unit-option" (click)="_showUnitOption($event)" [style.width]="unitOptionWidth" >{{showUnit}}<span class="toggle"></span></div>
+ <div *ngIf="isShowUnitOption" class="plx-text-input-unit-group" [style.margin-left]="inputWidth">
+ <li *ngFor="let option of unitOptions" (click)="_setUnit(option)" [ngClass]="{'group-selected':showUnit===option}">{{option}}</li>
+ </div>
+ <div *ngIf="!required && !notShowOption" class="text-input-optional">{{optionalLabel}}</div>
+ <div *ngIf="isShowHintLabel" class="text-input-hint">{{hintLabel}}</div>
+ <div *ngIf = "isOpenDataList || isOpenSuffixList" class = "text-input-dataList">
+ <li *ngFor="let data of displayDataList" (click)="chooseInputData(data)">{{data}}</li>
+ </div>
+ <span *ngIf="passwordSwitch" placement="bottom" plxTooltip="{{tooltipText}}" [ngClass]="showPassword?'ict-eye-closed':'ict-eye'"
+ class="plx-input-passwordSwitch" (click)='switch()'
+ (mouseover)="isPwdSwithHover=true" (mouseleave)="isPwdSwithHover=false"
+ (mousedown)="isPwdSwithClick=true" (mouseup)="isPwdSwithClick=false"></span>
+</div>
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.less b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.less
new file mode 100644
index 00000000..6a93c1c1
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.less
@@ -0,0 +1,423 @@
+@import "../../assets/components/themes/default/theme.less";
+@import "../../assets/components/themes/common/plx-input.less";
+
+@input-short-width: 120px;
+@padding-left: 10px;
+@padding: 10px;
+@border-width: 1px;
+@unit-span-width: 45px;
+@unit-option-width: 84px;
+@short-unit-span-width: 40px;
+@prefix-span-width: 70px;
+@prefix-option-width: 84px;
+@short-prefix-span-width: 40px;
+@password-switch: 40px;
+
+.font {
+ font-family: @font-family;
+ font-size: @font-size;
+}
+
+.text-input {
+ //height: @input-height;
+ //position: relative;
+ //margin-bottom: 0;
+ display: inline-block;
+
+ .caret-down {
+ display: block;
+ width: 0;
+ height: 0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid lighten(@fonticon-color, 5%);
+ margin-top: 4px;
+ margin-bottom: 10px;
+
+ &.caret-down-hover:hover, &.caret-down-hover:active {
+ border-top: 4px solid @primary-color;
+ }
+ }
+ .caret-up {
+ display: block;
+ width: 0;
+ height: 0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-bottom: 4px solid lighten(@fonticon-color, 5%);
+ margin-top: 10px;
+
+ &.caret-up-hover:hover, &.caret-up-hover:active {
+ border-bottom: 4px solid @primary-color;
+ }
+ }
+ .toggle {
+ float: right;
+ margin-right: 10px;
+ margin-top: 14px;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid lighten(@fonticon-color, 5%);
+ }
+ .text-input-dataList {
+ margin-top: 2px;
+ position: absolute;
+ z-index: @z-index-dropdown;
+ border: 1px solid @gray-grade-7;
+ background: #fff;
+ cursor: pointer;
+ border-radius: @radius;
+ li {
+ list-style: none;
+ height: @input-height;
+ width: @input-width;
+ padding-left: @padding-left;
+ &:hover {
+ background-color: @hover-bg-color;
+ }
+ }
+ }
+}
+
+.input-span {
+ display: inline-block;
+ overflow: visible;
+ padding: 0;
+ position: relative;
+}
+
+.text-input-with-hint {
+ margin-bottom: -8px;
+ :host(.ng-touched.ng-invalid.input-blur) & {
+ height: auto;
+ margin-bottom: 0;
+ }
+}
+
+.plx-text-input-unit-group, .plx-text-input-prefix-group {
+ position: absolute;
+ margin-top: @overlay-margin-top;
+ width: @unit-option-width;
+ z-index: @z-index-dropdown;
+ border-radius: @radius;
+ background: @component-bg;
+ border: 1px solid @border-color-base;
+ .shadow;
+ cursor: pointer;
+ li {
+ padding-left: 10px;
+ height: @input-height;
+ list-style: none;
+ line-height: @input-height;
+ font-size: @font-size;
+ &:hover {
+ background-color: @hover-bg-color;
+ }
+ &.group-selected, &.group-selected:hover {
+ background-color: @selected-bg-color;
+ color: @text-color;
+ }
+ }
+}
+
+.text-input-optional {
+ display: inline-block;
+ margin-right: 6px;
+ padding-left:5px;
+}
+
+.input-right-border .plx-input {
+ border-right: 1px solid @primary-color;
+}
+
+.input-left-border .plx-input {
+ border-left: 1px solid @primary-color;
+}
+
+.text-input-hint {
+ top: 42px;
+ left: 10px;
+ font-family: @font-family;
+ font-size: @font-size;
+ color: @disabled-text-color;
+ :host(.ng-touched.ng-invalid.input-blur) & {
+ display: none;
+ }
+}
+
+.text-input-prefix {
+ .font;
+ display: inline-block;
+ width: @prefix-span-width;
+ height: @input-height;
+ text-align: center;
+ line-height: @input-height;
+ border-top-left-radius: @radius;
+ border-bottom-left-radius: @radius;
+ color: @disabled-text-color;
+ border: 1px solid @border-color-base;
+ border-right: 0;
+ vertical-align: middle;
+ .short-text-input & {
+ width: @short-prefix-span-width;
+ }
+ .input-span-focus & {
+ border-color: @primary-color;
+ }
+ .input-invalid.ng-dirty.ng-invalid.ng-touched.input-blur &,
+ .input-invalid.ng-dirty.ng-invalid.ng-touched.input-blur .input-span-focus:focus & {
+ border-color: @error-color;
+ }
+}
+
+.input-unit {
+ .font;
+ display: inline-block;
+ height: @input-height;
+ text-align: center;
+ line-height: @input-height;
+ border-top-right-radius: @radius;
+ border-bottom-right-radius: @radius;
+}
+
+.text-input-unit {
+ border: 1px solid @border-color-base;
+ border-left: 0;
+ .input-unit;
+ color: @disabled-text-color;
+ width: @unit-span-width;
+ text-align: center;
+ vertical-align: middle;
+ .short-text-input & {
+ width: @short-unit-span-width;
+ }
+ .input-span-focus & {
+ border-color: @primary-color;
+ }
+}
+
+.text-input-prefix-option {
+ .font;
+ display: inline-block;
+ height: @input-height;
+ text-align: center;
+ line-height: @input-height;
+ border-top-left-radius: @radius;
+ border-bottom-left-radius: @radius;
+ width: @prefix-option-width;
+ text-align: left;
+ padding-left: 10px;
+ cursor: pointer;
+ border: 1px solid @border-color-base;;
+ border-right: 0;
+ vertical-align: middle;
+ &.prefix-focus {
+ border-color: @primary-color;
+ }
+}
+
+.text-input-unit-option {
+ &:extend(.input-unit);
+ width: @unit-option-width;
+ text-align: left;
+ padding-left: 10px;
+ cursor: pointer;
+ border: 1px solid @border-color-base;;
+ border-left: 0;
+ vertical-align: middle;
+ .input-span-focus & {
+ border-color: @primary-color;
+ }
+}
+
+.text-input-with-unitOption {
+ div.unit-focus {
+ border-color: @primary-color;
+ }
+}
+
+.plx-input {
+ .short-text-input & {
+ width: @input-short-width;
+ }
+
+ .text-input-with-unit & {
+ width: @input-width - @unit-span-width;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .text-input-with-unitOption & {
+ width: @input-width - @unit-option-width;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .text-input-with-prefix & {
+ width: @input-width - @prefix-span-width;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ .text-input-with-prefixOption & {
+ width: @input-width - @prefix-option-width;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ .text-input-with-passwordSwith & {
+ width: @input-width - @password-switch;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .text-input-with-prefix.text-input-with-unit & {
+ width: @input-width - @prefix-span-width - @unit-span-width;
+ }
+
+ .text-input-with-prefix.text-input-with-unitOption & {
+ width: @input-width - @prefix-span-width - @unit-option-width;
+ }
+
+ .short-text-input.text-input-with-prefix & {
+ width: @input-short-width - @short-prefix-span-width;
+ }
+
+ .short-text-input.text-input-with-unit & {
+ width: @input-short-width - @short-unit-span-width;
+ }
+
+ .short-text-input.text-input-with-prefix.text-input-with-unit & {
+ width: @input-short-width - @short-prefix-span-width - @short-unit-span-width;
+ }
+}
+
+.input-spinner() {
+ cursor: pointer;
+ display: block;
+ font-size: 12px;
+ position: absolute;
+ margin: 0;
+ right: 0;
+ overflow: hidden;
+ border: none;
+ padding: 0;
+ text-align: center;
+ vertical-align: middle;
+ width: 18px;
+}
+
+.input-spinner-up {
+ .input-spinner();
+ top: 0;
+}
+
+.input-spinner-down {
+ .input-spinner();
+ bottom: 0;
+}
+
+:host(.plx-input-sm) {
+ .plx-input-sm-common;
+}
+
+.plx-input-sm {
+ .plx-input-sm-common;
+}
+
+.plx-input-sm-common {
+ .plx-input {
+ height: @input-height-sm;
+ line-height: @input-height-sm;
+ }
+ .text-input-prefix,
+ .text-input-unit,
+ .text-input-unit-option,
+ .text-input-prefix-option {
+ height: @input-height-sm;
+ line-height: @input-height-sm;
+ }
+ div.text-input-dataList {
+ height: @input-height-sm;
+ }
+ .toggle {
+ margin-top: 11px;
+ }
+ .caret-down {
+ margin-bottom: 8px;
+ }
+ .caret-up {
+ margin-top: 8px;
+ }
+ .plx-input-passwordSwitch {
+ line-height: @input-height-sm - 2px;
+ }
+}
+
+.plx-input-passwordSwitch {
+ display: inline-block;
+ line-height: @input-height - 2px;
+ width: @password-switch;
+ text-align: center;
+ vertical-align: middle;
+ background-color: @common-color;
+ border: 1px solid @border-color-base;
+ border-left: 0;
+ border-bottom-right-radius: @radius;
+ border-top-right-radius: @radius;
+ cursor: pointer;
+ &:focus,
+ &:hover {
+ border-color: @btn-common-color-border-hover;
+ background-color: @common-color-hover;
+ &.ict-eye-closed, &.ict-eye {
+ color: @btn-common-color-text-hover;
+ }
+ }
+ &:active {
+ background-color: @common-color-click;
+ border-color: @btn-common-color-border-click;
+ &.ict-eye-closed, &.ict-eye {
+ color: @btn-common-color-text-click;
+ }
+ }
+ &.ict-eye-closed, &.ict-eye {
+ color: @fonticon-color;
+ font-size: 16px;
+ }
+ }
+.input-right-border-pwdswith-hover .plx-input {
+ border-right-color: @btn-common-color-border-hover;
+}
+.input-right-border-pwdswith-click .plx-input {
+ border-right-color: @btn-common-color-border-click;
+}
+
+.plx-text-input-ip-dot {
+ display: inline-block;
+ vertical-align: bottom;
+ color: #999;
+}
+
+.plx-text-input-error {
+ font-size: 12px;
+ color: @error-color;
+ margin-top: 5px;
+}
+
+:host(.plx-text-input-ip-invalid) {
+ .plx-text-input-ip-invalid-common;
+}
+
+.plx-text-input-ip-invalid {
+ .plx-text-input-ip-invalid-common;
+}
+
+.plx-text-input-ip-invalid-common {
+ .plx-input {
+ border-color: @error-color;
+ }
+ .input-span-focus .plx-input {
+ border-color: @primary-color;
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.module.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.module.ts
new file mode 100644
index 00000000..4374770a
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.module.ts
@@ -0,0 +1,31 @@
+import {CommonModule} from '@angular/common';
+import {NgModule} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+
+import {PlxTooltipModule} from '../plx-tooltip/plx-tooltip.module';
+import {Ipv4ValidatorDirective} from './ipv4-validator.directive';
+import {Ipv6ValidatorDirective} from './ipv6-validator.directive';
+import {MaxValidatorDirective} from './max-validator.directive';
+import {MinValidatorDirective} from './min-validator.directive';
+import {PlxTextInputComponent} from './text-input.component';
+import {PlxValidateOnBlurDirective} from './validate-on-blur.directive';
+import {PlxTextInputIpComponent} from './text-input-ip.component';
+import {PlxTextInputIpAddressComponent} from './text-input-ip-address.component';
+
+
+@NgModule({
+ imports: [CommonModule, FormsModule, PlxTooltipModule],
+ declarations: [
+ PlxTextInputComponent, Ipv4ValidatorDirective, Ipv6ValidatorDirective,
+ MaxValidatorDirective, MinValidatorDirective, PlxValidateOnBlurDirective,
+ PlxTextInputIpComponent, PlxTextInputIpAddressComponent
+ ],
+ exports: [
+ PlxTextInputComponent, Ipv4ValidatorDirective, Ipv6ValidatorDirective,
+ MaxValidatorDirective, MinValidatorDirective, PlxValidateOnBlurDirective,
+ PlxTextInputIpComponent, PlxTextInputIpAddressComponent
+ ]
+})
+
+export class PlxTextInputModule {
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/validate-on-blur.directive.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/validate-on-blur.directive.ts
new file mode 100644
index 00000000..b4a940c8
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-text-input/validate-on-blur.directive.ts
@@ -0,0 +1,18 @@
+import {Directive, HostListener} from '@angular/core';
+import {NgControl} from '@angular/forms';
+
+@Directive({selector: '[validateOnBlur]'})
+
+export class PlxValidateOnBlurDirective {
+ constructor(private formControl: NgControl) {}
+
+ @HostListener('focus')
+ onFocus() {
+ // this.formControl.control.markAsUntouched(false);
+ }
+
+ @HostListener('blur')
+ onBlur() {
+ // this.formControl.control.markAsTouched(true);
+ }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts
new file mode 100644
index 00000000..4a19dd17
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts
@@ -0,0 +1,11 @@
+import {PlxTooltipConfig} from './plx-tooltip-config';
+
+describe('plx-tooltip-config', () => {
+ it('should have sensible default values', () => {
+ const config = new PlxTooltipConfig();
+
+ expect(config.placement).toBe('top');
+ expect(config.triggers).toBe('hover');
+ expect(config.container).toBeUndefined();
+ });
+});
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts
new file mode 100644
index 00000000..dd1cc254
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts
@@ -0,0 +1,13 @@
+import {Injectable} from '@angular/core';
+
+/**
+ * Configuration service for the PlxTooltip directive.
+ * You can inject this service, typically in your root component, and customize the values of its properties in
+ * order to provide default values for all the tooltips used in the application.
+ */
+@Injectable()
+export class PlxTooltipConfig {
+ public placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
+ public triggers = 'hover';
+ public container: string;
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less
new file mode 100644
index 00000000..d7ce015f
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less
@@ -0,0 +1,241 @@
+@import "../../assets/components/themes/common/plx-input.less";
+
+@tooltip-arrow-border-width: 4px;
+@tooltip-arrow-border-width-before: 5px;
+@tooltip-arrow-border-height: @tooltip-arrow-border-width-before - @tooltip-arrow-border-width;
+@tooltip-arrow-away: 5px;
+@tooltip-arrow-background-color: #595959;
+@tooltip-arrow-border-color: #595959;
+@tooltip-away-host: 3px;
+
+.plx-tooltip {
+ font-family: @font-family;
+ font-size: @font-size;
+ opacity: 1;
+ position: absolute;
+ z-index: 10001;
+ display: block;
+ font-style: normal;
+ font-weight: normal;
+ letter-spacing: normal;
+ line-break: auto;
+ line-height: 1.5;
+ text-align: left;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ white-space: normal;
+ word-break: normal;
+ word-spacing: normal;
+ word-wrap: break-word;
+ &::before,
+ &::after {
+ content: "";
+ position: absolute;
+ display: block;
+ width: 0;
+ height: 0;
+ border: solid transparent;
+ }
+ &::before {
+ border-width: @tooltip-arrow-border-width-before;
+ }
+ &::after {
+ border-width: @tooltip-arrow-border-width;
+ }
+}
+
+.plx-tooltip-inner {
+ min-width: 60px;
+ max-width: 200px;
+ padding: 3px 8px;
+ color: #fff;
+ text-align: center;
+ background-color: #000;
+}
+
+.plx-tooltip.show {
+ font-size: @font-size;
+ opacity: 1;
+}
+.plx-tooltip.show .plx-tooltip-inner {
+ background-color: #595959;
+ border-radius: @radius;
+ padding: 0px 12px;
+ height: 30px;
+ line-height: 30px;
+}
+
+.plx-tooltip-top-common {
+ margin-top: -(@tooltip-arrow-border-width + @tooltip-away-host);
+ &::before {
+ border-top-color: @tooltip-arrow-border-color;
+ border-bottom-width: 0;
+ bottom: -@tooltip-arrow-border-width-before;
+ }
+ &::after {
+ border-top-color: @tooltip-arrow-background-color;
+ border-bottom-width: 0;
+ bottom: -@tooltip-arrow-border-width;
+ }
+}
+.plx-tooltip-top {
+ .plx-tooltip-top-common;
+ &::before {
+ left: 50%;
+ margin-left: -@tooltip-arrow-border-width-before;
+ }
+ &::after {
+ left: 50%;
+ margin-left: -@tooltip-arrow-border-width;
+ }
+}
+.plx-tooltip.plx-tooltip-top-left {
+ .plx-tooltip-top-common;
+ &::before {
+ left: @tooltip-arrow-away;
+ }
+ &::after {
+ left: @tooltip-arrow-away + @tooltip-arrow-border-height;
+ }
+}
+.plx-tooltip.plx-tooltip-top-right {
+ .plx-tooltip-top-common;
+ &::before {
+ right: @tooltip-arrow-away;
+ }
+ &::after {
+ right: @tooltip-arrow-away + @tooltip-arrow-border-height;
+ }
+}
+
+.plx-tooltip-right-common {
+ margin-left: @tooltip-arrow-border-width + @tooltip-away-host;
+ &::before {
+ border-right-color: @tooltip-arrow-border-color;
+ border-left-width: 0;
+ left: -@tooltip-arrow-border-width-before;
+ }
+ &::after {
+ border-right-color: @tooltip-arrow-background-color;
+ border-left-width: 0;
+ left: -@tooltip-arrow-border-width;
+ }
+}
+.plx-tooltip.plx-tooltip-right {
+ .plx-tooltip-right-common;
+ &::before {
+ top: 50%;
+ margin-top: -@tooltip-arrow-border-width-before;
+ }
+ &::after {
+ top: 50%;
+ margin-top: -@tooltip-arrow-border-width;
+ }
+}
+.plx-tooltip.plx-tooltip-right-top {
+ .plx-tooltip-right-common;
+ &::before {
+ top: @tooltip-arrow-away;
+ }
+ &::after {
+ top: @tooltip-arrow-away + @tooltip-arrow-border-height;
+ }
+}
+.plx-tooltip.plx-tooltip-right-bottom {
+ .plx-tooltip-right-common;
+ &::before {
+ bottom: @tooltip-arrow-away;
+ }
+ &::after {
+ bottom: @tooltip-arrow-away + @tooltip-arrow-border-height;
+ }
+}
+
+.plx-tooltip-bottom-common {
+ margin-top: @tooltip-arrow-border-width + @tooltip-away-host;
+ &::before {
+ border-bottom-color: @tooltip-arrow-border-color;
+ border-top-width: 0;
+ top: -@tooltip-arrow-border-width-before;
+ }
+ &::after {
+ border-bottom-color: @tooltip-arrow-background-color;
+ border-top-width: 0;
+ top: -@tooltip-arrow-border-width;
+ }
+}
+.plx-tooltip.plx-tooltip-bottom {
+ .plx-tooltip-bottom-common;
+ &::before {
+ left: 50%;
+ margin-left: -@tooltip-arrow-border-width-before;
+ }
+ &::after {
+ left: 50%;
+ margin-left: -@tooltip-arrow-border-width;
+ }
+}
+.plx-tooltip.plx-tooltip-bottom-left {
+ .plx-tooltip-bottom-common;
+ &::before {
+ left: @tooltip-arrow-away;
+ }
+ &::after {
+ left: @tooltip-arrow-away + @tooltip-arrow-border-height;
+ }
+}
+.plx-tooltip.plx-tooltip-bottom-right {
+ .plx-tooltip-bottom-common;
+ &::before {
+ right: @tooltip-arrow-away;
+ }
+ &::after {
+ right: @tooltip-arrow-away + @tooltip-arrow-border-height;
+ }
+}
+
+.plx-tooltip-left-common {
+ margin-left: -(@tooltip-arrow-border-width + @tooltip-away-host);
+ &::before {
+ border-left-color: @tooltip-arrow-border-color;
+ border-right-width: 0;
+ right: -@tooltip-arrow-border-width-before;
+ }
+ &::after {
+ border-left-color: @tooltip-arrow-background-color;
+ border-right-width: 0;
+ right: -@tooltip-arrow-border-width;
+ }
+}
+
+.plx-tooltip.plx-tooltip-left {
+ .plx-tooltip-left-common;
+ &::before {
+ top: 50%;
+ margin-top: -@tooltip-arrow-border-width-before;
+ }
+ &::after {
+ top: 50%;
+ margin-top: -@tooltip-arrow-border-width;
+ }
+}
+
+.plx-tooltip.plx-tooltip-left-top {
+ .plx-tooltip-left-common;
+ &::before {
+ top: @tooltip-arrow-away;
+ }
+ &::after {
+ top: @tooltip-arrow-away + @tooltip-arrow-border-height;
+ }
+}
+.plx-tooltip.plx-tooltip-left-bottom {
+ .plx-tooltip-left-common;
+ &::before {
+ bottom: @tooltip-arrow-away;
+ }
+ &::after {
+ bottom: @tooltip-arrow-away + @tooltip-arrow-border-height;
+ }
+} \ No newline at end of file
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts
new file mode 100644
index 00000000..062ded1c
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts
@@ -0,0 +1,12 @@
+import {NgModule, ModuleWithProviders} from '@angular/core';
+
+import {PlxTooltip, PlxTooltipWindow} from './plx-tooltip';
+import {PlxTooltipConfig} from './plx-tooltip-config';
+
+export {PlxTooltipConfig} from './plx-tooltip-config';
+export {PlxTooltip} from './plx-tooltip';
+
+@NgModule({declarations: [PlxTooltip, PlxTooltipWindow], exports: [PlxTooltip], entryComponents: [PlxTooltipWindow]})
+export class PlxTooltipModule {
+ public static forRoot(): ModuleWithProviders { return {ngModule: PlxTooltipModule, providers: [PlxTooltipConfig]}; }
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts
new file mode 100644
index 00000000..cd2d8a6a
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts
@@ -0,0 +1,485 @@
+import {TestBed, ComponentFixture, inject} from '@angular/core/testing';
+import {createGenericTestComponent} from '../test/common';
+
+import {By} from '@angular/platform-browser';
+import {Component, ViewChild, ChangeDetectionStrategy} from '@angular/core';
+
+import {PlxTooltipModule} from './plx-tooltip.module';
+import {PlxTooltipWindow, PlxTooltip} from './plx-tooltip';
+import {PlxTooltipConfig} from './plx-tooltip-config';
+
+const createTestComponent =
+ (html: string) => <ComponentFixture<TestComponent>>createGenericTestComponent(html, TestComponent);
+
+const createOnPushTestComponent =
+ (html: string) => <ComponentFixture<TestOnPushComponent>>createGenericTestComponent(html, TestOnPushComponent);
+
+describe('plx-tooltip-window', () => {
+ beforeEach(() => { TestBed.configureTestingModule({imports: [PlxTooltipModule.forRoot()]}); });
+
+ it('should render tooltip on top by default', () => {
+ const fixture = TestBed.createComponent(PlxTooltipWindow);
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement).toHaveCssClass('tooltip');
+ expect(fixture.nativeElement).toHaveCssClass('tooltip-top');
+ expect(fixture.nativeElement.getAttribute('role')).toBe('tooltip');
+ });
+
+ it('should position tooltips as requested', () => {
+ const fixture = TestBed.createComponent(PlxTooltipWindow);
+ fixture.componentInstance.placement = 'left';
+ fixture.detectChanges();
+ expect(fixture.nativeElement).toHaveCssClass('tooltip-left');
+ });
+});
+
+describe('plx-tooltip', () => {
+
+ beforeEach(() => {
+ TestBed.configureTestingModule(
+ {declarations: [TestComponent, TestOnPushComponent], imports: [PlxTooltipModule.forRoot()]});
+ });
+
+ function getWindow(element) { return element.querySelector('plx-tooltip-window'); }
+
+ describe('basic functionality', () => {
+
+ it('should open and close a tooltip - default settings and content as string', () => {
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+ const defaultConfig = new PlxTooltipConfig();
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ const windowEl = getWindow(fixture.nativeElement);
+
+ expect(windowEl).toHaveCssClass('tooltip');
+ expect(windowEl).toHaveCssClass(`tooltip-${defaultConfig.placement}`);
+ expect(windowEl.textContent.trim()).toBe('Great tip!');
+ expect(windowEl.getAttribute('role')).toBe('tooltip');
+ expect(windowEl.getAttribute('id')).toBe('plx-tooltip-0');
+ expect(windowEl.parentNode).toBe(fixture.nativeElement);
+ expect(directive.nativeElement.getAttribute('aria-describedby')).toBe('plx-tooltip-0');
+
+ directive.triggerEventHandler('mouseleave', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull();
+ });
+
+ it('should open and close a tooltip - default settings and content from a template', () => {
+ const fixture = createTestComponent(`<template #t>Hello, {{name}}!</template><div [plxTooltip]="t"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ const windowEl = getWindow(fixture.nativeElement);
+
+ expect(windowEl).toHaveCssClass('tooltip');
+ expect(windowEl).toHaveCssClass('tooltip-top');
+ expect(windowEl.textContent.trim()).toBe('Hello, World!');
+ expect(windowEl.getAttribute('role')).toBe('tooltip');
+ expect(windowEl.getAttribute('id')).toBe('plx-tooltip-1');
+ expect(windowEl.parentNode).toBe(fixture.nativeElement);
+ expect(directive.nativeElement.getAttribute('aria-describedby')).toBe('plx-tooltip-1');
+
+ directive.triggerEventHandler('mouseleave', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull();
+ });
+
+ it('should open and close a tooltip - default settings, content from a template and context supplied', () => {
+ const fixture = createTestComponent(`<template #t let-name="name">Hello, {{name}}!</template><div [plxTooltip]="t"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.context.tooltip.open({name: 'John'});
+ fixture.detectChanges();
+ const windowEl = getWindow(fixture.nativeElement);
+
+ expect(windowEl).toHaveCssClass('tooltip');
+ expect(windowEl).toHaveCssClass('tooltip-top');
+ expect(windowEl.textContent.trim()).toBe('Hello, John!');
+ expect(windowEl.getAttribute('role')).toBe('tooltip');
+ expect(windowEl.getAttribute('id')).toBe('plx-tooltip-2');
+ expect(windowEl.parentNode).toBe(fixture.nativeElement);
+ expect(directive.nativeElement.getAttribute('aria-describedby')).toBe('plx-tooltip-2');
+
+ directive.triggerEventHandler('mouseleave', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull();
+ });
+
+ it('should not open a tooltip if content is falsy', () => {
+ const fixture = createTestComponent(`<div [plxTooltip]="notExisting"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ const windowEl = getWindow(fixture.nativeElement);
+
+ expect(windowEl).toBeNull();
+ });
+
+ it('should close the tooltip tooltip if content becomes falsy', () => {
+ const fixture = createTestComponent(`<div [plxTooltip]="name"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ fixture.componentInstance.name = null;
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ it('should allow re-opening previously closed tooltips', () => {
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ directive.triggerEventHandler('mouseleave', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+ });
+
+ it('should not leave dangling tooltips in the DOM', () => {
+ const fixture = createTestComponent(`<template [ngIf]="show"><div plxTooltip="Great tip!"></div></template>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ fixture.componentInstance.show = false;
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ it('should properly cleanup tooltips with manual triggers', () => {
+ const fixture = createTestComponent(`
+ <template [ngIf]="show">
+ <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip" (mouseenter)="t.open()"></div>
+ </template>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ fixture.componentInstance.show = false;
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ describe('positioning', () => {
+
+ it('should use requested position', () => {
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!" placement="left"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ const windowEl = getWindow(fixture.nativeElement);
+
+ expect(windowEl).toHaveCssClass('tooltip');
+ expect(windowEl).toHaveCssClass('tooltip-left');
+ expect(windowEl.textContent.trim()).toBe('Great tip!');
+ });
+
+ it('should properly position tooltips when a component is using the OnPush strategy', () => {
+ const fixture = createOnPushTestComponent(`<div plxTooltip="Great tip!" placement="left"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ const windowEl = getWindow(fixture.nativeElement);
+
+ expect(windowEl).toHaveCssClass('tooltip');
+ expect(windowEl).toHaveCssClass('tooltip-left');
+ expect(windowEl.textContent.trim()).toBe('Great tip!');
+ });
+ });
+
+ describe('triggers', () => {
+
+ it('should support toggle triggers', () => {
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!" triggers="click"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('click', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ directive.triggerEventHandler('click', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ it('should non-default toggle triggers', () => {
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!" triggers="mouseenter:click"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ directive.triggerEventHandler('click', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ it('should support multiple triggers', () => {
+ const fixture =
+ createTestComponent(`<div plxTooltip="Great tip!" triggers="mouseenter:mouseleave click"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ directive.triggerEventHandler('click', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ it('should not use default for manual triggers', () => {
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!" triggers="manual"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ it('should allow toggling for manual triggers', () => {
+ const fixture = createTestComponent(`
+ <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip"></div>
+ <button (click)="t.toggle()">T</button>`);
+ const button = fixture.nativeElement.querySelector('button');
+
+ button.click();
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ button.click();
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ it('should allow open / close for manual triggers', () => {
+ const fixture = createTestComponent(`
+ <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip"></div>
+ <button (click)="t.open()">O</button>
+ <button (click)="t.close()">C</button>`);
+
+ const buttons = fixture.nativeElement.querySelectorAll('button');
+
+ buttons[0].click(); // open
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ buttons[1].click(); // close
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+
+ it('should not throw when open called for manual triggers and open tooltip', () => {
+ const fixture = createTestComponent(`
+ <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip"></div>
+ <button (click)="t.open()">O</button>`);
+ const button = fixture.nativeElement.querySelector('button');
+
+ button.click(); // open
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+
+ button.click(); // open
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+ });
+
+ it('should not throw when closed called for manual triggers and closed tooltip', () => {
+ const fixture = createTestComponent(`
+ <div plxTooltip="Great tip!" triggers="manual" #t="plxTooltip"></div>
+ <button (click)="t.close()">C</button>`);
+
+ const button = fixture.nativeElement.querySelector('button');
+
+ button.click(); // close
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ });
+ });
+ });
+
+ describe('container', () => {
+
+ it('should be appended to the element matching the selector passed to "container"', () => {
+ const selector = 'body';
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!" container="` + selector + `"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ expect(getWindow(document.querySelector(selector))).not.toBeNull();
+ });
+
+ it('should properly destroy tooltips when the "container" option is used', () => {
+ const selector = 'body';
+ const fixture =
+ createTestComponent(`<div *ngIf="show" plxTooltip="Great tip!" container="` + selector + `"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ directive.triggerEventHandler('mouseenter', {});
+ fixture.detectChanges();
+
+ expect(getWindow(document.querySelector(selector))).not.toBeNull();
+ fixture.componentRef.instance.show = false;
+ fixture.detectChanges();
+ expect(getWindow(document.querySelector(selector))).toBeNull();
+ });
+ });
+
+ describe('visibility', () => {
+ it('should emit events when showing and hiding popover', () => {
+ const fixture = createTestComponent(
+ `<div plxTooltip="Great tip!" triggers="click" (shown)="shown()" (hidden)="hidden()"></div>`);
+ const directive = fixture.debugElement.query(By.directive(PlxTooltip));
+
+ let shownSpy = spyOn(fixture.componentInstance, 'shown');
+ let hiddenSpy = spyOn(fixture.componentInstance, 'hidden');
+
+ directive.triggerEventHandler('click', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+ expect(shownSpy).toHaveBeenCalled();
+
+ directive.triggerEventHandler('click', {});
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ expect(hiddenSpy).toHaveBeenCalled();
+ });
+
+ it('should not emit close event when already closed', () => {
+ const fixture = createTestComponent(
+ `<div plxTooltip="Great tip!" triggers="manual" (shown)="shown()" (hidden)="hidden()"></div>`);
+
+ let shownSpy = spyOn(fixture.componentInstance, 'shown');
+ let hiddenSpy = spyOn(fixture.componentInstance, 'hidden');
+
+ fixture.componentInstance.tooltip.open();
+ fixture.detectChanges();
+
+ fixture.componentInstance.tooltip.open();
+ fixture.detectChanges();
+
+ expect(getWindow(fixture.nativeElement)).not.toBeNull();
+ expect(shownSpy).toHaveBeenCalled();
+ expect(shownSpy.calls.count()).toEqual(1);
+ expect(hiddenSpy).not.toHaveBeenCalled();
+ });
+
+ it('should not emit open event when already opened', () => {
+ const fixture = createTestComponent(
+ `<div plxTooltip="Great tip!" triggers="manual" (shown)="shown()" (hidden)="hidden()"></div>`);
+
+ let shownSpy = spyOn(fixture.componentInstance, 'shown');
+ let hiddenSpy = spyOn(fixture.componentInstance, 'hidden');
+
+ fixture.componentInstance.tooltip.close();
+ fixture.detectChanges();
+ expect(getWindow(fixture.nativeElement)).toBeNull();
+ expect(shownSpy).not.toHaveBeenCalled();
+ expect(hiddenSpy).toHaveBeenCalled();
+ });
+
+ it('should report correct visibility', () => {
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!" triggers="manual"></div>`);
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.tooltip.isOpen()).toBeFalsy();
+
+ fixture.componentInstance.tooltip.open();
+ fixture.detectChanges();
+ expect(fixture.componentInstance.tooltip.isOpen()).toBeTruthy();
+
+ fixture.componentInstance.tooltip.close();
+ fixture.detectChanges();
+ expect(fixture.componentInstance.tooltip.isOpen()).toBeFalsy();
+ });
+ });
+
+ describe('Custom config', () => {
+ let config: PlxTooltipConfig;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({imports: [PlxTooltipModule.forRoot()]});
+ TestBed.overrideComponent(TestComponent, {set: {template: `<div plxTooltip="Great tip!"></div>`}});
+ });
+
+ beforeEach(inject([PlxTooltipConfig], (c: PlxTooltipConfig) => {
+ config = c;
+ config.placement = 'bottom';
+ config.triggers = 'click';
+ config.container = 'body';
+ }));
+
+ it('should initialize inputs with provided config', () => {
+ const fixture = TestBed.createComponent(TestComponent);
+ fixture.detectChanges();
+ const tooltip = fixture.componentInstance.tooltip;
+
+ expect(tooltip.placement).toBe(config.placement);
+ expect(tooltip.triggers).toBe(config.triggers);
+ expect(tooltip.container).toBe(config.container);
+ });
+ });
+
+ describe('Custom config as provider', () => {
+ let config = new PlxTooltipConfig();
+ config.placement = 'bottom';
+ config.triggers = 'click';
+ config.container = 'body';
+
+ beforeEach(() => {
+ TestBed.configureTestingModule(
+ {imports: [PlxTooltipModule.forRoot()], providers: [{provide: PlxTooltipConfig, useValue: config}]});
+ });
+
+ it('should initialize inputs with provided config as provider', () => {
+ const fixture = createTestComponent(`<div plxTooltip="Great tip!"></div>`);
+ const tooltip = fixture.componentInstance.tooltip;
+
+ expect(tooltip.placement).toBe(config.placement);
+ expect(tooltip.triggers).toBe(config.triggers);
+ expect(tooltip.container).toBe(config.container);
+ });
+ });
+});
+
+@Component({selector: 'test-cmpt', template: ``})
+export class TestComponent {
+ name = 'World';
+ show = true;
+
+ @ViewChild(PlxTooltip) tooltip: PlxTooltip;
+
+ shown() {}
+ hidden() {}
+}
+
+@Component({selector: 'test-onpush-cmpt', changeDetection: ChangeDetectionStrategy.OnPush, template: ``})
+export class TestOnPushComponent {
+}
diff --git a/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts
new file mode 100644
index 00000000..f52cc11d
--- /dev/null
+++ b/sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts
@@ -0,0 +1,176 @@
+import {
+ Component,
+ Directive,
+ Input,
+ Output,
+ EventEmitter,
+ ChangeDetectionStrategy,
+ OnInit,
+ OnDestroy,
+ Injector,
+ Renderer,
+ ComponentRef,
+ ElementRef,
+ TemplateRef,
+ ViewContainerRef,
+ ComponentFactoryResolver,
+ NgZone, ViewEncapsulation
+} from '@angular/core';
+import {listenToTriggers} from '../util/triggers';
+import {positionElements, getPlacement} from '../util/positioning';
+import {PopupService} from '../util/popup';
+import {PlxTooltipConfig} from './plx-tooltip-config';
+
+let nextId = 0;
+
+@Component({
+ selector: 'plx-tooltip-window',
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {'[class]': '"plx-tooltip show plx-tooltip-" + placement', 'role': 'tooltip', '[id]': 'id'},
+ template: `
+ <div class="plx-tooltip-inner"><ng-content></ng-content></div>
+ `,
+ styleUrls: ['./plx-tooltip.less']
+})
+export class PlxTooltipWindow {
+ @Input() public placement: 'top' | 'bottom' | 'left' | 'right' = 'top';
+ @Input() public id: string;
+}
+
+/**
+ * A lightweight, extensible directive for fancy tooltip creation.
+ */
+@Directive({selector: '[plxTooltip]', exportAs: 'plxTooltip'})
+export class PlxTooltip implements OnInit, OnDestroy {
+ /**
+ * Placement of a tooltip. Accepts: "top", "bottom", "left", "right"
+ */
+ @Input() public placement: 'top' | 'bottom' | 'left' | 'right';
+ /**
+ * Specifies events that should trigger. Supports a space separated list of event names.
+ */
+ @Input() public triggers: string;
+ /**
+ * A selector specifying the element the tooltip should be appended to.
+ * Currently only supports "body".
+ */
+ @Input() public container: string;
+ /**
+ * Emits an event when the tooltip is shown
+ */
+ @Output() public shown = new EventEmitter();
+ /**
+ * Emits an event when the tooltip is hidden
+ */
+ @Output() public hidden = new EventEmitter();
+
+ private _plxTooltip: string | TemplateRef<any>;
+ private _plxTooltipWindowId = `plx-tooltip-${nextId++}`;
+ private _popupService: PopupService<PlxTooltipWindow>;
+ private _windowRef: ComponentRef<PlxTooltipWindow>;
+ private _unregisterListenersFn;
+ private _zoneSubscription: any;
+
+ constructor(private _elementRef: ElementRef, private _renderer: Renderer, injector: Injector,
+ componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: PlxTooltipConfig,
+ ngZone: NgZone) {
+ this.placement = config.placement;
+ this.triggers = config.triggers;
+ this.container = config.container;
+ this._popupService = new PopupService<PlxTooltipWindow>(
+ PlxTooltipWindow, injector, viewContainerRef, _renderer, componentFactoryResolver);
+
+ this._zoneSubscription = ngZone.onStable.subscribe(() => {
+ if (this._windowRef) {
+ positionElements(
+ this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement,
+ this.container === 'body');
+ let tmpPlace = getPlacement(this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement);
+ this._windowRef.instance.placement = tmpPlace;
+ this._windowRef.changeDetectorRef.detectChanges();
+ }
+ });
+ }
+
+ /**
+ * Content to be displayed as tooltip. If falsy, the tooltip won't open.
+ */
+ @Input()
+ set plxTooltip(value: string | TemplateRef<any>) {
+ this._plxTooltip = value;
+ if (!value && this._windowRef) {
+ this.close();
+ }
+ }
+
+ get plxTooltip() {
+ return this._plxTooltip;
+ }
+
+ /**
+ * Opens an element’s tooltip. This is considered a “manual” triggering of the tooltip.
+ * The context is an optional value to be injected into the tooltip template when it is created.
+ */
+ public open(context?: any) {
+ if (!this._windowRef && this._plxTooltip) {
+ this._windowRef = this._popupService.open(this._plxTooltip, context);
+ // let tmpPlace = getPlacement(this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement);
+ this._windowRef.instance.placement = this.placement;
+ this._windowRef.instance.id = this._plxTooltipWindowId;
+
+ this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', this._plxTooltipWindowId);
+
+ if (this.container === 'body') {
+ window.document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement);
+ }
+
+ // we need to manually invoke change detection since events registered via
+ // Renderer::listen() - to be determined if this is a bug in the Angular itself
+ this._windowRef.changeDetectorRef.markForCheck();
+ this.shown.emit();
+ }
+ }
+
+ /**
+ * Closes an element’s tooltip. This is considered a “manual” triggering of the tooltip.
+ */
+ public close(): void {
+ if (this._windowRef !== null) {
+ this._renderer.setElementAttribute(this._elementRef.nativeElement, 'aria-describedby', null);
+ this._popupService.close();
+ this._windowRef = null;
+ this.hidden.emit();
+ }
+ }
+
+ /**
+ * Toggles an element’s tooltip. This is considered a “manual” triggering of the tooltip.
+ */
+ public toggle(): void {
+ if (this._windowRef) {
+ this.close();
+ } else {
+ this.open();
+ }
+ }
+
+ /**
+ * Returns whether or not the tooltip is currently being shown
+ */
+ public isOpen(): boolean {
+ return !!this._windowRef;
+ }
+
+ public ngOnInit() {
+ this._unregisterListenersFn = listenToTriggers(
+ this._renderer, this._elementRef.nativeElement, this.triggers, this.open.bind(this), this.close.bind(this),
+ this.toggle.bind(this));
+ }
+
+ public ngOnDestroy() {
+ this.close();
+ this._unregisterListenersFn();
+ this._zoneSubscription.unsubscribe();
+ }
+}