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..31d8e317ea --- /dev/null +++ b/demo/src/app/components/+timepicker/demos/custom-validation/custom-validation.html @@ -0,0 +1,7 @@ +

Illustrates custom validation, you have to select time between 11:00 and 12:59

+ +
+ +
+ +
Time is: {{myTime}}
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..3b8b9d54eb --- /dev/null +++ b/demo/src/app/components/+timepicker/demos/custom-validation/custom-validation.ts @@ -0,0 +1,14 @@ +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; + console.log('control', control); +} 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..53229bb148 100644 --- a/demo/src/app/components/+timepicker/demos/meridian/meridian.html +++ b/demo/src/app/components/+timepicker/demos/meridian/meridian.html @@ -2,7 +2,12 @@
Time is: {{mytime}}
-
+
+
+ + + +
Time is: {{mytime2}}
diff --git a/demo/src/app/components/+timepicker/demos/meridian/meridian.ts b/demo/src/app/components/+timepicker/demos/meridian/meridian.ts index 76e7a37165..815b0a287d 100644 --- a/demo/src/app/components/+timepicker/demos/meridian/meridian.ts +++ b/demo/src/app/components/+timepicker/demos/meridian/meridian.ts @@ -5,12 +5,15 @@ 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 mytime2: Date = new Date(); + + public meridianText = ['12h', '24h']; + + 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..55a4a556ed --- /dev/null +++ b/demo/src/app/components/+timepicker/demos/mousewheel-arrowkeys/mousewheel-arrowkeys.html @@ -0,0 +1,11 @@ +

Without mousewheel

+ + + +
Time is: {{myTime1}}
+ +

Without arrowkeys

+ + + +
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/src/mini-ngrx/index.ts b/src/mini-ngrx/index.ts new file mode 100644 index 0000000000..769b69d140 --- /dev/null +++ b/src/mini-ngrx/index.ts @@ -0,0 +1,16 @@ +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'; + +/** + * sample usage class + * + * + * + */ diff --git a/src/mini-ngrx/state.class.ts b/src/mini-ngrx/state.class.ts new file mode 100644 index 0000000000..b5c9f55f83 --- /dev/null +++ b/src/mini-ngrx/state.class.ts @@ -0,0 +1,23 @@ +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..3a5b33a7a7 --- /dev/null +++ b/src/mini-ngrx/store.class.ts @@ -0,0 +1,39 @@ +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/old-timepicker/index.ts b/src/old-timepicker/index.ts new file mode 100644 index 0000000000..fe8a961676 --- /dev/null +++ b/src/old-timepicker/index.ts @@ -0,0 +1,3 @@ +export { TimepickerConfig } from './timepicker.config'; +export { TimepickerComponent } from './timepicker.component'; +export { TimepickerModule } from './timepicker.module'; diff --git a/src/old-timepicker/timepicker.component.ts b/src/old-timepicker/timepicker.component.ts new file mode 100644 index 0000000000..2815233587 --- /dev/null +++ b/src/old-timepicker/timepicker.component.ts @@ -0,0 +1,386 @@ +// tslint:disable max-file-line-count +import { Component, Input, OnInit, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { TimepickerConfig } from './timepicker.config'; + +export const TIMEPICKER_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, + 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', + template: ` + + + + + + + + + + + + + + + + + + + + + +
     
    + + : + +
     
    + `, + providers: [TIMEPICKER_CONTROL_VALUE_ACCESSOR] +}) +export class TimepickerComponent implements ControlValueAccessor, OnInit { + /** hours change step */ + @Input() public hourStep: number; + /** hours change step */ + @Input() public minuteStep: number; + /** if true hours and minutes fields will be readonly */ + @Input() public readonlyInput: boolean; + /** if true scroll inside hours and minutes inputs will change time */ + @Input() public mousewheel: boolean; + /** if true up/down arrowkeys inside hours and minutes inputs will change time */ + @Input() public arrowkeys: boolean; + /** if true spinner arrows above and below the inputs will be shown */ + @Input() public showSpinners: boolean; + /** minimum time user can select */ + @Input() public min: Date; + /** maximum time user can select */ + @Input() public max: Date; + /** meridian labels based on locale */ + @Input() public meridians: string[]; + + /** 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; + } + + 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(); + }*/ + } + + public onChange: any = Function.prototype; + public onTouched: any = Function.prototype; + + // input values + public hours: string; + public minutes: string; + + // validation + public invalidHours: any; + public invalidMinutes: any; + + public meridian: any; // ?? + + // result value + protected _selected: Date = new Date(); + protected _showMeridian: boolean; + + protected get selected(): Date { + return this._selected; + } + + protected set selected(v: Date) { + if (v) { + this._selected = v; + this.updateTemplate(); + this.onChange(this.selected); + } + } + + protected config: TimepickerConfig; + + public constructor(_config: TimepickerConfig) { + this.config = _config; + Object.assign(this, _config); + } + + // 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(); + } + + 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; + } + + public registerOnChange(fn: (_: any) => {}): void { + this.onChange = fn; + } + + public registerOnTouched(fn: () => {}): void { + this.onTouched = fn; + } + + public setDisabledState(isDisabled: boolean): void { + this.readonlyInput = isDisabled; + } + + 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'*/); + } + } + + 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); + } + } + + 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'*/); + } + } + + public minutesOnBlur(): void { + if (this.readonlyInput) { + return; + } + + if (!this.invalidMinutes && parseInt(this.minutes, 10) < 10) { + this.minutes = this.pad(this.minutes); + } + } + + public incrementHours(): void { + if (!this.noIncrementHours()) { + this.addMinutesToSelected(this.hourStep * 60); + } + } + + public decrementHours(): void { + if (!this.noDecrementHours()) { + this.addMinutesToSelected(-this.hourStep * 60); + } + } + + public incrementMinutes(): void { + if (!this.noIncrementMinutes()) { + this.addMinutesToSelected(this.minuteStep); + } + } + + public decrementMinutes(): void { + if (!this.noDecrementMinutes()) { + this.addMinutesToSelected(-this.minuteStep); + } + } + + 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); + } + } + + 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; + } + } + + protected refresh(/*type?:string*/): void { + // this.makeValid(); + this.updateTemplate(); + this.onChange(this.selected); + } + + 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); + + if (!this.meridians) { + this.meridians = this.config.meridians; + } + + 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; + } + + if (this.showMeridian) { + if (hours === 12) { + hours = 0; + } + if (this.meridian === this.meridians[1]) { + hours = hours + 12; + } + } + 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(); + } +} diff --git a/src/old-timepicker/timepicker.config.ts b/src/old-timepicker/timepicker.config.ts new file mode 100644 index 0000000000..d976e4a588 --- /dev/null +++ b/src/old-timepicker/timepicker.config.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; + +/** Provides default configuration values for timepicker */ +@Injectable() +export class TimepickerConfig { + /** hours change step */ + public hourStep: number = 1; + /** hours change step */ + public minuteStep: number = 5; + /** if true works in 12H mode and displays AM/PM. If false works in 24H mode and hides AM/PM */ + public showMeridian: boolean = true; + /** meridian labels based on locale */ + public meridians:string[] = ['AM', 'PM']; + /** if true hours and minutes fields will be readonly */ + public readonlyInput: boolean = false; + /** if true scroll inside hours and minutes inputs will change time */ + public mousewheel: boolean = true; + /** if true up/down arrowkeys inside hours and minutes inputs will change time */ + public arrowkeys: boolean = true; + /** if true spinner arrows above and below the inputs will be shown */ + public showSpinners: boolean = true; + /** minimum time user can select */ + public min: number = void 0; + /** maximum time user can select */ + public max: number = void 0; +} diff --git a/src/old-timepicker/timepicker.module.ts b/src/old-timepicker/timepicker.module.ts new file mode 100644 index 0000000000..3c39a0b4cc --- /dev/null +++ b/src/old-timepicker/timepicker.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { TimepickerComponent } from './timepicker.component'; +import { TimepickerConfig } from './timepicker.config'; + +@NgModule({ + imports: [CommonModule, FormsModule], + declarations: [TimepickerComponent], + exports: [TimepickerComponent, FormsModule] +}) +export class TimepickerModule { + public static forRoot(): ModuleWithProviders { + return { + ngModule: TimepickerModule, + providers: [TimepickerConfig] + }; + } +} diff --git a/src/spec/timepicker/timepicker.component.spec.ts b/src/spec/timepicker/timepicker.component.spec.ts new file mode 100644 index 0000000000..72fbccdfab --- /dev/null +++ b/src/spec/timepicker/timepicker.component.spec.ts @@ -0,0 +1,175 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TimepickerComponent } from '../../timepicker/timepicker.component'; +import { TimepickerConfig } from '../../timepicker/timepicker.config'; +import { TimepickerActions } from '../../timepicker/reducer/timepicker.actions'; +import { Data } from '@angular/router'; + +fdescribe('Component: timepicker', () => { + let fixture: ComponentFixture; + let context: TimepickerComponent; + let nativeEl: any; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TimepickerComponent], + providers: [ + TimepickerConfig, + TimepickerActions + ] + }); + + fixture = TestBed.createComponent(TimepickerComponent); + context = fixture.componentInstance; + nativeEl = fixture.nativeElement; + fixture.detectChanges(); + }); + + it('should prevDef', () => { + context.prevDef(new Event('customWheel')); + }); + + it('should wheelSign', () => { + context.wheelSign(new Event('customWheel')); + }); + + it('should canBeChanged wheel', () => { + context.mousewheel = false; + context.canBeChanged('wheel'); + }); + + it('should canBeChanged key', () => { + context.arrowkeys = false; + context.canBeChanged('key'); + }); + + it('should canBeChanged true', () => { + context.readonlyInput = true; + context.canBeChanged(); + }); + + it('should changeHours', () => { + context.changeHours(3); + }); + + it('should changeHours canIncrementHours', () => { + context.canIncrementHours = false; + context.changeHours(3); + }); + + it('should changeHours canDecrementHours', () => { + context.canDecrementHours = false; + context.changeHours(-3); + }); + + it('should changeHours wheel', () => { + context.mousewheel = false; + context.changeHours(3, 'wheel'); + }); + + it('should changeMinutes', () => { + context.changeMinutes(3); + }); + + it('should changeMinutes canIncrementHours', () => { + context.canIncrementMinutes = false; + context.changeMinutes(3); + }); + + it('should changeMinutes canDecrementHours', () => { + context.canDecrementMinutes = false; + context.changeMinutes(-3); + }); + + it('should changeMinutes wheel', () => { + context.mousewheel = false; + context.changeMinutes(3, 'wheel'); + }); + + it('should changeSeconds', () => { + context.changeSeconds(3); + }); + + it('should changeSeconds canIncrementHours', () => { + context.canIncrementSeconds = false; + context.changeSeconds(3); + }); + + it('should changeSeconds canDecrementHours', () => { + context.canDecrementSeconds = false; + context.changeSeconds(-3); + }); + + it('should changeSeconds wheel', () => { + context.mousewheel = false; + context.changeSeconds(3, 'wheel'); + }); + + it('should updateHours', () => { + context.readonlyInput = true; + context.updateHours('0'); + }); + + it('should updateHours', () => { + context.updateHours(''); + context.updateHours('-1'); + context.updateHours('1'); + }); + + it('should updateMinutes', () => { + context.readonlyInput = true; + context.updateMinutes('0'); + }); + + it('should updateMinutes', () => { + context.updateMinutes(''); + context.updateMinutes('-1'); + context.updateMinutes('1'); + }); + + it('should updateSeconds', () => { + context.readonlyInput = true; + context.updateSeconds('0'); + }); + + it('should updateSeconds', () => { + context.updateSeconds(''); + context.updateSeconds('-1'); + context.updateSeconds('1'); + }); + + it('should toggleMeridian true', () => { + context.showMeridian = true; + context.toggleMeridian(); + }); + + it('should toggleMeridian false', () => { + context.showMeridian = false; + context.toggleMeridian(); + }); + + it('should writeValue', () => { + context.writeValue('1'); + }); + + it('should writeValue', () => { + context.writeValue(''); + }); + + it('should registerOnChange', () => { + context.registerOnChange((val: any) => val); + }); + + it('should registerOnTouched', () => { + context.registerOnTouched(() => true); + }); + + it('should setDisabledState', () => { + context.setDisabledState(true); + }); + + it('should showMeridian change', () => { + context.showMeridian = false; + (context as any)._renderTime('-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..8ad1fccae1 --- /dev/null +++ b/src/timepicker/reducer/timepicker.actions.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { TimeUnit } from '../timepicker.models'; +import { Action } from '../../mini-ngrx/index'; + +@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'; + + writeValue(value: Date | string) { + return { + type: TimepickerActions.WRITE_VALUE, + payload: value + }; + } + + changeHours(value: number) { + return { + type: TimepickerActions.CHANGE_HOURS, + payload: value + }; + } + + changeMinutes(value: number) { + return { + type: TimepickerActions.CHANGE_MINUTES, + payload: value + }; + } + + changeSeconds(value: number): Action { + return { + type: TimepickerActions.CHANGE_SECONDS, + payload: value + }; + } + + setTimeUnit(value: TimeUnit): Action { + return { + type: TimepickerActions.SET_TIME_UNIT, + payload: value + }; + } +} diff --git a/src/timepicker/reducer/timepicker.reducer.ts b/src/timepicker/reducer/timepicker.reducer.ts new file mode 100644 index 0000000000..f6b4ea7e34 --- /dev/null +++ b/src/timepicker/reducer/timepicker.reducer.ts @@ -0,0 +1,39 @@ +import { TimepickerActions } from './timepicker.actions'; +import { changeTime, setTime } from '../timepicker.utils'; +import { Action } from '../../mini-ngrx/index'; + +export interface TimepickerState { + value: Date; +} + +export const initialState = {} 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): { + const _newTime = changeTime(state.value, {hour: action.payload}); + + return Object.assign({}, state, {value: _newTime}); + } + case (TimepickerActions.CHANGE_MINUTES): { + const _newTime = changeTime(state.value, {minute: action.payload}); + + return Object.assign({}, state, {value: _newTime}); + } + case (TimepickerActions.CHANGE_SECONDS): { + const _newTime = changeTime(state.value, {seconds: action.payload}); + + return Object.assign({}, state, {value: _newTime}); + } + case (TimepickerActions.SET_TIME_UNIT): { + const _newTime = setTime(state.value, action.payload); + + return Object.assign({}, state, {value: _newTime}); + } + 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..01e550e702 --- /dev/null +++ b/src/timepicker/timepicker-controls.util.ts @@ -0,0 +1,89 @@ +import { setTime } from './timepicker.utils'; +import { TimepickerComponentState, TimepickerControls } from './timepicker.models'; + +export function timepickerControls(state: TimepickerComponentState): TimepickerControls { + const {min, max, value, hourStep, minuteStep, secondsStep, showSeconds} = state; + const res = {} as TimepickerControls; + + if (!min && !max) { + res.canIncrementHours = true; + res.canIncrementMinutes = true; + res.canIncrementSeconds = true; + res.canDecrementHours = true; + res.canDecrementMinutes = true; + res.canDecrementSeconds = true; + + return res; + } + + const hour = value.getHours(); + const minute = value.getMinutes(); + const seconds = showSeconds ? value.getSeconds() : 0; + +// compare dates + if (max) { + const _newHour = setTime(max, { + hour: hour + hourStep, + minute, seconds + }); + // res.canIncrementHours = max.getHours() >= (value.getHours() + hourStep); + res.canIncrementHours = max >= _newHour; + if (res.canIncrementHours) { + res.canIncrementMinutes = true; + res.canIncrementSeconds = true; + } else { + const _newMinutes = setTime(max, { + hour, + minute: minute + minuteStep, + seconds + }); + // res.canIncrementMinutes = max.getMinutes() >= (value.getMinutes() + minuteStep); + res.canIncrementMinutes = max >= _newMinutes; + if (res.canIncrementMinutes) { + res.canIncrementSeconds = true; + } else { + const _newSeconds = setTime(max, { + hour, + minute, + seconds: seconds + secondsStep + }); + + res.canIncrementSeconds = max >= _newSeconds; + } + } + } + + if (min) { + const _newHour = setTime(min, { + hour: hour - hourStep, + minute, seconds + }); + // res.canIncrementHours = min.getHours() >= (value.getHours() + hourStep); + res.canDecrementHours = min <= _newHour; + if (res.canDecrementHours) { + res.canDecrementMinutes = true; + res.canDecrementSeconds = true; + } else { + const _newMinutes = setTime(min, { + hour, + minute: minute - minuteStep, + seconds + }); + // res.canDecrementMinutes = min.getMinutes() <= (value.getMinutes() + minuteStep); + res.canDecrementMinutes = min <= _newMinutes; + if (res.canDecrementMinutes) { + res.canDecrementSeconds = true; + } else { + const _newSeconds = setTime(min, { + hour, + minute, + seconds: seconds - secondsStep + }); + + res.canDecrementSeconds = min <= _newSeconds; + } + } + } + + return res; +} diff --git a/src/timepicker/timepicker.component.ts b/src/timepicker/timepicker.component.ts index 2815233587..54d55b405c 100644 --- a/src/timepicker/timepicker.component.ts +++ b/src/timepicker/timepicker.component.ts @@ -1,386 +1,416 @@ -// 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, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { timepickerControls } from './timepicker-controls.util'; + +import { TimepickerActions } from './reducer/timepicker.actions'; import { TimepickerConfig } from './timepicker.config'; +import { TimepickerStore } from './reducer/timepicker.store'; +import { isValidDate, padNumber, parseTime } from './timepicker.utils'; +import { TimepickerControls } from './timepicker.models'; 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, TimepickerControls { /** 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; - } + // ui variables + hours: string; + minutes: string; + seconds: string; + meridian: string; - 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(); - }*/ + get isSpinnersVisible(): boolean { + return this.showSpinners && !this.readonlyInput; } - public onChange: any = Function.prototype; - public onTouched: any = Function.prototype; - - // 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, + _cd: ChangeDetectorRef, + private _store: TimepickerStore, + private _timepickerActions: TimepickerActions) { + Object.assign(this, _config); + _store + .select((state) => state.value) + .subscribe((value) => { + // update UI values if date changed + this._renderTime(value); + this._renderControls(value); + this.onChange(value); + _cd.markForCheck(); + }); } - protected set selected(v: Date) { - if (v) { - this._selected = v; - this.updateTemplate(); - this.onChange(this.selected); - } + prevDef($event: any) { + $event.preventDefault(); } - protected config: TimepickerConfig; - - public constructor(_config: TimepickerConfig) { - this.config = _config; - Object.assign(this, _config); + wheelSign($event: any): number { + return Math.sign($event.deltaY as number) * -1; } - // todo: add formatter value to Date object - public ngOnInit(): void { - // todo: take in account $locale.DATETIME_FORMATS.AMPMS; - if (this.mousewheel) { - // this.setupMousewheelEvents(); + canBeChanged(source?: 'wheel' | 'key'): boolean { + if (source === 'wheel' && !this.mousewheel) { + return false; } - if (this.arrowkeys) { - // this.setupArrowkeyEvents(); + if (source === 'key' && !this.arrowkeys) { + return false; } - // this.setupInputEvents(); + if (this.readonlyInput) { + return false; + } + + return true; } - public writeValue(v: any): void { - if (v === this.selected) { + changeHours(step: number, source?: 'wheel' | 'key'): void { + if (!this.canBeChanged(source)) { return; } - if (v && v instanceof Date) { - this.selected = v; + + if (step > 0 && !this.canIncrementHours) { return; } - this.selected = v ? new Date(v) : void 0; - } - - public registerOnChange(fn: (_: any) => {}): void { - this.onChange = fn; - } - - public registerOnTouched(fn: () => {}): void { - this.onTouched = fn; - } - - public setDisabledState(isDisabled: boolean): void { - this.readonlyInput = isDisabled; - } - - public updateHours(): void { - if (this.readonlyInput) { + if (step < 0 && !this.canDecrementHours) { return; } - let hours = this.getHoursFromTemplate(); - let minutes = this.getMinutesFromTemplate(); - this.invalidHours = !isDefined(hours); - this.invalidMinutes = !isDefined(minutes); + this._store.dispatch(this._timepickerActions.changeHours(step)); + } - if (this.invalidHours || this.invalidMinutes) { - // TODO: needed a validation functionality. + changeMinutes(step: number, source?: 'wheel' | 'key'): void { + if (!this.canBeChanged(source)) { 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); + if (step > 0 && !this.canIncrementMinutes) { return; - } else { - this.refresh(/*'h'*/); } - } - - public hoursOnBlur(): void { - if (this.readonlyInput) { + if (step < 0 && !this.canDecrementMinutes) { return; } - // todo: binded with validation - if (!this.invalidHours && parseInt(this.hours, 10) < 10) { - this.hours = this.pad(this.hours); - } + this._store.dispatch(this._timepickerActions.changeMinutes(step)); } - public updateMinutes(): void { - if (this.readonlyInput) { + changeSeconds(step: number, source?: 'wheel' | 'key'): void { + if (!this.canBeChanged(source)) { 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. + if (step > 0 && !this.canIncrementSeconds) { 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); + if (step < 0 && !this.canDecrementSeconds) { return; - } else { - this.refresh(/*'m'*/); } + + this._store.dispatch(this._timepickerActions.changeSeconds(step)); } - public minutesOnBlur(): void { - if (this.readonlyInput) { + updateHours(hour: string): void { + if (!this.canBeChanged()) { return; } - if (!this.invalidMinutes && parseInt(this.minutes, 10) < 10) { - this.minutes = this.pad(this.minutes); - } - } + const dex = 10; + const _hoursPerDay = 24; + const _newHour = parseInt(hour, dex); - public incrementHours(): void { - if (!this.noIncrementHours()) { - this.addMinutesToSelected(this.hourStep * 60); - } - } + if (isNaN(_newHour) || _newHour < 0 || _newHour > _hoursPerDay) { + this.hours = ''; + this.invalidHours = true; - public decrementHours(): void { - if (!this.noDecrementHours()) { - this.addMinutesToSelected(-this.hourStep * 60); + return; } + this.invalidHours = false; + this._store.dispatch(this._timepickerActions + .setTimeUnit({hour: _newHour % _hoursPerDay})); } - public incrementMinutes(): void { - if (!this.noIncrementMinutes()) { - this.addMinutesToSelected(this.minuteStep); - } - } + updateMinutes(minute: string) { + const dex = 10; + const _minutesPerHour = 60; + const _newMinute = parseInt(minute, dex); - public decrementMinutes(): void { - if (!this.noDecrementMinutes()) { - this.addMinutesToSelected(-this.minuteStep); + if (isNaN(_newMinute) || _newMinute < 0 || _newMinute > _minutesPerHour) { + this.minutes = ''; + this.invalidMinutes = true; + + return; } - } - public noIncrementHours(): boolean { - let incrementedSelected = addMinutes(this.selected, this.hourStep * 60); - return incrementedSelected > this.max || - (incrementedSelected < this.selected && incrementedSelected < this.min); + this.invalidMinutes = false; + this._store.dispatch(this._timepickerActions + .setTimeUnit({minute: _newMinute % _minutesPerHour})); } - public noDecrementHours(): boolean { - let decrementedSelected = addMinutes(this.selected, -this.hourStep * 60); - return decrementedSelected < this.min || - (decrementedSelected > this.selected && decrementedSelected > this.max); - } + updateSeconds(seconds: string) { + const dex = 10; + const _secondsPerMinute = 60; + const _newSeconds = parseInt(seconds, dex); - public noIncrementMinutes(): boolean { - let incrementedSelected = addMinutes(this.selected, this.minuteStep); - return incrementedSelected > this.max || - (incrementedSelected < this.selected && incrementedSelected < this.min); - } + if (isNaN(_newSeconds) || _newSeconds < 0 || _newSeconds > _secondsPerMinute) { + this.minutes = ''; + this.invalidMinutes = true; - public noDecrementMinutes(): boolean { - let decrementedSelected = addMinutes(this.selected, -this.minuteStep); - return decrementedSelected < this.min || - (decrementedSelected > this.selected && decrementedSelected > this.max); + return; + } + this.invalidMinutes = false; + this._store.dispatch(this._timepickerActions + .setTimeUnit({minute: _newSeconds % _secondsPerMinute})); } - public toggleMeridian(): void { - if (!this.noToggleMeridian()) { - let sign = this.selected.getHours() < 12 ? 1 : -1; - this.addMinutesToSelected(12 * 60 * sign); + toggleMeridian(): void { + if (!this.showMeridian) { + return; } - } - public noToggleMeridian(): boolean { - if (this.readonlyInput) { - return true; - } + const _hoursPerDayHalf = 12; + this._store.dispatch(this._timepickerActions.changeHours(_hoursPerDayHalf)); + } - if (this.selected.getHours() < 13) { - return addMinutes(this.selected, 12 * 60) > this.max; - } else { - return addMinutes(this.selected, -12 * 60) < this.min; + /** + * Write a new value to the element. + */ + writeValue(obj: any): void { + if (isValidDate(obj)) { + this._store.dispatch(this._timepickerActions.writeValue(parseTime(obj))); } } - protected refresh(/*type?:string*/): void { - // this.makeValid(); - this.updateTemplate(); - this.onChange(this.selected); + /** + * Set the function to be called when the control receives a change event. + */ + registerOnChange(fn: (_: any) => {}): void { + this.onChange = fn; } - protected updateTemplate(/*keyboardChange?:any*/): void { - let hours = this.selected.getHours(); - let minutes = this.selected.getMinutes(); + /** + * Set the function to be called when the control receives a touch event. + */ + registerOnTouched(fn: () => {}): void { + this.onTouched = fn; + } - if (this.showMeridian) { - // Convert 24 to 12 hour system - hours = (hours === 0 || hours === 12) ? 12 : hours % 12; - } + /** + * 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; + } - // 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(); + this.hours = padNumber(_hours); + this.minutes = padNumber(_value.getMinutes()); + this.seconds = padNumber(_value.getUTCSeconds()); } - protected addMinutesToSelected(minutes: any): void { - this.selected = addMinutes(this.selected, minutes); - this.refresh(); + private _renderControls(value: Date): void { + const {min, max, hourStep, minuteStep, secondsStep, showSeconds} = this; + const controlsState = timepickerControls({ + value, min, max, hourStep, minuteStep, secondsStep, showSeconds + }); + Object.assign(this, controlsState); } } diff --git a/src/timepicker/timepicker.config.ts b/src/timepicker/timepicker.config.ts index d976e4a588..aaea9c00a4 100644 --- a/src/timepicker/timepicker.config.ts +++ b/src/timepicker/timepicker.config.ts @@ -4,23 +4,23 @@ 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; /** 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; /** 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..f8df456cf4 --- /dev/null +++ b/src/timepicker/timepicker.models.ts @@ -0,0 +1,25 @@ +export interface TimeUnit { + hour?: number; + minute?: number; + seconds?: number; +} + +export interface TimepickerControls { + canIncrementHours: boolean; + canIncrementMinutes: boolean; + canIncrementSeconds: boolean; + + canDecrementHours: boolean; + canDecrementMinutes: boolean; + canDecrementSeconds: boolean; +} + +export interface TimepickerComponentState { + value: Date; + min: Date; + max: Date; + hourStep: number; + minuteStep: number; + secondsStep: number; + showSeconds: boolean; +} 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..992b2f82a0 --- /dev/null +++ b/src/timepicker/timepicker.utils.ts @@ -0,0 +1,88 @@ +import { TimeUnit } from './timepicker.models'; + +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 parseTime(value: string | Date): Date { + if (typeof value === 'string') { + return new Date(value); + } + + return value; +} + +export function changeTime(value: Date, diff: TimeUnit): Date { + if (!value) { + const _value = new Date(); + + return changeTime(new Date(_value.getFullYear(), _value.getMonth(), _value.getDate(), + 0, 0, 0, _value.getMilliseconds()), diff); + } + + const _hoursPerDay = 24; + // const _minutesPerHour = 60; + // const _secondsPerMinute = 60; + + let hour = value.getHours(); + let minutes = value.getMinutes(); + let seconds = value.getSeconds(); + + if (diff.hour) { + hour = (hour + diff.hour) % _hoursPerDay; + if (hour < 0) { + hour += _hoursPerDay; + } + } + + if (diff.minute) { + minutes = (minutes + diff.minute); + // minutes = (minutes + diff.minute) % _minutesPerHour; + // if (minutes < 0) { + // minutes += _minutesPerHour; + // } + } + + if (diff.seconds) { + seconds = (seconds + diff.seconds); + // seconds = (seconds + diff.seconds) % _secondsPerMinute; + // if (seconds < 0) { + // seconds += _secondsPerMinute; + // } + } + + return new Date(value.getFullYear(), value.getMonth(), value.getDate(), + hour, minutes, seconds, value.getMilliseconds()); +} + +export function setTime(value: Date, opts: TimeUnit): Date { + if (!value) { + return value; + } + + const hour = (opts.hour || opts.hour === 0) ? opts.hour : value.getHours(); + const minute = (opts.minute || opts.minute === 0) ? opts.minute : value.getMinutes(); + const seconds = (opts.seconds || opts.seconds === 0) ? opts.seconds : value.getSeconds(); + + return new Date(value.getFullYear(), value.getMonth(), value.getDate(), + hour, minute, seconds, value.getMilliseconds()); +} + +export function padNumber(value: number): string { + const _value = value.toString(); + if (_value.length > 1) { return _value; } + + return `0${_value}`; +}