diff --git a/components/time-picker/demo/use12Hours.md b/components/time-picker/demo/use12Hours.md new file mode 100644 index 00000000000..818476717e1 --- /dev/null +++ b/components/time-picker/demo/use12Hours.md @@ -0,0 +1,14 @@ +--- +order: 9 +title: + zh-CN: 12小时制 + en-US: 12 hours +--- + +## zh-CN + +12小时制的时间选择器,默认format为 `h:mm:ss a` + +## en-US + +TimePicker of 12 hours with default format `h:mm:ss a` diff --git a/components/time-picker/demo/use12Hours.ts b/components/time-picker/demo/use12Hours.ts new file mode 100644 index 00000000000..0817894d531 --- /dev/null +++ b/components/time-picker/demo/use12Hours.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-time-picker-use12Hours', + template: ` + + + `, + styles: [ + ` + nz-time-picker { + margin: 0 8px 12px 0; + } + ` + ] +}) +export class NzDemoTimePickerUse12HoursComponent { + time: Date | null = null; +} diff --git a/components/time-picker/doc/index.en-US.md b/components/time-picker/doc/index.en-US.md index d50b34d305f..adba8b6d98d 100644 --- a/components/time-picker/doc/index.en-US.md +++ b/components/time-picker/doc/index.en-US.md @@ -38,6 +38,7 @@ By clicking the input box, you can select a time from a popup panel. | `[nzOpen]` | whether to popup panel, double binding | `boolean` | `false` | | `[nzPlaceHolder]` | display when there's no value | `string` | `"Select a time"` | | `[nzPopupClassName]` | className of panel | `string` | `''` | +| `[nzUse12Hours]` | display as 12 hours format, with default format `h:mm:ss a` | `boolean` | `false` | | `(ngModelChange)` | a callback function, can be executed when the selected time is changing | `EventEmitter` | - | | `(nzOpenChange)` | a callback function which will be called while panel opening/closing | `EventEmitter` | - | diff --git a/components/time-picker/doc/index.zh-CN.md b/components/time-picker/doc/index.zh-CN.md index ee398e4f421..f2dca1a496c 100644 --- a/components/time-picker/doc/index.zh-CN.md +++ b/components/time-picker/doc/index.zh-CN.md @@ -39,6 +39,7 @@ title: TimePicker | `[nzOpen]` | 面板是否打开,可双向绑定 | `boolean` | `false` | | `[nzPlaceHolder]` | 没有值的时候显示的内容 | `string` | `"请选择时间"` | | `[nzPopupClassName]` | 弹出层类名 | `string` | `''` | +| `[nzUse12Hours]` | 使用12小时制,为true时format默认为`h:mm:ss a` | `boolean` | `false` | | `(ngModelChange)` | 时间发生变化的回调 | `EventEmitter` | - | | `(nzOpenChange)` | 面板打开/关闭时的回调 | `EventEmitter` | - | diff --git a/components/time-picker/nz-time-picker-panel.component.html b/components/time-picker/nz-time-picker-panel.component.html index 88db37beb74..aa36a6eaf19 100644 --- a/components/time-picker/nz-time-picker-panel.component.html +++ b/components/time-picker/nz-time-picker-panel.component.html @@ -69,6 +69,24 @@ +
+
    + +
  • + {{ range.value }} +
  • +
    +
+
diff --git a/components/time-picker/nz-time-picker-panel.component.spec.ts b/components/time-picker/nz-time-picker-panel.component.spec.ts index 5e03ac97cbe..cb365e91876 100644 --- a/components/time-picker/nz-time-picker-panel.component.spec.ts +++ b/components/time-picker/nz-time-picker-panel.component.spec.ts @@ -4,13 +4,18 @@ import { FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NzI18nModule } from '../i18n/nz-i18n.module'; import { NzTimePickerPanelComponent } from './nz-time-picker-panel.component'; - describe('time-picker-panel', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [FormsModule, NzI18nModule], schemas: [NO_ERRORS_SCHEMA], - declarations: [NzTimePickerPanelComponent, NzTestTimePanelComponent, NzTestTimePanelDisabledComponent] + declarations: [ + NzTimePickerPanelComponent, + NzTestTimePanelComponent, + NzTestTimePanelDisabledComponent, + NzTest12HourTimePanelComponent, + NzTest12HourTimePanelDisabeledComponent + ] }); TestBed.compileComponents(); })); @@ -149,6 +154,134 @@ describe('time-picker-panel', () => { expect(listOfSelectContainer[2].firstElementChild.children.length).toBe(54); })); }); + describe('12-hour time-picker-panel', () => { + let panelElement: DebugElement; + let fixture12Hour: ComponentFixture; + let testComponent: NzTest12HourTimePanelComponent; + beforeEach(() => { + fixture12Hour = TestBed.createComponent(NzTest12HourTimePanelComponent); + testComponent = fixture12Hour.debugElement.componentInstance; + fixture12Hour.detectChanges(); + panelElement = fixture12Hour.debugElement.query(By.directive(NzTimePickerPanelComponent)); + }); + it('basic 12-hour time-picker-panel', fakeAsync(() => { + fixture12Hour.detectChanges(); + expect(testComponent.nzTimePickerPanelComponent.enabledColumns).toBe(4); + const listColumns: HTMLElement[] = panelElement.nativeElement.querySelectorAll('.ant-time-picker-panel-select'); + expect(listColumns[0].querySelectorAll('li')[0].innerText).toBe('12'); + const hour12labels = listColumns[3].querySelectorAll('li'); + expect(hour12labels[0].innerText).toBe('am'); + expect(hour12labels[1].innerText).toBe('pm'); + })); + it('default value 12-hour time-picker-panel', fakeAsync(() => { + testComponent.nzTimePickerPanelComponent.opened = true; + fixture12Hour.detectChanges(); + tick(1000); + fixture12Hour.detectChanges(); + const listOfSelectedLi = panelElement.nativeElement.querySelectorAll( + '.ant-time-picker-panel-select-option-selected' + ); + expect(listOfSelectedLi[0].innerText).toBe('12'); + expect(listOfSelectedLi[1].innerText).toBe('00'); + expect(listOfSelectedLi[2].innerText).toBe('00'); + expect(listOfSelectedLi[3].innerText).toBe('am'); + })); + it('should scroll work in 12-hour', fakeAsync(() => { + fixture12Hour.componentInstance.openValue = new Date(0, 0, 0, 5, 6, 7); + fixture12Hour.componentInstance.nzTimePickerPanelComponent.select12Hours({ index: 1, value: 'pm' }); + fixture12Hour.componentInstance.nzTimePickerPanelComponent.opened = true; + fixture12Hour.detectChanges(); + tick(1000); + fixture12Hour.detectChanges(); + let listOfSelectedLi = panelElement.nativeElement.querySelectorAll( + '.ant-time-picker-panel-select-option-selected' + ); + expect(listOfSelectedLi[0].innerText).toBe('05'); + expect(listOfSelectedLi[1].innerText).toBe('06'); + expect(listOfSelectedLi[2].innerText).toBe('07'); + expect(listOfSelectedLi[3].innerText).toBe('pm'); + fixture12Hour.componentInstance.nzTimePickerPanelComponent.opened = false; + fixture12Hour.detectChanges(); + tick(1000); + fixture12Hour.detectChanges(); + fixture12Hour.componentInstance.value = new Date(0, 0, 0, 6, 7, 8); + fixture12Hour.detectChanges(); + fixture12Hour.componentInstance.nzTimePickerPanelComponent.opened = true; + fixture12Hour.detectChanges(); + tick(1000); + fixture12Hour.detectChanges(); + listOfSelectedLi = panelElement.nativeElement.querySelectorAll('.ant-time-picker-panel-select-option-selected'); + expect(listOfSelectedLi[0].innerText).toBe('06'); + expect(listOfSelectedLi[1].innerText).toBe('07'); + expect(listOfSelectedLi[2].innerText).toBe('08'); + })); + it('select hour and 12-hour in 12-hour-time-picker-panel', fakeAsync(() => { + fixture12Hour.detectChanges(); + testComponent.nzTimePickerPanelComponent.selectHour({ index: 3, disabled: false }); + testComponent.nzTimePickerPanelComponent.select12Hours({ index: 1, value: 'pm' }); + fixture12Hour.detectChanges(); + flush(); + fixture12Hour.detectChanges(); + expect(testComponent.value.getHours()).toBe(15); + testComponent.nzTimePickerPanelComponent.select12Hours({ index: 0, value: 'am' }); + fixture12Hour.detectChanges(); + flush(); + fixture12Hour.detectChanges(); + expect(testComponent.value.getHours()).toBe(3); + })); + it('hour step in 12-hour-time-picker-panel', fakeAsync(() => { + testComponent.hourStep = 2; + fixture12Hour.detectChanges(); + const listOfHourContainer = panelElement.nativeElement.querySelectorAll('.ant-time-picker-panel-select'); + expect(listOfHourContainer[0].firstElementChild.children.length).toEqual(6); + })); + }); + + describe('disabled and format 12-hour time-picker-panel', () => { + let panelElement: DebugElement; + let fixture12Hour: ComponentFixture; + let testComponent: NzTest12HourTimePanelDisabeledComponent; + + beforeEach(() => { + fixture12Hour = TestBed.createComponent(NzTest12HourTimePanelDisabeledComponent); + testComponent = fixture12Hour.debugElement.componentInstance; + fixture12Hour.detectChanges(); + panelElement = fixture12Hour.debugElement.query(By.directive(NzTimePickerPanelComponent)); + }); + + it('format in 12-hour-time-pick-panel', fakeAsync(() => { + testComponent.format = 'hh:mm:ss A'; + fixture12Hour.detectChanges(); + const list12HourLi = panelElement.nativeElement + .querySelectorAll('.ant-time-picker-panel-select')[3] + .querySelectorAll('li'); + expect(list12HourLi[0].innerText).toBe('AM'); + expect(list12HourLi[1].innerText).toBe('PM'); + })); + + it('disabled hour in 12-hour-time-picker-panel', fakeAsync(() => { + fixture12Hour.detectChanges(); + flush(); + testComponent.disabledHours = (): number[] => [0, 3, 4, 5, 12, 18, 19, 20, 24]; + fixture12Hour.detectChanges(); + let listHourLi = panelElement.nativeElement + .querySelectorAll('.ant-time-picker-panel-select')[0] + .querySelectorAll('li'); + expect(listHourLi[0].classList).toContain('ant-time-picker-panel-select-option-disabled'); + expect(listHourLi[3].classList).toContain('ant-time-picker-panel-select-option-disabled'); + expect(listHourLi[4].classList).toContain('ant-time-picker-panel-select-option-disabled'); + expect(listHourLi[5].classList).toContain('ant-time-picker-panel-select-option-disabled'); + testComponent.nzTimePickerPanelComponent.select12Hours({ index: 1, value: 'pm' }); + fixture12Hour.detectChanges(); + listHourLi = panelElement.nativeElement + .querySelectorAll('.ant-time-picker-panel-select')[0] + .querySelectorAll('li'); + expect(listHourLi[0].classList).toContain('ant-time-picker-panel-select-option-disabled'); + expect(listHourLi[6].classList).toContain('ant-time-picker-panel-select-option-disabled'); + expect(listHourLi[7].classList).toContain('ant-time-picker-panel-select-option-disabled'); + expect(listHourLi[8].classList).toContain('ant-time-picker-panel-select-option-disabled'); + })); + }); }); @Component({ @@ -229,3 +362,61 @@ export class NzTestTimePanelDisabledComponent { } } } +@Component({ + selector: 'nz-test-12-hour-time-panel', + encapsulation: ViewEncapsulation.None, + template: ` + + + `, + styleUrls: ['../style/index.less', './style/index.less'] +}) +export class NzTest12HourTimePanelComponent { + @ViewChild(NzTimePickerPanelComponent) nzTimePickerPanelComponent: NzTimePickerPanelComponent; + format = 'hh:mm:ss a'; + hourStep = 1; + value: Date; + openValue = new Date(0, 0, 0, 0, 0, 0); +} +@Component({ + selector: 'nz-test-12-hour-time-panel', + encapsulation: ViewEncapsulation.None, + template: ` + + + `, + styleUrls: ['../style/index.less', './style/index.less'] +}) +export class NzTest12HourTimePanelDisabeledComponent { + @ViewChild(NzTimePickerPanelComponent) nzTimePickerPanelComponent: NzTimePickerPanelComponent; + format = 'hh:mm:ss a'; + value = new Date(0, 0, 0, 1, 1, 1); + disabledHours = (): number[] => []; + disabledMinutes(hour: number): number[] { + if (hour === 4) { + return [20, 21, 22, 23, 24, 25]; + } else { + return []; + } + } + disabledSeconds(hour: number, minute: number): number[] { + if (hour === 5 && minute === 1) { + return [20, 21, 22, 23, 24, 25]; + } else { + return []; + } + } +} diff --git a/components/time-picker/nz-time-picker-panel.component.ts b/components/time-picker/nz-time-picker-panel.component.ts index 6e3c38040b8..9c364db378f 100644 --- a/components/time-picker/nz-time-picker-panel.component.ts +++ b/components/time-picker/nz-time-picker-panel.component.ts @@ -5,8 +5,10 @@ import { DebugElement, ElementRef, Input, + OnChanges, OnDestroy, OnInit, + SimpleChanges, TemplateRef, ViewChild, ViewEncapsulation @@ -18,15 +20,16 @@ import { takeUntil } from 'rxjs/operators'; import { reqAnimFrame } from '../core/polyfill/request-animation'; import { NzUpdateHostClassService as UpdateCls } from '../core/services/update-host-class.service'; +import { InputBoolean } from '../core/util'; import { isNotNil } from '../core/util/check'; import { NzTimeValueAccessorDirective } from './nz-time-value-accessor.directive'; import { TimeHolder } from './time-holder'; -function makeRange(length: number, step: number = 1): number[] { - return new Array(Math.ceil(length / step)).fill(0).map((_, i) => i * step); +function makeRange(length: number, step: number = 1, start: number = 0): number[] { + return new Array(Math.ceil(length / step)).fill(0).map((_, i) => (i + start) * step); } -export type NzTimePickerUnit = 'hour' | 'minute' | 'second'; +export type NzTimePickerUnit = 'hour' | 'minute' | 'second' | '12-hour'; @Component({ encapsulation: ViewEncapsulation.None, @@ -35,7 +38,7 @@ export type NzTimePickerUnit = 'hour' | 'minute' | 'second'; templateUrl: './nz-time-picker-panel.component.html', providers: [UpdateCls, { provide: NG_VALUE_ACCESSOR, useExisting: NzTimePickerPanelComponent, multi: true }] }) -export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, OnDestroy { +export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, OnDestroy, OnChanges { private _nzHourStep = 1; private _nzMinuteStep = 1; private _nzSecondStep = 1; @@ -58,15 +61,20 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, hourRange: ReadonlyArray<{ index: number; disabled: boolean }>; minuteRange: ReadonlyArray<{ index: number; disabled: boolean }>; secondRange: ReadonlyArray<{ index: number; disabled: boolean }>; + use12HoursRange: ReadonlyArray<{ index: number; value: string }>; @ViewChild(NzTimeValueAccessorDirective) nzTimeValueAccessorDirective: NzTimeValueAccessorDirective; + @ViewChild('hourListElement') hourListElement: DebugElement; @ViewChild('minuteListElement') minuteListElement: DebugElement; @ViewChild('secondListElement') secondListElement: DebugElement; + @ViewChild('use12HoursListElement') use12HoursListElement: DebugElement; + @Input() nzInDatePicker: boolean = false; // If inside a date-picker, more diff works need to be done @Input() nzAddOn: TemplateRef; @Input() nzHideDisabledOptions = false; @Input() nzClearText: string; @Input() nzPlaceHolder: string; + @Input() @InputBoolean() nzUse12Hours = false; @Input() set nzAllowEmpty(value: boolean) { @@ -158,6 +166,9 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, if (this.secondEnabled) { this.enabledColumns++; } + if (this.nzUse12Hours) { + this.build12Hours(); + } } } @@ -210,12 +221,40 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, } buildHours(): void { - this.hourRange = makeRange(24, this.nzHourStep).map(r => { + let hourRanges = 24; + let disabledHours = this.nzDisabledHours && this.nzDisabledHours(); + let startIndex = 0; + if (this.nzUse12Hours) { + hourRanges = 12; + if (disabledHours) { + if (this.time.selected12Hours === 'PM') { + /** + * Filter and transform hours which greater or equal to 12 + * [0, 1, 2, ..., 12, 13, 14, 15, ..., 23] => [12, 1, 2, 3, ..., 11] + */ + disabledHours = disabledHours.filter(i => i >= 12).map(i => (i > 12 ? i - 12 : i)); + } else { + /** + * Filter and transform hours which less than 12 + * [0, 1, 2,..., 12, 13, 14, 15, ...23] => [12, 1, 2, 3, ..., 11] + */ + disabledHours = disabledHours.filter(i => i < 12 || i === 24).map(i => (i === 24 || i === 0 ? 12 : i)); + } + } + startIndex = 1; + } + this.hourRange = makeRange(hourRanges, this.nzHourStep, startIndex).map(r => { return { index: r, - disabled: this.nzDisabledHours && this.nzDisabledHours().indexOf(r) !== -1 + disabled: this.nzDisabledHours && disabledHours.indexOf(r) !== -1 }; }); + if (this.nzUse12Hours && this.hourRange[this.hourRange.length - 1].index === 12) { + const temp = [...this.hourRange]; + temp.unshift(temp[temp.length - 1]); + temp.splice(temp.length - 1, 1); + this.hourRange = temp; + } } buildMinutes(): void { @@ -237,10 +276,25 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, }); } + build12Hours(): void { + const isUpperForamt = this._format.includes('A'); + this.use12HoursRange = [ + { + index: 0, + value: isUpperForamt ? 'AM' : 'am' + }, + { + index: 1, + value: isUpperForamt ? 'PM' : 'pm' + } + ]; + } + buildTimes(): void { this.buildHours(); this.buildMinutes(); this.buildSeconds(); + this.build12Hours(); } selectHour(hour: { index: number; disabled: boolean }): void { @@ -268,6 +322,20 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, this.scrollToSelected(this.secondListElement.nativeElement, second.index, 120, 'second'); } + select12Hours(value: { index: number; value: string }): void { + this.time.selected12Hours = value.value; + if (this._disabledHours) { + this.buildHours(); + } + if (this._disabledMinutes) { + this.buildMinutes(); + } + if (this._disabledSeconds) { + this.buildSeconds(); + } + this.scrollToSelected(this.use12HoursListElement.nativeElement, value.index, 120, '12-hour'); + } + scrollToSelected(instance: HTMLElement, index: number, duration: number = 0, unit: NzTimePickerUnit): void { const transIndex = this.translateIndex(index, unit); const currentOption = (instance.children[0].children[transIndex] || @@ -282,10 +350,13 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, } else if (unit === 'minute') { const disabledMinutes = this.nzDisabledMinutes && this.nzDisabledMinutes(this.time.hours!); return this.calcIndex(disabledMinutes, this.minuteRange.map(item => item.index).indexOf(index)); - } else { + } else if (unit === 'second') { // second const disabledSeconds = this.nzDisabledSeconds && this.nzDisabledSeconds(this.time.hours!, this.time.minutes!); return this.calcIndex(disabledSeconds, this.secondRange.map(item => item.index).indexOf(index)); + } else { + // 12-hour + return this.calcIndex([], this.use12HoursRange.map(item => item.index).indexOf(index)); } } @@ -341,7 +412,10 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, } isSelectedHour(hour: { index: number; disabled: boolean }): boolean { - return hour.index === this.time.hours || (!isNotNil(this.time.hours) && hour.index === this.time.defaultHours); + return ( + hour.index === this.time.viewHours || + (!isNotNil(this.time.viewHours) && hour.index === this.time.defaultViewHours) + ); } isSelectedMinute(minute: { index: number; disabled: boolean }): boolean { @@ -356,13 +430,20 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, ); } + isSelected12Hours(value: { index: number; value: string }): boolean { + return ( + value.value.toUpperCase() === this.time.selected12Hours || + (!isNotNil(this.time.selected12Hours) && value.value.toUpperCase() === this.time.default12Hours) + ); + } + initPosition(): void { setTimeout(() => { if (this.hourEnabled && this.hourListElement) { - if (isNotNil(this.time.hours)) { - this.scrollToSelected(this.hourListElement.nativeElement, this.time.hours!, 0, 'hour'); + if (isNotNil(this.time.viewHours)) { + this.scrollToSelected(this.hourListElement.nativeElement, this.time.viewHours!, 0, 'hour'); } else { - this.scrollToSelected(this.hourListElement.nativeElement, this.time.defaultHours, 0, 'hour'); + this.scrollToSelected(this.hourListElement.nativeElement, this.time.defaultViewHours, 0, 'hour'); } } if (this.minuteEnabled && this.minuteListElement) { @@ -379,6 +460,13 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, this.scrollToSelected(this.secondListElement.nativeElement, this.time.defaultSeconds, 0, 'second'); } } + if (this.nzUse12Hours && this.use12HoursListElement) { + const selectedHours = isNotNil(this.time.selected12Hours) + ? this.time.selected12Hours + : this.time.default12Hours; + const index = selectedHours === 'AM' ? 0 : 1; + this.scrollToSelected(this.use12HoursListElement.nativeElement, index, 0, '12-hour'); + } }); } @@ -402,8 +490,16 @@ export class NzTimePickerPanelComponent implements ControlValueAccessor, OnInit, this.unsubscribe$.complete(); } + ngOnChanges(changes: SimpleChanges): void { + const { nzUse12Hours } = changes; + if (nzUse12Hours && !nzUse12Hours.previousValue && nzUse12Hours.currentValue) { + this.build12Hours(); + this.enabledColumns++; + } + } + writeValue(value: Date): void { - this.time.value = value; + this.time.setValue(value, this.nzUse12Hours); this.buildTimes(); // Mark this component to be checked manually with internal properties changing (see: https://github.com/angular/angular/issues/10816) diff --git a/components/time-picker/nz-time-picker.component.html b/components/time-picker/nz-time-picker.component.html index 1d09aad15b5..338fd8585a1 100644 --- a/components/time-picker/nz-time-picker.component.html +++ b/components/time-picker/nz-time-picker.component.html @@ -44,6 +44,7 @@ [nzDisabledSeconds]="nzDisabledSeconds" [nzPlaceHolder]="nzPlaceHolder || ('TimePicker.placeholder' | nzI18n)" [nzHideDisabledOptions]="nzHideDisabledOptions" + [nzUse12Hours]="nzUse12Hours" [nzDefaultOpenValue]="nzDefaultOpenValue" [nzAddOn]="nzAddOn" [opened]="nzOpen" diff --git a/components/time-picker/nz-time-picker.component.spec.ts b/components/time-picker/nz-time-picker.component.spec.ts index 8e6652178f4..73d3839b2e3 100644 --- a/components/time-picker/nz-time-picker.component.spec.ts +++ b/components/time-picker/nz-time-picker.component.spec.ts @@ -97,6 +97,11 @@ describe('time-picker', () => { fixture.detectChanges(); expect(testComponent.date).toBeNull(); })); + it('should support default nzfomat in 12-hours', () => { + testComponent.use12Hours = true; + fixture.detectChanges(); + expect(testComponent.nzTimePickerComponent.nzFormat).toBe('h:mm:ss a'); + }); }); }); @@ -109,6 +114,7 @@ describe('time-picker', () => { [(nzOpen)]="open" (nzOpenChange)="openChange($event)" [nzDisabled]="disabled" + [nzUse12Hours]="use12Hours" > ` }) @@ -118,5 +124,6 @@ export class NzTestTimePickerComponent { autoFocus = false; date = new Date(); disabled = false; + use12Hours = false; @ViewChild(NzTimePickerComponent) nzTimePickerComponent: NzTimePickerComponent; } diff --git a/components/time-picker/nz-time-picker.component.ts b/components/time-picker/nz-time-picker.component.ts index 1eb785b68ec..52a33b5531e 100644 --- a/components/time-picker/nz-time-picker.component.ts +++ b/components/time-picker/nz-time-picker.component.ts @@ -7,9 +7,11 @@ import { ElementRef, EventEmitter, Input, + OnChanges, OnInit, Output, Renderer2, + SimpleChanges, TemplateRef, ViewChild, ViewEncapsulation @@ -28,7 +30,7 @@ import { toBoolean } from '../core/util/convert'; animations: [slideMotion], providers: [UpdateCls, { provide: NG_VALUE_ACCESSOR, useExisting: NzTimePickerComponent, multi: true }] }) -export class NzTimePickerComponent implements ControlValueAccessor, OnInit, AfterViewInit { +export class NzTimePickerComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges { private _disabled = false; private _value: Date | null = null; private _allowEmpty = true; @@ -63,6 +65,7 @@ export class NzTimePickerComponent implements ControlValueAccessor, OnInit, Afte @Input() nzDisabledSeconds: (hour: number, minute: number) => number[]; @Input() nzFormat = 'HH:mm:ss'; @Input() nzOpen = false; + @Input() nzUse12Hours = false; @Output() readonly nzOpenChange = new EventEmitter(); @Input() @@ -180,6 +183,13 @@ export class NzTimePickerComponent implements ControlValueAccessor, OnInit, Afte this.origin = new CdkOverlayOrigin(this.element); } + ngOnChanges(changes: SimpleChanges): void { + const { nzUse12Hours, nzFormat } = changes; + if (nzUse12Hours && !nzUse12Hours.previousValue && nzUse12Hours.currentValue && !nzFormat) { + this.nzFormat = 'h:mm:ss a'; + } + } + ngAfterViewInit(): void { this.isInit = true; this.updateAutoFocus(); diff --git a/components/time-picker/time-holder.spec.ts b/components/time-picker/time-holder.spec.ts index a65566d0e11..ea354425024 100644 --- a/components/time-picker/time-holder.spec.ts +++ b/components/time-picker/time-holder.spec.ts @@ -74,4 +74,33 @@ describe('time holder', () => { holder.setMinutes(23, false); expect(holder.value).toEqual(new Date(2001, 10, 1, 23, 23, 20)); }); + it('should 12-hour worked', () => { + const holder = new TimeHolder().setValue(new Date(0, 0, 0, 0, 0, 0)); + holder.setUse12Hours(true); + holder.selected12Hours = 'pm'; + holder.setHours(3, false); + expect(holder.viewHours).toBe(3); + expect(holder.realHours).toBe(15); + const date = new Date(0, 0, 0, 15, 0, 0, 0); + expect(mathSecondRound(holder.value!)).toEqual(mathSecondRound(date)); + }); + it('should set defaultRealHours and defaultViewHours correctly', () => { + const holder = new TimeHolder().setValue(undefined, true).setDefaultOpenValue(new Date(0, 0, 0, 15, 2, 3)); + expect(holder.defaultRealHours).toBe(15); + expect(holder.defaultViewHours).toBe(3); + }); + it('should set default selected 12-hours with value', () => { + const holderPM = new TimeHolder().setValue(new Date(0, 0, 0, 15, 2, 3), true); + expect(holderPM.selected12Hours).toBe('PM'); + const holderAM = new TimeHolder().setValue(new Date(0, 0, 0, 0, 2, 3), true); + expect(holderAM.selected12Hours).toBe('AM'); + }); + it('should transform special value in 12-hour', () => { + const holder = new TimeHolder().setValue(new Date(), true); + holder.selected12Hours = 'am'; + holder.setHours(12, false); + expect(holder.realHours).toBe(0); + holder.selected12Hours = 'pm'; + expect(holder.realHours).toBe(12); + }); }); diff --git a/components/time-picker/time-holder.ts b/components/time-picker/time-holder.ts index 031ad43a470..e69ec3ecb25 100644 --- a/components/time-picker/time-holder.ts +++ b/components/time-picker/time-holder.ts @@ -6,6 +6,8 @@ export class TimeHolder { private _seconds: number | undefined = undefined; private _hours: number | undefined = undefined; private _minutes: number | undefined = undefined; + private _selected12Hours: string | undefined = undefined; + private _use12Hours: boolean = false; private _defaultOpenValue: Date = new Date(); private _value: Date | undefined; private _changes = new Subject(); @@ -43,6 +45,11 @@ export class TimeHolder { return this; } + setUse12Hours(value: boolean): this { + this._use12Hours = value; + return this; + } + get changes(): Observable { return this._changes.asObservable(); } @@ -58,13 +65,19 @@ export class TimeHolder { this._hours = this._value!.getHours(); this._minutes = this._value!.getMinutes(); this._seconds = this._value!.getSeconds(); + if (this._use12Hours && isNotNil(this._hours)) { + this._selected12Hours = this._hours >= 12 ? 'PM' : 'AM'; + } } else { this._clear(); } } } - setValue(value: Date | undefined): this { + setValue(value: Date | undefined, use12Hours?: boolean): this { + if (isNotNil(use12Hours)) { + this._use12Hours = use12Hours as boolean; + } this.value = value; return this; } @@ -82,6 +95,7 @@ export class TimeHolder { this._hours = undefined; this._minutes = undefined; this._seconds = undefined; + this._selected12Hours = undefined; } private update(): void { @@ -106,6 +120,20 @@ export class TimeHolder { this._value!.setSeconds(this.seconds!); } + if (this._use12Hours) { + if (!isNotNil(this._selected12Hours)) { + this._selected12Hours = this.default12Hours; + } + if (this.selected12Hours === 'PM' && this._hours! < 12) { + this._hours! += 12; + this._value!.setHours(this._hours!); + } + if (this.selected12Hours === 'AM' && this._hours! >= 12) { + this._hours! -= 12; + this._value!.setHours(this._hours!); + } + } + this._value = new Date(this._value!); } this.changed(); @@ -115,13 +143,50 @@ export class TimeHolder { this._changes.next(this._value); } + /** + * @description + * UI view hours + * Get viewHours which is selected in `time-picker-panel` and its range is [12, 1, 2, ..., 11] + */ + get viewHours(): number | undefined { + return this._use12Hours && isNotNil(this._hours) ? this.calculateViewHour(this._hours!) : this._hours; + } + + /** + * @description + * Value hours + * Get realHours and its range is [0, 1, 2, ..., 22, 23] + */ + get realHours(): number | undefined { + return this._hours; + } + + /** + * @description + * Same as realHours + * @see realHours + */ get hours(): number | undefined { return this._hours; } + /** + * @description + * Set viewHours to realHours + */ set hours(value: number | undefined) { if (value !== this._hours) { - this._hours = value; + if (this._use12Hours) { + if (this.selected12Hours === 'PM' && value !== 12) { + this._hours! = (value as number) + 12; + } else if (this.selected12Hours === 'AM' && value === 12) { + this._hours = 0; + } else { + this._hours = value; + } + } else { + this._hours = value; + } this.update(); } } @@ -148,6 +213,17 @@ export class TimeHolder { } } + get selected12Hours(): string | undefined { + return this._selected12Hours; + } + + set selected12Hours(value: string | undefined) { + if (value!.toUpperCase() !== this._selected12Hours) { + this._selected12Hours = value!.toUpperCase(); + this.update(); + } + } + get defaultOpenValue(): Date { return this._defaultOpenValue; } @@ -164,6 +240,29 @@ export class TimeHolder { return this; } + /** + * @description + * Get deafultViewHours when defaultOpenValue is setted + * @see viewHours + */ + get defaultViewHours(): number { + const hours = this._defaultOpenValue.getHours(); + return this._use12Hours && isNotNil(hours) ? this.calculateViewHour(hours) : hours; + } + + /** + * @description + * Get defaultRealHours when defaultOpenValue is setted + * @see realHours + */ + get defaultRealHours(): number { + return this._defaultOpenValue.getHours(); + } + + /** + * @description + * Same as defaultRealHours + */ get defaultHours(): number { return this._defaultOpenValue.getHours(); } @@ -176,5 +275,20 @@ export class TimeHolder { return this._defaultOpenValue.getSeconds(); } + get default12Hours(): string { + return this._defaultOpenValue.getHours() >= 12 ? 'PM' : 'AM'; + } + constructor() {} + + private calculateViewHour(value: number): number { + const selected12Hours = this._selected12Hours || this.default12Hours; + if (selected12Hours === 'PM' && value > 12) { + return value - 12; + } + if (selected12Hours === 'AM' && value === 0) { + return 12; + } + return value; + } }