Include paletx components 05/39005/1
authorYuanHu <yuan.hu1@zte.com.cn>
Tue, 27 Mar 2018 09:33:22 +0000 (17:33 +0800)
committerYuanHu <yuan.hu1@zte.com.cn>
Tue, 27 Mar 2018 09:33:22 +0000 (17:33 +0800)
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>
46 files changed:
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/numberedFixLen.pipe.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.html [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.less [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.component.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/picker.module.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.html [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/pickerrange.component.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover-config.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/popover.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/time.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker-config.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.less [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/timepicker.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/popup.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/positioning.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/triggers.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-datepicker/util/util.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.spec.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-backdrop.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-dismiss-reasons.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-ref.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-stack.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.spec.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal-window.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.less [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.module.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.spec.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-modal/modal.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/index.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv4-validator.directive.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/ipv6-validator.directive.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/max-validator.directive.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/min-validator.directive.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip-address.component.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input-ip.component.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.component.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.html [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.less [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/text-input.module.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-text-input/validate-on-blur.directive.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.spec.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip-config.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.less [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.module.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.spec.ts [new file with mode: 0644]
sdc-workflow-designer-ui/src/app/paletx/plx-tooltip/plx-tooltip.ts [new file with mode: 0644]

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 (file)
index 0000000..9d26b16
--- /dev/null
@@ -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 (file)
index 0000000..8e4102c
--- /dev/null
@@ -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 (file)
index 0000000..8e50660
--- /dev/null
@@ -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 (file)
index 0000000..493e0cb
--- /dev/null
@@ -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 (file)
index 0000000..0511ad7
--- /dev/null
@@ -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 (file)
index 0000000..2b1986f
--- /dev/null
@@ -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 (file)
index 0000000..a84e098
--- /dev/null
@@ -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 (file)
index 0000000..5ac773c
--- /dev/null
@@ -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 (file)
index 0000000..3d05412
--- /dev/null
@@ -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 (file)
index 0000000..ab31a49
--- /dev/null
@@ -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 (file)
index 0000000..8b75286
--- /dev/null
@@ -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 (file)
index 0000000..60acfa6
--- /dev/null
@@ -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 (file)
index 0000000..45dd7a4
--- /dev/null
@@ -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 (file)
index 0000000..56c26d6
--- /dev/null
@@ -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 (file)
index 0000000..ed9005c
--- /dev/null
@@ -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 (file)
index 0000000..8197de5
--- /dev/null
@@ -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 (file)
index 0000000..fcabe96
--- /dev/null
@@ -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 (file)
index 0000000..887b66e
--- /dev/null
@@ -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 (file)
index 0000000..07e2ff8
--- /dev/null
@@ -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 (file)
index 0000000..0839585
--- /dev/null
@@ -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 (file)
index 0000000..061dc70
--- /dev/null
@@ -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 (file)
index 0000000..37f5b17
--- /dev/null
@@ -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 (file)
index 0000000..5767bfe
--- /dev/null
@@ -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 (file)
index 0000000..eda5b39
--- /dev/null
@@ -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 (file)
index 0000000..c17a7fd
--- /dev/null
@@ -0,0 +1,125 @@
+@import "../../assets/components/themes/default/theme.less";\r
+\r
+plx-modal-window {\r
+  .modal {\r
+    position: fixed;\r
+    top: 0;\r
+    right: 0;\r
+    bottom: 0;\r
+    left: 0;\r
+    display: none;\r
+    outline: 0;\r
+    z-index: 10000;\r
+  }\r
+  .modal-dialog {\r
+    position: relative;\r
+    max-width: 600px;\r
+    margin: 30px auto;\r
+    &.modal-sm {\r
+      max-width: 600px;\r
+    }\r
+    &.modal-lg {\r
+      max-width: 1000px;\r
+    }\r
+  }\r
+  .modal-content {\r
+    position: relative;\r
+    display: flex;\r
+    -webkit-box-orient: vertical;\r
+    -webkit-box-direction: normal;\r
+    -ms-flex-direction: column;\r
+    flex-direction: column;\r
+    background-color: @component-bg;\r
+    background-clip: padding-box;\r
+    border-radius: @radius;\r
+    box-shadow: 0 5px 15px @shadow-color;\r
+    outline: 0;\r
+    .modal-header {\r
+      border-bottom: 0;\r
+      display: flex;\r
+      -webkit-box-align: center;\r
+      -ms-flex-align: center;\r
+      align-items: center;\r
+      -webkit-box-pack: justify;\r
+      -ms-flex-pack: justify;\r
+      justify-content: space-between;\r
+      padding: 15px;\r
+    }\r
+    .modal-body {\r
+      .form-group:last-child, form:last-child {\r
+        margin-bottom: 0;\r
+      }\r
+    }\r
+    .modal-footer {\r
+      display: block;\r
+      border-top: 0;\r
+      margin-top: 0;\r
+      padding: 0 15px 15px 15px;\r
+    }\r
+    .modal-title {\r
+      font-size: @font-size-title-level1;\r
+      margin-bottom: 0;\r
+      line-height: 1.5;\r
+    }\r
+    .modal-btn {\r
+      text-align: center;\r
+      font-size: 0;\r
+    }\r
+  }\r
+  .close {\r
+    color: @fonticon-color;\r
+    font-size: @font-size-title-level2;\r
+    text-shadow: none;\r
+    width: 24px;\r
+    height: 24px;\r
+    background: @scene-textcolor;\r
+    border-radius: 20px;\r
+    padding-bottom: 2px;\r
+    outline: none;\r
+    &:hover {\r
+      color: @fonticon-color;\r
+      background: @fonticon-bg-color-hover;\r
+    }\r
+  }\r
+  .alert-modal {\r
+    &.row {\r
+      margin-left: 100px;\r
+      margin-bottom: 30px;\r
+      text-align: left;\r
+      .tip-img {\r
+        display: inline-block;\r
+        width: 52px;\r
+        height: 52px;\r
+        border-radius: 50px;\r
+        font-size: 45px;\r
+        text-align: center;\r
+        line-height: 1;\r
+        margin-top: -5px;\r
+        margin-right: 15px;\r
+        &::before {\r
+          content: "!";\r
+        }\r
+      }\r
+      .tip-info {\r
+        width: 300px;\r
+        .alert-title {\r
+          font-size: @font-size-title-level2;\r
+          color: @title-text-color;\r
+        }\r
+        .alert-result {\r
+          margin-top: 5px;\r
+          font-size: @font-size;\r
+          color: @unselected-text-color;\r
+        }\r
+      }\r
+      .warning {\r
+        border: 3px solid @warning-color;\r
+        color: @warning-color;\r
+      }\r
+      .error {\r
+        border: 3px solid @error-color;\r
+        color: @error-color;\r
+      }\r
+    }\r
+  }\r
+}
\ 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 (file)
index 0000000..67fdb47
--- /dev/null
@@ -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 (file)
index 0000000..a99c86b
--- /dev/null
@@ -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 (file)
index 0000000..5935eee
--- /dev/null
@@ -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 (file)
index 0000000..c677a94
--- /dev/null
@@ -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 (file)
index 0000000..312ea5f
--- /dev/null
@@ -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 (file)
index 0000000..4518203
--- /dev/null
@@ -0,0 +1,24 @@
+import {Directive, forwardRef} from '@angular/core';\r
+import {AbstractControl, NG_VALIDATORS, Validators} from '@angular/forms';\r
+\r
+@Directive({\r
+       selector: '[ipv6][ngModel],[ipv6][formControl],[ipv6][formControlName]',\r
+       providers: [{\r
+               provide: NG_VALIDATORS,\r
+               useExisting: forwardRef(() => Ipv6ValidatorDirective),\r
+               multi: true\r
+       }],\r
+})\r
+\r
+export class Ipv6ValidatorDirective {\r
+       validate(c: AbstractControl) {\r
+               if (Validators.required(c) !== undefined &&\r
+                       Validators.required(c) !== null) {\r
+                       return null;\r
+               }\r
+               const ipv6Reg =\r
+                       /^\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*$/;\r
+               let regex = new RegExp(ipv6Reg);\r
+               return regex.test(c.value) ? null : {'ipv6': true};\r
+       }4\r
+}\r
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 (file)
index 0000000..143dccc
--- /dev/null
@@ -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 (file)
index 0000000..260a93e
--- /dev/null
@@ -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 (file)
index 0000000..501d232
--- /dev/null
@@ -0,0 +1,170 @@
+import {Component, EventEmitter, forwardRef, Input, OnInit, Output} from '@angular/core';\r
+import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';\r
+import {BooleanFieldValue} from '../core/boolean-field-value';\r
+\r
+const noop = () => {};\r
+\r
+export const PX_TEXT_INPUT_IP_ADDRESS_CONTROL_VALUE_ACCESSOR: any = {\r
+       provide: NG_VALUE_ACCESSOR,\r
+       useExisting: forwardRef(() => PlxTextInputIpAddressComponent),\r
+       multi: true\r
+};\r
+@Component({\r
+       selector: 'plx-text-input-ip-address',\r
+       template: `\r
+               <div>\r
+                       <plx-text-input #textInputIpAddress type="text" [(ngModel)]="ipValue"\r
+                        [width]="'280px'" [numberShowSpinner]="false" minlength="1"\r
+                        (keyup)="keyUp($event)" (paste)="paste($event)" class="{{sizeClass}}"></plx-text-input>\r
+                       <div class="plx-text-input-error">{{errMsg}}</div>\r
+               </div>\r
+       `,\r
+       styleUrls: ['text-input.less'],\r
+       host: {'style': 'display: inline-block;'},\r
+       providers: [PX_TEXT_INPUT_IP_ADDRESS_CONTROL_VALUE_ACCESSOR]\r
+})\r
+\r
+export class PlxTextInputIpAddressComponent implements OnInit, ControlValueAccessor {\r
+       @Input() lang: string = 'zh'; //zh|en\r
+       @Input() size: string; //空代表普通尺寸,sm代表小尺寸\r
+       @Input() ipAddressCheckTip: string = ''; //\r
+\r
+       @Input() @BooleanFieldValue() required: boolean = false;\r
+\r
+       @Input() public ipValue: string;\r
+       @Output() public ipValueChange: EventEmitter<any> = new EventEmitter<any>();\r
+\r
+       @Input() public ipValueFlg : boolean;\r
+       @Output() public ipValueFlgChange: EventEmitter<any> = new EventEmitter<any>();\r
+\r
+       private isNull : boolean = true;\r
+\r
+       /** Callback registered via registerOnTouched (ControlValueAccessor) */\r
+       private _onTouchedCallback: () => void = noop;\r
+       /** Callback registered via registerOnChange (ControlValueAccessor) */\r
+       private _onChangeCallback: (_: any) => void = noop;\r
+\r
+       public errMsgs = {\r
+               'zh': {\r
+                       'empty': '此项不能为空',\r
+                       'invalidate': 'IP格式不对',\r
+                       'range': '请输入正确的IPV4地址或IPV6地址',\r
+                       'range-IPV4': '请输入正确的IPV4',\r
+                       'range-IPV6': '请输入正确的IPV6'\r
+               },\r
+               'en': {\r
+                       'empty': 'IP can not be empty',\r
+                       'invalidate': 'IP format is incorrect',\r
+                       'range': 'IP range is  IPV4 or IPV6',\r
+                       'range-IPV4': 'IP range is  IPV4',\r
+                       'range-IPV6': 'IP range is IPV6'\r
+               }\r
+       };\r
+       public errMsg: string;\r
+       public sizeClass: string;\r
+\r
+       constructor() {\r
+       }\r
+\r
+       ngOnInit(): void {\r
+               if (this.size === 'sm') {\r
+                       this.sizeClass = 'plx-input-sm';\r
+               }\r
+               this.isNull = this.ipValueFlg;\r
+               if(this.repIPStr(this.ipValue) === ''&& !this.ipValueFlg){\r
+                       this.ipValueFlg = false;\r
+                       this.emitValue();\r
+               }\r
+       }\r
+\r
+       public keyUp(event: any): void {\r
+               this.setValueToOutside(this.validate());\r
+               this.emitValue();\r
+       }\r
+\r
+       public paste(event: any): void{\r
+               setTimeout(() => {\r
+                       this.ipValue = event.target.value;\r
+                       this.setValueToOutside(this.validate());\r
+                       this.emitValue();\r
+               }, 0);\r
+       }\r
+\r
+       private emitValue(){\r
+               this.ipValueChange.emit(this.ipValue);\r
+               this.ipValueFlgChange.emit(this.ipValueFlg);\r
+       }\r
+\r
+       private setValueToOutside(validateFlg: boolean): void {\r
+               this.ipValueFlg = validateFlg;\r
+               let value;\r
+               if (validateFlg) {\r
+                       if (this.ipValue) {\r
+                               value = this.ipValue;\r
+                       }\r
+                       if(this.ipValue === ""  && !this.isNull){\r
+                               this.ipValueFlg = false;\r
+                       }\r
+               } else {\r
+                       value = false;\r
+               }\r
+               this._onChangeCallback(value);\r
+       }\r
+\r
+       writeValue(value: any): void {\r
+      //\r
+      this.errMsg = '';\r
+      this.ipValue = value;\r
+      if (value) {\r
+          this.validate();\r
+      }\r
+       }\r
+\r
+       registerOnChange(fn: any) {\r
+               this._onChangeCallback = fn;\r
+       }\r
+\r
+       registerOnTouched(fn: any) {\r
+               this._onTouchedCallback = fn;\r
+       }\r
+\r
+       public validate(): boolean {\r
+               this.errMsg = '';\r
+               if (this.required) {\r
+                       if (!this.ipValue) {\r
+                               this.errMsg = this.errMsgs[this.lang]['empty'];\r
+                               return false;\r
+                       }\r
+               }\r
+               if ((this.ipValue) && (!this.ipValue)) {\r
+                       this.errMsg = this.errMsgs[this.lang]['invalidate'];\r
+                       return false;\r
+               }\r
+               let blackStr = this.repIPStr(this.ipValue);\r
+               if(this.ipAddressCheckTip === ''){\r
+                       if(this.ipValue !== '' && blackStr === ''){\r
+                               this.errMsg = this.errMsgs[this.lang]['range'];\r
+                               return false;\r
+                       }\r
+               }else{\r
+                       if(this.ipValue !== '' && this.ipAddressCheckTip !== blackStr) {\r
+                               this.errMsg = this.errMsgs[this.lang]['range-'+ this.ipAddressCheckTip];\r
+                               return false;\r
+                       }\r
+               }\r
+               return true;\r
+       }\r
+\r
+       private repIPStr(value: any): string {\r
+               let blackStr = '';\r
+               var regip4 = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;\r
+               if (regip4.test(value)) {\r
+                       return "IPV4";\r
+               }\r
+               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*$/;\r
+               if (regip6.test(value)) {\r
+                       return "IPV6";\r
+               }\r
+               return blackStr;\r
+       }\r
+}\r
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 (file)
index 0000000..7c9d616
--- /dev/null
@@ -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 (file)
index 0000000..9b5a01e
--- /dev/null
@@ -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 (file)
index 0000000..9065bad
--- /dev/null
@@ -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 (file)
index 0000000..6a93c1c
--- /dev/null
@@ -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 (file)
index 0000000..4374770
--- /dev/null
@@ -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 (file)
index 0000000..b4a940c
--- /dev/null
@@ -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 (file)
index 0000000..4a19dd1
--- /dev/null
@@ -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 (file)
index 0000000..dd1cc25
--- /dev/null
@@ -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 (file)
index 0000000..d7ce015
--- /dev/null
@@ -0,0 +1,241 @@
+@import "../../assets/components/themes/common/plx-input.less";\r
+\r
+@tooltip-arrow-border-width: 4px;\r
+@tooltip-arrow-border-width-before: 5px;\r
+@tooltip-arrow-border-height: @tooltip-arrow-border-width-before - @tooltip-arrow-border-width;\r
+@tooltip-arrow-away: 5px;\r
+@tooltip-arrow-background-color: #595959;\r
+@tooltip-arrow-border-color: #595959;\r
+@tooltip-away-host: 3px;\r
+\r
+.plx-tooltip {\r
+    font-family: @font-family;\r
+    font-size: @font-size;\r
+    opacity: 1;\r
+    position: absolute;\r
+    z-index: 10001;\r
+    display: block;\r
+    font-style: normal;\r
+    font-weight: normal;\r
+    letter-spacing: normal;\r
+    line-break: auto;\r
+    line-height: 1.5;\r
+    text-align: left;\r
+    text-decoration: none;\r
+    text-shadow: none;\r
+    text-transform: none;\r
+    white-space: normal;\r
+    word-break: normal;\r
+    word-spacing: normal;\r
+    word-wrap: break-word;\r
+    &::before,\r
+    &::after {\r
+        content: "";\r
+        position: absolute;\r
+        display: block;\r
+        width: 0;\r
+        height: 0;\r
+        border: solid transparent;\r
+    }\r
+    &::before {\r
+        border-width: @tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        border-width: @tooltip-arrow-border-width;\r
+    }\r
+}\r
+\r
+.plx-tooltip-inner {\r
+    min-width: 60px;\r
+    max-width: 200px;\r
+    padding: 3px 8px;\r
+    color: #fff;\r
+    text-align: center;\r
+    background-color: #000;\r
+}\r
+\r
+.plx-tooltip.show {\r
+       font-size: @font-size;\r
+       opacity: 1;\r
+}\r
+.plx-tooltip.show .plx-tooltip-inner {\r
+       background-color: #595959;\r
+       border-radius: @radius;\r
+       padding: 0px 12px;\r
+       height: 30px;\r
+       line-height: 30px;\r
+}\r
+\r
+.plx-tooltip-top-common {\r
+    margin-top: -(@tooltip-arrow-border-width + @tooltip-away-host);\r
+    &::before {\r
+        border-top-color: @tooltip-arrow-border-color;\r
+        border-bottom-width: 0;\r
+        bottom: -@tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        border-top-color: @tooltip-arrow-background-color;\r
+        border-bottom-width: 0;\r
+        bottom: -@tooltip-arrow-border-width;\r
+    }\r
+}\r
+.plx-tooltip-top {\r
+    .plx-tooltip-top-common;\r
+    &::before {\r
+        left: 50%;\r
+        margin-left: -@tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        left: 50%;\r
+        margin-left: -@tooltip-arrow-border-width;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-top-left {\r
+    .plx-tooltip-top-common;\r
+    &::before {\r
+        left: @tooltip-arrow-away;\r
+    }\r
+    &::after {\r
+        left: @tooltip-arrow-away + @tooltip-arrow-border-height;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-top-right {\r
+    .plx-tooltip-top-common;\r
+    &::before {\r
+        right: @tooltip-arrow-away;\r
+    }\r
+    &::after {\r
+        right: @tooltip-arrow-away + @tooltip-arrow-border-height;\r
+    }\r
+}\r
+\r
+.plx-tooltip-right-common {\r
+    margin-left: @tooltip-arrow-border-width + @tooltip-away-host;\r
+    &::before {\r
+        border-right-color: @tooltip-arrow-border-color;\r
+        border-left-width: 0;\r
+        left: -@tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        border-right-color: @tooltip-arrow-background-color;\r
+        border-left-width: 0;\r
+        left: -@tooltip-arrow-border-width;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-right {\r
+    .plx-tooltip-right-common;\r
+    &::before {\r
+        top: 50%;\r
+        margin-top: -@tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        top: 50%;\r
+        margin-top: -@tooltip-arrow-border-width;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-right-top {\r
+    .plx-tooltip-right-common;\r
+    &::before {\r
+        top: @tooltip-arrow-away;\r
+    }\r
+    &::after {\r
+        top: @tooltip-arrow-away + @tooltip-arrow-border-height;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-right-bottom {\r
+    .plx-tooltip-right-common;\r
+    &::before {\r
+        bottom: @tooltip-arrow-away;\r
+    }\r
+    &::after {\r
+        bottom: @tooltip-arrow-away + @tooltip-arrow-border-height;\r
+    }\r
+}\r
+\r
+.plx-tooltip-bottom-common {\r
+    margin-top: @tooltip-arrow-border-width + @tooltip-away-host;\r
+    &::before {\r
+        border-bottom-color: @tooltip-arrow-border-color;\r
+        border-top-width: 0;\r
+        top: -@tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        border-bottom-color: @tooltip-arrow-background-color;\r
+        border-top-width: 0;\r
+        top: -@tooltip-arrow-border-width;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-bottom {\r
+    .plx-tooltip-bottom-common;\r
+    &::before {\r
+        left: 50%;\r
+        margin-left: -@tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        left: 50%;\r
+        margin-left: -@tooltip-arrow-border-width;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-bottom-left {\r
+    .plx-tooltip-bottom-common;\r
+    &::before {\r
+        left: @tooltip-arrow-away;\r
+    }\r
+    &::after {\r
+        left: @tooltip-arrow-away + @tooltip-arrow-border-height;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-bottom-right {\r
+    .plx-tooltip-bottom-common;\r
+    &::before {\r
+        right: @tooltip-arrow-away;\r
+    }\r
+    &::after {\r
+        right: @tooltip-arrow-away + @tooltip-arrow-border-height;\r
+    }\r
+}\r
+\r
+.plx-tooltip-left-common {\r
+    margin-left: -(@tooltip-arrow-border-width + @tooltip-away-host);\r
+    &::before {\r
+        border-left-color: @tooltip-arrow-border-color;\r
+        border-right-width: 0;\r
+        right: -@tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        border-left-color: @tooltip-arrow-background-color;\r
+        border-right-width: 0;\r
+        right: -@tooltip-arrow-border-width;\r
+    }\r
+}\r
+\r
+.plx-tooltip.plx-tooltip-left {\r
+    .plx-tooltip-left-common;\r
+    &::before {\r
+        top: 50%;\r
+        margin-top: -@tooltip-arrow-border-width-before;\r
+    }\r
+    &::after {\r
+        top: 50%;\r
+        margin-top: -@tooltip-arrow-border-width;\r
+    }\r
+}\r
+\r
+.plx-tooltip.plx-tooltip-left-top {\r
+    .plx-tooltip-left-common;\r
+    &::before {\r
+        top: @tooltip-arrow-away;\r
+    }\r
+    &::after {\r
+        top: @tooltip-arrow-away + @tooltip-arrow-border-height;\r
+    }\r
+}\r
+.plx-tooltip.plx-tooltip-left-bottom {\r
+    .plx-tooltip-left-common;\r
+    &::before {\r
+        bottom: @tooltip-arrow-away;\r
+    }\r
+    &::after {\r
+        bottom: @tooltip-arrow-away + @tooltip-arrow-border-height;\r
+    }\r
+}
\ 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 (file)
index 0000000..062ded1
--- /dev/null
@@ -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 (file)
index 0000000..cd2d8a6
--- /dev/null
@@ -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 (file)
index 0000000..f52cc11
--- /dev/null
@@ -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();
+       }
+}