diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts
index a41fb67474..80dc539422 100644
--- a/demo/src/app/app.module.ts
+++ b/demo/src/app/app.module.ts
@@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
+import { FormsModule} from '@angular/forms';
import { RouterModule } from '@angular/router';
import { Ng2PageScrollModule } from 'ng2-page-scroll/ng2-page-scroll';
import { AppComponent } from './app.component';
diff --git a/demo/src/app/components/+timepicker/demo-timepicker.module.ts b/demo/src/app/components/+timepicker/demo-timepicker.module.ts
index 12104708c3..7d391f4280 100644
--- a/demo/src/app/components/+timepicker/demo-timepicker.module.ts
+++ b/demo/src/app/components/+timepicker/demo-timepicker.module.ts
@@ -1,6 +1,6 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { TimepickerModule } from 'ngx-bootstrap/timepicker';
@@ -17,6 +17,7 @@ import { routes } from './demo-timepicker.routes';
imports: [
CommonModule,
FormsModule,
+ ReactiveFormsModule,
SharedModule,
TimepickerModule.forRoot(),
RouterModule.forChild(routes)
diff --git a/demo/src/app/components/+timepicker/demos/custom-validation/custom-validation.html b/demo/src/app/components/+timepicker/demos/custom-validation/custom-validation.html
new file mode 100644
index 0000000000..282e2be738
--- /dev/null
+++ b/demo/src/app/components/+timepicker/demos/custom-validation/custom-validation.html
@@ -0,0 +1,12 @@
+
Illustrates custom validation, you have to select time between 11:00 and 12:59
+
+
+
+
+
+
+ Time is: {{myTime}}
+
+Invalid time
diff --git a/demo/src/app/components/+timepicker/demos/custom-validation/custom-validation.ts b/demo/src/app/components/+timepicker/demos/custom-validation/custom-validation.ts
new file mode 100644
index 0000000000..474a24c633
--- /dev/null
+++ b/demo/src/app/components/+timepicker/demos/custom-validation/custom-validation.ts
@@ -0,0 +1,23 @@
+import { Component } from '@angular/core';
+import { FormControl } from '@angular/forms';
+
+@Component({
+ selector: 'demo-timepicker-custom-validation',
+ templateUrl: './custom-validation.html'
+})
+export class DemoTimepickerCustomValidationComponent {
+ public myTime: Date;
+
+ public ctrl = new FormControl('', (control: FormControl) => {
+ const value = control.value;
+ if (!value) {
+ return null;
+ }
+ const hours = value.getHours();
+ if (hours < 11 || hours > 12) {
+ return {outOfRange: true};
+ }
+
+ return null;
+ });
+}
diff --git a/demo/src/app/components/+timepicker/demos/dynamic/dynamic.html b/demo/src/app/components/+timepicker/demos/dynamic/dynamic.html
index 7d728855e9..524f285c5f 100644
--- a/demo/src/app/components/+timepicker/demos/dynamic/dynamic.html
+++ b/demo/src/app/components/+timepicker/demos/dynamic/dynamic.html
@@ -1,6 +1,7 @@
-
+
Time is: {{mytime}}
+Invalid time format
diff --git a/demo/src/app/components/+timepicker/demos/dynamic/dynamic.ts b/demo/src/app/components/+timepicker/demos/dynamic/dynamic.ts
index 5802c4246a..f103e6aff2 100644
--- a/demo/src/app/components/+timepicker/demos/dynamic/dynamic.ts
+++ b/demo/src/app/components/+timepicker/demos/dynamic/dynamic.ts
@@ -7,6 +7,7 @@ import { Component } from '@angular/core';
export class DemoTimepickerDynamicComponent {
public mytime: Date = new Date();
+ public isValid: boolean;
public update(): void {
let d = new Date();
diff --git a/demo/src/app/components/+timepicker/demos/index.ts b/demo/src/app/components/+timepicker/demos/index.ts
index 3fcb2e56c5..0e6d204e32 100644
--- a/demo/src/app/components/+timepicker/demos/index.ts
+++ b/demo/src/app/components/+timepicker/demos/index.ts
@@ -4,10 +4,16 @@ import { DemoTimepickerMeridianComponent } from './meridian/meridian';
import { DemoTimepickerDisabledComponent } from './disabled/disabled';
import { DemoTimepickerCustomComponent } from './custom/custom';
import { DemoTimepickerDynamicComponent } from './dynamic/dynamic';
+import { DemoTimepickerMinMaxComponent } from './min-max/min-max';
+import { DemoTimepickerSecondsComponent } from './seconds/seconds';
+import { DemoTimepickerMousewheelArrowkeysComponent } from './mousewheel-arrowkeys/mousewheel-arrowkeys';
+import { DemoTimepickerCustomValidationComponent } from './custom-validation/custom-validation';
export const DEMO_COMPONENTS = [
DemoTimepickerBasicComponent, DemoTimepickerConfigComponent, DemoTimepickerMeridianComponent,
- DemoTimepickerDisabledComponent, DemoTimepickerCustomComponent, DemoTimepickerDynamicComponent
+ DemoTimepickerMinMaxComponent, DemoTimepickerDisabledComponent, DemoTimepickerCustomComponent,
+ DemoTimepickerDynamicComponent, DemoTimepickerSecondsComponent, DemoTimepickerMousewheelArrowkeysComponent,
+ DemoTimepickerCustomValidationComponent
];
export const DEMOS = {
@@ -19,6 +25,10 @@ export const DEMOS = {
component: require('!!raw-loader?lang=typescript!./meridian/meridian'),
html: require('!!raw-loader?lang=markup!./meridian/meridian.html')
},
+ minmax: {
+ component: require('!!raw-loader?lang=typescript!./min-max/min-max'),
+ html: require('!!raw-loader?lang=markup!./min-max/min-max.html')
+ },
disabled: {
component: require('!!raw-loader?lang=typescript!./disabled/disabled'),
html: require('!!raw-loader?lang=markup!./disabled/disabled.html')
@@ -34,5 +44,17 @@ export const DEMOS = {
config: {
component: require('!!raw-loader?lang=typescript!./config/config'),
html: require('!!raw-loader?lang=markup!./config/config.html')
+ },
+ seconds: {
+ component: require('!!raw-loader?lang=typescript!./seconds/seconds'),
+ html: require('!!raw-loader?lang=markup!./seconds/seconds.html')
+ },
+ mousewheel: {
+ component: require('!!raw-loader?lang=typescript!./mousewheel-arrowkeys/mousewheel-arrowkeys'),
+ html: require('!!raw-loader?lang=markup!./mousewheel-arrowkeys/mousewheel-arrowkeys.html')
+ },
+ customvalidation: {
+ component: require('!!raw-loader?lang=typescript!./custom-validation/custom-validation'),
+ html: require('!!raw-loader?lang=markup!./custom-validation/custom-validation.html')
}
};
diff --git a/demo/src/app/components/+timepicker/demos/meridian/meridian.html b/demo/src/app/components/+timepicker/demos/meridian/meridian.html
index ff60681c2c..b12725f611 100644
--- a/demo/src/app/components/+timepicker/demos/meridian/meridian.html
+++ b/demo/src/app/components/+timepicker/demos/meridian/meridian.html
@@ -2,7 +2,7 @@
Time is: {{mytime}}
-
+
diff --git a/demo/src/app/components/+timepicker/demos/meridian/meridian.ts b/demo/src/app/components/+timepicker/demos/meridian/meridian.ts
index 76e7a37165..6ec59f67c4 100644
--- a/demo/src/app/components/+timepicker/demos/meridian/meridian.ts
+++ b/demo/src/app/components/+timepicker/demos/meridian/meridian.ts
@@ -5,12 +5,11 @@ import { Component } from '@angular/core';
templateUrl: './meridian.html'
})
export class DemoTimepickerMeridianComponent {
- public ismeridian:boolean = true;
+ public ismeridian: boolean = true;
- public mytime:Date = new Date();
+ public mytime: Date = new Date();
- public toggleMode():void {
+ public toggleMode(): void {
this.ismeridian = !this.ismeridian;
}
-
}
diff --git a/demo/src/app/components/+timepicker/demos/min-max/min-max.html b/demo/src/app/components/+timepicker/demos/min-max/min-max.html
new file mode 100644
index 0000000000..ae707b97c8
--- /dev/null
+++ b/demo/src/app/components/+timepicker/demos/min-max/min-max.html
@@ -0,0 +1,3 @@
+
+
+Time is: {{myTime}}
diff --git a/demo/src/app/components/+timepicker/demos/min-max/min-max.ts b/demo/src/app/components/+timepicker/demos/min-max/min-max.ts
new file mode 100644
index 0000000000..60493f10b0
--- /dev/null
+++ b/demo/src/app/components/+timepicker/demos/min-max/min-max.ts
@@ -0,0 +1,18 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'demo-timepicker-min-max',
+ templateUrl: './min-max.html'
+})
+export class DemoTimepickerMinMaxComponent {
+ public myTime: Date = new Date();
+ public minTime: Date = new Date();
+ public maxTime: Date = new Date();
+
+ constructor() {
+ this.minTime.setHours(8);
+ this.minTime.setMinutes(0);
+ this.maxTime.setHours(17);
+ this.maxTime.setMinutes(0);
+ }
+}
diff --git a/demo/src/app/components/+timepicker/demos/mousewheel-arrowkeys/mousewheel-arrowkeys.html b/demo/src/app/components/+timepicker/demos/mousewheel-arrowkeys/mousewheel-arrowkeys.html
new file mode 100644
index 0000000000..6b36e993f0
--- /dev/null
+++ b/demo/src/app/components/+timepicker/demos/mousewheel-arrowkeys/mousewheel-arrowkeys.html
@@ -0,0 +1,11 @@
+Mouse wheel disabled
+
+
+
+Time is: {{myTime1}}
+
+Arrow keys disabled
+
+
+
+Time is: {{myTime2}}
diff --git a/demo/src/app/components/+timepicker/demos/mousewheel-arrowkeys/mousewheel-arrowkeys.ts b/demo/src/app/components/+timepicker/demos/mousewheel-arrowkeys/mousewheel-arrowkeys.ts
new file mode 100644
index 0000000000..acad0f6a12
--- /dev/null
+++ b/demo/src/app/components/+timepicker/demos/mousewheel-arrowkeys/mousewheel-arrowkeys.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'demo-timepicker-mousewheel-arrowkeys',
+ templateUrl: './mousewheel-arrowkeys.html'
+})
+export class DemoTimepickerMousewheelArrowkeysComponent {
+ public myTime1: Date = new Date();
+ public myTime2: Date = new Date();
+}
diff --git a/demo/src/app/components/+timepicker/demos/seconds/seconds.html b/demo/src/app/components/+timepicker/demos/seconds/seconds.html
new file mode 100644
index 0000000000..33e9d95e40
--- /dev/null
+++ b/demo/src/app/components/+timepicker/demos/seconds/seconds.html
@@ -0,0 +1,3 @@
+
+
+Time is: {{myTime}}
diff --git a/demo/src/app/components/+timepicker/demos/seconds/seconds.ts b/demo/src/app/components/+timepicker/demos/seconds/seconds.ts
new file mode 100644
index 0000000000..1ffc772eea
--- /dev/null
+++ b/demo/src/app/components/+timepicker/demos/seconds/seconds.ts
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'demo-timepicker-seconds',
+ templateUrl: './seconds.html'
+})
+export class DemoTimepickerSecondsComponent {
+ public myTime: Date = new Date();
+ public showSec: boolean = true;
+}
diff --git a/demo/src/app/components/+timepicker/timepicker-section.component.ts b/demo/src/app/components/+timepicker/timepicker-section.component.ts
index e40b400339..29dfee9fb7 100644
--- a/demo/src/app/components/+timepicker/timepicker-section.component.ts
+++ b/demo/src/app/components/+timepicker/timepicker-section.component.ts
@@ -16,10 +16,14 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');
API Reference
@@ -45,6 +49,16 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');
+
+ Min - Max
+
+
+
+
+ Show seconds
+
+
+
Disabled
@@ -54,6 +68,11 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');
Custom steps
+
+
+ Custom validation
+
+
Dynamic
@@ -65,6 +84,11 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');
+
+ Mouse wheel and Arrow keys
+
+
+
API Reference
diff --git a/demo/src/ng-api-doc.ts b/demo/src/ng-api-doc.ts
index 34a2e50736..dd8428ecce 100644
--- a/demo/src/ng-api-doc.ts
+++ b/demo/src/ng-api-doc.ts
@@ -1719,6 +1719,20 @@ export const ngdoc: any = {
}
]
},
+ "TimepickerActions": {
+ "fileName": "src/timepicker/reducer/timepicker.actions.ts",
+ "className": "TimepickerActions",
+ "description": "",
+ "methods": [],
+ "properties": []
+ },
+ "TimepickerStore": {
+ "fileName": "src/timepicker/reducer/timepicker.store.ts",
+ "className": "TimepickerStore",
+ "description": "",
+ "methods": [],
+ "properties": []
+ },
"TimepickerComponent": {
"fileName": "src/timepicker/timepicker.component.ts",
"className": "TimepickerComponent",
@@ -1765,10 +1779,20 @@ export const ngdoc: any = {
"type": "boolean",
"description": "if true hours and minutes fields will be readonly
\n"
},
+ {
+ "name": "secondsStep",
+ "type": "number",
+ "description": "seconds change step
\n"
+ },
{
"name": "showMeridian",
"type": "boolean",
- "description": "if true works in 12H mode and displays AM/PM. If false works in 24H mode and hides AM/PM
\n"
+ "description": ""
+ },
+ {
+ "name": "showSeconds",
+ "type": "boolean",
+ "description": ""
},
{
"name": "showSpinners",
@@ -1776,7 +1800,12 @@ export const ngdoc: any = {
"description": "if true spinner arrows above and below the inputs will be shown
\n"
}
],
- "outputs": [],
+ "outputs": [
+ {
+ "name": "isValid",
+ "description": "emits true if value is a valid date
\n"
+ }
+ ],
"properties": [],
"methods": []
},
@@ -1800,7 +1829,7 @@ export const ngdoc: any = {
},
{
"name": "max",
- "type": "number",
+ "type": "Date",
"description": "maximum time user can select
\n"
},
{
@@ -1810,7 +1839,7 @@ export const ngdoc: any = {
},
{
"name": "min",
- "type": "number",
+ "type": "Date",
"description": "minimum time user can select
\n"
},
{
@@ -1831,12 +1860,24 @@ export const ngdoc: any = {
"type": "boolean",
"description": "if true hours and minutes fields will be readonly
\n"
},
+ {
+ "name": "secondsStep",
+ "defaultValue": "10",
+ "type": "number",
+ "description": "seconds changes step
\n"
+ },
{
"name": "showMeridian",
"defaultValue": "true",
"type": "boolean",
"description": "if true works in 12H mode and displays AM/PM. If false works in 24H mode and hides AM/PM
\n"
},
+ {
+ "name": "showSeconds",
+ "defaultValue": "false",
+ "type": "boolean",
+ "description": "show seconds in timepicker
\n"
+ },
{
"name": "showSpinners",
"defaultValue": "true",
@@ -1845,6 +1886,34 @@ export const ngdoc: any = {
}
]
},
+ "Time": {
+ "fileName": "src/timepicker/timepicker.models.ts",
+ "className": "Time",
+ "description": "",
+ "methods": [],
+ "properties": []
+ },
+ "TimepickerControls": {
+ "fileName": "src/timepicker/timepicker.models.ts",
+ "className": "TimepickerControls",
+ "description": "",
+ "methods": [],
+ "properties": []
+ },
+ "TimepickerComponentState": {
+ "fileName": "src/timepicker/timepicker.models.ts",
+ "className": "TimepickerComponentState",
+ "description": "",
+ "methods": [],
+ "properties": []
+ },
+ "TimeChangeEvent": {
+ "fileName": "src/timepicker/timepicker.models.ts",
+ "className": "TimeChangeEvent",
+ "description": "",
+ "methods": [],
+ "properties": []
+ },
"TooltipContainerComponent": {
"fileName": "src/tooltip/tooltip-container.component.ts",
"className": "TooltipContainerComponent",
diff --git a/src/mini-ngrx/LICENCE b/src/mini-ngrx/LICENCE
new file mode 100644
index 0000000000..9b03581392
--- /dev/null
+++ b/src/mini-ngrx/LICENCE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 ngrx
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/mini-ngrx/index.ts b/src/mini-ngrx/index.ts
new file mode 100644
index 0000000000..9bcda8e916
--- /dev/null
+++ b/src/mini-ngrx/index.ts
@@ -0,0 +1,9 @@
+export interface Action {
+ type: string;
+ payload?: any;
+}
+
+export type ActionReducer = (state: T, action: Action) => T;
+
+export { MiniState } from './state.class';
+export { MiniStore } from './store.class';
diff --git a/src/mini-ngrx/state.class.ts b/src/mini-ngrx/state.class.ts
new file mode 100644
index 0000000000..4835ab7927
--- /dev/null
+++ b/src/mini-ngrx/state.class.ts
@@ -0,0 +1,26 @@
+/**
+ * @copyright ngrx
+ */
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import { Observable } from 'rxjs/Observable';
+import { Action, ActionReducer } from './index';
+import { observeOn } from 'rxjs/operator/observeOn';
+import { queue } from 'rxjs/scheduler/queue';
+import { scan } from 'rxjs/operator/scan';
+
+export class MiniState extends BehaviorSubject {
+ constructor(_initialState: T, actionsDispatcher$: Observable, reducer: ActionReducer) {
+ super(_initialState);
+
+ const actionInQueue$ = observeOn.call(actionsDispatcher$, queue);
+ const state$ = scan.call(actionInQueue$, (state: T, action: Action) => {
+ if (!action) {
+ return state;
+ }
+
+ return reducer(state, action);
+ }, _initialState);
+
+ state$.subscribe((value: T) => this.next(value));
+ }
+}
diff --git a/src/mini-ngrx/store.class.ts b/src/mini-ngrx/store.class.ts
new file mode 100644
index 0000000000..f636999a8b
--- /dev/null
+++ b/src/mini-ngrx/store.class.ts
@@ -0,0 +1,42 @@
+/**
+ * @copyright ngrx
+ */
+import { Observable } from 'rxjs/Observable';
+import { Observer } from 'rxjs/Observer';
+import { Operator } from 'rxjs/Operator';
+import { distinctUntilChanged } from 'rxjs/operator/distinctUntilChanged';
+
+import { map } from 'rxjs/operator/map';
+import { Action, ActionReducer } from './index';
+
+export class MiniStore extends Observable implements Observer {
+
+ constructor(private _dispatcher: Observer,
+ private _reducer: ActionReducer,
+ state$: Observable) {
+ super();
+
+ this.source = state$;
+ }
+
+ select(pathOrMapFn: (state: T) => R): Observable {
+ const mapped$: Observable = map.call(this, pathOrMapFn);
+
+ return distinctUntilChanged.call(mapped$);
+ }
+
+ lift(operator: Operator): MiniStore {
+ const store = new MiniStore(this._dispatcher, this._reducer, this);
+ store.operator = operator;
+
+ return store;
+ }
+
+ dispatch(action: Action) { this._dispatcher.next(action); }
+
+ next(action: Action) { this._dispatcher.next(action); }
+
+ error(err: any) { this._dispatcher.error(err); }
+
+ complete() {/*noop*/}
+}
diff --git a/src/spec/timepicker/timepicker-controls.util.spec.ts b/src/spec/timepicker/timepicker-controls.util.spec.ts
new file mode 100644
index 0000000000..65eb772ba7
--- /dev/null
+++ b/src/spec/timepicker/timepicker-controls.util.spec.ts
@@ -0,0 +1,175 @@
+import {
+ TimeChangeEvent,
+ TimepickerComponentState,
+ TimepickerControls
+} from '../../timepicker/timepicker.models';
+
+import {
+ canChangeHours, canChangeMinutes, canChangeSeconds,
+ canChangeValue, getControlsValue, timepickerControls
+} from '../../timepicker/timepicker-controls.util';
+
+function testTime(hours?: number, minutes?: number, seconds?: number) {
+ let time = new Date();
+ time.setHours(hours || 0);
+ time.setMinutes(minutes || 0);
+ time.setSeconds(seconds || 0);
+ return time;
+}
+
+describe('Runtime coverage. Util: Timepicker-controls', () => {
+ let state: TimepickerComponentState;
+ let controls: TimepickerControls;
+ let event: TimeChangeEvent;
+
+ beforeEach(() => {
+ state = {
+ min: null,
+ max: null,
+ hourStep: 1,
+ minuteStep: 5,
+ secondsStep: 10,
+ readonlyInput: false,
+ mousewheel: true,
+ arrowkeys: true,
+ showSpinners: true,
+ showMeridian: true,
+ showSeconds: false,
+ meridians: ['AM', 'PM']
+ };
+
+ controls = {
+ canIncrementHours: false,
+ canIncrementMinutes: false,
+ canIncrementSeconds: false,
+ canDecrementHours: false,
+ canDecrementMinutes: false,
+ canDecrementSeconds: false
+ };
+
+ event = {
+ step: 1,
+ source: ''
+ };
+ });
+
+ it('should can change value read only', () => {
+ canChangeValue(state, event);
+
+ state.readonlyInput = true;
+ canChangeValue(state, event);
+ });
+
+ it('should can change value event', () => {
+ canChangeValue(state);
+ canChangeValue(state, event);
+ });
+
+ it('should can change value event source and wheel', () => {
+ event.source = 'wheel';
+ state.mousewheel = false;
+
+ canChangeValue(state, event);
+ });
+
+ it('should can change value event source and key', () => {
+ event.source = 'key';
+ state.arrowkeys = false;
+
+ canChangeValue(state, event);
+ });
+
+ it('should change Hours', () => {
+ canChangeHours(event, controls);
+ });
+
+ it('should change Hours no step', () => {
+ event.step = null;
+ canChangeHours(event, controls);
+ });
+
+ it('should change Hours step is -1', () => {
+ event.step = -1;
+ canChangeHours(event, controls);
+ });
+
+ it('should change Hours can increment', () => {
+ controls.canIncrementHours = true;
+ canChangeHours(event, controls);
+ });
+
+ it('should change Minutes', () => {
+ canChangeMinutes(event, controls);
+ });
+
+ it('should change Minutes no step', () => {
+ event.step = null;
+ canChangeMinutes(event, controls);
+ });
+
+ it('should change Minutes step is -1', () => {
+ event.step = -1;
+ canChangeMinutes(event, controls);
+ });
+
+ it('should change Minutes can increment', () => {
+ controls.canIncrementMinutes = true;
+ canChangeMinutes(event, controls);
+ });
+
+ it('should change Seconds', () => {
+ canChangeSeconds(event, controls);
+ });
+
+ it('should change Seconds no step', () => {
+ event.step = null;
+ canChangeSeconds(event, controls);
+ });
+
+ it('should change Seconds step is -1', () => {
+ event.step = -1;
+ canChangeSeconds(event, controls);
+ });
+
+ it('should change Seconds can increment', () => {
+ controls.canIncrementSeconds = true;
+ canChangeSeconds(event, controls);
+ });
+
+ it('should get controls value', () => {
+ getControlsValue(state);
+ });
+
+ it('should set data in timepicker controls', () => {
+ timepickerControls(new Date(), state);
+ });
+
+ it('should set data in timepicker controls without date', () => {
+ // unreachable code
+ });
+
+ it('should set data in timepicker controls without showSeconds', () => {
+ state.showSeconds = true;
+ timepickerControls(new Date(), state);
+ });
+
+ it('should set data in timepicker controls with max', () => {
+ state.max = new Date();
+ timepickerControls(new Date(), state);
+ });
+
+ it('should set data in timepicker controls with max greater to control time', () => {
+ state.max = testTime(1);
+ timepickerControls(testTime(), state);
+ });
+
+ it('should set data in timepicker controls with min', () => {
+ state.min = new Date();
+ timepickerControls(new Date(), state);
+ });
+
+ it('should set data in timepicker controls with min greater to control time', () => {
+ state.min = testTime(1);
+ timepickerControls(testTime(), state);
+ });
+});
diff --git a/src/spec/timepicker/timepicker.component.spec.ts b/src/spec/timepicker/timepicker.component.spec.ts
new file mode 100644
index 0000000000..c2a616fa5e
--- /dev/null
+++ b/src/spec/timepicker/timepicker.component.spec.ts
@@ -0,0 +1,1010 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import { fireEvent } from '../../../scripts/helpers';
+
+import { TimepickerConfig } from '../../timepicker/timepicker.config';
+import { TimepickerActions } from '../../timepicker/reducer/timepicker.actions';
+import { TimepickerModule } from '../../timepicker/timepicker.module';
+import { TimepickerComponent } from '../../timepicker/timepicker.component';
+import { By } from '@angular/platform-browser';
+
+function getInputElements(fixture: any) {
+ return fixture.nativeElement.querySelectorAll('input') as HTMLInputElement;
+}
+
+function getElements(fixture: any, selector: string) {
+ return fixture.nativeElement.querySelectorAll(selector) as HTMLElement;
+}
+
+function getDebugElements(fixture: any, selector: string) {
+ return fixture.debugElement.queryAll(By.css(selector));
+}
+
+function testTime(hours?: number, minutes?: number, seconds?: number) {
+ let time = new Date();
+ time.setHours(hours || 0);
+ time.setMinutes(minutes || 0);
+ time.setSeconds(seconds || 0);
+ return time;
+}
+
+describe('Component: timepicker', () => {
+ let fixture: ComponentFixture;
+ let component: TimepickerComponent;
+ let inputHours: HTMLInputElement;
+ let inputMinutes: HTMLInputElement;
+ let inputSeconds: HTMLInputElement;
+ let inputDebugHours: any;
+ let inputDebugMinutes: any;
+ let inputDebugSeconds: any;
+ let buttonMeridian: HTMLElement;
+ let buttonDebugMeridian: any;
+ let buttonChanges: HTMLElement;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TimepickerModule.forRoot(),
+ FormsModule,
+ ReactiveFormsModule
+ ],
+ providers: [
+ TimepickerConfig,
+ TimepickerActions
+ ]
+ });
+ });
+
+ describe('default configuration', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ buttonChanges = getElements(fixture, 'a.btn');
+ buttonMeridian = getElements(fixture, 'button');
+ });
+
+ // поле часы и минуты отображаются
+ it('should seconds fields is not display', () => {
+ expect(inputHours).toBeTruthy();
+ expect(inputMinutes).toBeTruthy();
+ });
+ // поле секунды не отображается
+ it('should seconds fields is not display', () => {
+ expect(inputSeconds).toBeFalsy();
+ });
+ // поле часы и минуты должно быть пустым
+ it('should be empty inputs fields hours and minutes', () => {
+ expect(inputHours.value).toBeFalsy();
+ expect(inputMinutes.value).toBeFalsy();
+ });
+ // должны отображаться кнопки изменения времени
+ it('should visible change buttons', () => {
+ expect(buttonChanges).toBeTruthy();
+ });
+ // должна отображаться кнопка меридиана
+ it('should visible meridian button', () => {
+ expect(buttonMeridian).toBeTruthy();
+ });
+ });
+
+ describe('validate input fields with default state', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ buttonChanges = getElements(fixture, 'a.btn');
+ });
+
+ // проверить данные в поле минуты корректные данные
+ it('should validate the data in the minutes input with valid data', () => {
+ fireEvent(inputMinutes, 'change');
+
+ component.writeValue(testTime(0,12,0));
+
+ fixture.detectChanges();
+
+ expect(inputMinutes.value).toEqual('12');
+ });
+ // проверить данные в поле минуты корректные данные с неполным значением
+ it('should validate the data in the minutes input with valid data with half value', () => {
+ component.writeValue(testTime(0,2,0));
+ fixture.detectChanges();
+
+ expect(inputMinutes.value).toEqual('02');
+ });
+ // установить время путем нажатия на кнопку изменения времени
+ it('should set time in a input field after click on input change button', () => {
+ expect(inputHours.value).toBeFalsy();
+ expect(inputMinutes.value).toBeFalsy();
+
+ fireEvent(buttonChanges[0], 'click');
+
+ fixture.detectChanges();
+
+ expect(inputHours.value).toBe('01');
+ expect(inputMinutes.value).toBe('00');
+ });
+ });
+
+ describe('validate input fields with property of showMeridian switch on', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ buttonMeridian = getElements(fixture, 'button')[0];
+ buttonDebugMeridian = getDebugElements(fixture, 'button')[0];
+ });
+
+ // отобразить кнопку AM/PM при состоянии showMeridian по умолчанию
+ it('should default state showMeridian display AM/PM button', () => {
+ expect(buttonMeridian).toBeTruthy();
+ });
+ // проверить данные в поле ввода Часы при вормате времени 12h
+ it('should validate the data in the hours input at time format 12h', () => {
+ fireEvent(inputHours, 'change');
+
+ component.writeValue(testTime(22));
+
+ fixture.detectChanges();
+
+ expect(inputHours.value).toEqual('10');
+ });
+ // изменить временной период после клика на кнопку AM/PM
+ it('should change time period after click on AM/PM button', () => {
+ expect(buttonMeridian.textContent.trim()).toBe(component.meridians[0]);
+
+ buttonDebugMeridian.triggerEventHandler('click', null);
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(buttonMeridian.textContent.trim()).toBe(component.meridians[1]);
+ });
+ });
+ // изменить временной период после клика на кнопку AM/PM без readonlyInput
+ it('should change time period after click on AM/PM button without readonlyInput', () => {
+ component.readonlyInput = false;
+ component.showMeridian = false;
+
+ fixture.detectChanges();
+
+ buttonDebugMeridian.triggerEventHandler('click', null);
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(buttonMeridian.textContent.trim()).toBe(component.meridians[0]);
+ });
+ });
+ // изменить временной период после клика на кнопку AM/PM с readonlyInput
+ it('should change time period after click on AM/PM button with readonlyInput', () => {
+ component.readonlyInput = false;
+ component.showMeridian = true;
+
+ fixture.detectChanges();
+
+ buttonDebugMeridian.triggerEventHandler('click', null);
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(buttonMeridian.textContent.trim()).toBe(component.meridians[1]);
+ });
+ });
+ });
+
+ describe('validate input fields with property of showMeridian switch off', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ buttonMeridian = getElements(fixture, 'button')[0];
+ inputHours = getInputElements(fixture)[0];
+ });
+
+ // не отобразить кнопку AM/PM если showMeridian выключен
+ it('should not display AM/PM button if showMeridian switch off', () => {
+ expect(buttonMeridian).toBeTruthy();
+
+ component.showMeridian = false;
+ component.writeValue(testTime());
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ buttonMeridian = getElements(fixture, 'button')[0];
+ expect(buttonMeridian).toBeFalsy();
+ });
+ });
+ // проверить данные в поле часы при формате времени 24h
+ it('should validate the data in the hours input at time format 24h', () => {
+ component.showMeridian = false;
+
+ component.writeValue(testTime(22));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(inputHours.value).toBe('22');
+ });
+ });
+ });
+
+ describe('validate input fields with property of max', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ buttonChanges = getElements(fixture, 'a.btn');
+ });
+
+ // заблокировать кнопку увеличения часов
+ it('should block the hours / minutes increment button if clicking on it will cause exceeding the max value', () => {
+ component.max = testTime(18);
+ component.writeValue(testTime(17,50));
+ fixture.detectChanges();
+
+ expect(buttonChanges[0]).toHaveCssClass('disabled');
+ expect(buttonChanges[1]).not.toHaveCssClass('disabled');
+
+ component.writeValue(testTime(17,57));
+ fixture.detectChanges();
+
+ expect(buttonChanges[0]).toHaveCssClass('disabled');
+ expect(buttonChanges[1]).toHaveCssClass('disabled');
+ });
+ });
+
+ describe('validate input fields with property of min', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ buttonChanges = getElements(fixture, 'a.btn');
+ });
+
+ // заблокировать кнопку уменьшения часов
+ it('should block the hours / minutes decrement button if clicking on it will cause exceeding the min value', () => {
+ component.min = testTime(13);
+ component.writeValue(testTime(13,22));
+ fixture.detectChanges();
+
+ expect(buttonChanges[2]).toHaveCssClass('disabled');
+ expect(buttonChanges[3]).not.toHaveCssClass('disabled');
+
+ component.writeValue(testTime(13, 2));
+ fixture.detectChanges();
+
+ expect(buttonChanges[2]).toHaveCssClass('disabled');
+ expect(buttonChanges[3]).toHaveCssClass('disabled');
+
+ });
+ });
+
+ describe('display seconds fields with property of showSeconds', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputSeconds = getInputElements(fixture)[2];
+ });
+
+ // отображать поле секунды если showSeconds включен
+ it('should display seconds field if showMeridian switch on', () => {
+ component.showSeconds = true;
+
+ component.writeValue(testTime());
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+
+ fireEvent(inputSeconds, 'change');
+
+ expect(inputSeconds).toBeTruthy();
+ });
+ });
+ // проверить данные в поле секунды
+ it('should validate the data in the seconds input', () => {
+ component.showSeconds = true;
+
+ component.writeValue(testTime(2,6,7));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+
+ fireEvent(inputSeconds, 'change');
+
+ expect(inputSeconds.value).toBe('07');
+ });
+ });
+ });
+
+ describe('input fields with property of readonlyInput', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ inputSeconds = getInputElements(fixture)[2];
+ buttonChanges = getElements(fixture, 'a.btn');
+ });
+ // должна быть возможность ввода значений
+ it('should be possible to enter values', () => {
+ expect(inputHours.getAttribute('readonly')).toBeFalsy();
+ expect(inputMinutes.getAttribute('readonly')).toBeFalsy();
+
+ component.showSeconds = true;
+ component.writeValue(testTime());
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ fixture.detectChanges();
+ return fixture.whenStable();
+ })
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+
+ expect(inputSeconds.getAttribute('readonly')).toBeFalsy();
+ });
+ });
+ // должна отображать кнопки изменения времени
+ it('should be display is time change buttons', () => {
+ expect(buttonChanges).toBeTruthy();
+ });
+ // не должно быть возможности ввода значений
+ it('should be impossible to enter values', () => {
+ component.readonlyInput = true;
+ component.showSeconds = true;
+
+ component.writeValue(testTime());
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ fixture.detectChanges();
+ return fixture.whenStable();
+ })
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+
+ expect(inputHours.getAttribute('readonly')).toBe('');
+ expect(inputMinutes.getAttribute('readonly')).toBe('');
+ expect(inputSeconds.getAttribute('readonly')).toBe('');
+ });
+ });
+ // не должны отображаться кнопки изменения времени
+ it('should not display is time change buttons', () => {
+ expect(buttonChanges).toBeTruthy();
+
+ component.readonlyInput = true;
+
+ component.writeValue(testTime());
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ const buttonsHidden = fixture.nativeElement.querySelector('a.btn');
+ expect(buttonsHidden.parentElement.parentElement.className).toContain('hidden');
+ });
+ });
+ });
+
+ describe('input fields hour with property of hourStep', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ inputSeconds = getInputElements(fixture)[2];
+ buttonChanges = getElements(fixture, 'a.btn');
+ });
+ // добавить в поле ввода часы значение с учетом hourStep инкримент
+ it('should add to the hour input field value, hourStep value increment', () => {
+ component.hourStep = 2;
+
+ component.writeValue(testTime());
+
+ fireEvent(buttonChanges[0], 'click');
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(inputHours.value).toBe('02');
+ });
+ });
+ // добавить в поле ввода часы значение с учетом hourStep декримент
+ it('should add to the hour input field value, hourStep value decrement', () => {
+ component.hourStep = 2;
+
+ component.writeValue(testTime(6));
+
+ fireEvent(buttonChanges[2], 'click');
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(inputHours.value).toBe('04');
+ });
+ });
+ // вычесть в поле ввода часы значение с учетом minuteStep инкримент
+ it('should input field value, minuteStep value increment', () => {
+ component.minuteStep = 12;
+
+ component.writeValue(testTime(6,22));
+
+ fireEvent(buttonChanges[1], 'click');
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(inputMinutes.value).toBe('34');
+ });
+ });
+ // вычесть в поле ввода часы значение с учетом minuteStep декримент
+ it('should input field value, minuteStep value decrement', () => {
+ component.minuteStep = 12;
+
+ component.writeValue(testTime(6,22));
+
+ fireEvent(buttonChanges[3], 'click');
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(inputMinutes.value).toBe('10');
+ });
+ });
+ // вычесть в поле ввода часы значение с учетом secondsStep инкримент
+ it('should input field value, secondsStep value increment', () => {
+ component.showSeconds = true;
+ component.secondsStep = 10;
+
+ component.writeValue(testTime(6,22, 30));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ buttonChanges = getElements(fixture, 'a.btn');
+
+ fireEvent(buttonChanges[2], 'click');
+
+ fixture.detectChanges();
+ return fixture.whenStable();
+ })
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+ expect(inputSeconds.value).toBe('40');
+ });
+ });
+ // вычесть в поле ввода часы значение с учетом secondsStep декримент
+ it('should input field value, secondsStep value decrement', () => {
+ component.showSeconds = true;
+ component.secondsStep = 10;
+
+ component.writeValue(testTime(6,22, 30));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ buttonChanges = getElements(fixture, 'a.btn');
+ fireEvent(buttonChanges[5], 'click');
+
+ fixture.detectChanges();
+ return fixture.whenStable();
+ })
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+ expect(inputSeconds.value).toBe('20');
+ });
+ });
+ });
+
+ describe('hide change button', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ buttonChanges = getElements(fixture, 'a.btn');
+ });
+
+ // скрыть кнопки изменения времени
+ it('should hide change button', () => {
+ component.showSpinners = false;
+
+ component.writeValue(testTime());
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ const buttonsHidden = fixture.nativeElement.querySelector('a.btn');
+ expect(buttonsHidden.parentElement.parentElement.className).toContain('hidden');
+ });
+ });
+ });
+
+ describe('validate mousewheel', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ inputSeconds = getInputElements(fixture)[2];
+ inputDebugHours = getDebugElements(fixture, 'input')[0];
+ inputDebugMinutes = getDebugElements(fixture, 'input')[1];
+ inputDebugSeconds = getDebugElements(fixture, 'input')[2];
+ });
+
+ // измененить часы колесом мыши инкремент
+ it('should can change hours value with the mouse wheel increment', () => {
+ const methodSpy = spyOn(component, 'changeHours').and.callThrough();
+ component.hourStep = 3;
+
+ component.writeValue(testTime(6,30,30));
+ fixture.detectChanges();
+
+ const wheelEvent = new WheelEvent(inputDebugHours, {deltaY: -1});
+
+ inputDebugHours.triggerEventHandler('wheel', wheelEvent);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputHours.value).toEqual('09');
+ expect(methodSpy).toHaveBeenCalledWith(component.hourStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ // измененить минуты колесом мыши инкремент
+ it('should can change minutes value with the mouse wheel increment', () => {
+ const methodSpy = spyOn(component, 'changeMinutes').and.callThrough();
+ component.minuteStep = 3;
+
+ component.writeValue(testTime(6,30,30));
+ fixture.detectChanges();
+
+ const wheelEvent = new WheelEvent(inputDebugMinutes, {deltaY: -1});
+
+ inputDebugMinutes.triggerEventHandler('wheel', wheelEvent);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputMinutes.value).toEqual('33');
+ expect(methodSpy).toHaveBeenCalledWith(component.minuteStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ // измененить секунды колесом мыши инкремент
+ it('should can change seconds value with the mouse wheel increment', () => {
+ const methodSpy = spyOn(component, 'changeSeconds').and.callThrough();
+
+ component.showSeconds = true;
+ component.secondsStep = 3;
+
+ component.writeValue(testTime(6,30,30));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ inputDebugSeconds = getDebugElements(fixture, 'input')[2];
+
+ let wheelEvent = new WheelEvent(inputDebugSeconds, {deltaY: -1});
+ inputDebugSeconds.triggerEventHandler('wheel', wheelEvent);
+
+ fixture.detectChanges();
+
+ inputSeconds = getInputElements(fixture)[2];
+
+ expect(inputSeconds.value).toEqual('33');
+ expect(methodSpy).toHaveBeenCalledWith(component.secondsStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ // измененить часы колесом мыши декремент
+ it('should can change hours value with the mouse wheel decrement', () => {
+ const methodSpy = spyOn(component, 'changeHours').and.callThrough();
+ component.hourStep = 3;
+
+ component.writeValue(testTime(6,30,30));
+ fixture.detectChanges();
+
+ const wheelEvent = new WheelEvent(inputDebugHours, {deltaY: 1});
+
+ inputDebugHours.triggerEventHandler('wheel', wheelEvent);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputHours.value).toEqual('03');
+ expect(methodSpy).toHaveBeenCalledWith(component.hourStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ // измененить минуты колесом мыши декремент
+ it('should can change minutes value with the mouse wheel decrement', () => {
+ const methodSpy = spyOn(component, 'changeMinutes').and.callThrough();
+ component.minuteStep = 3;
+
+ component.writeValue(testTime(6,30,30));
+ fixture.detectChanges();
+
+ const wheelEvent = new WheelEvent(inputDebugMinutes, {deltaY: 1});
+
+ inputDebugMinutes.triggerEventHandler('wheel', wheelEvent);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputMinutes.value).toEqual('27');
+ expect(methodSpy).toHaveBeenCalledWith(component.minuteStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ // измененить секунды колесом мыши декремент
+ it('should can change seconds value with the mouse wheel decrement', () => {
+ const methodSpy = spyOn(component, 'changeSeconds').and.callThrough();
+ component.secondsStep = 3;
+ component.showSeconds = true;
+
+ component.writeValue(testTime(6,30,30));
+
+ fixture.detectChanges();
+ fixture.whenStable().then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+ inputDebugSeconds = getDebugElements(fixture, 'input')[2];
+
+ const wheelEvent = new WheelEvent(inputDebugSeconds, {deltaY: 1});
+
+ inputDebugSeconds.triggerEventHandler('wheel', wheelEvent);
+
+ fixture.detectChanges();
+
+ expect(inputSeconds.value).toEqual('27');
+ expect(methodSpy).toHaveBeenCalledWith(component.secondsStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ // отключить изменение часы колесом мыши
+ it('should can not change hours value with the mouse wheel', () => {
+ const methodSpy = spyOn(component, 'changeHours').and.callThrough();
+ component.hourStep = 3;
+ component.mousewheel = false;
+
+ component.writeValue(testTime(6,30,30));
+ fixture.detectChanges();
+
+ const wheelEvent = new WheelEvent(inputDebugHours, {deltaY: 1});
+
+ inputDebugHours.triggerEventHandler('wheel', wheelEvent);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputHours.value).toEqual('06');
+ expect(methodSpy).toHaveBeenCalledWith(component.hourStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ // отключить изменение минуты колесом мыши
+ it('should can not change minutes value with the mouse wheel', () => {
+ const methodSpy = spyOn(component, 'changeMinutes').and.callThrough();
+ component.minuteStep = 3;
+ component.mousewheel = false;
+
+ component.writeValue(testTime(6,30,30));
+ fixture.detectChanges();
+
+ const wheelEvent = new WheelEvent(inputDebugMinutes, {deltaY: 1});
+
+ inputDebugMinutes.triggerEventHandler('wheel', wheelEvent);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputMinutes.value).toEqual('30');
+ expect(methodSpy).toHaveBeenCalledWith(component.minuteStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ // отключить изменение секунды колесом мыши
+ it('should can not change seconds value with the mouse wheel', () => {
+ const methodSpy = spyOn(component, 'changeSeconds').and.callThrough();
+ component.showSeconds = true;
+ component.secondsStep = 3;
+ component.mousewheel = false;
+
+ component.writeValue(testTime(6,30,30));
+
+ const wheelEvent = new WheelEvent(inputDebugSeconds, {deltaY: 1});
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+ inputDebugSeconds = getDebugElements(fixture, 'input')[2];
+
+ inputDebugSeconds.triggerEventHandler('wheel', wheelEvent);
+
+ fixture.detectChanges();
+
+ expect(inputSeconds.value).toEqual('30');
+ expect(methodSpy).toHaveBeenCalledWith(component.secondsStep * component.wheelSign(wheelEvent), 'wheel');
+ });
+ });
+ });
+
+ describe('validate arrowkeys', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ inputSeconds = getInputElements(fixture)[2];
+ inputDebugHours = getDebugElements(fixture, 'input')[0];
+ inputDebugMinutes = getDebugElements(fixture, 'input')[1];
+ inputDebugSeconds = getDebugElements(fixture, 'input')[2];
+ });
+
+ // изменить часы кнопками вверх
+ it('should can change hours value with the arrow keys up', () => {
+ const methodSpy = spyOn(component, 'changeHours').and.callThrough();
+ component.hourStep = 3;
+
+ component.writeValue(testTime(6,2,3));
+ fixture.detectChanges();
+
+ inputDebugHours.triggerEventHandler('keydown.ArrowUp', null);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputHours.value).toEqual('09');
+ expect(methodSpy).toHaveBeenCalledWith(component.hourStep, 'key');
+ });
+ });
+ // изменить минуты кнопками вверх
+ it('should can change minutes value with the arrow keys up', () => {
+ const methodSpy = spyOn(component, 'changeMinutes').and.callThrough();
+ component.minuteStep = 3;
+
+ component.writeValue(testTime(6,2,3));
+ fixture.detectChanges();
+
+ inputDebugMinutes.triggerEventHandler('keydown.ArrowUp', null);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputMinutes.value).toEqual('05');
+ expect(methodSpy).toHaveBeenCalledWith(component.minuteStep, 'key');
+ });
+ });
+ // изменить секунды кнопками вверх
+ it('should can change seconds value with the arrow keys up', () => {
+ const methodSpy = spyOn(component, 'changeSeconds').and.callThrough();
+ component.showSeconds = true;
+ component.secondsStep = 3;
+
+ component.writeValue(testTime(6,2,3));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ inputDebugSeconds = getDebugElements(fixture, 'input')[2];
+
+ inputDebugSeconds.triggerEventHandler('keydown.ArrowUp', null);
+
+ fixture.detectChanges();
+ return fixture.whenStable();
+ })
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+
+ expect(inputSeconds.value).toEqual('06');
+ expect(methodSpy).toHaveBeenCalledWith(component.secondsStep, 'key');
+ });
+ });
+ // изменить часы кнопками вниз
+ it('should can not change hours value with the arrow keys down', () => {
+ const methodSpy = spyOn(component, 'changeHours').and.callThrough();
+ component.hourStep = 3;
+
+ component.writeValue(testTime(6,2,3));
+ fixture.detectChanges();
+
+ inputDebugHours.triggerEventHandler('keydown.ArrowDown', null);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputHours.value).toEqual('03');
+ expect(methodSpy).toHaveBeenCalledWith(-component.hourStep, 'key');
+ });
+ });
+ // изменить минуты кнопками вниз
+ it('should can not change minutes value with the arrow keys down', () => {
+ const methodSpy = spyOn(component, 'changeMinutes').and.callThrough();
+ component.minuteStep = 3;
+
+ component.writeValue(testTime(6,2,3));
+ fixture.detectChanges();
+
+ inputDebugMinutes.triggerEventHandler('keydown.ArrowDown', null);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputMinutes.value).toEqual('59');
+ expect(methodSpy).toHaveBeenCalledWith(-component.minuteStep, 'key');
+ });
+ });
+ // изменить секунды кнопками вниз
+ it('should can not change seconds value with the arrow keys down', () => {
+ const methodSpy = spyOn(component, 'changeSeconds').and.callThrough();
+
+ component.showSeconds = true;
+ component.secondsStep = 3;
+
+ component.writeValue(testTime(6,2,3));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ inputDebugSeconds = getDebugElements(fixture, 'input')[2];
+
+ inputDebugSeconds.triggerEventHandler('keydown.ArrowDown', null);
+
+ fixture.detectChanges();
+ return fixture.whenStable();
+ })
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+
+ expect(inputSeconds.value).toEqual('00');
+ expect(methodSpy).toHaveBeenCalledWith(-component.secondsStep, 'key');
+ });
+ });
+ // отключить часы времени кнопками
+ it('should can not change hours value with the arrow keys', () => {
+ const methodSpy = spyOn(component, 'changeHours').and.callThrough();
+ component.hourStep = 3;
+ component.arrowkeys = false;
+
+ component.writeValue(testTime(6,2,3));
+ fixture.detectChanges();
+
+ inputDebugHours.triggerEventHandler('keydown.ArrowUp', null);
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(inputHours.value).toEqual('06');
+ expect(methodSpy).toHaveBeenCalledWith(component.hourStep, 'key');
+ });
+ });
+ // отключить минуты времени кнопками
+ it('should can not change minutes value with the arrow keys', () => {
+ const methodSpy = spyOn(component, 'changeMinutes').and.callThrough();
+ component.minuteStep = 3;
+ component.arrowkeys = false;
+
+ component.writeValue(testTime(6,2,3));
+ fixture.detectChanges();
+
+ inputDebugMinutes.triggerEventHandler('keydown.ArrowUp', null);
+ fixture.detectChanges();
+
+ fixture.whenStable().then(() => {
+ expect(inputMinutes.value).toEqual('02');
+ expect(methodSpy).toHaveBeenCalledWith(component.minuteStep, 'key');
+ });
+ });
+ // отключить секунды времени кнопками
+ it('should can not change seconds value with the arrow keys', () => {
+ const methodSpy = spyOn(component, 'changeSeconds').and.callThrough();
+
+ component.showSeconds = true;
+ component.secondsStep = 3;
+ component.arrowkeys = false;
+
+ component.writeValue(testTime(6,2,3));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ inputDebugSeconds = getDebugElements(fixture, 'input')[2];
+
+ inputDebugSeconds.triggerEventHandler('keydown.ArrowUp', null);
+
+ fixture.detectChanges();
+ return fixture.whenStable();
+ })
+ .then(() => {
+ inputSeconds = getInputElements(fixture)[2];
+
+ expect(inputSeconds.value).toEqual('03');
+ expect(methodSpy).toHaveBeenCalledWith(component.secondsStep, 'key');
+ });
+ });
+ });
+
+ describe('custom validate', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TimepickerComponent);
+ fixture.detectChanges();
+
+ component = fixture.componentInstance;
+ inputHours = getInputElements(fixture)[0];
+ inputMinutes = getInputElements(fixture)[1];
+ inputSeconds = getInputElements(fixture)[2];
+ });
+
+ // отставить поля не заполненными
+ it('should leave the input fields not specified', () => {
+ expect(inputHours.value).toBe('');
+ expect(inputMinutes.value).toBe('');
+ });
+ // не верное значение поля должно сбрасывать время
+ it('should clear model if values are invalid', () => {
+ component.showSeconds = true;
+ component.writeValue(testTime(12,12,12));
+ fixture.detectChanges();
+ inputSeconds = getInputElements(fixture)[2];
+
+ expect(inputHours.value).toBe('12');
+ expect(inputMinutes.value).toBe('12');
+ expect(inputSeconds.value).toBe('12');
+
+ const methodSpy = spyOn(component, 'onChange').and.callThrough();
+ component.hours = '99';
+ component.minutes = '99';
+ component.seconds = '99';
+ component._updateTime();
+ fixture.detectChanges();
+
+ expect(methodSpy).toHaveBeenCalledWith(null);
+ });
+ // верное значение поля
+ it('should valid value in input fields', () => {
+ component.showSeconds = true;
+ component.showMeridian = false;
+
+ component.writeValue(testTime(11,25,45));
+
+ fixture.detectChanges();
+
+ expect(inputHours.value).toBeGreaterThan(0);
+ expect(inputHours.value).toBeLessThan(13);
+
+ expect(inputMinutes.value).toBeGreaterThan(-1);
+ expect(inputMinutes.value).toBeLessThan(60);
+
+ component.writeValue(testTime(22,25,45));
+
+ fixture.detectChanges();
+ fixture.whenStable()
+ .then(() => {
+ expect(inputHours.value).toBeGreaterThan(-1);
+ expect(inputHours.value).toBeLessThan(24);
+
+ inputSeconds = getInputElements(fixture)[2];
+ expect(inputSeconds.value).toBeGreaterThan(-1);
+ expect(inputSeconds.value).toBeLessThan(60);
+ });
+ });
+ });
+});
diff --git a/src/spec/timepicker/timepicker.utils.spec.ts b/src/spec/timepicker/timepicker.utils.spec.ts
new file mode 100644
index 0000000000..2e5b00c626
--- /dev/null
+++ b/src/spec/timepicker/timepicker.utils.spec.ts
@@ -0,0 +1,135 @@
+import {
+ changeTime,
+ createDate,
+ isNumber,
+ isValidDate,
+ padNumber,
+ parseHours,
+ parseMinutes,
+ parseSeconds,
+ parseTime,
+ setTime,
+ toNumber
+} from '../../timepicker/timepicker.utils';
+
+function testTime(hours?: number, minutes?: number, seconds?: number) {
+ let time = new Date();
+ time.setHours(hours || 0);
+ time.setMinutes(minutes || 0);
+ time.setSeconds(seconds || 0);
+ return time;
+}
+
+function modelTime(hours: string | number, minutes: string | number, second: string | number, PM: boolean) {
+ let time = {
+ hour: hours || null,
+ minute: minutes || null,
+ seconds: second || null,
+ isPM: PM || null
+ };
+ return time;
+}
+
+describe('Runtime coverage. Utils: Timepicker', () => {
+ it('should is not empty', () => {
+ isValidDate();
+ });
+
+ it('should is empty', () => {
+ isValidDate(testTime());
+ });
+
+ it('should date is interface Data', () => {
+ let time = new Date();
+ time.setHours(NaN);
+ isValidDate(time);
+ });
+
+ it('should date is string', () => {
+ isValidDate('123');
+ });
+
+ it('should to number', () => {
+ toNumber(12);
+ });
+
+ it('should to string', () => {
+ toNumber('12');
+ });
+
+ it('should date is string', () => {
+ isNumber('12');
+ });
+
+ it('should parse hours valid value', () => {
+ parseHours(12);
+ });
+
+ it('should parse hours invalid value', () => {
+ parseHours('q');
+ });
+
+ it('should parse minutes valid value', () => {
+ parseMinutes(12);
+ });
+
+ it('should parse minutes invalid value', () => {
+ parseMinutes('q');
+ });
+
+ it('should parse seconds valid value', () => {
+ parseSeconds(12);
+ });
+
+ it('should parse seconds invalid value', () => {
+ parseSeconds('q');
+ });
+
+ it('should parse time string value', () => {
+ parseTime('12');
+ });
+
+ it('should parse time date value', () => {
+ parseTime(testTime());
+ });
+
+ it('should change time valid value', () => {
+ changeTime(testTime(), modelTime(1, 2, 3, true));
+ });
+
+ it('should change time invalid diff', () => {
+ changeTime(testTime(), modelTime(-1, 0, 0, false));
+ });
+
+ it('should change time invalid diff hour NaN', () => {
+ changeTime(testTime(), modelTime(NaN, 0, 0, false));
+ });
+
+ it('should set time opts true', () => {
+ setTime(testTime(), modelTime(0, 0, 0, true));
+ });
+
+ it('should set time opts false', () => {
+ setTime(testTime(), modelTime(0, 0, 0, false));
+ });
+
+ it('should set time opts hours NaN', () => {
+ setTime(testTime(), modelTime(1, 1, 0, false));
+ });
+
+ it('should create date', () => {
+ createDate(testTime(), 10, 20, 30);
+ });
+
+ it('should create date false', () => {
+ createDate(testTime(), 10, 20, 30);
+ });
+
+ it('should pad number', () => {
+ padNumber(10);
+ });
+
+ it('should pad number length', () => {
+ padNumber(1);
+ });
+});
diff --git a/src/timepicker/index.ts b/src/timepicker/index.ts
index fe8a961676..9a3e8c1661 100644
--- a/src/timepicker/index.ts
+++ b/src/timepicker/index.ts
@@ -1,3 +1,5 @@
-export { TimepickerConfig } from './timepicker.config';
export { TimepickerComponent } from './timepicker.component';
+export { TimepickerActions } from './reducer/timepicker.actions';
+export { TimepickerStore } from './reducer/timepicker.store';
+export { TimepickerConfig } from './timepicker.config';
export { TimepickerModule } from './timepicker.module';
diff --git a/src/timepicker/reducer/timepicker.actions.ts b/src/timepicker/reducer/timepicker.actions.ts
new file mode 100644
index 0000000000..6229eb5115
--- /dev/null
+++ b/src/timepicker/reducer/timepicker.actions.ts
@@ -0,0 +1,55 @@
+import { Injectable } from '@angular/core';
+import { Action } from '../../mini-ngrx/index';
+import { TimeChangeEvent, TimepickerComponentState, Time } from '../timepicker.models';
+
+@Injectable()
+export class TimepickerActions {
+ static readonly WRITE_VALUE = '[timepicker] write value from ng model';
+ static readonly CHANGE_HOURS = '[timepicker] change hours';
+ static readonly CHANGE_MINUTES = '[timepicker] change minutes';
+ static readonly CHANGE_SECONDS = '[timepicker] change seconds';
+ static readonly SET_TIME_UNIT = '[timepicker] set time unit';
+ static readonly UPDATE_CONTROLS = '[timepicker] update controls';
+
+ writeValue(value: Date | string) {
+ return {
+ type: TimepickerActions.WRITE_VALUE,
+ payload: value
+ };
+ }
+
+ changeHours(event: TimeChangeEvent) {
+ return {
+ type: TimepickerActions.CHANGE_HOURS,
+ payload: event
+ };
+ }
+
+ changeMinutes(event: TimeChangeEvent) {
+ return {
+ type: TimepickerActions.CHANGE_MINUTES,
+ payload: event
+ };
+ }
+
+ changeSeconds(event: TimeChangeEvent): Action {
+ return {
+ type: TimepickerActions.CHANGE_SECONDS,
+ payload: event
+ };
+ }
+
+ setTime(value: Time): Action {
+ return {
+ type: TimepickerActions.SET_TIME_UNIT,
+ payload: value
+ };
+ }
+
+ updateControls(value: TimepickerComponentState): Action {
+ return {
+ type: TimepickerActions.UPDATE_CONTROLS,
+ payload: value
+ };
+ }
+}
diff --git a/src/timepicker/reducer/timepicker.reducer.ts b/src/timepicker/reducer/timepicker.reducer.ts
new file mode 100644
index 0000000000..93843beba7
--- /dev/null
+++ b/src/timepicker/reducer/timepicker.reducer.ts
@@ -0,0 +1,94 @@
+import { Action } from '../../mini-ngrx/index';
+import {
+ canChangeHours,
+ canChangeMinutes,
+ canChangeSeconds,
+ canChangeValue,
+ timepickerControls
+} from '../timepicker-controls.util';
+import { TimepickerConfig } from '../timepicker.config';
+import { TimepickerComponentState, TimepickerControls } from '../timepicker.models';
+import { changeTime, setTime } from '../timepicker.utils';
+import { TimepickerActions } from './timepicker.actions';
+
+export class TimepickerState {
+ value: Date;
+ config: TimepickerComponentState;
+ controls: TimepickerControls;
+}
+
+export const initialState = {
+ config: new TimepickerConfig(),
+ controls: {
+ canIncrementHours: true,
+ canIncrementMinutes: true,
+ canIncrementSeconds: true,
+
+ canDecrementHours: true,
+ canDecrementMinutes: true,
+ canDecrementSeconds: true
+ }
+} as TimepickerState;
+
+export function timepickerReducer(state = initialState, action: Action) {
+ switch (action.type) {
+ case(TimepickerActions.WRITE_VALUE): {
+ return Object.assign({}, state, {value: action.payload});
+ }
+
+ case (TimepickerActions.CHANGE_HOURS): {
+ if (!canChangeValue(state.config, action.payload) ||
+ !canChangeHours(action.payload, state.controls)) {
+ return state;
+ }
+
+ const _newTime = changeTime(state.value, {hour: action.payload.step});
+
+ return Object.assign({}, state, {value: _newTime});
+ }
+
+ case (TimepickerActions.CHANGE_MINUTES): {
+ if (!canChangeValue(state.config, action.payload) ||
+ !canChangeMinutes(action.payload, state.controls)) {
+ return state;
+ }
+
+ const _newTime = changeTime(state.value, {minute: action.payload.step});
+
+ return Object.assign({}, state, {value: _newTime});
+ }
+
+ case (TimepickerActions.CHANGE_SECONDS): {
+ if (!canChangeValue(state.config, action.payload) ||
+ !canChangeSeconds(action.payload, state.controls)) {
+ return state;
+ }
+
+ const _newTime = changeTime(state.value, {seconds: action.payload.step});
+
+ return Object.assign({}, state, {value: _newTime});
+ }
+
+ case (TimepickerActions.SET_TIME_UNIT): {
+ if (!canChangeValue(state.config)) {
+ return state;
+ }
+
+ const _newTime = setTime(state.value, action.payload);
+
+ return Object.assign({}, state, {value: _newTime});
+ }
+
+ case (TimepickerActions.UPDATE_CONTROLS): {
+ const _newControlsState = timepickerControls(state.value, action.payload);
+
+ return Object.assign({}, state, {
+ config: action.payload,
+ controls: _newControlsState
+ });
+ }
+
+ default:
+ return state;
+ }
+}
diff --git a/src/timepicker/reducer/timepicker.store.ts b/src/timepicker/reducer/timepicker.store.ts
new file mode 100644
index 0000000000..4191ef4b00
--- /dev/null
+++ b/src/timepicker/reducer/timepicker.store.ts
@@ -0,0 +1,16 @@
+import { Injectable } from '@angular/core';
+import { timepickerReducer, TimepickerState, initialState } from './timepicker.reducer';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+
+import { Action } from '../../mini-ngrx/index';
+import { MiniStore } from '../../mini-ngrx/store.class';
+import { MiniState } from '../../mini-ngrx/state.class';
+
+@Injectable()
+export class TimepickerStore extends MiniStore {
+ constructor() {
+ const _dispatcher = new BehaviorSubject({type: '[mini-ngrx] dispatcher init'});
+ const state = new MiniState(initialState, _dispatcher, timepickerReducer);
+ super(_dispatcher, timepickerReducer, state);
+ }
+}
diff --git a/src/timepicker/timepicker-controls.util.ts b/src/timepicker/timepicker-controls.util.ts
new file mode 100644
index 0000000000..6cfa039e11
--- /dev/null
+++ b/src/timepicker/timepicker-controls.util.ts
@@ -0,0 +1,129 @@
+import { changeTime, setTime } from './timepicker.utils';
+import { TimeChangeEvent, TimepickerComponentState, TimepickerControls } from './timepicker.models';
+
+export function canChangeValue(state: TimepickerComponentState, event?: TimeChangeEvent): boolean {
+ if (state.readonlyInput) {
+ return false;
+ }
+
+ if (event) {
+ if (event.source === 'wheel' && !state.mousewheel) {
+ return false;
+ }
+
+ if (event.source === 'key' && !state.arrowkeys) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+export function canChangeHours(event: TimeChangeEvent, controls: TimepickerControls): boolean {
+ if (!event.step) {
+ return false;
+ }
+
+ if (event.step > 0 && !controls.canIncrementHours) {
+ return false;
+ }
+
+ if (event.step < 0 && !controls.canDecrementHours) {
+ return false;
+ }
+
+ return true;
+}
+
+export function canChangeMinutes(event: TimeChangeEvent, controls: TimepickerControls): boolean {
+ if (!event.step) {
+ return false;
+ }
+ if (event.step > 0 && !controls.canIncrementMinutes) {
+ return false;
+ }
+ if (event.step < 0 && !controls.canDecrementMinutes) {
+ return false;
+ }
+
+ return true;
+}
+
+export function canChangeSeconds(event: TimeChangeEvent, controls: TimepickerControls): boolean {
+ if (!event.step) {
+ return false;
+ }
+ if (event.step > 0 && !controls.canIncrementSeconds) {
+ return false;
+ }
+ if (event.step < 0 && !controls.canDecrementSeconds) {
+ return false;
+ }
+
+ return true;
+}
+
+export function getControlsValue(state: TimepickerComponentState): TimepickerComponentState {
+ const {
+ hourStep, minuteStep, secondsStep,
+ readonlyInput, mousewheel, arrowkeys,
+ showSpinners, showMeridian, showSeconds,
+ meridians, min, max
+ } = state;
+ return {
+ hourStep, minuteStep, secondsStep,
+ readonlyInput, mousewheel, arrowkeys,
+ showSpinners, showMeridian, showSeconds,
+ meridians, min, max
+ };
+}
+
+export function timepickerControls(value: Date, state: TimepickerComponentState): TimepickerControls {
+ const {min, max, hourStep, minuteStep, secondsStep, showSeconds} = state;
+ const res = {
+ canIncrementHours: true,
+ canIncrementMinutes: true,
+ canIncrementSeconds: true,
+
+ canDecrementHours: true,
+ canDecrementMinutes: true,
+ canDecrementSeconds: true
+ } as TimepickerControls;
+
+ if (!value) {
+ return res;
+ }
+
+// compare dates
+ if (max) {
+ const _newHour = changeTime(value, { hour: hourStep });
+ res.canIncrementHours = max > _newHour;
+
+ if (!res.canIncrementHours) {
+ const _newMinutes = changeTime(value, { minute: minuteStep });
+ res.canIncrementMinutes = showSeconds ? max > _newMinutes : max >= _newMinutes ;
+ }
+
+ if (!res.canIncrementMinutes) {
+ const _newSeconds = changeTime(value, { seconds: secondsStep });
+ res.canIncrementSeconds = max >= _newSeconds;
+ }
+ }
+
+ if (min) {
+ const _newHour = changeTime(value, { hour: -hourStep });
+ res.canDecrementHours = min < _newHour;
+
+ if (!res.canDecrementHours) {
+ const _newMinutes = changeTime(value, { minute: -minuteStep });
+ res.canDecrementMinutes = showSeconds ? min < _newMinutes : min <= _newMinutes;
+ }
+
+ if (!res.canDecrementMinutes) {
+ const _newSeconds = changeTime(value, { seconds: -secondsStep });
+ res.canDecrementSeconds = min <= _newSeconds;
+ }
+ }
+
+ return res;
+}
diff --git a/src/timepicker/timepicker.component.ts b/src/timepicker/timepicker.component.ts
index 2815233587..b41a3fdc0e 100644
--- a/src/timepicker/timepicker.component.ts
+++ b/src/timepicker/timepicker.component.ts
@@ -1,386 +1,360 @@
-// tslint:disable max-file-line-count
-import { Component, Input, OnInit, forwardRef } from '@angular/core';
+/* tslint:disable:no-forward-ref max-file-line-count */
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component, EventEmitter,
+ forwardRef,
+ Input,
+ OnChanges, Output,
+ SimpleChanges
+} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+import { TimepickerActions } from './reducer/timepicker.actions';
+import { TimepickerStore } from './reducer/timepicker.store';
+import { getControlsValue } from './timepicker-controls.util';
import { TimepickerConfig } from './timepicker.config';
+import { TimeChangeSource, TimepickerComponentState, TimepickerControls } from './timepicker.models';
+import { isValidDate, padNumber, parseTime, isInputValid } from './timepicker.utils';
export const TIMEPICKER_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
+ // tslint:disable-next-line
useExisting: forwardRef(() => TimepickerComponent),
multi: true
};
-// todo: refactor directive has to many functions! (extract to stateless helper)
-// todo: use moment js?
-// todo: implement `time` validator
-// todo: replace increment/decrement blockers with getters, or extract
-// todo: unify work with selected
-function isDefined(value: any): boolean {
- return typeof value !== 'undefined';
-}
-
-function addMinutes(date: any, minutes: number): Date {
- let dt = new Date(date.getTime() + minutes * 60000);
- let newDate = new Date(date);
- newDate.setHours(dt.getHours(), dt.getMinutes());
- return newDate;
-}
-
@Component({
selector: 'timepicker',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [TIMEPICKER_CONTROL_VALUE_ACCESSOR, TimepickerStore],
template: `
- `,
- providers: [TIMEPICKER_CONTROL_VALUE_ACCESSOR]
+ `
})
-export class TimepickerComponent implements ControlValueAccessor, OnInit {
+export class TimepickerComponent implements ControlValueAccessor, TimepickerComponentState, TimepickerControls, OnChanges {
/** hours change step */
- @Input() public hourStep: number;
+ @Input() hourStep: number;
/** hours change step */
- @Input() public minuteStep: number;
+ @Input() minuteStep: number;
+ /** seconds change step */
+ @Input() secondsStep: number;
/** if true hours and minutes fields will be readonly */
- @Input() public readonlyInput: boolean;
+ @Input() readonlyInput: boolean;
/** if true scroll inside hours and minutes inputs will change time */
- @Input() public mousewheel: boolean;
+ @Input() mousewheel: boolean;
/** if true up/down arrowkeys inside hours and minutes inputs will change time */
- @Input() public arrowkeys: boolean;
+ @Input() arrowkeys: boolean;
/** if true spinner arrows above and below the inputs will be shown */
- @Input() public showSpinners: boolean;
+ @Input() showSpinners: boolean;
+ @Input() showMeridian: boolean;
+ @Input() showSeconds: boolean;
+
+ /** meridian labels based on locale */
+ @Input() meridians: string[];
+
/** minimum time user can select */
- @Input() public min: Date;
+ @Input() min: Date;
/** maximum time user can select */
- @Input() public max: Date;
- /** meridian labels based on locale */
- @Input() public meridians: string[];
+ @Input() max: Date;
- /** if true works in 12H mode and displays AM/PM. If false works in 24H mode and hides AM/PM */
- @Input()
- public get showMeridian(): boolean {
- return this._showMeridian;
- }
+ /** emits true if value is a valid date */
+ @Output() isValid: EventEmitter = new EventEmitter();
- public set showMeridian(value: boolean) {
- this._showMeridian = value;
- // || !this.$error.time
- // if (true) {
- this.updateTemplate();
- return;
- // }
- // Evaluate from template
- /*let hours = this.getHoursFromTemplate();
- let minutes = this.getMinutesFromTemplate();
- if (isDefined(hours) && isDefined(minutes)) {
- this.selected.setHours(hours);
- this.refresh();
- }*/
- }
+ // ui variables
+ hours: string;
+ minutes: string;
+ seconds: string;
+ meridian: string;
- public onChange: any = Function.prototype;
- public onTouched: any = Function.prototype;
+ get isSpinnersVisible(): boolean {
+ return this.showSpinners && !this.readonlyInput;
+ }
- // input values
- public hours: string;
- public minutes: string;
+ // min\max validation for input fields
+ invalidHours = false;
+ invalidMinutes = false;
+ invalidSeconds = false;
- // validation
- public invalidHours: any;
- public invalidMinutes: any;
+ // time picker controls state
+ canIncrementHours: boolean;
+ canIncrementMinutes: boolean;
+ canIncrementSeconds: boolean;
- public meridian: any; // ??
+ canDecrementHours: boolean;
+ canDecrementMinutes: boolean;
+ canDecrementSeconds: boolean;
- // result value
- protected _selected: Date = new Date();
- protected _showMeridian: boolean;
+ // control value accessor methods
+ onChange: any = Function.prototype;
+ onTouched: any = Function.prototype;
- protected get selected(): Date {
- return this._selected;
+ constructor(_config: TimepickerConfig,
+ private _cd: ChangeDetectorRef,
+ private _store: TimepickerStore,
+ private _timepickerActions: TimepickerActions) {
+ Object.assign(this, _config);
+ // todo: add unsubscribe
+ _store
+ .select((state) => state.value)
+ .subscribe((value) => {
+ // update UI values if date changed
+ this._renderTime(value);
+ this.onChange(value);
+
+ this._store.dispatch(this._timepickerActions.updateControls(getControlsValue(this)));
+ });
+
+ _store
+ .select((state) => state.controls)
+ .subscribe((controlsState) => {
+ this.isValid.emit(isInputValid(this.hours, this.minutes, this.seconds, this.isPM()));
+ Object.assign(this, controlsState);
+ _cd.markForCheck();
+ });
}
- protected set selected(v: Date) {
- if (v) {
- this._selected = v;
- this.updateTemplate();
- this.onChange(this.selected);
- }
+ isPM(): boolean {
+ return this.showMeridian && this.meridian === this.meridians[1];
}
- protected config: TimepickerConfig;
-
- public constructor(_config: TimepickerConfig) {
- this.config = _config;
- Object.assign(this, _config);
+ prevDef($event: any) {
+ $event.preventDefault();
}
- // todo: add formatter value to Date object
- public ngOnInit(): void {
- // todo: take in account $locale.DATETIME_FORMATS.AMPMS;
- if (this.mousewheel) {
- // this.setupMousewheelEvents();
- }
-
- if (this.arrowkeys) {
- // this.setupArrowkeyEvents();
- }
-
- // this.setupInputEvents();
+ wheelSign($event: any): number {
+ return Math.sign($event.deltaY as number) * -1;
}
- public writeValue(v: any): void {
- if (v === this.selected) {
- return;
- }
- if (v && v instanceof Date) {
- this.selected = v;
- return;
- }
- this.selected = v ? new Date(v) : void 0;
+ ngOnChanges(changes: SimpleChanges): void {
+ this._store.dispatch(this._timepickerActions.updateControls(getControlsValue(this)));
}
- public registerOnChange(fn: (_: any) => {}): void {
- this.onChange = fn;
+ changeHours(step: number, source: TimeChangeSource = ''): void {
+ this._store.dispatch(this._timepickerActions.changeHours({step, source}));
}
- public registerOnTouched(fn: () => {}): void {
- this.onTouched = fn;
+ changeMinutes(step: number, source: TimeChangeSource = ''): void {
+ this._store.dispatch(this._timepickerActions.changeMinutes({step, source}));
}
- public setDisabledState(isDisabled: boolean): void {
- this.readonlyInput = isDisabled;
+ changeSeconds(step: number, source: TimeChangeSource = ''): void {
+ this._store.dispatch(this._timepickerActions.changeSeconds({step, source}));
}
- public updateHours(): void {
- if (this.readonlyInput) {
- return;
- }
-
- let hours = this.getHoursFromTemplate();
- let minutes = this.getMinutesFromTemplate();
- this.invalidHours = !isDefined(hours);
- this.invalidMinutes = !isDefined(minutes);
-
- if (this.invalidHours || this.invalidMinutes) {
- // TODO: needed a validation functionality.
- return;
- // todo: validation?
- // invalidate(true);
- }
-
- this.selected.setHours(hours);
- this.invalidHours = (this.selected < this.min || this.selected > this.max);
- if (this.invalidHours) {
- // todo: validation?
- // invalidate(true);
- return;
- } else {
- this.refresh(/*'h'*/);
- }
+ updateHours(hours: string): void {
+ this.hours = hours;
+ this._updateTime();
}
- public hoursOnBlur(): void {
- if (this.readonlyInput) {
- return;
- }
-
- // todo: binded with validation
- if (!this.invalidHours && parseInt(this.hours, 10) < 10) {
- this.hours = this.pad(this.hours);
- }
+ updateMinutes(minutes: string) {
+ this.minutes = minutes;
+ this._updateTime();
}
- public updateMinutes(): void {
- if (this.readonlyInput) {
- return;
- }
-
- let minutes = this.getMinutesFromTemplate();
- let hours = this.getHoursFromTemplate();
- this.invalidMinutes = !isDefined(minutes);
- this.invalidHours = !isDefined(hours);
-
- if (this.invalidMinutes || this.invalidHours) {
- // TODO: needed a validation functionality.
- return;
- // todo: validation
- // invalidate(undefined, true);
- }
-
- this.selected.setMinutes(minutes);
- this.invalidMinutes = (this.selected < this.min || this.selected > this.max);
- if (this.invalidMinutes) {
- // todo: validation
- // invalidate(undefined, true);
- return;
- } else {
- this.refresh(/*'m'*/);
- }
+ updateSeconds(seconds: string) {
+ this.seconds = seconds;
+ this._updateTime();
}
- public minutesOnBlur(): void {
- if (this.readonlyInput) {
+ _updateTime() {
+ if (!isInputValid(this.hours, this.minutes, this.seconds, this.isPM())) {
+ this.onChange(null);
return;
}
-
- if (!this.invalidMinutes && parseInt(this.minutes, 10) < 10) {
- this.minutes = this.pad(this.minutes);
- }
+ this._store.dispatch(this._timepickerActions
+ .setTime({
+ hour: this.hours,
+ minute: this.minutes,
+ seconds: this.seconds,
+ isPM: this.isPM()
+ }));
}
- public incrementHours(): void {
- if (!this.noIncrementHours()) {
- this.addMinutesToSelected(this.hourStep * 60);
- }
- }
-
- public decrementHours(): void {
- if (!this.noDecrementHours()) {
- this.addMinutesToSelected(-this.hourStep * 60);
+ toggleMeridian(): void {
+ if (!this.showMeridian || this.readonlyInput) {
+ return;
}
- }
- public incrementMinutes(): void {
- if (!this.noIncrementMinutes()) {
- this.addMinutesToSelected(this.minuteStep);
- }
+ const _hoursPerDayHalf = 12;
+ this._store.dispatch(this._timepickerActions.changeHours({step: _hoursPerDayHalf, source: ''}));
}
- public decrementMinutes(): void {
- if (!this.noDecrementMinutes()) {
- this.addMinutesToSelected(-this.minuteStep);
+ /**
+ * Write a new value to the element.
+ */
+ writeValue(obj: any): void {
+ if (isValidDate(obj)) {
+ this._store.dispatch(this._timepickerActions.writeValue(parseTime(obj)));
}
}
- public noIncrementHours(): boolean {
- let incrementedSelected = addMinutes(this.selected, this.hourStep * 60);
- return incrementedSelected > this.max ||
- (incrementedSelected < this.selected && incrementedSelected < this.min);
- }
-
- public noDecrementHours(): boolean {
- let decrementedSelected = addMinutes(this.selected, -this.hourStep * 60);
- return decrementedSelected < this.min ||
- (decrementedSelected > this.selected && decrementedSelected > this.max);
- }
-
- public noIncrementMinutes(): boolean {
- let incrementedSelected = addMinutes(this.selected, this.minuteStep);
- return incrementedSelected > this.max ||
- (incrementedSelected < this.selected && incrementedSelected < this.min);
- }
-
- public noDecrementMinutes(): boolean {
- let decrementedSelected = addMinutes(this.selected, -this.minuteStep);
- return decrementedSelected < this.min ||
- (decrementedSelected > this.selected && decrementedSelected > this.max);
-
- }
-
- public toggleMeridian(): void {
- if (!this.noToggleMeridian()) {
- let sign = this.selected.getHours() < 12 ? 1 : -1;
- this.addMinutesToSelected(12 * 60 * sign);
- }
+ /**
+ * Set the function to be called when the control receives a change event.
+ */
+ registerOnChange(fn: (_: any) => {}): void {
+ this.onChange = fn;
}
- public noToggleMeridian(): boolean {
- if (this.readonlyInput) {
- return true;
- }
-
- if (this.selected.getHours() < 13) {
- return addMinutes(this.selected, 12 * 60) > this.max;
- } else {
- return addMinutes(this.selected, -12 * 60) < this.min;
- }
+ /**
+ * Set the function to be called when the control receives a touch event.
+ */
+ registerOnTouched(fn: () => {}): void {
+ this.onTouched = fn;
}
- protected refresh(/*type?:string*/): void {
- // this.makeValid();
- this.updateTemplate();
- this.onChange(this.selected);
+ /**
+ * This function is called when the control status changes to or from "DISABLED".
+ * Depending on the value, it will enable or disable the appropriate DOM element.
+ *
+ * @param isDisabled
+ */
+ setDisabledState(isDisabled: boolean): void {
+ this.readonlyInput = isDisabled;
}
- protected updateTemplate(/*keyboardChange?:any*/): void {
- let hours = this.selected.getHours();
- let minutes = this.selected.getMinutes();
-
- if (this.showMeridian) {
- // Convert 24 to 12 hour system
- hours = (hours === 0 || hours === 12) ? 12 : hours % 12;
- }
-
- // this.hours = keyboardChange === 'h' ? hours : this.pad(hours);
- // if (keyboardChange !== 'm') {
- // this.minutes = this.pad(minutes);
- // }
- this.hours = this.pad(hours);
- this.minutes = this.pad(minutes);
+ private _renderTime(value: string | Date): void {
+ if (!isValidDate(value)) {
+ this.hours = '';
+ this.minutes = '';
+ this.seconds = '';
+ this.meridian = this.meridians[0];
- if (!this.meridians) {
- this.meridians = this.config.meridians;
+ return;
}
- this.meridian = this.selected.getHours() < 12
- ? this.meridians[0]
- : this.meridians[1];
- }
-
- protected getHoursFromTemplate(): number {
- let hours = parseInt(this.hours, 10);
- let valid = this.showMeridian
- ? (hours > 0 && hours < 13)
- : (hours >= 0 && hours < 24);
- if (!valid) {
- return void 0;
- }
+ const _value = parseTime(value);
+ const _hoursPerDayHalf = 12;
+ let _hours = _value.getHours();
if (this.showMeridian) {
- if (hours === 12) {
- hours = 0;
- }
- if (this.meridian === this.meridians[1]) {
- hours = hours + 12;
+ this.meridian = this.meridians[_hours >= _hoursPerDayHalf ? 1 : 0];
+ _hours = _hours % _hoursPerDayHalf;
+ // should be 12 PM, not 00 PM
+ if (_hours === 0) {
+ _hours = _hoursPerDayHalf;
}
}
- return hours;
- }
-
- protected getMinutesFromTemplate(): number {
- let minutes = parseInt(this.minutes, 10);
- return (minutes >= 0 && minutes < 60) ? minutes : undefined;
- }
-
- protected pad(value: string|number): string {
- return (isDefined(value) && value.toString().length < 2)
- ? '0' + value
- : value.toString();
- }
- protected addMinutesToSelected(minutes: any): void {
- this.selected = addMinutes(this.selected, minutes);
- this.refresh();
+ this.hours = padNumber(_hours);
+ this.minutes = padNumber(_value.getMinutes());
+ this.seconds = padNumber(_value.getUTCSeconds());
}
}
diff --git a/src/timepicker/timepicker.config.ts b/src/timepicker/timepicker.config.ts
index d976e4a588..b782d77111 100644
--- a/src/timepicker/timepicker.config.ts
+++ b/src/timepicker/timepicker.config.ts
@@ -4,23 +4,27 @@ import { Injectable } from '@angular/core';
@Injectable()
export class TimepickerConfig {
/** hours change step */
- public hourStep: number = 1;
+ hourStep = 1;
/** hours change step */
- public minuteStep: number = 5;
+ minuteStep = 5;
+ /** seconds changes step */
+ secondsStep = 10;
/** if true works in 12H mode and displays AM/PM. If false works in 24H mode and hides AM/PM */
- public showMeridian: boolean = true;
+ showMeridian = true;
/** meridian labels based on locale */
- public meridians:string[] = ['AM', 'PM'];
+ meridians = ['AM', 'PM'];
/** if true hours and minutes fields will be readonly */
- public readonlyInput: boolean = false;
+ readonlyInput = false;
/** if true scroll inside hours and minutes inputs will change time */
- public mousewheel: boolean = true;
+ mousewheel = true;
/** if true up/down arrowkeys inside hours and minutes inputs will change time */
- public arrowkeys: boolean = true;
+ arrowkeys = true;
/** if true spinner arrows above and below the inputs will be shown */
- public showSpinners: boolean = true;
+ showSpinners = true;
+ /** show seconds in timepicker */
+ showSeconds = false;
/** minimum time user can select */
- public min: number = void 0;
+ min: Date;
/** maximum time user can select */
- public max: number = void 0;
+ max: Date;
}
diff --git a/src/timepicker/timepicker.models.ts b/src/timepicker/timepicker.models.ts
new file mode 100644
index 0000000000..69f8ba5a20
--- /dev/null
+++ b/src/timepicker/timepicker.models.ts
@@ -0,0 +1,43 @@
+export interface Time {
+ hour?: string | number;
+ minute?: string | number;
+ seconds?: string | number;
+ isPM?: boolean;
+}
+
+export interface TimepickerControls {
+ canIncrementHours: boolean;
+ canIncrementMinutes: boolean;
+ canIncrementSeconds: boolean;
+
+ canDecrementHours: boolean;
+ canDecrementMinutes: boolean;
+ canDecrementSeconds: boolean;
+}
+
+export interface TimepickerComponentState {
+ min: Date;
+ max: Date;
+
+ hourStep: number;
+ minuteStep: number;
+ secondsStep: number;
+
+ readonlyInput: boolean;
+
+ mousewheel: boolean;
+ arrowkeys: boolean;
+
+ showSpinners: boolean;
+ showMeridian: boolean;
+ showSeconds: boolean;
+
+ meridians: string[];
+}
+
+export type TimeChangeSource = 'wheel' | 'key' | '';
+
+export interface TimeChangeEvent {
+ step: number;
+ source: TimeChangeSource;
+}
diff --git a/src/timepicker/timepicker.module.ts b/src/timepicker/timepicker.module.ts
index 3c39a0b4cc..f0fb71564c 100644
--- a/src/timepicker/timepicker.module.ts
+++ b/src/timepicker/timepicker.module.ts
@@ -1,19 +1,21 @@
+import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
-import { NgModule, ModuleWithProviders } from '@angular/core';
+
import { TimepickerComponent } from './timepicker.component';
+import { TimepickerActions } from './reducer/timepicker.actions';
import { TimepickerConfig } from './timepicker.config';
+import { TimepickerStore } from './reducer/timepicker.store';
@NgModule({
- imports: [CommonModule, FormsModule],
+ imports: [CommonModule],
declarations: [TimepickerComponent],
- exports: [TimepickerComponent, FormsModule]
+ exports: [TimepickerComponent]
})
export class TimepickerModule {
- public static forRoot(): ModuleWithProviders {
+ static forRoot(): ModuleWithProviders {
return {
ngModule: TimepickerModule,
- providers: [TimepickerConfig]
+ providers: [TimepickerConfig, TimepickerActions, TimepickerStore]
};
}
}
diff --git a/src/timepicker/timepicker.utils.ts b/src/timepicker/timepicker.utils.ts
new file mode 100644
index 0000000000..fe61cdc9b9
--- /dev/null
+++ b/src/timepicker/timepicker.utils.ts
@@ -0,0 +1,144 @@
+import { Time } from './timepicker.models';
+
+const dex = 10;
+const hoursPerDay = 24;
+const hoursPerDayHalf = 12;
+const minutesPerHour = 60;
+const secondsPerMinute = 60;
+
+export function isValidDate(value?: string | Date): boolean {
+ if (!value) {
+ return false;
+ }
+
+ if (value instanceof Date && isNaN(value.getHours())) {
+ return false;
+ }
+
+ if (typeof value === 'string') {
+ return isValidDate(new Date(value));
+ }
+
+ return true;
+}
+
+export function toNumber(value: string | number): number {
+ if (typeof value === 'number') {
+ return value;
+ }
+
+ return parseInt(value, dex);
+}
+
+export function isNumber(value: string): boolean {
+ return !isNaN(toNumber(value));
+}
+
+export function parseHours(value: string | number, isPM: boolean = false): number {
+ const hour = toNumber(value);
+ if (isNaN(hour) || hour < 0 || hour > (isPM ? hoursPerDayHalf : hoursPerDay)) {
+ return NaN;
+ }
+
+ return hour;
+}
+
+export function parseMinutes(value: string | number): number {
+ const minute = toNumber(value);
+ if (isNaN(minute) || minute < 0 || minute > minutesPerHour) {
+ return NaN;
+ }
+
+ return minute;
+}
+
+export function parseSeconds(value: string | number): number {
+ const seconds = toNumber(value);
+ if (isNaN(seconds) || seconds < 0 || seconds > secondsPerMinute) {
+ return NaN;
+ }
+
+ return seconds;
+}
+
+export function parseTime(value: string | Date): Date {
+ if (typeof value === 'string') {
+ return new Date(value);
+ }
+
+ return value;
+}
+
+export function changeTime(value: Date, diff: Time): Date {
+ if (!value) {
+ return changeTime(createDate(new Date(),0,0, 0), diff);
+ }
+
+ let hour = value.getHours();
+ let minutes = value.getMinutes();
+ let seconds = value.getSeconds();
+
+ if (diff.hour) {
+ hour = (hour + toNumber(diff.hour)) % hoursPerDay;
+ if (hour < 0) {
+ hour += hoursPerDay;
+ }
+ }
+
+ if (diff.minute) {
+ minutes = (minutes + toNumber(diff.minute));
+ }
+
+ if (diff.seconds) {
+ seconds = (seconds + toNumber(diff.seconds));
+ }
+
+ return createDate(value, hour, minutes, seconds);
+}
+
+export function setTime(value: Date, opts: Time): Date {
+ let hour = parseHours(opts.hour);
+ const minute = parseMinutes(opts.minute);
+ const seconds = parseSeconds(opts.seconds) || 0;
+
+ if (opts.isPM) {
+ hour += hoursPerDayHalf;
+ }
+
+ // fixme: unreachable code, value is mandatory
+ if (!value) {
+ if (!isNaN(hour) && !isNaN(minute)) {
+ return createDate(new Date(), hour, minute, seconds);
+ }
+
+ return value;
+ }
+
+ if (isNaN(hour) || isNaN(minute)) {
+ return value;
+ }
+
+ return createDate(value, hour, minute, seconds);
+}
+
+export function createDate(value: Date, hours: number, minutes: number, seconds: number): Date {
+
+ // fixme: unreachable code, value is mandatory
+ const _value = value || new Date();
+ return new Date(_value.getFullYear(), _value.getMonth(), _value.getDate(),
+ hours, minutes, seconds, _value.getMilliseconds());
+}
+
+export function padNumber(value: number): string {
+ const _value = value.toString();
+ if (_value.length > 1) { return _value; }
+
+ return `0${_value}`;
+}
+
+export function isInputValid(hours: string, minutes: string, seconds: string = '0', isPM: boolean): boolean {
+ if (isNaN(parseHours(hours, isPM)) || isNaN(parseMinutes(minutes)) || isNaN(parseSeconds(seconds))) {
+ return false;
+ }
+ return true;
+}