diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html index 331b77e28c2d..70a7cc1e6556 100644 --- a/src/demo-app/datepicker/datepicker-demo.html +++ b/src/demo-app/datepicker/datepicker-demo.html @@ -1,7 +1,6 @@

Work in progress, not ready for use.

- - +
{{selected?.toNativeDate()}}
diff --git a/src/demo-app/datepicker/datepicker-demo.scss b/src/demo-app/datepicker/datepicker-demo.scss new file mode 100644 index 000000000000..c98cba687f8c --- /dev/null +++ b/src/demo-app/datepicker/datepicker-demo.scss @@ -0,0 +1,3 @@ +md-calendar { + width: 300px; +} diff --git a/src/demo-app/datepicker/datepicker-demo.ts b/src/demo-app/datepicker/datepicker-demo.ts index 38c4e8247c3c..240e721a5ef9 100644 --- a/src/demo-app/datepicker/datepicker-demo.ts +++ b/src/demo-app/datepicker/datepicker-demo.ts @@ -5,9 +5,11 @@ import {SimpleDate} from '@angular/material'; @Component({ moduleId: module.id, selector: 'datepicker-demo', - templateUrl: 'datepicker-demo.html' + templateUrl: 'datepicker-demo.html', + styleUrls: ['datepicker-demo.css'], }) export class DatepickerDemo { + startAt = new SimpleDate(2017, 0, 1); date = SimpleDate.today(); selected: SimpleDate; } diff --git a/src/lib/core/datetime/simple-date.ts b/src/lib/core/datetime/simple-date.ts index 75a498425d57..abb904fbab7a 100644 --- a/src/lib/core/datetime/simple-date.ts +++ b/src/lib/core/datetime/simple-date.ts @@ -52,11 +52,21 @@ export class SimpleDate { * Adds an amount of time (in days, months, and years) to the date. * @param amount The amount of time to add. */ - add(amount: {days: number, months: number, years: number}): SimpleDate { + add(amount: {days?: number, months?: number, years?: number}): SimpleDate { return new SimpleDate( - this.year + amount.years || 0, - this.month + amount.months || 0, - this.date + amount.days || 0); + this.year + (amount.years || 0), + this.month + (amount.months || 0), + this.date + (amount.days || 0)); + } + + /** + * Compares this SimpleDate with another SimpleDate. + * @param other The other SimpleDate + * @returns 0 if the dates are equal, a number less than 0 if this date is earlier, + * a number greater than 0 if this date is greater. + */ + compare(other: SimpleDate): number { + return this.year - other.year || this.month - other.month || this.date - other.date; } /** Converts the SimpleDate to a native JS Date object. */ diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html new file mode 100644 index 000000000000..4c30a6bd9037 --- /dev/null +++ b/src/lib/datepicker/calendar.html @@ -0,0 +1,39 @@ +
+ +
+ + +
+ + + +
{{day}}
+ + + + + + diff --git a/src/lib/datepicker/calendar.scss b/src/lib/datepicker/calendar.scss new file mode 100644 index 000000000000..8192360fad9b --- /dev/null +++ b/src/lib/datepicker/calendar.scss @@ -0,0 +1,36 @@ +$mat-calendar-arrow-size: 5px !default; + +.mat-calendar { + display: block; +} + +.mat-calendar-header { + display: flex; +} + +.mat-calendar-spacer { + flex: 1 1 auto; +} + +.mat-calendar-button { + background: transparent; + padding: 0; + margin: 0; + border: none; + outline: none; +} + +.mat-calendar-button > svg { + vertical-align: middle; +} + +.mat-calendar-arrow { + display: inline-block; + width: 0; + height: 0; + border-left: $mat-calendar-arrow-size solid transparent; + border-right: $mat-calendar-arrow-size solid transparent; + border-top: $mat-calendar-arrow-size solid; + margin: 0 $mat-calendar-arrow-size; + vertical-align: middle; +} diff --git a/src/lib/datepicker/calendar.spec.ts b/src/lib/datepicker/calendar.spec.ts new file mode 100644 index 000000000000..27608c2b655b --- /dev/null +++ b/src/lib/datepicker/calendar.spec.ts @@ -0,0 +1,128 @@ +import {async, TestBed, ComponentFixture} from '@angular/core/testing'; +import {MdDatepickerModule} from './index'; +import {Component} from '@angular/core'; +import {SimpleDate} from '../core/datetime/simple-date'; +import {MdCalendar} from './calendar'; +import {By} from '@angular/platform-browser'; + + +describe('MdCalendar', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MdDatepickerModule], + declarations: [ + StandardCalendar, + ], + }); + + TestBed.compileComponents(); + })); + + describe('standard calendar', () => { + let fixture: ComponentFixture; + let testComponent: StandardCalendar; + let calendarElement: HTMLElement; + let periodButton: HTMLElement; + let prevButton: HTMLElement; + let nextButton: HTMLElement; + let calendarInstance: MdCalendar; + + beforeEach(() => { + fixture = TestBed.createComponent(StandardCalendar); + fixture.detectChanges(); + + let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar)); + calendarElement = calendarDebugElement.nativeElement; + periodButton = calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement; + prevButton = calendarElement.querySelector('.mat-calendar-previous-button') as HTMLElement; + nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLElement; + + calendarInstance = calendarDebugElement.componentInstance; + testComponent = fixture.componentInstance; + }); + + it('should be in month view with specified month visible', () => { + expect(calendarInstance._monthView).toBe(true, 'should be in month view'); + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + }); + + it('should toggle view when period clicked', () => { + expect(calendarInstance._monthView).toBe(true, 'should be in month view'); + + periodButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(false, 'should be in year view'); + + periodButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(true, 'should be in month view'); + }); + + it('should go to next and previous month', () => { + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + + nextButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 1, 1)); + + prevButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + }); + + it('should go to previous and next year', () => { + periodButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(false, 'should be in year view'); + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + + nextButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1)); + + prevButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + }); + + it('should go back to month view after selecting month in year view', () => { + periodButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(false, 'should be in year view'); + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 0, 1)); + + let monthCells = calendarElement.querySelectorAll('.mat-calendar-table-cell'); + (monthCells[monthCells.length - 1] as HTMLElement).click(); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(true, 'should be in month view'); + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 11, 1)); + expect(testComponent.selected).toBeFalsy('no date should be selected yet'); + }); + + it('should select date in month view', () => { + let monthCells = calendarElement.querySelectorAll('.mat-calendar-table-cell'); + (monthCells[monthCells.length - 1] as HTMLElement).click(); + fixture.detectChanges(); + + expect(calendarInstance._monthView).toBe(true, 'should be in month view'); + expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 31)); + }); + }); +}); + + +@Component({ + template: `` +}) +class StandardCalendar { + selected: SimpleDate; +} diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts new file mode 100644 index 000000000000..cd840713e05f --- /dev/null +++ b/src/lib/datepicker/calendar.ts @@ -0,0 +1,108 @@ +import { + ChangeDetectionStrategy, + ViewEncapsulation, + Component, + Input, + AfterContentInit, Output, EventEmitter +} from '@angular/core'; +import {SimpleDate} from '../core/datetime/simple-date'; +import {CalendarLocale} from '../core/datetime/calendar-locale'; + + +/** + * A calendar that is used as part of the datepicker. + * @docs-private + */ +@Component({ + moduleId: module.id, + selector: 'md-calendar', + templateUrl: 'calendar.html', + styleUrls: ['calendar.css'], + host: { + '[class.mat-calendar]': 'true', + }, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MdCalendar implements AfterContentInit { + /** A date representing the period (month or year) to start the calendar in. */ + @Input() + get startAt() {return this._startAt; } + set startAt(value: any) { this._startAt = this._locale.parseDate(value); } + private _startAt: SimpleDate; + + /** Whether the calendar should be started in month or year view. */ + @Input() startView: 'month' | 'year' = 'month'; + + /** The currently selected date. */ + @Input() + get selected() { return this._selected; } + set selected(value: any) { this._selected = this._locale.parseDate(value); } + private _selected: SimpleDate; + + /** Emits when the currently selected date changes. */ + @Output() selectedChange = new EventEmitter(); + + /** + * A date representing the current period shown in the calendar. The current period is always + * normalized to the 1st of a month, this prevents date overflow issues (e.g. adding a month to + * January 31st and overflowing into March). + */ + get _currentPeriod() { return this._normalizedCurrentPeriod; } + set _currentPeriod(value: SimpleDate) { + this._normalizedCurrentPeriod = new SimpleDate(value.year, value.month, 1); + } + private _normalizedCurrentPeriod: SimpleDate; + + /** Whether the calendar is in month view. */ + _monthView: boolean; + + /** The names of the weekdays. */ + _weekdays: string[]; + + /** The label for the current calendar view. */ + get _label(): string { + return this._monthView ? this._locale.getCalendarMonthHeaderLabel(this._currentPeriod) : + this._locale.getCalendarYearHeaderLabel(this._currentPeriod); + } + + constructor(private _locale: CalendarLocale) { + this._weekdays = this._locale.narrowDays.slice(this._locale.firstDayOfWeek) + .concat(this._locale.narrowDays.slice(0, this._locale.firstDayOfWeek)); + } + + ngAfterContentInit() { + this._currentPeriod = this.startAt || SimpleDate.today(); + this._monthView = this.startView != 'year'; + } + + /** Handles date selection in the month view. */ + _dateSelected(date: SimpleDate) { + if ((!date || !this.selected) && date != this.selected || date.compare(this.selected)) { + this.selectedChange.emit(date); + } + } + + /** Handles month selection in the year view. */ + _monthSelected(month: SimpleDate) { + this._currentPeriod = month; + this._monthView = true; + } + + /** Handles user clicks on the period label. */ + _currentPeriodClicked() { + this._monthView = !this._monthView; + } + + /** Handles user clicks on the previous button. */ + _previousClicked() { + let amount = this._monthView ? {months: -1} : {years: -1}; + this._currentPeriod = this._currentPeriod.add(amount); + } + + /** Handles user clicks on the next button. */ + _nextClicked() { + let amount = this._monthView ? {months: 1} : {years: 1}; + this._currentPeriod = this._currentPeriod.add(amount); + } +} diff --git a/src/lib/datepicker/index.ts b/src/lib/datepicker/index.ts index 9bea0410509a..5f6c2f471738 100644 --- a/src/lib/datepicker/index.ts +++ b/src/lib/datepicker/index.ts @@ -8,8 +8,10 @@ import {OverlayModule} from '../core/overlay/overlay-directives'; import {MdDatepicker} from './datepicker'; import {MdDatepickerInput} from './datepicker-input'; import {MdDialogModule} from '../dialog/index'; +import {MdCalendar} from './calendar'; +export * from './calendar'; export * from './calendar-table'; export * from './datepicker'; export * from './datepicker-input'; @@ -19,7 +21,7 @@ export * from './year-view'; @NgModule({ imports: [CommonModule, DatetimeModule, MdDialogModule, OverlayModule], - exports: [MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView], - declarations: [MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView], + exports: [MdCalendar, MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView], + declarations: [MdCalendar, MdCalendarTable, MdDatepicker, MdDatepickerInput, MdMonthView, MdYearView], }) export class MdDatepickerModule {}