diff --git a/CHANGELOG.md b/CHANGELOG.md index df6ce56a..629b067a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file +## 2.8.0 (2018-10-23) + +### Features + +* feat(ngx-material-timepicker): add option `minutesGap` that defines a gap between minutes, closes [(#51)](https://github.com/Agranom/ngx-material-timepicker/issues/51) + ## 2.7.0 (2018-10-21) ### Features diff --git a/README.md b/README.md index d49167df..d128371a 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ Selector: `ngx-material-timepicker` ESC: boolean | Disable or enable closing timepicker by ESC. | | @Input() enableKeyboardInput: boolean | To disable or enable changing time through a keyboard on the timepicker dial without interaction with a clock face. Set `false` by default | +| @Input() + minutesGap: number | To define a gap between minutes. Set `1` by default | | @Output() timeSet: EventEmitter\ | Emits time when that was set. | | @Output() diff --git a/package.json b/package.json index 4eb694db..5d4ceb6d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ngx-material-timepicker", "description": "Handy material design timepicker for angular", - "version": "2.7.0", + "version": "2.8.0", "license": "MIT", "author": "Vitalii Boiko ", "keywords": [ diff --git a/src/app/app.component.html b/src/app/app.component.html index 7223b19e..ff8fa081 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -131,6 +131,25 @@

Examples

+
+ Minutes Gap + +
+

+ You can set up a gap for minutes. It is '1' by default. +

+
+ + +
+
+ + +
+
+
Custom settings examples @@ -156,7 +175,7 @@

Examples

- +
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fd833d1c..7e48fbb8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -80,6 +80,20 @@ export class AppComponent { `; + minutesGapCode = ` +
+ + +
+ +
+ + +
+ `; + customSettings = `
diff --git a/src/app/material-timepicker/components/timepicker-dial/ngx-material-timepicker-dial.component.ts b/src/app/material-timepicker/components/timepicker-dial/ngx-material-timepicker-dial.component.ts index 67392056..3f6d481d 100644 --- a/src/app/material-timepicker/components/timepicker-dial/ngx-material-timepicker-dial.component.ts +++ b/src/app/material-timepicker/components/timepicker-dial/ngx-material-timepicker-dial.component.ts @@ -27,6 +27,7 @@ export class NgxMaterialTimepickerDialComponent implements OnChanges { @Input() minTime: Moment; @Input() maxTime: Moment; @Input() isEditable: boolean; + @Input() minutesGap: number; @Output() periodChanged = new EventEmitter(); @Output() timeUnitChanged = new EventEmitter(); @@ -47,7 +48,7 @@ export class NgxMaterialTimepickerDialComponent implements OnChanges { } if (changes['period'] && changes['period'].currentValue || changes['hour'] && changes['hour'].currentValue) { - const minutes = TimepickerTime.getMinutes(); + const minutes = TimepickerTime.getMinutes(this.minutesGap); this.minutes = TimepickerTime.disableMinutes(minutes, +this.hour, { min: this.minTime, diff --git a/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.html b/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.html index ac179b15..d8bdd19c 100644 --- a/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.html +++ b/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.html @@ -4,7 +4,7 @@ [style.transform]="'rotateZ('+ time.angle +'deg) translateX(-50%)' | styleSanitizer" *ngFor="let time of faceTime.slice(0, 12); trackBy: trackByTime"> {{time.time}} + [ngClass]="{'active': isHourSelected(time.time), 'disabled': time.disabled}">{{time.time}}
@@ -13,7 +13,7 @@ [style.height.px]="innerClockFaceSize" *ngFor="let time of faceTime.slice(12, 24); trackBy: trackByTime"> + [ngClass]="{'active': isHourSelected(time.time), 'disabled': time.disabled}"> {{time.time}}
@@ -28,8 +28,8 @@ [style.transform]="'rotateZ('+ time.angle +'deg) translateX(-50%)' | styleSanitizer" *ngFor="let time of faceTime; trackBy: trackByTime"> - {{time.time % 5 === 0 ? time.time : ''}} + [ngClass]="{'active': isMinuteSelected(time.time), 'disabled': time.disabled}"> + {{time.time | minutesFormatter: minutesGap}} diff --git a/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.spec.ts b/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.spec.ts index ad48c08a..bf49bc83 100644 --- a/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.spec.ts +++ b/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.spec.ts @@ -4,6 +4,7 @@ import {ElementRef, NO_ERRORS_SCHEMA, SimpleChanges} from '@angular/core'; import {ClockFaceTime} from '../../models/clock-face-time.interface'; import {StyleSanitizerPipe} from '../../pipes/style-sanitizer.pipe'; import {TimeUnit} from '../../models/time-unit.enum'; +import {MinutesFormatterPipe} from '../../pipes/minutes-formatter.pipe'; describe('NgxMaterialTimepickerFaceComponent', () => { @@ -12,7 +13,11 @@ describe('NgxMaterialTimepickerFaceComponent', () => { beforeEach(() => { fixture = TestBed.configureTestingModule({ - declarations: [NgxMaterialTimepickerFaceComponent, StyleSanitizerPipe], + declarations: [ + NgxMaterialTimepickerFaceComponent, + StyleSanitizerPipe, + MinutesFormatterPipe + ], schemas: [NO_ERRORS_SCHEMA] }).createComponent(NgxMaterialTimepickerFaceComponent); @@ -284,6 +289,35 @@ describe('NgxMaterialTimepickerFaceComponent', () => { tick(); expect(counter).toBe(1); })); + + it(`should return 'true' or 'false' whether hour is selected or not`, () => { + component.selectedTime = {time: 1, angle: 30}; + component.isClockFaceDisabled = false; + + expect(component.isHourSelected(1)).toBeTruthy(); + expect(component.isHourSelected(2)).toBeFalsy(); + + component.isClockFaceDisabled = true; + expect(component.isHourSelected(1)).toBeFalsy(); + }); + + it(`should return 'true' or 'false' whether minute is selected or not`, () => { + const minute = 10; + component.selectedTime = {time: minute, angle: 30}; + component.isClockFaceDisabled = false; + component.minutesGap = 10; + + expect(component.isMinuteSelected(minute)).toBeTruthy(); + expect(component.isMinuteSelected(5)).toBeFalsy(); + + component.isClockFaceDisabled = true; + expect(component.isMinuteSelected(minute)).toBeFalsy(); + + component.isClockFaceDisabled = false; + component.minutesGap = undefined; + component.selectedTime.time = 5; + expect(component.isMinuteSelected(5)).toBeTruthy(); + }); }); }); diff --git a/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.ts b/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.ts index 8195819c..df432ad3 100644 --- a/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.ts +++ b/src/app/material-timepicker/components/timepicker-face/ngx-material-timepicker-face.component.ts @@ -40,6 +40,8 @@ export class NgxMaterialTimepickerFaceComponent implements AfterViewInit, OnChan @Input() selectedTime: ClockFaceTime; @Input() unit: TimeUnit; @Input() format: number; + @Input() minutesGap: number; + @Output() timeChange = new EventEmitter(); @Output() timeSelected = new EventEmitter(); @@ -128,6 +130,14 @@ export class NgxMaterialTimepickerFaceComponent implements AfterViewInit, OnChan this.isStarted = false; } + isHourSelected(hour: number): boolean { + return (hour === this.selectedTime.time) && !this.isClockFaceDisabled; + } + + isMinuteSelected(minute: number): boolean { + return ((this.selectedTime.time === minute) && (minute % (this.minutesGap || 5) === 0)) && !this.isClockFaceDisabled; + } + private setClockHandPosition(): void { if (this.format === 24) { if (this.selectedTime.time > 12 || this.selectedTime.time === '00') { diff --git a/src/app/material-timepicker/components/timepicker-minutes-face/ngx-material-timepicker-minutes-face.component.html b/src/app/material-timepicker/components/timepicker-minutes-face/ngx-material-timepicker-minutes-face.component.html index 37cdced8..f9e79bf7 100644 --- a/src/app/material-timepicker/components/timepicker-minutes-face/ngx-material-timepicker-minutes-face.component.html +++ b/src/app/material-timepicker/components/timepicker-minutes-face/ngx-material-timepicker-minutes-face.component.html @@ -1,2 +1,3 @@ diff --git a/src/app/material-timepicker/components/timepicker-minutes-face/ngx-material-timepicker-minutes-face.component.ts b/src/app/material-timepicker/components/timepicker-minutes-face/ngx-material-timepicker-minutes-face.component.ts index f39412fa..b785bfbf 100644 --- a/src/app/material-timepicker/components/timepicker-minutes-face/ngx-material-timepicker-minutes-face.component.ts +++ b/src/app/material-timepicker/components/timepicker-minutes-face/ngx-material-timepicker-minutes-face.component.ts @@ -21,15 +21,14 @@ export class NgxMaterialTimepickerMinutesFaceComponent implements OnChanges { @Input() minTime: Moment; @Input() maxTime: Moment; @Input() format: number; - @Output() minuteChange = new EventEmitter(); + @Input() minutesGap: number; - constructor() { - this.minutesList = TimepickerTime.getMinutes(); - } + @Output() minuteChange = new EventEmitter(); ngOnChanges(changes: SimpleChanges) { if (changes['period'] && changes['period'].currentValue) { - this.minutesList = TimepickerTime.disableMinutes(this.minutesList, this.selectedHour, { + const minutes = TimepickerTime.getMinutes(this.minutesGap); + this.minutesList = TimepickerTime.disableMinutes(minutes, this.selectedHour, { min: this.minTime, max: this.maxTime, format: this.format, diff --git a/src/app/material-timepicker/ngx-material-timepicker.component.html b/src/app/material-timepicker/ngx-material-timepicker.component.html index e1d114a2..4b31d754 100644 --- a/src/app/material-timepicker/ngx-material-timepicker.component.html +++ b/src/app/material-timepicker/ngx-material-timepicker.component.html @@ -8,6 +8,7 @@ [period]="selectedPeriod" [activeTimeUnit]="activeTimeUnit" [minTime]="minTime" [maxTime]="maxTime" [isEditable]="enableKeyboardInput" + [minutesGap]="minutesGap" (periodChanged)="changePeriod($event)" (timeUnitChanged)="changeTimeUnit($event)" (hourChanged)="onHourChange($event)" @@ -41,6 +42,7 @@ [maxTime]="maxTime" [format]="format" [period]="selectedPeriod" + [minutesGap]="minutesGap" (minuteChange)="onMinuteChange($event)">
diff --git a/src/app/material-timepicker/ngx-material-timepicker.component.spec.ts b/src/app/material-timepicker/ngx-material-timepicker.component.spec.ts index 1603ad86..3bc31bed 100644 --- a/src/app/material-timepicker/ngx-material-timepicker.component.spec.ts +++ b/src/app/material-timepicker/ngx-material-timepicker.component.spec.ts @@ -161,6 +161,26 @@ describe('NgxMaterialTimepickerComponent', () => { expect(spy).toHaveBeenCalledTimes(0); }); + it('should set minutesGap to 5', () => { + expect(component.minutesGap).toBeUndefined(); + component.minutesGap = 5; + + expect(component.minutesGap).toBe(5); + }); + + it('should set minutesGap to 1', () => { + expect(component.minutesGap).toBeUndefined(); + component.minutesGap = 65; + + expect(component.minutesGap).toBe(1); + }); + + it('should convert minutesGap to int', () => { + component.minutesGap = 6.5; + + expect(component.minutesGap).toBe(6); + }); + describe('Timepicker subscriptions', () => { const hour = {time: 11, angle: 360}; const minute = {time: 44, angle: 36}; diff --git a/src/app/material-timepicker/ngx-material-timepicker.component.ts b/src/app/material-timepicker/ngx-material-timepicker.component.ts index 05f5910e..3de60d8f 100644 --- a/src/app/material-timepicker/ngx-material-timepicker.component.ts +++ b/src/app/material-timepicker/ngx-material-timepicker.component.ts @@ -41,7 +41,6 @@ export class NgxMaterialTimepickerComponent implements OnInit, OnDestroy { selectedMinute: ClockFaceTime; selectedPeriod: TimePeriod; - timePeriod = TimePeriod; timeUnit = TimeUnit; activeTimeUnit = TimeUnit.HOUR; @@ -53,9 +52,20 @@ export class NgxMaterialTimepickerComponent implements OnInit, OnDestroy { @Input('ESC') isEsc = true; @Input() enableKeyboardInput: boolean; + @Input() + set minutesGap(gap: number) { + gap = Math.floor(gap); + this._minutesGap = gap <= 59 ? gap : 1; + } + + get minutesGap(): number { + return this._minutesGap; + } + @Output() timeSet = new EventEmitter(); @Output() closed = new EventEmitter(); + private _minutesGap: number; private timepickerInput: TimepickerDirective; private subscriptions: Subscription[] = []; diff --git a/src/app/material-timepicker/ngx-material-timepicker.module.ts b/src/app/material-timepicker/ngx-material-timepicker.module.ts index 0404a777..c4f92fcf 100644 --- a/src/app/material-timepicker/ngx-material-timepicker.module.ts +++ b/src/app/material-timepicker/ngx-material-timepicker.module.ts @@ -30,6 +30,7 @@ import {NgxMaterialTimepickerDialComponent} from './components/timepicker-dial/n import { NgxMaterialTimepickerDialControlComponent } from './components/timepicker-dial-control/ngx-material-timepicker-dial-control.component'; +import { MinutesFormatterPipe } from './pipes/minutes-formatter.pipe'; @NgModule({ imports: [ @@ -57,7 +58,8 @@ import { TimepickerDirective, OverlayDirective, FocusAnchorDirective, - NgxMaterialTimepickerToggleIconDirective + NgxMaterialTimepickerToggleIconDirective, + MinutesFormatterPipe ] }) export class NgxMaterialTimepickerModule { diff --git a/src/app/material-timepicker/pipes/minutes-formatter.pipe.spec.ts b/src/app/material-timepicker/pipes/minutes-formatter.pipe.spec.ts new file mode 100644 index 00000000..475fa64e --- /dev/null +++ b/src/app/material-timepicker/pipes/minutes-formatter.pipe.spec.ts @@ -0,0 +1,26 @@ +import {MinutesFormatterPipe} from './minutes-formatter.pipe'; + +describe('MinutesFormatterPipe', () => { + let pipe: MinutesFormatterPipe; + + beforeEach(() => { + pipe = new MinutesFormatterPipe(); + }); + + it('should create an instance', () => { + expect(pipe).toBeTruthy(); + }); + + it('should return undefined or null without formatting', () => { + expect(pipe.transform(undefined)).toBeUndefined(); + expect(pipe.transform(null)).toBeNull(); + }); + + it('should return minute', () => { + expect(pipe.transform(3, 3)).toBe(3); + }); + + it('should return empty string', () => { + expect(pipe.transform(3)).toBe(''); + }); +}); diff --git a/src/app/material-timepicker/pipes/minutes-formatter.pipe.ts b/src/app/material-timepicker/pipes/minutes-formatter.pipe.ts new file mode 100644 index 00000000..a9a00761 --- /dev/null +++ b/src/app/material-timepicker/pipes/minutes-formatter.pipe.ts @@ -0,0 +1,16 @@ +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({ + name: 'minutesFormatter' +}) +export class MinutesFormatterPipe implements PipeTransform { + + transform(minute: number, gap = 5): number | string { + if (!minute) { + return minute; + } + + return minute % gap === 0 ? minute : ''; + } + +} diff --git a/src/app/material-timepicker/timepicker-time.namespace.spec.ts b/src/app/material-timepicker/timepicker-time.namespace.spec.ts index ee6a8748..3eb3c25b 100644 --- a/src/app/material-timepicker/timepicker-time.namespace.spec.ts +++ b/src/app/material-timepicker/timepicker-time.namespace.spec.ts @@ -103,9 +103,11 @@ describe('TimepickerTime', () => { const minutes = TimepickerTime.getMinutes(); - it('should return array with 60 minutes', () => { + it('should return array with 60 minutes by default', () => { const angleStep = 360 / 60; + expect(minutes.length).toBe(60); + for (let i = 0; i < minutes.length; i++) { const angle = i * angleStep; @@ -113,6 +115,20 @@ describe('TimepickerTime', () => { } }); + it('should return minutes with gap in 5 minutes', () => { + const gap = 5; + const minutesWithGap = TimepickerTime.getMinutes(gap); + const angleStep = 360 / 60; + + expect(minutesWithGap.length).toBe(12); + + for (let i = 0; i < minutesWithGap.length; i++) { + const angle = i * angleStep * gap; + + expect(minutesWithGap[i]).toEqual({time: i === 0 ? '00' : i * gap, angle: angle !== 0 ? angle : 360}); + } + }); + it('should disable minutes with min time', () => { let disabledMinutes = TimepickerTime.disableMinutes(minutes, 1, { min, diff --git a/src/app/material-timepicker/timepicker-time.namespace.ts b/src/app/material-timepicker/timepicker-time.namespace.ts index 33c4c268..7f7393cf 100644 --- a/src/app/material-timepicker/timepicker-time.namespace.ts +++ b/src/app/material-timepicker/timepicker-time.namespace.ts @@ -33,15 +33,18 @@ export namespace TimepickerTime { return hours; } - export function getMinutes(): ClockFaceTime[] { - const minutes = 60; - const angleStep = 360 / minutes; - - return Array(minutes).fill(0).map((v, i) => { - const index = (v + i); - const angle = angleStep * index; - return {time: index === 0 ? '00' : index, angle: angle !== 0 ? angle : 360}; - }); + export function getMinutes(gap = 1): ClockFaceTime[] { + const minutesCount = 60; + const angleStep = 360 / minutesCount; + const minutes = []; + + for (let i = 0; i < minutesCount; i++) { + const angle = angleStep * i; + if (i % gap === 0) { + minutes.push({time: i === 0 ? '00' : i, angle: angle !== 0 ? angle : 360}); + } + } + return minutes; } export function disableMinutes(minutes: ClockFaceTime[], selectedHour: number, config: DisabledTimeConfig) {