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;
}
}
|