From 8f1ba7621b1282cbc6e2d8561b3fad15a010a6af Mon Sep 17 00:00:00 2001 From: mmalerba Date: Tue, 14 Mar 2017 14:57:37 -0700 Subject: [PATCH] feat(datepicker): min & max dates + dateFilter (#3556) * don't allow going past min/max date * add back missing mdSuffix * added date filtering logic * added working date filter * disabled style for prev/next buttons * add unit tests * fix lint * address comments --- src/demo-app/datepicker/datepicker-demo.html | 6 +- src/demo-app/datepicker/datepicker-demo.ts | 2 + src/lib/core/datetime/simple-date.spec.ts | 19 +++ src/lib/core/datetime/simple-date.ts | 18 +++ src/lib/datepicker/_datepicker-theme.scss | 22 +++- src/lib/datepicker/calendar-table.html | 3 +- src/lib/datepicker/calendar-table.spec.ts | 53 +++++++- src/lib/datepicker/calendar-table.ts | 11 +- src/lib/datepicker/calendar.html | 8 +- src/lib/datepicker/calendar.scss | 5 + src/lib/datepicker/calendar.spec.ts | 126 +++++++++++++++++++ src/lib/datepicker/calendar.ts | 48 ++++++- src/lib/datepicker/datepicker.html | 3 + src/lib/datepicker/datepicker.ts | 26 +++- src/lib/datepicker/month-view.spec.ts | 32 +++++ src/lib/datepicker/month-view.ts | 9 +- src/lib/datepicker/year-view.html | 3 +- src/lib/datepicker/year-view.spec.ts | 38 ++++++ src/lib/datepicker/year-view.ts | 36 ++++-- 19 files changed, 439 insertions(+), 29 deletions(-) diff --git a/src/demo-app/datepicker/datepicker-demo.html b/src/demo-app/datepicker/datepicker-demo.html index c975b470e636..faee6d1cb911 100644 --- a/src/demo-app/datepicker/datepicker-demo.html +++ b/src/demo-app/datepicker/datepicker-demo.html @@ -12,7 +12,7 @@

Work in progress, not ready for use.

- +

@@ -20,6 +20,8 @@

Work in progress, not ready for use.

- + +

diff --git a/src/demo-app/datepicker/datepicker-demo.ts b/src/demo-app/datepicker/datepicker-demo.ts index f0927ec1203c..5c681a100b8f 100644 --- a/src/demo-app/datepicker/datepicker-demo.ts +++ b/src/demo-app/datepicker/datepicker-demo.ts @@ -11,4 +11,6 @@ import {SimpleDate} from '@angular/material'; export class DatepickerDemo { date: SimpleDate; touch = false; + dateFilter = (date: SimpleDate) => !this._blacklistedMonths.has(date.month) && date.date % 2 == 0; + private _blacklistedMonths = new Set([2, 3]); } diff --git a/src/lib/core/datetime/simple-date.spec.ts b/src/lib/core/datetime/simple-date.spec.ts index e47dead290bc..9f49d2e3038c 100644 --- a/src/lib/core/datetime/simple-date.spec.ts +++ b/src/lib/core/datetime/simple-date.spec.ts @@ -37,4 +37,23 @@ describe('SimpleDate', () => { expect(new SimpleDate(2017, 0, 1).add({years: 1, months: 1, days: 1})) .toEqual(new SimpleDate(2018, 1, 2)); }); + + it('clamps date at lower bound', () => { + let date = new SimpleDate(2017, 0, 1); + let lower = new SimpleDate(2018, 1, 2); + let upper = new SimpleDate(2019, 2, 3); + expect(date.clamp(lower, upper)).toEqual(lower); + }); + + it('clamps date at upper bound', () => { + let date = new SimpleDate(2020, 0, 1); + let lower = new SimpleDate(2018, 1, 2); + let upper = new SimpleDate(2019, 2, 3); + expect(date.clamp(lower, upper)).toEqual(upper); + }); + + it('clamp treats null as unbounded', () => { + let date = new SimpleDate(2017, 0, 1); + expect(date.clamp(null, null)).toEqual(date); + }); }); diff --git a/src/lib/core/datetime/simple-date.ts b/src/lib/core/datetime/simple-date.ts index abb904fbab7a..6546cd923ea2 100644 --- a/src/lib/core/datetime/simple-date.ts +++ b/src/lib/core/datetime/simple-date.ts @@ -51,6 +51,7 @@ export class SimpleDate { /** * Adds an amount of time (in days, months, and years) to the date. * @param amount The amount of time to add. + * @returns A new SimpleDate with the given amount of time added. */ add(amount: {days?: number, months?: number, years?: number}): SimpleDate { return new SimpleDate( @@ -69,6 +70,23 @@ export class SimpleDate { return this.year - other.year || this.month - other.month || this.date - other.date; } + /** + * Clamps the date between the given min and max dates. + * @param min The minimum date + * @param max The maximum date + * @returns A new SimpleDate equal to this one clamped between the given min and max dates. + */ + clamp(min: SimpleDate, max: SimpleDate): SimpleDate { + let clampedDate: SimpleDate = this; + if (min && this.compare(min) < 0) { + clampedDate = min; + } + if (max && this.compare(max) > 0) { + clampedDate = max; + } + return new SimpleDate(clampedDate.year, clampedDate.month, clampedDate.date); + } + /** Converts the SimpleDate to a native JS Date object. */ toNativeDate(): Date { return new Date(this.year, this.month, this.date); diff --git a/src/lib/datepicker/_datepicker-theme.scss b/src/lib/datepicker/_datepicker-theme.scss index 5cf64b335323..d3904a327186 100644 --- a/src/lib/datepicker/_datepicker-theme.scss +++ b/src/lib/datepicker/_datepicker-theme.scss @@ -3,11 +3,14 @@ @mixin mat-datepicker-theme($theme) { - $mat-datepicker-selected-today-box-shadow-width: 1px; $primary: map-get($theme, primary); $foreground: map-get($theme, foreground); $background: map-get($theme, background); + $mat-datepicker-selected-today-box-shadow-width: 1px; + $mat-datepicker-selected-fade-amount: 0.6; + $mat-datepicker-today-fade-amount: 0.2; + .mat-calendar { background-color: mat-color($background, card); } @@ -36,10 +39,14 @@ .mat-calendar-table-cell-content { color: mat-color($foreground, text); border-color: transparent; + + .mat-calendar-table-disabled > &:not(.mat-calendar-table-selected) { + color: mat-color($foreground, disabled-text); + } } - .mat-calendar-table-cell:hover { - .mat-calendar-table-cell-content:not(.mat-calendar-table-selected) { + :not(.mat-calendar-table-disabled):hover { + & > .mat-calendar-table-cell-content:not(.mat-calendar-table-selected) { background-color: mat-color($background, hover); } } @@ -47,6 +54,10 @@ .mat-calendar-table-selected { background-color: mat-color($primary); color: mat-color($primary, default-contrast); + + .mat-calendar-table-disabled > & { + background-color: fade-out(mat-color($primary), $mat-datepicker-selected-fade-amount); + } } .mat-calendar-table-today { @@ -54,6 +65,11 @@ // Note: though it's not text, the border is a hint about the fact that this is today's date, // so we use the hint color. border-color: mat-color($foreground, hint-text); + + .mat-calendar-table-disabled > & { + border-color: + fade-out(mat-color($foreground, hint-text), $mat-datepicker-today-fade-amount); + } } &.mat-calendar-table-selected { diff --git a/src/lib/datepicker/calendar-table.html b/src/lib/datepicker/calendar-table.html index 6df7a677b4e4..37b29b265d79 100644 --- a/src/lib/datepicker/calendar-table.html +++ b/src/lib/datepicker/calendar-table.html @@ -13,7 +13,8 @@ + [class.mat-calendar-table-disabled]="!item.enabled" + (click)="_cellClicked(item)">
diff --git a/src/lib/datepicker/calendar-table.spec.ts b/src/lib/datepicker/calendar-table.spec.ts index 3ebf0dbce262..081d99ac1d4f 100644 --- a/src/lib/datepicker/calendar-table.spec.ts +++ b/src/lib/datepicker/calendar-table.spec.ts @@ -2,6 +2,7 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {Component} from '@angular/core'; import {MdCalendarCell, MdCalendarTable} from './calendar-table'; import {By} from '@angular/platform-browser'; +import {SimpleDate} from '../core/datetime/simple-date'; describe('MdCalendarTable', () => { @@ -12,6 +13,7 @@ describe('MdCalendarTable', () => { // Test components. StandardCalendarTable, + CalendarTableWithDisabledCells, ], }); @@ -85,6 +87,38 @@ describe('MdCalendarTable', () => { .toContain('mat-calendar-table-selected', 'today should be selected'); }); }); + + describe('calendar table with disabled cells', () => { + let fixture: ComponentFixture; + let testComponent: CalendarTableWithDisabledCells; + let calendarTableNativeElement: Element; + let cellEls: NodeListOf; + + beforeEach(() => { + fixture = TestBed.createComponent(CalendarTableWithDisabledCells); + fixture.detectChanges(); + + let calendarTableDebugElement = fixture.debugElement.query(By.directive(MdCalendarTable)); + calendarTableNativeElement = calendarTableDebugElement.nativeElement; + testComponent = fixture.componentInstance; + cellEls = calendarTableNativeElement.querySelectorAll('.mat-calendar-table-cell'); + }); + + it('should only allow selection of disabled cells when allowDisabledSelection is true', () => { + (cellEls[0] as HTMLElement).click(); + fixture.detectChanges(); + + expect(testComponent.selected).toBeFalsy(); + + testComponent.allowDisabledSelection = true; + fixture.detectChanges(); + + (cellEls[0] as HTMLElement).click(); + fixture.detectChanges(); + + expect(testComponent.selected).toBe(1); + }); + }); }); @@ -112,6 +146,23 @@ class StandardCalendarTable { } +@Component({ + template: ` + ` +}) +class CalendarTableWithDisabledCells { + rows = [[1, 2, 3, 4]].map(r => r.map(d => { + let cell = createCell(d); + cell.enabled = d % 2 == 0; + return cell; + })); + allowDisabledSelection = false; + selected: SimpleDate; +} + + function createCell(value: number) { - return new MdCalendarCell(value, `${value}`); + return new MdCalendarCell(value, `${value}`, true); } diff --git a/src/lib/datepicker/calendar-table.ts b/src/lib/datepicker/calendar-table.ts index 70999010cbd5..aabe9c76fe70 100644 --- a/src/lib/datepicker/calendar-table.ts +++ b/src/lib/datepicker/calendar-table.ts @@ -13,7 +13,7 @@ import { * @docs-private */ export class MdCalendarCell { - constructor(public value: number, public displayValue: string) {} + constructor(public value: number, public displayValue: string, public enabled: boolean) {} } @@ -48,14 +48,17 @@ export class MdCalendarTable { /** The number of columns in the table. */ @Input() numCols = 7; + /** Whether to allow selection of disabled cells. */ + @Input() allowDisabledSelection = false; + /** Emits when a new value is selected. */ @Output() selectedValueChange = new EventEmitter(); - _cellClicked(value: number) { - if (this.selectedValue && this.selectedValue === value) { + _cellClicked(cell: MdCalendarCell) { + if (!this.allowDisabledSelection && !cell.enabled) { return; } - this.selectedValueChange.emit(value); + this.selectedValueChange.emit(cell.value); } /** The number of blank cells to put at the beginning for the first row. */ diff --git a/src/lib/datepicker/calendar.html b/src/lib/datepicker/calendar.html index 1c4921e792fb..e570acda369f 100644 --- a/src/lib/datepicker/calendar.html +++ b/src/lib/datepicker/calendar.html @@ -6,14 +6,16 @@
- -
diff --git a/src/lib/datepicker/calendar.scss b/src/lib/datepicker/calendar.scss index 2dabff7e0c7b..2d81baf71ade 100644 --- a/src/lib/datepicker/calendar.scss +++ b/src/lib/datepicker/calendar.scss @@ -8,6 +8,7 @@ $mat-calendar-controls-start-padding: calc(100% / 14 - 6px); $mat-calendar-controls-end-padding: calc(100% / 14 - 12px); $mat-calendar-period-font-size: 14px; $mat-calendar-arrow-size: 5px !default; +$mat-calendar-arrow-disabled-opacity: 0.5 !default; $mat-calendar-weekday-table-font-size: 11px !default; @@ -41,6 +42,10 @@ $mat-calendar-weekday-table-font-size: 11px !default; margin: 0; border: none; outline: none; + + &.mat-calendar-disabled { + opacity: $mat-calendar-arrow-disabled-opacity; + } } .mat-calendar-period-button { diff --git a/src/lib/datepicker/calendar.spec.ts b/src/lib/datepicker/calendar.spec.ts index e0de6b649550..7c40c71ce0a7 100644 --- a/src/lib/datepicker/calendar.spec.ts +++ b/src/lib/datepicker/calendar.spec.ts @@ -23,6 +23,8 @@ describe('MdCalendar', () => { // Test components. StandardCalendar, + CalendarWithMinMax, + CalendarWithDateFilter, ], }); @@ -128,6 +130,108 @@ describe('MdCalendar', () => { expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 31)); }); }); + + describe('calendar with min and max date', () => { + let fixture: ComponentFixture; + let testComponent: CalendarWithMinMax; + let calendarElement: HTMLElement; + let prevButton: HTMLElement; + let nextButton: HTMLElement; + let calendarInstance: MdCalendar; + + beforeEach(() => { + fixture = TestBed.createComponent(CalendarWithMinMax); + + let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar)); + calendarElement = calendarDebugElement.nativeElement; + 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 clamp startAt value below min date', () => { + testComponent.startAt = new SimpleDate(2000, 0, 1); + fixture.detectChanges(); + + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2016, 0, 1)); + }); + + it('should clamp startAt value above max date', () => { + testComponent.startAt = new SimpleDate(2020, 0, 1); + fixture.detectChanges(); + + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1)); + }); + + it('should not go back past min date', () => { + testComponent.startAt = new SimpleDate(2016, 1, 1); + fixture.detectChanges(); + + expect(prevButton.classList).not.toContain('mat-calendar-disabled'); + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2016, 1, 1)); + + prevButton.click(); + fixture.detectChanges(); + + expect(prevButton.classList).toContain('mat-calendar-disabled'); + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2016, 0, 1)); + + prevButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2016, 0, 1)); + }); + + it('should not go forward past max date', () => { + testComponent.startAt = new SimpleDate(2017, 11, 1); + fixture.detectChanges(); + + expect(nextButton.classList).not.toContain('mat-calendar-disabled'); + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2017, 11, 1)); + + nextButton.click(); + fixture.detectChanges(); + + expect(nextButton.classList).toContain('mat-calendar-disabled'); + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1)); + + nextButton.click(); + fixture.detectChanges(); + + expect(calendarInstance._currentPeriod).toEqual(new SimpleDate(2018, 0, 1)); + }); + }); + + describe('calendar with date filter', () => { + let fixture: ComponentFixture; + let testComponent: CalendarWithDateFilter; + let calendarElement: HTMLElement; + let calendarInstance: MdCalendar; + + beforeEach(() => { + fixture = TestBed.createComponent(CalendarWithDateFilter); + fixture.detectChanges(); + + let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar)); + calendarElement = calendarDebugElement.nativeElement; + calendarInstance = calendarDebugElement.componentInstance; + testComponent = fixture.componentInstance; + }); + + it('should disable and prevent selection of filtered dates', () => { + let cells = calendarElement.querySelectorAll('.mat-calendar-table-cell'); + (cells[0] as HTMLElement).click(); + fixture.detectChanges(); + + expect(testComponent.selected).toBeFalsy(); + + (cells[1] as HTMLElement).click(); + fixture.detectChanges(); + + expect(testComponent.selected).toEqual(new SimpleDate(2017, 0, 2)); + }); + }); }); @@ -137,3 +241,25 @@ describe('MdCalendar', () => { class StandardCalendar { selected: SimpleDate; } + + +@Component({ + template: `` +}) +class CalendarWithMinMax { + startAt: SimpleDate; +} + + +@Component({ + template: ` + + ` +}) +class CalendarWithDateFilter { + selected: SimpleDate; + + dateFilter (date: SimpleDate) { + return date.date % 2 == 0; + } +} diff --git a/src/lib/datepicker/calendar.ts b/src/lib/datepicker/calendar.ts index cca226f0de45..ed2d587a84a2 100644 --- a/src/lib/datepicker/calendar.ts +++ b/src/lib/datepicker/calendar.ts @@ -27,7 +27,7 @@ import {CalendarLocale} from '../core/datetime/calendar-locale'; export class MdCalendar implements AfterContentInit { /** A date representing the period (month or year) to start the calendar in. */ @Input() - get startAt() {return this._startAt; } + get startAt() { return this._startAt; } set startAt(value: any) { this._startAt = this._locale.parseDate(value); } private _startAt: SimpleDate; @@ -40,9 +40,32 @@ export class MdCalendar implements AfterContentInit { set selected(value: any) { this._selected = this._locale.parseDate(value); } private _selected: SimpleDate; + /** The minimum selectable date. */ + @Input() + get minDate(): SimpleDate { return this._minDate; }; + set minDate(date: SimpleDate) { this._minDate = this._locale.parseDate(date); } + private _minDate: SimpleDate; + + /** The maximum selectable date. */ + @Input() + get maxDate(): SimpleDate { return this._maxDate; }; + set maxDate(date: SimpleDate) { this._maxDate = this._locale.parseDate(date); } + private _maxDate: SimpleDate; + + /** A function used to filter which dates are selectable. */ + @Input() dateFilter: (date: SimpleDate) => boolean; + /** Emits when the currently selected date changes. */ @Output() selectedChange = new EventEmitter(); + /** Date filter for the month and year views. */ + _dateFilterForViews = (date: SimpleDate) => { + return !!date && + (!this.dateFilter || this.dateFilter(date)) && + (!this.minDate || date.compare(this.minDate) >= 0) && + (!this.maxDate || date.compare(this.maxDate) <= 0); + } + /** * 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 @@ -50,7 +73,8 @@ export class MdCalendar implements AfterContentInit { */ get _currentPeriod() { return this._normalizedCurrentPeriod; } set _currentPeriod(value: SimpleDate) { - this._normalizedCurrentPeriod = new SimpleDate(value.year, value.month, 1); + const clampedValue = value.clamp(this.minDate, this.maxDate); + this._normalizedCurrentPeriod = new SimpleDate(clampedValue.year, clampedValue.month, 1); } private _normalizedCurrentPeriod: SimpleDate; @@ -106,4 +130,24 @@ export class MdCalendar implements AfterContentInit { let amount = this._monthView ? {months: 1} : {years: 1}; this._currentPeriod = this._currentPeriod.add(amount); } + + /** Whether the previous period button is enabled. */ + _previousEnabled() { + if (!this.minDate) { + return true; + } + return !this.minDate || !this._isSameView(this._currentPeriod, this.minDate); + } + + /** Whether the next period button is enabled. */ + _nextEnabled() { + return !this.maxDate || !this._isSameView(this._currentPeriod, this.maxDate); + } + + /** Whether the two dates represent the same view in the current view mode (month or year). */ + private _isSameView(date1: SimpleDate, date2: SimpleDate) { + return this._monthView ? + date1.year == date2.year && date1.month == date2.month : + date1.year == date2.year; + } } diff --git a/src/lib/datepicker/datepicker.html b/src/lib/datepicker/datepicker.html index bf20f86edd3d..369f064a0ee9 100644 --- a/src/lib/datepicker/datepicker.html +++ b/src/lib/datepicker/datepicker.html @@ -4,6 +4,9 @@ [class.mat-datepicker-touch]="touchUi" [class.mat-datepicker-non-touch]="!touchUi" [startAt]="startAt" + [minDate]="minDate" + [maxDate]="maxDate" + [dateFilter]="dateFilter" [(selected)]="_selected"> diff --git a/src/lib/datepicker/datepicker.ts b/src/lib/datepicker/datepicker.ts index a2f8fb9a94d8..fee217176ba5 100644 --- a/src/lib/datepicker/datepicker.ts +++ b/src/lib/datepicker/datepicker.ts @@ -1,9 +1,11 @@ import { ChangeDetectionStrategy, - Component, EventEmitter, + Component, + EventEmitter, Input, OnDestroy, - Optional, Output, + Optional, + Output, TemplateRef, ViewChild, ViewContainerRef, @@ -64,12 +66,32 @@ export class MdDatepicker implements OnDestroy { @Input() touchUi = false; + /** The minimum selectable date. */ + @Input() + get minDate(): SimpleDate { return this._minDate; }; + set minDate(date: SimpleDate) { this._minDate = this._locale.parseDate(date); } + private _minDate: SimpleDate; + + /** The maximum selectable date. */ + @Input() + get maxDate(): SimpleDate { return this._maxDate; }; + set maxDate(date: SimpleDate) { this._maxDate = this._locale.parseDate(date); } + private _maxDate: SimpleDate; + + /** A function used to filter which dates are selectable. */ + @Input() + dateFilter: (date: SimpleDate) => boolean; + + /** Emits new selected date when selected date changes. */ @Output() selectedChanged = new EventEmitter(); + /** Whether the calendar is open. */ opened = false; + /** The id for the datepicker calendar. */ id = `md-datepicker-${datepickerUid++}`; + /** The currently selected date. */ get _selected(): SimpleDate { return this._datepickerInput ? this._datepickerInput.value : null; } diff --git a/src/lib/datepicker/month-view.spec.ts b/src/lib/datepicker/month-view.spec.ts index 3b19a23401ce..5a3fee271e61 100644 --- a/src/lib/datepicker/month-view.spec.ts +++ b/src/lib/datepicker/month-view.spec.ts @@ -19,6 +19,7 @@ describe('MdMonthView', () => { // Test components. StandardMonthView, + MonthViewWithDateFilter, ], }); @@ -71,6 +72,27 @@ describe('MdMonthView', () => { expect(selectedEl.innerHTML.trim()).toBe('31'); }); }); + + describe('month view with date filter', () => { + let fixture: ComponentFixture; + let testComponent: MonthViewWithDateFilter; + let monthViewNativeElement: Element; + + beforeEach(() => { + fixture = TestBed.createComponent(MonthViewWithDateFilter); + fixture.detectChanges(); + + let monthViewDebugElement = fixture.debugElement.query(By.directive(MdMonthView)); + monthViewNativeElement = monthViewDebugElement.nativeElement; + testComponent = fixture.componentInstance; + }); + + it('should disabled filtered dates', () => { + let cells = monthViewNativeElement.querySelectorAll('.mat-calendar-table-cell'); + expect(cells[0].classList).toContain('mat-calendar-table-disabled'); + expect(cells[1].classList).not.toContain('mat-calendar-table-disabled'); + }); + }); }); @@ -81,3 +103,13 @@ class StandardMonthView { date = new SimpleDate(2017, 0, 5); selected = new SimpleDate(2017, 0, 10); } + + +@Component({ + template: `` +}) +class MonthViewWithDateFilter { + dateFilter(date: SimpleDate) { + return date.date % 2 == 0; + } +} diff --git a/src/lib/datepicker/month-view.ts b/src/lib/datepicker/month-view.ts index 8fb387b9e358..ced1014d9e00 100644 --- a/src/lib/datepicker/month-view.ts +++ b/src/lib/datepicker/month-view.ts @@ -47,6 +47,9 @@ export class MdMonthView implements AfterContentInit { } private _selected: SimpleDate; + /** A function used to filter which dates are selectable. */ + @Input() dateFilter: (date: SimpleDate) => boolean; + /** Emits when a new date is selected. */ @Output() selectedChange = new EventEmitter(); @@ -76,7 +79,7 @@ export class MdMonthView implements AfterContentInit { /** Handles when a new date is selected. */ _dateSelected(date: number) { - if (this.selected && this.selected.date == date) { + if (this._selectedDate == date) { return; } this.selectedChange.emit(new SimpleDate(this.date.year, this.date.month, date)); @@ -104,8 +107,10 @@ export class MdMonthView implements AfterContentInit { this._weeks.push([]); cell = 0; } + let enabled = !this.dateFilter || + this.dateFilter(new SimpleDate(this.date.year, this.date.month, i + 1)); this._weeks[this._weeks.length - 1] - .push(new MdCalendarCell(i + 1, this._locale.dates[i + 1])); + .push(new MdCalendarCell(i + 1, this._locale.dates[i + 1], enabled)); } } diff --git a/src/lib/datepicker/year-view.html b/src/lib/datepicker/year-view.html index c2aa74d8e08c..2e70db4cf0a3 100644 --- a/src/lib/datepicker/year-view.html +++ b/src/lib/datepicker/year-view.html @@ -1,4 +1,5 @@ - { // Test components. StandardYearView, + YearViewWithDateFilter, ], }); @@ -71,6 +72,27 @@ describe('MdYearView', () => { expect(selectedEl.innerHTML.trim()).toBe('DEC'); }); }); + + describe('year view with date filter', () => { + let fixture: ComponentFixture; + let testComponent: YearViewWithDateFilter; + let yearViewNativeElement: Element; + + beforeEach(() => { + fixture = TestBed.createComponent(YearViewWithDateFilter); + fixture.detectChanges(); + + let yearViewDebugElement = fixture.debugElement.query(By.directive(MdYearView)); + yearViewNativeElement = yearViewDebugElement.nativeElement; + testComponent = fixture.componentInstance; + }); + + it('should disabled months with no enabled days', () => { + let cells = yearViewNativeElement.querySelectorAll('.mat-calendar-table-cell'); + expect(cells[0].classList).not.toContain('mat-calendar-table-disabled'); + expect(cells[1].classList).toContain('mat-calendar-table-disabled'); + }); + }); }); @@ -81,3 +103,19 @@ class StandardYearView { date = new SimpleDate(2017, 0, 5); selected = new SimpleDate(2017, 2, 10); } + + +@Component({ + template: `` +}) +class YearViewWithDateFilter { + dateFilter(date: SimpleDate) { + if (date.month == 0) { + return date.date == 10; + } + if (date.month == 1) { + return false; + } + return true; + } +} diff --git a/src/lib/datepicker/year-view.ts b/src/lib/datepicker/year-view.ts index e49ea0b6b4b8..40f5d8f1bbd2 100644 --- a/src/lib/datepicker/year-view.ts +++ b/src/lib/datepicker/year-view.ts @@ -42,6 +42,9 @@ export class MdYearView implements AfterContentInit { } private _selected: SimpleDate; + /** A function used to filter which dates are selectable. */ + @Input() dateFilter: (date: SimpleDate) => boolean; + /** Emits when a new month is selected. */ @Output() selectedChange = new EventEmitter(); @@ -60,11 +63,7 @@ export class MdYearView implements AfterContentInit { */ _selectedMonth: number; - constructor(private _locale: CalendarLocale) { - // First row of months only contains 5 elements so we can fit the year label on the same row. - this._months = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9, 10, 11]].map(row => row.map( - month => this._createCellForMonth(month))); - } + constructor(private _locale: CalendarLocale) {} ngAfterContentInit() { this._init(); @@ -72,9 +71,6 @@ export class MdYearView implements AfterContentInit { /** Handles when a new month is selected. */ _monthSelected(month: number) { - if (this.selected && this.selected.month == month) { - return; - } this.selectedChange.emit(new SimpleDate(this.date.year, month, 1)); } @@ -83,6 +79,10 @@ export class MdYearView implements AfterContentInit { this._selectedMonth = this._getMonthInCurrentYear(this.selected); this._todayMonth = this._getMonthInCurrentYear(SimpleDate.today()); this._yearLabel = this._locale.getCalendarYearHeaderLabel(this._date); + + // First row of months only contains 5 elements so we can fit the year label on the same row. + this._months = [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9, 10, 11]].map(row => row.map( + month => this._createCellForMonth(month))); } /** @@ -95,6 +95,24 @@ export class MdYearView implements AfterContentInit { /** Creates an MdCalendarCell for the given month. */ private _createCellForMonth(month: number) { - return new MdCalendarCell(month, this._locale.shortMonths[month].toLocaleUpperCase()); + return new MdCalendarCell( + month, this._locale.shortMonths[month].toLocaleUpperCase(), this._isMonthEnabled(month)); + } + + /** Whether the given month is enabled. */ + private _isMonthEnabled(month: number) { + if (!this.dateFilter) { + return true; + } + + // If any date in the month is enabled count the month as enabled. + for (let date = new SimpleDate(this.date.year, month, 1); date.month === month; + date = date.add({days: 1})) { + if (this.dateFilter(date)) { + return true; + } + } + + return false; } }