feat(datepicker): merge datepicker branch into master (#4404)

This commit is contained in:
mmalerba
2017-05-12 09:40:51 -07:00
committed by Kara
parent 1ec88e031d
commit 123d7eced4
47 changed files with 4562 additions and 13 deletions
@@ -0,0 +1,65 @@
<h2>Options</h2>
<p>
<md-checkbox [(ngModel)]="touch">Use touch UI</md-checkbox>
<md-checkbox [(ngModel)]="filterOdd">Filter odd months and dates</md-checkbox>
<md-checkbox [(ngModel)]="yearView">Start in year view</md-checkbox>
</p>
<p>
<md-input-container>
<input mdInput [mdDatepicker]="minDatePicker" [(ngModel)]="minDate" placeholder="Min date">
<button mdSuffix [mdDatepickerToggle]="minDatePicker"></button>
</md-input-container>
<md-datepicker #minDatePicker [touchUi]="touch"></md-datepicker>
<md-input-container>
<input mdInput [mdDatepicker]="maxDatePicker" [(ngModel)]="maxDate" placeholder="Max date">
<button mdSuffix [mdDatepickerToggle]="maxDatePicker"></button>
</md-input-container>
<md-datepicker #maxDatePicker [touchUi]="touch"></md-datepicker>
</p>
<p>
<md-input-container>
<input mdInput [mdDatepicker]="startAtPicker" [(ngModel)]="startAt" placeholder="Start at date">
<button mdSuffix [mdDatepickerToggle]="startAtPicker"></button>
</md-input-container>
<md-datepicker #startAtPicker [touchUi]="touch"></md-datepicker>
</p>
<h2>Result</h2>
<p>
<button [mdDatepickerToggle]="resultPicker"></button>
<md-input-container>
<input mdInput
#resultPickerModel="ngModel"
[mdDatepicker]="resultPicker"
[(ngModel)]="date"
[min]="minDate"
[max]="maxDate"
[mdDatepickerFilter]="filterOdd ? dateFilter : null"
placeholder="Pick a date">
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerMin')">Too early!</md-error>
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerMax')">Too late!</md-error>
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerFilter')">Date unavailable!</md-error>
</md-input-container>
<md-datepicker
#resultPicker
[touchUi]="touch"
[startAt]="startAt"
[startView]="yearView ? 'year' : 'month'">
</md-datepicker>
</p>
<p>
<input [mdDatepicker]="resultPicker2"
[(ngModel)]="date"
[min]="minDate"
[max]="maxDate"
[mdDatepickerFilter]="filterOdd ? dateFilter : null"
placeholder="Pick a date">
<button [mdDatepickerToggle]="resultPicker2"></button>
<md-datepicker
#resultPicker2
[touchUi]="touch"
[startAt]="startAt"
[startView]="yearView ? 'year' : 'month'">
</md-datepicker>
</p>
@@ -0,0 +1,3 @@
md-calendar {
width: 300px;
}
@@ -0,0 +1,19 @@
import {Component} from '@angular/core';
@Component({
moduleId: module.id,
selector: 'datepicker-demo',
templateUrl: 'datepicker-demo.html',
styleUrls: ['datepicker-demo.css'],
})
export class DatepickerDemo {
touch: boolean;
filterOdd: boolean;
yearView: boolean;
minDate: Date;
maxDate: Date;
startAt: Date;
date: Date;
dateFilter = (date: Date) => date.getMonth() % 2 == 1 && date.getDate() % 2 == 0;
}
+11 -7
View File
@@ -1,4 +1,4 @@
import {NgModule, ApplicationRef} from '@angular/core';
import {ApplicationRef, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {HttpModule} from '@angular/http';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
@@ -6,14 +6,15 @@ import {RouterModule} from '@angular/router';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {DemoApp, Home} from './demo-app/demo-app';
import {
MaterialModule,
OverlayContainer,
FullscreenOverlayContainer,
MaterialModule,
MdNativeDateModule,
MdSelectionModule,
OverlayContainer
} from '@angular/material';
import {DEMO_APP_ROUTES} from './demo-app/routes';
import {ProgressBarDemo} from './progress-bar/progress-bar-demo';
import {JazzDialog, ContentElementDialog, DialogDemo, IFrameDialog} from './dialog/dialog-demo';
import {ContentElementDialog, DialogDemo, IFrameDialog, JazzDialog} from './dialog/dialog-demo';
import {RippleDemo} from './ripple/ripple-demo';
import {IconDemo} from './icon/icon-demo';
import {GesturesDemo} from './gestures/gestures-demo';
@@ -27,22 +28,23 @@ import {ListDemo} from './list/list-demo';
import {BaselineDemo} from './baseline/baseline-demo';
import {GridListDemo} from './grid-list/grid-list-demo';
import {LiveAnnouncerDemo} from './live-announcer/live-announcer-demo';
import {OverlayDemo, SpagettiPanel, RotiniPanel} from './overlay/overlay-demo';
import {OverlayDemo, RotiniPanel, SpagettiPanel} from './overlay/overlay-demo';
import {SlideToggleDemo} from './slide-toggle/slide-toggle-demo';
import {ToolbarDemo} from './toolbar/toolbar-demo';
import {ButtonDemo} from './button/button-demo';
import {MdCheckboxDemoNestedChecklist, CheckboxDemo} from './checkbox/checkbox-demo';
import {CheckboxDemo, MdCheckboxDemoNestedChecklist} from './checkbox/checkbox-demo';
import {SelectDemo} from './select/select-demo';
import {SliderDemo} from './slider/slider-demo';
import {SidenavDemo} from './sidenav/sidenav-demo';
import {SnackBarDemo} from './snack-bar/snack-bar-demo';
import {PortalDemo, ScienceJoke} from './portal/portal-demo';
import {MenuDemo} from './menu/menu-demo';
import {TabsDemo, SunnyTabContent, RainyTabContent, FoggyTabContent} from './tabs/tabs-demo';
import {FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo} from './tabs/tabs-demo';
import {PlatformDemo} from './platform/platform-demo';
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
import {InputDemo} from './input/input-demo';
import {StyleDemo} from './style/style-demo';
import {DatepickerDemo} from './datepicker/datepicker-demo';
@NgModule({
@@ -54,6 +56,7 @@ import {StyleDemo} from './style/style-demo';
ReactiveFormsModule,
RouterModule.forRoot(DEMO_APP_ROUTES),
MaterialModule,
MdNativeDateModule,
MdSelectionModule,
],
declarations: [
@@ -64,6 +67,7 @@ import {StyleDemo} from './style/style-demo';
CardDemo,
ChipsDemo,
CheckboxDemo,
DatepickerDemo,
DemoApp,
DialogDemo,
GesturesDemo,
+1
View File
@@ -30,6 +30,7 @@ export class DemoApp {
{name: 'Card', route: 'card'},
{name: 'Chips', route: 'chips'},
{name: 'Checkbox', route: 'checkbox'},
{name: 'Datepicker', route: 'datepicker'},
{name: 'Dialog', route: 'dialog'},
{name: 'Gestures', route: 'gestures'},
{name: 'Grid List', route: 'grid-list'},
+2
View File
@@ -32,6 +32,7 @@ import {PlatformDemo} from '../platform/platform-demo';
import {AutocompleteDemo} from '../autocomplete/autocomplete-demo';
import {InputDemo} from '../input/input-demo';
import {StyleDemo} from '../style/style-demo';
import {DatepickerDemo} from '../datepicker/datepicker-demo';
export const DEMO_APP_ROUTES: Routes = [
{path: '', component: Home},
@@ -39,6 +40,7 @@ export const DEMO_APP_ROUTES: Routes = [
{path: 'button', component: ButtonDemo},
{path: 'card', component: CardDemo},
{path: 'chips', component: ChipsDemo},
{path: 'datepicker', component: DatepickerDemo},
{path: 'radio', component: RadioDemo},
{path: 'select', component: SelectDemo},
{path: 'sidenav', component: SidenavDemo},
+2
View File
@@ -103,6 +103,8 @@ export {CompatibilityModule, NoConflictStyleCompatibilityMode} from './compatibi
// Common material module
export {MdCommonModule} from './common-behaviors/common-module';
// Datetime
export * from './datetime/index';
@NgModule({
imports: [
+200
View File
@@ -0,0 +1,200 @@
/** Adapts type `D` to be usable as a date by cdk-based components that work with dates. */
export abstract class DateAdapter<D> {
/** The locale to use for all dates. */
protected locale: any;
/**
* Gets the year component of the given date.
* @param date The date to extract the year from.
* @returns The year component.
*/
abstract getYear(date: D): number;
/**
* Gets the month component of the given date.
* @param date The date to extract the month from.
* @returns The month component (0-indexed, 0 = January).
*/
abstract getMonth(date: D): number;
/**
* Gets the date of the month component of the given date.
* @param date The date to extract the date of the month from.
* @returns The month component (1-indexed, 1 = first of month).
*/
abstract getDate(date: D): number;
/**
* Gets the day of the week component of the given date.
* @param date The date to extract the day of the week from.
* @returns The month component (0-indexed, 0 = Sunday).
*/
abstract getDayOfWeek(date: D): number;
/**
* Gets a list of names for the months.
* @param style The naming style (e.g. long = 'January', short = 'Jan', narrow = 'J').
* @returns An ordered list of all month names, starting with January.
*/
abstract getMonthNames(style: 'long' | 'short' | 'narrow'): string[];
/**
* Gets a list of names for the dates of the month.
* @returns An ordered list of all date of the month names, starting with '1'.
*/
abstract getDateNames(): string[];
/**
* Gets a list of names for the days of the week.
* @param style The naming style (e.g. long = 'Sunday', short = 'Sun', narrow = 'S').
* @returns An ordered list of all weekday names, starting with Sunday.
*/
abstract getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[];
/**
* Gets the name for the year of the given date.
* @param date The date to get the year name for.
* @returns The name of the given year (e.g. '2017').
*/
abstract getYearName(date: D): string;
/**
* Gets the first day of the week.
* @returns The first day of the week (0-indexed, 0 = Sunday).
*/
abstract getFirstDayOfWeek(): number;
/**
* Gets the number of days in the month of the given date.
* @param date The date whose month should be checked.
* @returns The number of days in the month of the given date.
*/
abstract getNumDaysInMonth(date: D): number;
/**
* Clones the given date.
* @param date The date to clone
* @returns A new date equal to the given date.
*/
abstract clone(date: D): D;
/**
* Creates a date with the given year, month, and date. Does not allow over/under-flow of the
* month and date.
* @param year The full year of the date. (e.g. 89 means the year 89, not the year 1989).
* @param month The month of the date (0-indexed, 0 = January). Must be an integer 0 - 11.
* @param date The date of month of the date. Must be an integer 1 - length of the given month.
* @returns The new date, or null if invalid.
*/
abstract createDate(year: number, month: number, date: number): D;
/**
* Gets today's date.
* @returns Today's date.
*/
abstract today(): D;
/**
* Parses a date from a value.
* @param value The value to parse.
* @param parseFormat The expected format of the value being parsed
* (type is implementation-dependent).
* @returns The parsed date, or null if date could not be parsed.
*/
abstract parse(value: any, parseFormat: any): D | null;
/**
* Formats a date as a string.
* @param date The value to parse.
* @param displayFormat The format to use to display the date as a string.
* @returns The parsed date, or null if date could not be parsed.
*/
abstract format(date: D, displayFormat: any): string;
/**
* Adds the given number of years to the date. Years are counted as if flipping 12 pages on the
* calendar for each year and then finding the closest date in the new month. For example when
* adding 1 year to Feb 29, 2016, the resulting date will be Feb 28, 2017.
* @param date The date to add years to.
* @param years The number of years to add (may be negative).
* @returns A new date equal to the given one with the specified number of years added.
*/
abstract addCalendarYears(date: D, years: number): D;
/**
* Adds the given number of months to the date. Months are counted as if flipping a page on the
* calendar for each month and then finding the closest date in the new month. For example when
* adding 1 month to Jan 31, 2017, the resulting date will be Feb 28, 2017.
* @param date The date to add months to.
* @param months The number of months to add (may be negative).
* @returns A new date equal to the given one with the specified number of months added.
*/
abstract addCalendarMonths(date: D, months: number): D;
/**
* Adds the given number of days to the date. Days are counted as if moving one cell on the
* calendar for each day.
* @param date The date to add days to.
* @param days The number of days to add (may be negative).
* @returns A new date equal to the given one with the specified number of days added.
*/
abstract addCalendarDays(date: D, days: number): D;
/**
* Gets the RFC 3339 compatible date string (https://tools.ietf.org/html/rfc3339) for the given
* date.
* @param date The date to get the ISO date string for.
* @returns The ISO date string date string.
*/
abstract getISODateString(date: D): string;
/**
* Sets the locale used for all dates.
* @param locale The new locale.
*/
setLocale(locale: any) {
this.locale = locale;
}
/**
* Compares two dates.
* @param first The first date to compare.
* @param second The second date to compare.
* @returns 0 if the dates are equal, a number less than 0 if the first date is earlier,
* a number greater than 0 if the first date is later.
*/
compareDate(first: D, second: D): number {
return this.getYear(first) - this.getYear(second) ||
this.getMonth(first) - this.getMonth(second) ||
this.getDate(first) - this.getDate(second);
}
/**
* Checks if two dates are equal.
* @param first The first date to check.
* @param second The second date to check.
* @returns {boolean} Whether the two dates are equal.
* Null dates are considered equal to other null dates.
*/
sameDate(first: D | null, second: D | null): boolean {
return first && second ? !this.compareDate(first, second) : first == second;
}
/**
* Clamp the given date between min and max dates.
* @param date The date to clamp.
* @param min The minimum value to allow. If null or omitted no min is enforced.
* @param max The maximum value to allow. If null or omitted no max is enforced.
* @returns `min` if `date` is less than `min`, `max` if date is greater than `max`,
* otherwise `date`.
*/
clampDate(date: D, min?: D | null, max?: D | null): D {
if (min && this.compareDate(date, min) < 0) {
return min;
}
if (max && this.compareDate(date, max) > 0) {
return max;
}
return date;
}
}
+17
View File
@@ -0,0 +1,17 @@
import {InjectionToken} from '@angular/core';
export type MdDateFormats = {
parse: {
dateInput: any
},
display: {
dateInput: any,
monthYearLabel: any,
dateA11yLabel: any,
monthYearA11yLabel: any,
}
};
export const MD_DATE_FORMATS = new InjectionToken<MdDateFormats>('md-date-formats');
+22
View File
@@ -0,0 +1,22 @@
import {NgModule} from '@angular/core';
import {DateAdapter} from './date-adapter';
import {NativeDateAdapter} from './native-date-adapter';
import {MD_DATE_FORMATS} from './date-formats';
import {MD_NATIVE_DATE_FORMATS} from './native-date-formats';
export * from './date-adapter';
export * from './native-date-adapter';
@NgModule({
providers: [{provide: DateAdapter, useClass: NativeDateAdapter}],
})
export class NativeDateModule {}
@NgModule({
imports: [NativeDateModule],
providers: [{provide: MD_DATE_FORMATS, useValue: MD_NATIVE_DATE_FORMATS}],
})
export class MdNativeDateModule {}
@@ -0,0 +1,299 @@
import {NativeDateAdapter} from './native-date-adapter';
import {Platform} from '../platform/index';
const SUPPORTS_INTL = typeof Intl != 'undefined';
// When constructing a Date, the month is zero-based. This can be confusing, since people are
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
NOV = 10, DEC = 11;
describe('NativeDateAdapter', () => {
let adapter;
let platform;
beforeEach(() => {
adapter = new NativeDateAdapter();
platform = new Platform();
});
it('should get year', () => {
expect(adapter.getYear(new Date(2017, JAN, 1))).toBe(2017);
});
it('should get month', () => {
expect(adapter.getMonth(new Date(2017, JAN, 1))).toBe(0);
});
it('should get date', () => {
expect(adapter.getDate(new Date(2017, JAN, 1))).toBe(1);
});
it('should get day of week', () => {
expect(adapter.getDayOfWeek(new Date(2017, JAN, 1))).toBe(0);
});
it('should get long month names', () => {
expect(adapter.getMonthNames('long')).toEqual([
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'
]);
});
it('should get long month names', () => {
expect(adapter.getMonthNames('short')).toEqual([
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]);
});
it('should get narrow month names', () => {
// Edge & IE use same value for short and narrow.
if (platform.EDGE || platform.TRIDENT) {
expect(adapter.getMonthNames('narrow')).toEqual([
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]);
} else {
expect(adapter.getMonthNames('narrow')).toEqual([
'J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'
]);
}
});
it('should get month names in a different locale', () => {
adapter.setLocale('ja-JP');
if (SUPPORTS_INTL) {
expect(adapter.getMonthNames('long')).toEqual([
'1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'
]);
} else {
expect(adapter.getMonthNames('long')).toEqual([
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'
]);
}
});
it('should get date names', () => {
expect(adapter.getDateNames()).toEqual([
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17',
'18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31'
]);
});
it('should get date names in a different locale', () => {
adapter.setLocale('ja-JP');
if (SUPPORTS_INTL) {
expect(adapter.getDateNames()).toEqual([
'1日', '2日', '3日', '4日', '5日', '6日', '7日', '8日', '9日', '10日', '11日', '12日',
'13日', '14日', '15日', '16日', '17日', '18日', '19日', '20日', '21日', '22日', '23日', '24日',
'25日', '26日', '27日', '28日', '29日', '30日', '31日'
]);
} else {
expect(adapter.getDateNames()).toEqual([
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17',
'18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31'
]);
}
});
it('should get long day of week names', () => {
expect(adapter.getDayOfWeekNames('long')).toEqual([
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
]);
});
it('should get short day of week names', () => {
expect(adapter.getDayOfWeekNames('short')).toEqual([
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
]);
});
it('should get narrow day of week names', () => {
// Edge & IE use two-letter narrow days.
if (platform.EDGE || platform.TRIDENT) {
expect(adapter.getDayOfWeekNames('narrow')).toEqual([
'Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'
]);
} else {
expect(adapter.getDayOfWeekNames('narrow')).toEqual([
'S', 'M', 'T', 'W', 'T', 'F', 'S'
]);
}
});
it('should get day of week names in a different locale', () => {
adapter.setLocale('ja-JP');
if (SUPPORTS_INTL) {
expect(adapter.getDayOfWeekNames('long')).toEqual([
'日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'
]);
} else {
expect(adapter.getDayOfWeekNames('long')).toEqual([
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
]);
}
});
it('should get year name', () => {
expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017');
});
it('should get year name in a different locale', () => {
adapter.setLocale('ja-JP');
if (SUPPORTS_INTL) {
expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017年');
} else {
expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017');
}
});
it('should get first day of week', () => {
expect(adapter.getFirstDayOfWeek()).toBe(0);
});
it('should create Date', () => {
expect(adapter.createDate(2017, JAN, 1)).toEqual(new Date(2017, JAN, 1));
});
it('should not create Date with month over/under-flow', () => {
expect(adapter.createDate(2017, DEC + 1, 1)).toBeNull();
expect(adapter.createDate(2017, JAN - 1, 1)).toBeNull();
});
it('should not create Date with date over/under-flow', () => {
expect(adapter.createDate(2017, JAN, 32)).toBeNull();
expect(adapter.createDate(2017, JAN, 0)).toBeNull();
});
it('should create Date with low year number', () => {
expect(adapter.createDate(-1, JAN, 1).getFullYear()).toBe(-1);
expect(adapter.createDate(0, JAN, 1).getFullYear()).toBe(0);
expect(adapter.createDate(50, JAN, 1).getFullYear()).toBe(50);
expect(adapter.createDate(99, JAN, 1).getFullYear()).toBe(99);
expect(adapter.createDate(100, JAN, 1).getFullYear()).toBe(100);
});
it("should get today's date", () => {
expect(adapter.sameDate(adapter.today(), new Date()))
.toBe(true, "should be equal to today's date");
});
it('should parse string', () => {
expect(adapter.parse('1/1/2017')).toEqual(new Date(2017, JAN, 1));
});
it('should parse number', () => {
let timestamp = new Date().getTime();
expect(adapter.parse(timestamp)).toEqual(new Date(timestamp));
});
it ('should parse Date', () => {
let date = new Date(2017, JAN, 1);
expect(adapter.parse(date)).toEqual(date);
expect(adapter.parse(date)).not.toBe(date);
});
it('should parse invalid value as null', () => {
expect(adapter.parse('hello')).toBeNull();
});
it('should format', () => {
if (SUPPORTS_INTL) {
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('1/1/2017');
} else {
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('Sun Jan 01 2017');
}
});
it('should format with custom format', () => {
if (SUPPORTS_INTL) {
expect(adapter.format(new Date(2017, JAN, 1), {
year: 'numeric',
month: 'long',
day: 'numeric'
})).toEqual('January 1, 2017');
} else {
expect(adapter.format(new Date(2017, JAN, 1), {
year: 'numeric',
month: 'long',
day: 'numeric'
})).toEqual('Sun Jan 01 2017');
}
});
it('should format with a different locale', () => {
adapter.setLocale('ja-JP');
if (SUPPORTS_INTL) {
// Edge & IE use a different format in Japanese.
if (platform.EDGE || platform.TRIDENT) {
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('2017年1月1日');
} else {
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('2017/1/1');
}
} else {
expect(adapter.format(new Date(2017, JAN, 1))).toEqual('Sun Jan 01 2017');
}
});
it('should add years', () => {
expect(adapter.addCalendarYears(new Date(2017, JAN, 1), 1)).toEqual(new Date(2018, JAN, 1));
expect(adapter.addCalendarYears(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, JAN, 1));
});
it('should respect leap years when adding years', () => {
expect(adapter.addCalendarYears(new Date(2016, FEB, 29), 1)).toEqual(new Date(2017, FEB, 28));
expect(adapter.addCalendarYears(new Date(2016, FEB, 29), -1)).toEqual(new Date(2015, FEB, 28));
});
it('should add months', () => {
expect(adapter.addCalendarMonths(new Date(2017, JAN, 1), 1)).toEqual(new Date(2017, FEB, 1));
expect(adapter.addCalendarMonths(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, DEC, 1));
});
it('should respect month length differences when adding months', () => {
expect(adapter.addCalendarMonths(new Date(2017, JAN, 31), 1)).toEqual(new Date(2017, FEB, 28));
expect(adapter.addCalendarMonths(new Date(2017, MAR, 31), -1)).toEqual(new Date(2017, FEB, 28));
});
it('should add days', () => {
expect(adapter.addCalendarDays(new Date(2017, JAN, 1), 1)).toEqual(new Date(2017, JAN, 2));
expect(adapter.addCalendarDays(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, DEC, 31));
});
it('should clone', () => {
let date = new Date(2017, JAN, 1);
expect(adapter.clone(date)).toEqual(date);
expect(adapter.clone(date)).not.toBe(date);
});
it('should compare dates', () => {
expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, JAN, 2))).toBeLessThan(0);
expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, FEB, 1))).toBeLessThan(0);
expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2018, JAN, 1))).toBeLessThan(0);
expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, JAN, 1))).toBe(0);
expect(adapter.compareDate(new Date(2018, JAN, 1), new Date(2017, JAN, 1))).toBeGreaterThan(0);
expect(adapter.compareDate(new Date(2017, FEB, 1), new Date(2017, JAN, 1))).toBeGreaterThan(0);
expect(adapter.compareDate(new Date(2017, JAN, 2), new Date(2017, JAN, 1))).toBeGreaterThan(0);
});
it('should clamp date at lower bound', () => {
expect(adapter.clampDate(
new Date(2017, JAN, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1)))
.toEqual(new Date(2018, JAN, 1));
});
it('should clamp date at upper bound', () => {
expect(adapter.clampDate(
new Date(2020, JAN, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1)))
.toEqual(new Date(2019, JAN, 1));
});
it('should clamp date already within bounds', () => {
expect(adapter.clampDate(
new Date(2018, FEB, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1)))
.toEqual(new Date(2018, FEB, 1));
});
});
@@ -0,0 +1,204 @@
import {DateAdapter} from './date-adapter';
// TODO(mmalerba): Remove when we no longer support safari 9.
/** Whether the browser supports the Intl API. */
const SUPPORTS_INTL_API = typeof Intl != 'undefined';
/** The default month names to use if Intl API is not available. */
const DEFAULT_MONTH_NAMES = {
'long': [
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December'
],
'short': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
'narrow': ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D']
};
/** The default date names to use if Intl API is not available. */
const DEFAULT_DATE_NAMES = range(31, i => String(i + 1));
/** The default day of the week names to use if Intl API is not available. */
const DEFAULT_DAY_OF_WEEK_NAMES = {
'long': ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
'short': ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
'narrow': ['S', 'M', 'T', 'W', 'T', 'F', 'S']
};
/** Creates an array and fills it with values. */
function range<T>(length: number, valueFunction: (index: number) => T): T[] {
return Array.apply(null, Array(length)).map((v: undefined, i: number) => valueFunction(i));
}
/** Adapts the native JS Date for use with cdk-based components that work with dates. */
export class NativeDateAdapter extends DateAdapter<Date> {
getYear(date: Date): number {
return date.getFullYear();
}
getMonth(date: Date): number {
return date.getMonth();
}
getDate(date: Date): number {
return date.getDate();
}
getDayOfWeek(date: Date): number {
return date.getDay();
}
getMonthNames(style: 'long' | 'short' | 'narrow'): string[] {
if (SUPPORTS_INTL_API) {
let dtf = new Intl.DateTimeFormat(this.locale, {month: style});
return range(12, i => this._stripDirectionalityCharacters(dtf.format(new Date(2017, i, 1))));
}
return DEFAULT_MONTH_NAMES[style];
}
getDateNames(): string[] {
if (SUPPORTS_INTL_API) {
let dtf = new Intl.DateTimeFormat(this.locale, {day: 'numeric'});
return range(31, i => this._stripDirectionalityCharacters(
dtf.format(new Date(2017, 0, i + 1))));
}
return DEFAULT_DATE_NAMES;
}
getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {
if (SUPPORTS_INTL_API) {
let dtf = new Intl.DateTimeFormat(this.locale, {weekday: style});
return range(7, i => this._stripDirectionalityCharacters(
dtf.format(new Date(2017, 0, i + 1))));
}
return DEFAULT_DAY_OF_WEEK_NAMES[style];
}
getYearName(date: Date): string {
if (SUPPORTS_INTL_API) {
let dtf = new Intl.DateTimeFormat(this.locale, {year: 'numeric'});
return this._stripDirectionalityCharacters(dtf.format(date));
}
return String(this.getYear(date));
}
getFirstDayOfWeek(): number {
// We can't tell using native JS Date what the first day of the week is, we default to Sunday.
return 0;
}
getNumDaysInMonth(date: Date): number {
return this.getDate(this._createDateWithOverflow(
this.getYear(date), this.getMonth(date) + 1, 0));
}
clone(date: Date): Date {
return this.createDate(this.getYear(date), this.getMonth(date), this.getDate(date));
}
createDate(year: number, month: number, date: number): Date {
// Check for invalid month and date (except upper bound on date which we have to check after
// creating the Date).
if (month < 0 || month > 11 || date < 1) {
return null;
}
let result = this._createDateWithOverflow(year, month, date);
// Check that the date wasn't above the upper bound for the month, causing the month to
// overflow.
if (result.getMonth() != month) {
return null;
}
return result;
}
today(): Date {
return new Date();
}
parse(value: any, parseFormat: Object): Date | null {
// We have no way using the native JS Date to set the parse format or locale, so we ignore these
// parameters.
let timestamp = typeof value == 'number' ? value : Date.parse(value);
return isNaN(timestamp) ? null : new Date(timestamp);
}
format(date: Date, displayFormat: Object): string {
if (SUPPORTS_INTL_API) {
let dtf = new Intl.DateTimeFormat(this.locale, displayFormat);
return this._stripDirectionalityCharacters(dtf.format(date));
}
return this._stripDirectionalityCharacters(date.toDateString());
}
addCalendarYears(date: Date, years: number): Date {
return this.addCalendarMonths(date, years * 12);
}
addCalendarMonths(date: Date, months: number): Date {
let newDate = this._createDateWithOverflow(
this.getYear(date), this.getMonth(date) + months, this.getDate(date));
// It's possible to wind up in the wrong month if the original month has more days than the new
// month. In this case we want to go to the last day of the desired month.
// Note: the additional + 12 % 12 ensures we end up with a positive number, since JS % doesn't
// guarantee this.
if (this.getMonth(newDate) != ((this.getMonth(date) + months) % 12 + 12) % 12) {
newDate = this._createDateWithOverflow(this.getYear(newDate), this.getMonth(newDate), 0);
}
return newDate;
}
addCalendarDays(date: Date, days: number): Date {
return this._createDateWithOverflow(
this.getYear(date), this.getMonth(date), this.getDate(date) + days);
}
getISODateString(date: Date): string {
return [
date.getUTCFullYear(),
this._2digit(date.getUTCMonth() + 1),
this._2digit(date.getUTCDate())
].join('-');
}
/** Creates a date but allows the month and date to overflow. */
private _createDateWithOverflow(year: number, month: number, date: number) {
let result = new Date(year, month, date);
// We need to correct for the fact that JS native Date treats years in range [0, 99] as
// abbreviations for 19xx.
if (year >= 0 && year < 100) {
result.setFullYear(this.getYear(result) - 1900);
}
return result;
}
/**
* Pads a number to make it two digits.
* @param n The number to pad.
* @returns The padded number.
*/
private _2digit(n: number) {
return ('00' + n).slice(-2);
}
/**
* Strip out unicode LTR and RTL characters. Edge and IE insert these into formatted dates while
* other browsers do not. We remove them to make output consistent and because they interfere with
* date parsing.
* @param s The string to strip direction characters from.
* @returns The stripped string.
*/
private _stripDirectionalityCharacters(s: string) {
return s.replace(/[\u200e\u200f]/g, '');
}
}
@@ -0,0 +1,14 @@
import {MdDateFormats} from './date-formats';
export const MD_NATIVE_DATE_FORMATS: MdDateFormats = {
parse: {
dateInput: null,
},
display: {
dateInput: {year: 'numeric', month: 'numeric', day: 'numeric'},
monthYearLabel: {year: 'numeric', month: 'short'},
dateA11yLabel: {year: 'numeric', month: 'long', day: 'numeric'},
monthYearA11yLabel: {year: 'numeric', month: 'long'},
}
};
+2
View File
@@ -6,6 +6,7 @@
@import '../../card/card-theme';
@import '../../checkbox/checkbox-theme';
@import '../../chips/chips-theme';
@import '../../datepicker/datepicker-theme';
@import '../../dialog/dialog-theme';
@import '../../grid-list/grid-list-theme';
@import '../../icon/icon-theme';
@@ -33,6 +34,7 @@
@include mat-card-theme($theme);
@include mat-checkbox-theme($theme);
@include mat-chips-theme($theme);
@include mat-datepicker-theme($theme);
@include mat-dialog-theme($theme);
@include mat-grid-list-theme($theme);
@include mat-icon-theme($theme);
+1
View File
@@ -0,0 +1 @@
Please see the official documentation at https://material.angular.io/components/component/datepicker
+80
View File
@@ -0,0 +1,80 @@
@import '../core/theming/palette';
@import '../core/theming/theming';
@mixin mat-datepicker-theme($theme) {
$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);
}
.mat-calendar-arrow {
border-top-color: mat-color($foreground, icon);
}
.mat-calendar-next-button,
.mat-calendar-previous-button {
color: mat-color($foreground, icon);
}
.mat-calendar-table-header {
color: mat-color($foreground, hint-text);
}
.mat-calendar-table-header-divider::after {
background: mat-color($foreground, divider);
}
.mat-calendar-body-label {
color: mat-color($foreground, secondary-text);
}
.mat-calendar-body-cell-content {
color: mat-color($foreground, text);
border-color: transparent;
.mat-calendar-body-disabled > &:not(.mat-calendar-body-selected) {
color: mat-color($foreground, disabled-text);
}
}
:not(.mat-calendar-body-disabled):hover,
.cdk-keyboard-focused .mat-calendar-body-active {
& > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected) {
background-color: mat-color($background, hover);
}
}
.mat-calendar-body-selected {
background-color: mat-color($primary);
color: mat-color($primary, default-contrast);
}
.mat-calendar-body-disabled > .mat-calendar-body-selected {
background-color: fade-out(mat-color($primary), $mat-datepicker-selected-fade-amount);
}
.mat-calendar-body-today {
&:not(.mat-calendar-body-selected) {
// 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-body-selected {
box-shadow: inset 0 0 0 $mat-datepicker-selected-today-box-shadow-width
mat-color($primary, default-contrast);
}
}
.mat-calendar-body-disabled > .mat-calendar-body-today:not(.mat-calendar-body-selected) {
border-color: fade-out(mat-color($foreground, hint-text), $mat-datepicker-today-fade-amount);
}
}
+35
View File
@@ -0,0 +1,35 @@
<!--
If there's not enough space in the first row, create a separate label row. We mark this row as
aria-hidden because we don't want it to be read out as one of the weeks in the month.
-->
<tr *ngIf="_firstRowOffset < labelMinRequiredCells" aria-hidden="true">
<td class="mat-calendar-body-label" [attr.colspan]="numCols" >{{label}}</td>
</tr>
<!-- Create the first row separately so we can include a special spacer cell. -->
<tr *ngFor="let row of rows; let rowIndex = index" role="row">
<!--
We mark this cell as aria-hidden so it doesn't get read out as one of the days in the week.
-->
<td *ngIf="rowIndex === 0 && _firstRowOffset"
aria-hidden="true"
class="mat-calendar-body-label"
[attr.colspan]="_firstRowOffset">
{{_firstRowOffset >= labelMinRequiredCells ? label : ''}}
</td>
<td *ngFor="let item of row; let colIndex = index"
role="gridcell"
class="mat-calendar-body-cell"
[tabindex]="_isActiveCell(rowIndex, colIndex) ? 0 : -1"
[class.mat-calendar-body-disabled]="!item.enabled"
[class.mat-calendar-body-active]="_isActiveCell(rowIndex, colIndex)"
[attr.aria-label]="item.ariaLabel"
[attr.aria-disabled]="!item.enabled || null"
(click)="_cellClicked(item)">
<div class="mat-calendar-body-cell-content"
[class.mat-calendar-body-selected]="selectedValue === item.value"
[class.mat-calendar-body-today]="todayValue === item.value">
{{item.displayValue}}
</div>
</td>
</tr>
+65
View File
@@ -0,0 +1,65 @@
$mat-calendar-body-font-size: 13px !default;
$mat-calendar-body-header-font-size: 14px !default;
$mat-calendar-body-label-padding-start: 5% !default;
$mat-calendar-body-label-translation: -6px !default;
$mat-calendar-body-cell-min-size: 32px !default;
$mat-calendar-body-cell-size: 100% / 7 !default;
$mat-calendar-body-cell-content-margin: 5% !default;
$mat-calendar-body-cell-content-border-width: 1px !default;
$mat-calendar-body-min-size: 7 * $mat-calendar-body-cell-min-size !default;
$mat-calendar-body-cell-padding: $mat-calendar-body-cell-size / 2 !default;
$mat-calendar-body-cell-content-size: 100% - $mat-calendar-body-cell-content-margin * 2 !default;
.mat-calendar-body {
font-size: $mat-calendar-body-font-size;
min-width: $mat-calendar-body-min-size;
}
.mat-calendar-body-label {
padding: $mat-calendar-body-cell-padding 0
$mat-calendar-body-cell-padding $mat-calendar-body-cell-padding;
height: 0;
line-height: 0;
transform: translateX($mat-calendar-body-label-translation);
text-align: left;
font-size: $mat-calendar-body-header-font-size;
font-weight: bold;
}
.mat-calendar-body-cell {
position: relative;
width: $mat-calendar-body-cell-size;
height: 0;
line-height: 0;
padding: $mat-calendar-body-cell-padding 0;
text-align: center;
outline: none;
}
.mat-calendar-body-cell-content {
position: absolute;
top: $mat-calendar-body-cell-content-margin;
left: $mat-calendar-body-cell-content-margin;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: $mat-calendar-body-cell-content-size;
height: $mat-calendar-body-cell-content-size;
border-width: $mat-calendar-body-cell-content-border-width;
border-style: solid;
border-radius: 50%;
}
[dir='rtl'] {
.mat-calendar-body-label {
padding: 0 $mat-calendar-body-cell-padding 0 0;
transform: translateX(-$mat-calendar-body-label-translation);
text-align: right;
}
}
+175
View File
@@ -0,0 +1,175 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {MdCalendarBody, MdCalendarCell} from './calendar-body';
import {By} from '@angular/platform-browser';
describe('MdCalendarBody', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
MdCalendarBody,
// Test components.
StandardCalendarBody,
CalendarBodyWithDisabledCells,
],
});
TestBed.compileComponents();
}));
describe('standard calendar body', () => {
let fixture: ComponentFixture<StandardCalendarBody>;
let testComponent: StandardCalendarBody;
let calendarBodyNativeElement: Element;
let rowEls: NodeListOf<Element>;
let labelEls: NodeListOf<Element>;
let cellEls: NodeListOf<Element>;
let refreshElementLists = () => {
rowEls = calendarBodyNativeElement.querySelectorAll('tr');
labelEls = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-label');
cellEls = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-cell');
};
beforeEach(() => {
fixture = TestBed.createComponent(StandardCalendarBody);
fixture.detectChanges();
let calendarBodyDebugElement = fixture.debugElement.query(By.directive(MdCalendarBody));
calendarBodyNativeElement = calendarBodyDebugElement.nativeElement;
testComponent = fixture.componentInstance;
refreshElementLists();
});
it('creates body', () => {
expect(rowEls.length).toBe(3);
expect(labelEls.length).toBe(1);
expect(cellEls.length).toBe(14);
});
it('highlights today', () => {
let todayCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-today');
expect(todayCell).not.toBeNull();
expect(todayCell.innerHTML.trim()).toBe('3');
});
it('highlights selected', () => {
let selectedCell = calendarBodyNativeElement.querySelector('.mat-calendar-body-selected');
expect(selectedCell).not.toBeNull();
expect(selectedCell.innerHTML.trim()).toBe('4');
});
it('places label in first row if space is available', () => {
testComponent.rows[0] = testComponent.rows[0].slice(3);
testComponent.rows = testComponent.rows.slice();
fixture.detectChanges();
refreshElementLists();
expect(rowEls.length).toBe(2);
expect(labelEls.length).toBe(1);
expect(cellEls.length).toBe(11);
expect(rowEls[0].firstElementChild.classList)
.toContain('mat-calendar-body-label', 'first cell should be the label');
expect(labelEls[0].getAttribute('colspan')).toBe('3');
});
it('cell should be selected on click', () => {
let todayElement =
calendarBodyNativeElement.querySelector('.mat-calendar-body-today') as HTMLElement;
todayElement.click();
fixture.detectChanges();
expect(todayElement.classList)
.toContain('mat-calendar-body-selected', 'today should be selected');
});
it('should mark active date', () => {
expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11');
expect(cellEls[10].classList).toContain('mat-calendar-body-active');
});
});
describe('calendar body with disabled cells', () => {
let fixture: ComponentFixture<CalendarBodyWithDisabledCells>;
let testComponent: CalendarBodyWithDisabledCells;
let calendarBodyNativeElement: Element;
let cellEls: NodeListOf<Element>;
beforeEach(() => {
fixture = TestBed.createComponent(CalendarBodyWithDisabledCells);
fixture.detectChanges();
let calendarBodyDebugElement = fixture.debugElement.query(By.directive(MdCalendarBody));
calendarBodyNativeElement = calendarBodyDebugElement.nativeElement;
testComponent = fixture.componentInstance;
cellEls = calendarBodyNativeElement.querySelectorAll('.mat-calendar-body-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);
});
});
});
@Component({
template: `<table md-calendar-body
[label]="label"
[rows]="rows"
[todayValue]="todayValue"
[selectedValue]="selectedValue"
[labelMinRequiredCells]="labelMinRequiredCells"
[numCols]="numCols"
[activeCell]="10"
(selectedValueChange)="onSelect($event)">
</table>`,
})
class StandardCalendarBody {
label = 'Jan 2017';
rows = [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14]].map(r => r.map(createCell));
todayValue = 3;
selectedValue = 4;
labelMinRequiredCells = 3;
numCols = 7;
onSelect(value: number) {
this.selectedValue = value;
}
}
@Component({
template: `<table md-calendar-body
[rows]="rows"
[allowDisabledSelection]="allowDisabledSelection"
(selectedValueChange)="selected = $event">
</table>`
})
class CalendarBodyWithDisabledCells {
rows = [[1, 2, 3, 4]].map(r => r.map(d => {
let cell = createCell(d);
cell.enabled = d % 2 == 0;
return cell;
}));
allowDisabledSelection = false;
selected: Date;
}
function createCell(value: number) {
return new MdCalendarCell(value, `${value}`, `${value}-label`, true);
}
+89
View File
@@ -0,0 +1,89 @@
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
ViewEncapsulation
} from '@angular/core';
/**
* An internal class that represents the data corresponding to a single calendar cell.
* @docs-private
*/
export class MdCalendarCell {
constructor(public value: number,
public displayValue: string,
public ariaLabel: string,
public enabled: boolean) {}
}
/**
* An internal component used to display calendar data in a table.
* @docs-private
*/
@Component({
moduleId: module.id,
selector: '[md-calendar-body]',
templateUrl: 'calendar-body.html',
styleUrls: ['calendar-body.css'],
host: {
'class': 'mat-calendar-body',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdCalendarBody {
/** The label for the table. (e.g. "Jan 2017"). */
@Input() label: string;
/** The cells to display in the table. */
@Input() rows: MdCalendarCell[][];
/** The value in the table that corresponds to today. */
@Input() todayValue: number;
/** The value in the table that is currently selected. */
@Input() selectedValue: number;
/** The minimum number of free cells needed to fit the label in the first row. */
@Input() labelMinRequiredCells: number;
/** The number of columns in the table. */
@Input() numCols = 7;
/** Whether to allow selection of disabled cells. */
@Input() allowDisabledSelection = false;
/** The cell number of the active cell in the table. */
@Input() activeCell = 0;
/** Emits when a new value is selected. */
@Output() selectedValueChange = new EventEmitter<number>();
_cellClicked(cell: MdCalendarCell): void {
if (!this.allowDisabledSelection && !cell.enabled) {
return;
}
this.selectedValueChange.emit(cell.value);
}
/** The number of blank cells to put at the beginning for the first row. */
get _firstRowOffset(): number {
return this.rows && this.rows.length && this.rows[0].length ?
this.numCols - this.rows[0].length : 0;
}
_isActiveCell(rowIndex: number, colIndex: number): boolean {
let cellNumber = rowIndex * this.numCols + colIndex;
// Account for the fact that the first row may not have as many cells.
if (rowIndex) {
cellNumber -= this._firstRowOffset;
}
return cellNumber == this.activeCell;
}
}
+35
View File
@@ -0,0 +1,35 @@
<div class="mat-calendar-header">
<div class="mat-calendar-controls">
<button md-button class="mat-calendar-period-button" (click)="_currentPeriodClicked()"
[attr.aria-label]="_periodButtonLabel">
{{_periodButtonText}}
<div class="mat-calendar-arrow" [class.mat-calendar-invert]="!_monthView"></div>
</button>
<div class="mat-calendar-spacer"></div>
<button md-icon-button class="mat-calendar-previous-button" [disabled]="!_previousEnabled()"
(click)="_previousClicked()" [attr.aria-label]="_prevButtonLabel">
</button>
<button md-icon-button class="mat-calendar-next-button" [disabled]="!_nextEnabled()"
(click)="_nextClicked()" [attr.aria-label]="_nextButtonLabel">
</button>
</div>
</div>
<div class="mat-calendar-content" (keydown)="_handleCalendarBodyKeydown($event)"
cdkMonitorSubtreeFocus>
<md-month-view
*ngIf="_monthView"
[activeDate]="_activeDate"
[selected]="selected"
[dateFilter]="_dateFilterForViews"
(selectedChange)="_dateSelected($event)">
</md-month-view>
<md-year-view
*ngIf="!_monthView"
[activeDate]="_activeDate"
[selected]="selected"
[dateFilter]="_dateFilterForViews"
(selectedChange)="_monthSelected($event)">
</md-year-view>
</div>
+122
View File
@@ -0,0 +1,122 @@
$mat-calendar-padding: 8px !default;
$mat-calendar-header-divider-width: 1px !default;
$mat-calendar-controls-vertical-padding: 5%;
// We want to indent to the middle of the first tile. There are 7 tiles, so 100% / 7 / 2.
// Then we back up a little bit since the text in the cells is center-aligned.
$mat-calendar-controls-start-padding: calc(100% / 14 - 22px);
// Same as above, but on other side for arrows.
$mat-calendar-controls-end-padding: calc(100% / 14 - 22px);
$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;
// Values chosen to approximate https://material.io/icons/#ic_navigate_before and
// https://material.io/icons/#ic_navigate_next as closely as possible.
$mat-calendar-prev-next-icon-border-width: 2px;
$mat-calendar-prev-next-icon-margin: 15.5px;
$mat-calendar-prev-icon-transform: translateX(2px) rotate(-45deg);
$mat-calendar-next-icon-transform: translateX(-2px) rotate(45deg);
.mat-calendar {
display: block;
}
.mat-calendar-header {
padding: $mat-calendar-padding $mat-calendar-padding 0 $mat-calendar-padding;
}
.mat-calendar-content {
padding: 0 $mat-calendar-padding $mat-calendar-padding $mat-calendar-padding;
outline: none;
}
.mat-calendar-controls {
display: flex;
padding: $mat-calendar-controls-vertical-padding $mat-calendar-controls-end-padding
$mat-calendar-controls-vertical-padding $mat-calendar-controls-start-padding;
}
.mat-calendar-spacer {
flex: 1 1 auto;
}
.mat-calendar-period-button {
font: inherit;
font-size: $mat-calendar-period-font-size;
font-weight: bold;
min-width: 0;
}
.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-width: $mat-calendar-arrow-size;
border-top-style: solid;
margin: 0 0 0 $mat-calendar-arrow-size;
vertical-align: middle;
&.mat-calendar-invert {
transform: rotate(180deg);
}
}
.mat-calendar-previous-button,
.mat-calendar-next-button {
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: $mat-calendar-prev-next-icon-margin;
border: 0 solid currentColor;
border-top-width: $mat-calendar-prev-next-icon-border-width;
}
}
.mat-calendar-previous-button::after {
border-left-width: $mat-calendar-prev-next-icon-border-width;
transform: $mat-calendar-prev-icon-transform;
}
.mat-calendar-next-button::after {
border-right-width: $mat-calendar-prev-next-icon-border-width;
transform: $mat-calendar-next-icon-transform;
}
.mat-calendar-table {
border-spacing: 0;
border-collapse: collapse;
width: 100%;
}
.mat-calendar-table-header th {
text-align: center;
font-size: $mat-calendar-weekday-table-font-size;
font-weight: normal;
padding: 0 0 $mat-calendar-padding 0;
}
.mat-calendar-table-header-divider {
position: relative;
height: $mat-calendar-header-divider-width;
// We use an absolutely positioned pseudo-element as the divider line for the table header so we
// can extend it all the way to the edge of the calendar.
&::after {
content: '';
position: absolute;
top: 0;
left: -$mat-calendar-padding;
right: -$mat-calendar-padding;
height: $mat-calendar-header-divider-width;
}
}
+622
View File
@@ -0,0 +1,622 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {MdCalendar} from './calendar';
import {By} from '@angular/platform-browser';
import {MdMonthView} from './month-view';
import {MdYearView} from './year-view';
import {MdCalendarBody} from './calendar-body';
import {
dispatchFakeEvent,
dispatchKeyboardEvent,
dispatchMouseEvent
} from '../core/testing/dispatch-events';
import {
DOWN_ARROW,
END,
ENTER,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW
} from '../core/keyboard/keycodes';
import {MdDatepickerIntl} from './datepicker-intl';
import {MdNativeDateModule} from '../core/datetime/index';
// When constructing a Date, the month is zero-based. This can be confusing, since people are
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
NOV = 10, DEC = 11;
describe('MdCalendar', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MdNativeDateModule,
],
declarations: [
MdCalendar,
MdCalendarBody,
MdMonthView,
MdYearView,
// Test components.
StandardCalendar,
CalendarWithMinMax,
CalendarWithDateFilter,
],
providers: [
MdDatepickerIntl,
],
});
TestBed.compileComponents();
}));
describe('standard calendar', () => {
let fixture: ComponentFixture<StandardCalendar>;
let testComponent: StandardCalendar;
let calendarElement: HTMLElement;
let periodButton: HTMLElement;
let prevButton: HTMLElement;
let nextButton: HTMLElement;
let calendarInstance: MdCalendar<Date>;
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 active', () => {
expect(calendarInstance._monthView).toBe(true, 'should be in month view');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
});
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._activeDate).toEqual(new Date(2017, JAN, 31));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 28));
});
it('should go to previous and next year', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._monthView).toBe(false, 'should be in year view');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 31));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
});
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._activeDate).toEqual(new Date(2017, JAN, 31));
let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-cell');
(monthCells[monthCells.length - 1] as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance._monthView).toBe(true, 'should be in month view');
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 31));
expect(testComponent.selected).toBeFalsy('no date should be selected yet');
});
it('should select date in month view', () => {
let monthCells = calendarElement.querySelectorAll('.mat-calendar-body-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 Date(2017, JAN, 31));
});
describe('a11y', () => {
describe('calendar body', () => {
let calendarBodyEl: HTMLElement;
beforeEach(() => {
calendarBodyEl = calendarElement.querySelector('.mat-calendar-content') as HTMLElement;
expect(calendarBodyEl).not.toBeNull();
dispatchFakeEvent(calendarBodyEl, 'focus');
fixture.detectChanges();
});
it('should initially set start date active', () => {
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
});
describe('month view', () => {
it('should decrement date on left arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
calendarInstance._activeDate = new Date(2017, JAN, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
});
it('should increment date on right arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 2));
});
it('should go up a row on up arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 24));
calendarInstance._activeDate = new Date(2017, JAN, 7);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
});
it('should go down a row on down arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 7));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 14));
});
it('should go to beginning of the month on home press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
});
it('should go to end of the month on end press', () => {
calendarInstance._activeDate = new Date(2017, JAN, 10);
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
});
it('should go back one month on page up press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, NOV, 30));
});
it('should go forward one month on page down press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAR, 28));
});
it('should select active date on enter', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(testComponent.selected).toBeNull();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(testComponent.selected).toEqual(new Date(2017, JAN, 30));
});
});
describe('year view', () => {
beforeEach(() => {
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
expect(calendarInstance._monthView).toBe(false);
});
it('should decrement month on left arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, DEC, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, NOV, 30));
});
it('should increment month on right arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAR, 28));
});
it('should go up a row on up arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, AUG, 31));
calendarInstance._activeDate = new Date(2017, JUL, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, JUL, 1));
calendarInstance._activeDate = new Date(2017, DEC, 10);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, MAY, 10));
});
it('should go down a row on down arrow press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, AUG, 31));
calendarInstance._activeDate = new Date(2017, JUN, 1);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JUN, 1));
calendarInstance._activeDate = new Date(2017, SEP, 30);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, FEB, 28));
});
it('should go to first month of the year on home press', () => {
calendarInstance._activeDate = new Date(2017, SEP, 30);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 30));
});
it('should go to last month of the year on end press', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 31));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', END);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 31));
});
it('should go back one year on page up press', () => {
calendarInstance._activeDate = new Date(2016, FEB, 29);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2015, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2014, FEB, 28));
});
it('should go forward one year on page down press', () => {
calendarInstance._activeDate = new Date(2016, FEB, 29);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, FEB, 28));
});
it('should return to month view on enter', () => {
dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(calendarInstance._monthView).toBe(true);
expect(calendarInstance._activeDate).toEqual(new Date(2017, FEB, 28));
expect(testComponent.selected).toBeNull();
});
});
});
});
});
describe('calendar with min and max date', () => {
let fixture: ComponentFixture<CalendarWithMinMax>;
let testComponent: CalendarWithMinMax;
let calendarElement: HTMLElement;
let prevButton: HTMLButtonElement;
let nextButton: HTMLButtonElement;
let calendarInstance: MdCalendar<Date>;
beforeEach(() => {
fixture = TestBed.createComponent(CalendarWithMinMax);
let calendarDebugElement = fixture.debugElement.query(By.directive(MdCalendar));
calendarElement = calendarDebugElement.nativeElement;
prevButton =
calendarElement.querySelector('.mat-calendar-previous-button') as HTMLButtonElement;
nextButton = calendarElement.querySelector('.mat-calendar-next-button') as HTMLButtonElement;
calendarInstance = calendarDebugElement.componentInstance;
testComponent = fixture.componentInstance;
});
it('should clamp startAt value below min date', () => {
testComponent.startAt = new Date(2000, JAN, 1);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should clamp startAt value above max date', () => {
testComponent.startAt = new Date(2020, JAN, 1);
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
});
it('should not go back past min date', () => {
testComponent.startAt = new Date(2016, FEB, 1);
fixture.detectChanges();
expect(prevButton.disabled).toBe(false, 'previous button should not be disabled');
expect(calendarInstance._activeDate).toEqual(new Date(2016, FEB, 1));
prevButton.click();
fixture.detectChanges();
expect(prevButton.disabled).toBe(true, 'previous button should be disabled');
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
prevButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2016, JAN, 1));
});
it('should not go forward past max date', () => {
testComponent.startAt = new Date(2017, DEC, 1);
fixture.detectChanges();
expect(nextButton.disabled).toBe(false, 'next button should not be disabled');
expect(calendarInstance._activeDate).toEqual(new Date(2017, DEC, 1));
nextButton.click();
fixture.detectChanges();
expect(nextButton.disabled).toBe(true, 'next button should be disabled');
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
nextButton.click();
fixture.detectChanges();
expect(calendarInstance._activeDate).toEqual(new Date(2018, JAN, 1));
});
});
describe('calendar with date filter', () => {
let fixture: ComponentFixture<CalendarWithDateFilter>;
let testComponent: CalendarWithDateFilter;
let calendarElement: HTMLElement;
let calendarInstance: MdCalendar<Date>;
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-body-cell');
(cells[0] as HTMLElement).click();
fixture.detectChanges();
expect(testComponent.selected).toBeFalsy();
(cells[1] as HTMLElement).click();
fixture.detectChanges();
expect(testComponent.selected).toEqual(new Date(2017, JAN, 2));
});
describe('a11y', () => {
let calendarBodyEl: HTMLElement;
beforeEach(() => {
calendarBodyEl = calendarElement.querySelector('.mat-calendar-content') as HTMLElement;
expect(calendarBodyEl).not.toBeNull();
dispatchFakeEvent(calendarBodyEl, 'focus');
fixture.detectChanges();
});
it('should not allow selection of disabled date in month view', () => {
expect(calendarInstance._monthView).toBe(true);
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 1));
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(testComponent.selected).toBeNull();
});
it('should allow entering month view at disabled month', () => {
let periodButton =
calendarElement.querySelector('.mat-calendar-period-button') as HTMLElement;
dispatchMouseEvent(periodButton, 'click');
fixture.detectChanges();
calendarInstance._activeDate = new Date(2017, NOV, 1);
fixture.detectChanges();
expect(calendarInstance._monthView).toBe(false);
dispatchKeyboardEvent(calendarBodyEl, 'keydown', ENTER);
fixture.detectChanges();
expect(calendarInstance._monthView).toBe(true);
expect(testComponent.selected).toBeNull();
});
});
});
});
@Component({
template: `<md-calendar [startAt]="startDate" [(selected)]="selected"></md-calendar>`
})
class StandardCalendar {
selected: Date = null;
startDate = new Date(2017, JAN, 31);
}
@Component({
template: `
<md-calendar [startAt]="startAt" [minDate]="minDate" [maxDate]="maxDate"></md-calendar>
`
})
class CalendarWithMinMax {
startAt: Date;
minDate = new Date(2016, JAN, 1);
maxDate = new Date(2018, JAN, 1);
}
@Component({
template: `
<md-calendar [startAt]="startDate" [(selected)]="selected" [dateFilter]="dateFilter">
</md-calendar>
`
})
class CalendarWithDateFilter {
selected: Date = null;
startDate = new Date(2017, JAN, 1);
dateFilter (date: Date) {
return date.getDate() % 2 == 0 && date.getMonth() != NOV;
}
}
+321
View File
@@ -0,0 +1,321 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Inject,
Input,
NgZone,
Optional,
Output,
ViewEncapsulation
} from '@angular/core';
import {
DOWN_ARROW,
END,
ENTER,
HOME,
LEFT_ARROW,
PAGE_DOWN,
PAGE_UP,
RIGHT_ARROW,
UP_ARROW
} from '../core/keyboard/keycodes';
import {DateAdapter} from '../core/datetime/index';
import {MdDatepickerIntl} from './datepicker-intl';
import {createMissingDateImplError} from './datepicker-errors';
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
/**
* 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<D> implements AfterContentInit {
/** A date representing the period (month or year) to start the calendar in. */
@Input() startAt: D;
/** Whether the calendar should be started in month or year view. */
@Input() startView: 'month' | 'year' = 'month';
/** The currently selected date. */
@Input() selected: D;
/** The minimum selectable date. */
@Input() minDate: D;
/** The maximum selectable date. */
@Input() maxDate: D;
/** A function used to filter which dates are selectable. */
@Input() dateFilter: (date: D) => boolean;
/** Emits when the currently selected date changes. */
@Output() selectedChange = new EventEmitter<D>();
/** Date filter for the month and year views. */
_dateFilterForViews = (date: D) => {
return !!date &&
(!this.dateFilter || this.dateFilter(date)) &&
(!this.minDate || this._dateAdapter.compareDate(date, this.minDate) >= 0) &&
(!this.maxDate || this._dateAdapter.compareDate(date, this.maxDate) <= 0);
}
/**
* The current active date. This determines which time period is shown and which date is
* highlighted when using keyboard navigation.
*/
get _activeDate(): D { return this._clampedActiveDate; }
set _activeDate(value: D) {
this._clampedActiveDate = this._dateAdapter.clampDate(value, this.minDate, this.maxDate);
}
private _clampedActiveDate: D;
/** Whether the calendar is in month view. */
_monthView: boolean;
/** The label for the current calendar view. */
get _periodButtonText(): string {
return this._monthView ?
this._dateAdapter.format(this._activeDate, this._dateFormats.display.monthYearLabel)
.toLocaleUpperCase() :
this._dateAdapter.getYearName(this._activeDate);
}
get _periodButtonLabel(): string {
return this._monthView ? this._intl.switchToYearViewLabel : this._intl.switchToMonthViewLabel;
}
/** The label for the the previous button. */
get _prevButtonLabel(): string {
return this._monthView ? this._intl.prevMonthLabel : this._intl.prevYearLabel;
}
/** The label for the the next button. */
get _nextButtonLabel(): string {
return this._monthView ? this._intl.nextMonthLabel : this._intl.nextYearLabel;
}
constructor(private _elementRef: ElementRef,
private _intl: MdDatepickerIntl,
private _ngZone: NgZone,
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
if (!this._dateFormats) {
throw createMissingDateImplError('MD_DATE_FORMATS');
}
}
ngAfterContentInit() {
this._activeDate = this.startAt || this._dateAdapter.today();
this._focusActiveCell();
this._monthView = this.startView != 'year';
}
/** Handles date selection in the month view. */
_dateSelected(date: D): void {
if (!this._dateAdapter.sameDate(date, this.selected)) {
this.selectedChange.emit(date);
}
}
/** Handles month selection in the year view. */
_monthSelected(month: D): void {
this._activeDate = month;
this._monthView = true;
}
/** Handles user clicks on the period label. */
_currentPeriodClicked(): void {
this._monthView = !this._monthView;
}
/** Handles user clicks on the previous button. */
_previousClicked(): void {
this._activeDate = this._monthView ?
this._dateAdapter.addCalendarMonths(this._activeDate, -1) :
this._dateAdapter.addCalendarYears(this._activeDate, -1);
}
/** Handles user clicks on the next button. */
_nextClicked(): void {
this._activeDate = this._monthView ?
this._dateAdapter.addCalendarMonths(this._activeDate, 1) :
this._dateAdapter.addCalendarYears(this._activeDate, 1);
}
/** Whether the previous period button is enabled. */
_previousEnabled(): boolean {
if (!this.minDate) {
return true;
}
return !this.minDate || !this._isSameView(this._activeDate, this.minDate);
}
/** Whether the next period button is enabled. */
_nextEnabled(): boolean {
return !this.maxDate || !this._isSameView(this._activeDate, this.maxDate);
}
/** Handles keydown events on the calendar body. */
_handleCalendarBodyKeydown(event: KeyboardEvent): void {
// TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
// disabled ones from being selected. This may not be ideal, we should look into whether
// navigation should skip over disabled dates, and if so, how to implement that efficiently.
if (this._monthView) {
this._handleCalendarBodyKeydownInMonthView(event);
} else {
this._handleCalendarBodyKeydownInYearView(event);
}
}
/** Focuses the active cell after the microtask queue is empty. */
_focusActiveCell() {
this._ngZone.runOutsideAngular(() => this._ngZone.onStable.first().subscribe(() => {
let activeEl = this._elementRef.nativeElement.querySelector('.mat-calendar-body-active');
activeEl.focus();
}));
}
/** Whether the two dates represent the same view in the current view mode (month or year). */
private _isSameView(date1: D, date2: D): boolean {
return this._monthView ?
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) &&
this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2) :
this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2);
}
/** Handles keydown events on the calendar body when calendar is in month view. */
private _handleCalendarBodyKeydownInMonthView(event: KeyboardEvent): void {
switch (event.keyCode) {
case LEFT_ARROW:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, -1);
break;
case RIGHT_ARROW:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 1);
break;
case UP_ARROW:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, -7);
break;
case DOWN_ARROW:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 7);
break;
case HOME:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate,
1 - this._dateAdapter.getDate(this._activeDate));
break;
case END:
this._activeDate = this._dateAdapter.addCalendarDays(this._activeDate,
(this._dateAdapter.getNumDaysInMonth(this._activeDate) -
this._dateAdapter.getDate(this._activeDate)));
break;
case PAGE_UP:
this._activeDate = event.altKey ?
this._dateAdapter.addCalendarYears(this._activeDate, -1) :
this._dateAdapter.addCalendarMonths(this._activeDate, -1);
break;
case PAGE_DOWN:
this._activeDate = event.altKey ?
this._dateAdapter.addCalendarYears(this._activeDate, 1) :
this._dateAdapter.addCalendarMonths(this._activeDate, 1);
break;
case ENTER:
if (this._dateFilterForViews(this._activeDate)) {
this._dateSelected(this._activeDate);
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
return;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/** Handles keydown events on the calendar body when calendar is in year view. */
private _handleCalendarBodyKeydownInYearView(event: KeyboardEvent): void {
switch (event.keyCode) {
case LEFT_ARROW:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, -1);
break;
case RIGHT_ARROW:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 1);
break;
case UP_ARROW:
this._activeDate = this._prevMonthInSameCol(this._activeDate);
break;
case DOWN_ARROW:
this._activeDate = this._nextMonthInSameCol(this._activeDate);
break;
case HOME:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
-this._dateAdapter.getMonth(this._activeDate));
break;
case END:
this._activeDate = this._dateAdapter.addCalendarMonths(this._activeDate,
11 - this._dateAdapter.getMonth(this._activeDate));
break;
case PAGE_UP:
this._activeDate =
this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? -10 : -1);
break;
case PAGE_DOWN:
this._activeDate =
this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? 10 : 1);
break;
case ENTER:
this._monthSelected(this._activeDate);
break;
default:
// Don't prevent default or focus active cell on keys that we don't explicitly handle.
return;
}
this._focusActiveCell();
// Prevent unexpected default actions such as form submission.
event.preventDefault();
}
/**
* Determine the date for the month that comes before the given month in the same column in the
* calendar table.
*/
private _prevMonthInSameCol(date: D): D {
// Determine how many months to jump forward given that there are 2 empty slots at the beginning
// of each year.
let increment = this._dateAdapter.getMonth(date) <= 4 ? -5 :
(this._dateAdapter.getMonth(date) >= 7 ? -7 : -12);
return this._dateAdapter.addCalendarMonths(date, increment);
}
/**
* Determine the date for the month that comes after the given month in the same column in the
* calendar table.
*/
private _nextMonthInSameCol(date: D): D {
// Determine how many months to jump forward given that there are 2 empty slots at the beginning
// of each year.
let increment = this._dateAdapter.getMonth(date) <= 4 ? 7 :
(this._dateAdapter.getMonth(date) >= 7 ? 5 : 12);
return this._dateAdapter.addCalendarMonths(date, increment);
}
}
@@ -0,0 +1,10 @@
<md-calendar cdkTrapFocus
[id]="datepicker.id"
[startAt]="datepicker.startAt"
[startView]="datepicker.startView"
[minDate]="datepicker._minDate"
[maxDate]="datepicker._maxDate"
[dateFilter]="datepicker._dateFilter"
[selected]="datepicker._selected"
(selectedChange)="datepicker._selectAndClose($event)">
</md-calendar>
@@ -0,0 +1,46 @@
@import '../core/style/elevation';
$md-datepicker-calendar-padding: 8px;
$md-datepicker-non-touch-calendar-cell-size: 40px;
$md-datepicker-non-touch-calendar-width:
$md-datepicker-non-touch-calendar-cell-size * 7 + $md-datepicker-calendar-padding * 2;
// Ideally the calendar would have a constant aspect ratio, no matter its size, and we would base
// these measurements off the aspect ratio. Unfortunately, the aspect ratio does change a little as
// the calendar grows, since some of the elements have pixel-based sizes. These numbers have been
// chosen to minimize extra whitespace at larger sizes, while still ensuring we won't need
// scrollbars at smaller sizes.
$md-datepicker-touch-width: 64vmin;
$md-datepicker-touch-height: 80vmin;
$md-datepicker-touch-min-width: 250px;
$md-datepicker-touch-min-height: 312px;
$md-datepicker-touch-max-width: 750px;
$md-datepicker-touch-max-height: 788px;
.mat-calendar {
width: $md-datepicker-non-touch-calendar-width;
@include mat-elevation(8);
}
.mat-datepicker-content-touch {
display: block;
// make sure the dialog scrolls rather than being cropped on ludicrously small screens
max-height: 80vh;
overflow: auto;
// TODO(mmalerba): hack to offset the padding of the dialog. Can be removed when we switch away
// from using dialog.
margin: -24px;
.mat-calendar {
width: $md-datepicker-touch-width;
height: $md-datepicker-touch-height;
min-width: $md-datepicker-touch-min-width;
min-height: $md-datepicker-touch-min-height;
max-width: $md-datepicker-touch-max-width;
max-height: $md-datepicker-touch-max-height;
@include mat-elevation(0);
}
}
+6
View File
@@ -0,0 +1,6 @@
/** @docs-private */
export function createMissingDateImplError(provider: string) {
return new Error(
`MdDatepicker: No provider found for ${provider}. You must import one of the following` +
`modules at your application root: MdNativeDateModule, or provide a custom implementation.`);
}
+232
View File
@@ -0,0 +1,232 @@
import {
AfterContentInit,
Directive,
ElementRef,
EventEmitter,
forwardRef,
Inject,
Input,
OnDestroy,
Optional,
Renderer2
} from '@angular/core';
import {MdDatepicker} from './datepicker';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import {Subscription} from 'rxjs/Subscription';
import {MdInputContainer} from '../input/input-container';
import {DOWN_ARROW} from '../core/keyboard/keycodes';
import {DateAdapter} from '../core/datetime/index';
import {createMissingDateImplError} from './datepicker-errors';
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
export const MD_DATEPICKER_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MdDatepickerInput),
multi: true
};
export const MD_DATEPICKER_VALIDATORS: any = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MdDatepickerInput),
multi: true
};
/** Directive used to connect an input to a MdDatepicker. */
@Directive({
selector: 'input[mdDatepicker], input[matDatepicker]',
providers: [MD_DATEPICKER_VALUE_ACCESSOR, MD_DATEPICKER_VALIDATORS],
host: {
'[attr.aria-expanded]': '_datepicker?.opened || "false"',
'[attr.aria-haspopup]': 'true',
'[attr.aria-owns]': '_datepicker?.id',
'[attr.min]': 'min ? _dateAdapter.getISODateString(min) : null',
'[attr.max]': 'max ? _dateAdapter.getISODateString(max) : null',
'(input)': '_onInput($event.target.value)',
'(blur)': '_onTouched()',
'(keydown)': '_onKeydown($event)',
}
})
export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAccessor, OnDestroy,
Validator {
/** The datepicker that this input is associated with. */
@Input()
set mdDatepicker(value: MdDatepicker<D>) {
if (value) {
this._datepicker = value;
this._datepicker._registerInput(this);
}
}
_datepicker: MdDatepicker<D>;
@Input() set matDatepicker(value: MdDatepicker<D>) { this.mdDatepicker = value; }
@Input() set mdDatepickerFilter(filter: (date: D | null) => boolean) {
this._dateFilter = filter;
this._validatorOnChange();
}
_dateFilter: (date: D | null) => boolean;
@Input() set matDatepickerFilter(filter: (date: D | null) => boolean) {
this.mdDatepickerFilter = filter;
}
/** The value of the input. */
@Input()
get value(): D {
return this._dateAdapter.parse(this._elementRef.nativeElement.value,
this._dateFormats.parse.dateInput);
}
set value(value: D) {
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
let oldDate = this.value;
this._renderer.setProperty(this._elementRef.nativeElement, 'value',
date ? this._dateAdapter.format(date, this._dateFormats.display.dateInput) : '');
if (!this._dateAdapter.sameDate(oldDate, date)) {
this._valueChange.emit(date);
}
}
/** The minimum valid date. */
@Input()
get min(): D { return this._min; }
set min(value: D) {
this._min = value;
this._validatorOnChange();
}
private _min: D;
/** The maximum valid date. */
@Input()
get max(): D { return this._max; }
set max(value: D) {
this._max = value;
this._validatorOnChange();
}
private _max: D;
/** Emits when the value changes (either due to user input or programmatic change). */
_valueChange = new EventEmitter<D>();
_onTouched = () => {};
private _cvaOnChange = (value: any) => {};
private _validatorOnChange = () => {};
private _datepickerSubscription: Subscription;
/** The form control validator for the min date. */
private _minValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
return (!this.min || !control.value ||
this._dateAdapter.compareDate(this.min, control.value) < 0) ?
null : {'mdDatepickerMin': {'min': this.min, 'actual': control.value}};
}
/** The form control validator for the max date. */
private _maxValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
return (!this.max || !control.value ||
this._dateAdapter.compareDate(this.max, control.value) > 0) ?
null : {'mdDatepickerMax': {'max': this.max, 'actual': control.value}};
}
/** The form control validator for the date filter. */
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
return !this._dateFilter || !control.value || this._dateFilter(control.value) ?
null : {'mdDatepickerFilter': true};
}
/** The combined form control validator for this input. */
private _validator: ValidatorFn =
Validators.compose([this._minValidator, this._maxValidator, this._filterValidator]);
constructor(
private _elementRef: ElementRef,
private _renderer: Renderer2,
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats,
@Optional() private _mdInputContainer: MdInputContainer) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
if (!this._dateFormats) {
throw createMissingDateImplError('MD_DATE_FORMATS');
}
}
ngAfterContentInit() {
if (this._datepicker) {
this._datepickerSubscription =
this._datepicker.selectedChanged.subscribe((selected: D) => {
this.value = selected;
this._cvaOnChange(selected);
});
}
}
ngOnDestroy() {
if (this._datepickerSubscription) {
this._datepickerSubscription.unsubscribe();
}
}
registerOnValidatorChange(fn: () => void): void {
this._validatorOnChange = fn;
}
validate(c: AbstractControl): ValidationErrors | null {
return this._validator ? this._validator(c) : null;
}
/**
* Gets the element that the datepicker popup should be connected to.
* @return The element to connect the popup to.
*/
getPopupConnectionElementRef(): ElementRef {
return this._mdInputContainer ? this._mdInputContainer.underlineRef : this._elementRef;
}
// Implemented as part of ControlValueAccessor
writeValue(value: D): void {
this.value = value;
}
// Implemented as part of ControlValueAccessor
registerOnChange(fn: (value: any) => void): void {
this._cvaOnChange = fn;
}
// Implemented as part of ControlValueAccessor
registerOnTouched(fn: () => void): void {
this._onTouched = fn;
}
// Implemented as part of ControlValueAccessor
setDisabledState(disabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', disabled);
}
_onKeydown(event: KeyboardEvent) {
if (event.altKey && event.keyCode === DOWN_ARROW) {
this._datepicker.open();
event.preventDefault();
}
}
_onInput(value: string) {
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
this._cvaOnChange(date);
this._valueChange.emit(date);
}
}
+30
View File
@@ -0,0 +1,30 @@
import {Injectable} from '@angular/core';
/** Datepicker data that requires internationalization. */
@Injectable()
export class MdDatepickerIntl {
/** A label for the calendar popup (used by screen readers). */
calendarLabel = 'Calendar';
/** A label for the button used to open the calendar popup (used by screen readers). */
openCalendarLabel = 'Open calendar';
/** A label for the previous month button (used by screen readers). */
prevMonthLabel = 'Previous month';
/** A label for the next month button (used by screen readers). */
nextMonthLabel = 'Next month';
/** A label for the previous year button (used by screen readers). */
prevYearLabel = 'Previous year';
/** A label for the next year button (used by screen readers). */
nextYearLabel = 'Next year';
/** A label for the 'switch to month view' button (used by screen readers). */
switchToMonthViewLabel = 'Change to month view';
/** A label for the 'switch to year view' button (used by screen readers). */
switchToYearViewLabel = 'Change to year view';
}
+14
View File
@@ -0,0 +1,14 @@
$mat-datepicker-toggle-icon-size: 24px !default;
.mat-datepicker-toggle {
display: inline-block;
// Note: SVG needs to be base64 encoded or it will not work on IE11.
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+PHBhdGggZD0iTTE5IDNoLTFWMWgtMnYySDhWMUg2djJINWMtMS4xMSAwLTEuOTkuOS0xLjk5IDJMMyAxOWMwIDEuMS44OSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY1YzAtMS4xLS45LTItMi0yem0wIDE2SDVWOGgxNHYxMXpNNyAxMGg1djVIN3oiLz48L3N2Zz4=') no-repeat;
background-size: contain;
height: $mat-datepicker-toggle-icon-size;
width: $mat-datepicker-toggle-icon-size;
border: none;
outline: none;
vertical-align: middle;
}
+34
View File
@@ -0,0 +1,34 @@
import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core';
import {MdDatepicker} from './datepicker';
import {MdDatepickerIntl} from './datepicker-intl';
@Component({
moduleId: module.id,
selector: 'button[mdDatepickerToggle], button[matDatepickerToggle]',
template: '',
styleUrls: ['datepicker-toggle.css'],
host: {
'[class.mat-datepicker-toggle]': 'true',
'[attr.aria-label]': '_intl.openCalendarLabel',
'(click)': '_open($event)',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdDatepickerToggle<D> {
@Input('mdDatepickerToggle') datepicker: MdDatepicker<D>;
@Input('matDatepickerToggle')
get _datepicker() { return this.datepicker; }
set _datepicker(v: MdDatepicker<D>) { this.datepicker = v; }
constructor(public _intl: MdDatepickerIntl) {}
_open(event: Event): void {
if (this.datepicker) {
this.datepicker.open();
event.stopPropagation();
}
}
}
+199
View File
@@ -0,0 +1,199 @@
The datepicker allows users to enter a date either through text input, or by choosing a date from
the calendar. It is made up of several components and directives that work together:
<!-- TODO: INSERT OVERVIEW EXAMPLE HERE -->
### Current state
Currently the datepicker is in the beginning stages and supports basic date selection functionality.
There are many more features that will be added in future iterations, including:
* Support for datetimes (e.g. May 2, 2017 at 12:30pm) and month + year only (e.g. May 2017)
* Support for selecting and displaying date ranges
* Support for custom time zones
* Infinite scrolling through calendar months
* Built in support for [Moment.js](https://momentjs.com/) dates
### Connecting a datepicker to an input
A datepicker is composed of a text input and a calendar pop-up, connected via the `mdDatepicker`
property on the text input.
```html
<input [mdDatepicker]="myDatepicker">
<md-datepicker #myDatepicker></md-datepicker>
```
An optional datepicker toggle button is available. A toggle can be added to the example above:
```html
<input [mdDatepicker]="myDatepicker">
<button [mdDatepickerToggle]="myDatepicker"></button>
<md-datepicker #myDatepicker></md-datepicker>
```
This works exactly the same with an input that is part of an `<md-input-container>` and the toggle
can easily be used as a prefix or suffix on the material input:
```html
<md-input-container>
<input mdInput [mdDatepicker]="myDatepicker">
<button mdSuffix [mdDatepickerToggle]="myDatepicker"></button>
</md-input-container>
<md-datepicker #myDatepicker></md-datepicker>
```
### Setting the calendar starting view
By default the calendar will open in month view, this can be changed by setting the `startView`
property of `md-datepicker` to `"year"`. In year view the user will see all months of the year and
then proceed to month view after choosing a month.
The month or year that the calendar opens to is determined by first checking if any date is
currently selected, if so it will open to the month or year containing that date. Otherwise it will
open to the month or year containing today's date. This behavior can be overridden by using the
`startAt` property of `md-datepicker`. In this case the calendar will open to the month or year
containing the `startAt` date.
```ts
startDate = new Date(1990, 0, 1);
```
```html
...
<md-datepicker startView="year" [startAt]="startDate"></md-datepicker>
```
### Date validation
There are three properties that add date validation to the datepicker input. The first two are the
`min` and `max` properties. In addition to enforcing validation on the input, these properties will
disable all dates on the calendar popup before or after the respective values and prevent the user
from advancing the calendar past the `month` or `year` (depending on current view) containing the
`min` or `max` date.
The second way to add date validation is using the `mdDatepickerFilter` property of the datepicker
input. This property accepts a function of `<D> => boolean` (where `<D>` is the date type used by
the datepicker, see section on
[choosing a date implementation](#choosing-a-date-implementation-and-date-format-settings)).
A result of `true` indicates that the date is valid and a result of `false` indicates that it is
not. Again this will also disable the dates on the calendar that are invalid. However, one important
difference between using `mdDatepickerFilter` vs using `min` or `max` is that filtering out all
dates before or after a certain point, will not prevent the user from advancing the calendar past
that point.
```ts
myFilter = (d: Date) => d.getFullYear() > 2005
minDate = new Date(2000, 0, 1);
maxDate = new Date(2020, 11, 31);
```
```html
<input [mdDatepicker]="d" [mdDatepickerFilter]="myFilter" [min]="minDate" [max]="maxDate" ngModel>
<md-datepicker #d></md-datepicker>
```
In this example the user can back past 2005, but all of the dates before then will be unselectable.
They will not be able to go further back in the calendar than 2000. If they manually type in a date
that is before the min, after the max, or filtered out, the input will have validation errors.
Each validation property has a different error that can be checked:
* A value that violates the `min` property will have a `mdDatepickerMin` error.
* A value that violates the `max` property will have a `mdDatepickerMax` error.
* A value that violates the `mdDatepickerFilter` property will have a `mdDatepickerFilter` error.
### Touch UI mode
The datepicker normally opens as a popup under the input. However this is not ideal for touch
devices that don't have as much screen real estate and need bigger click targets. For this reason
`md-datepicker` has a `touchUi` property that can be set to `true` in order to enable a more touch
friendly UI where the calendar opens in a large dialog.
### Manually opening and closing the calendar
The calendar popup can be programmatically controlled using the `open` and `close` methods on the
`md-datepicker`. It also has an `opened` property that reflects the status of the popup.
```ts
@Component({...})
export class MyComponent implements AfterViewInit {
@ViewChild(MdDatepicker) dp: MdDatepicker<Date>;
ngAfterViewInit() {
dp.open();
}
}
```
### Choosing a date implementation and date format settings
The datepicker was built to be date implementation agnostic. This means that it can be made to work
with a variety of different date implementations. However it also means that developers need to make
sure to provide the appropriate pieces for the datepicker to work with their chosen implementation.
The easiest way to ensure this is just to import one of the pre-made modules (currently
`MdNativeDateModule` is the only implementation that ships with material, but there are plans to add
a module for Moment.js support):
* `MdNativeDateModule` - support for native JavaScript Date object
These modules include providers for `DateAdapter` and `MD_DATE_FORMATS`
```ts
@NgModule({
imports: [MdDatepickerModule, MdNativeDateModule],
})
export class MyApp {}
```
Because `DateAdapter` is a generic class, `MdDatepicker` and `MdDatepickerInput` also need to be
made generic. When working with these classes (for example as a `ViewChild`) you should include the
appropriate generic type that corresponds to the `DateAdapter` implementation you are using. For
example:
```ts
@Component({...})
export class MyComponent {
@ViewChild(MdDatepicker) datepicker: MdDatepicker<Date>;
}
```
#### Customizing the date implementation
The datepicker does all of its interaction with date objects via the `DateAdapter`. Making the
datepicker work with a different date implementation is as easy as extending `DateAdapter`, and
using your subclass as the provider. You will also want to make sure that the `MD_DATE_FORMATS`
provided in your app are formats that can be understood by your date implementation.
```ts
@NgModule({
imports: [MdDatepickerModule],
providers: [
{provide: DateAdapter, useClass: MyDateAdapter},
{provide: MD_DATE_FORMATS, useValue: MY_DATE_FORMATS},
],
})
export class MyApp {}
```
#### Customizing the parse and display formats
The `MD_DATE_FORMATS` object is just a collection of formats that the datepicker uses when parsing
and displaying dates. These formats are passed through to the `DateAdapter` so you will want to make
sure that the format objects you're using are compatible with the `DateAdapter` used in your app.
This example shows how to use the native `Date` implementation from material, but with custom
formats.
```ts
@NgModule({
imports: [MdDatepickerModule],
providers: [
{provide: DateAdapter, useClass: NativeDateAdapter},
{provide: MD_DATE_FORMATS, useValue: MY_NATIVE_DATE_FORMATS},
],
})
export class MyApp {}
```
### Localizing labels and messages
The various text strings used by the datepicker are provided through `MdDatepickerIntl`.
Localization of these messages can be done by providing a subclass with translated values in your
application root module.
```ts
@NgModule({
imports: [MdDatepickerModule, MdNativeDateModule],
providers: [
{provide: MdDatepickerIntl, useClass: MyIntl},
],
})
export class MyApp {}
```
+626
View File
@@ -0,0 +1,626 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MdDatepickerModule} from './index';
import {Component, ViewChild} from '@angular/core';
import {MdDatepicker} from './datepicker';
import {MdDatepickerInput} from './datepicker-input';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {By} from '@angular/platform-browser';
import {dispatchFakeEvent, dispatchMouseEvent} from '../core/testing/dispatch-events';
import {MdInputModule} from '../input/index';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {MdNativeDateModule} from '../core/datetime/index';
// When constructing a Date, the month is zero-based. This can be confusing, since people are
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
NOV = 10, DEC = 11;
describe('MdDatepicker', () => {
describe('with MdNativeDateModule', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
MdDatepickerModule,
MdInputModule,
MdNativeDateModule,
NoopAnimationsModule,
ReactiveFormsModule,
],
declarations: [
DatepickerWithFilterAndValidation,
DatepickerWithFormControl,
DatepickerWithMinAndMax,
DatepickerWithNgModel,
DatepickerWithStartAt,
DatepickerWithToggle,
InputContainerDatepicker,
MultiInputDatepicker,
NoInputDatepicker,
StandardDatepicker,
],
});
TestBed.compileComponents();
}));
describe('standard datepicker', () => {
let fixture: ComponentFixture<StandardDatepicker>;
let testComponent: StandardDatepicker;
beforeEach(async(() => {
fixture = TestBed.createComponent(StandardDatepicker);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('open non-touch should open popup', async(() => {
expect(document.querySelector('.cdk-overlay-pane')).toBeNull();
testComponent.datepicker.open();
fixture.detectChanges();
expect(document.querySelector('.cdk-overlay-pane')).not.toBeNull();
}));
it('open touch should open dialog', async(() => {
testComponent.touch = true;
fixture.detectChanges();
expect(document.querySelector('md-dialog-container')).toBeNull();
testComponent.datepicker.open();
fixture.detectChanges();
expect(document.querySelector('md-dialog-container')).not.toBeNull();
}));
it('close should close popup', async(() => {
testComponent.datepicker.open();
fixture.detectChanges();
let popup = document.querySelector('.cdk-overlay-pane');
expect(popup).not.toBeNull();
expect(parseInt(getComputedStyle(popup).height)).not.toBe(0);
testComponent.datepicker.close();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(parseInt(getComputedStyle(popup).height)).toBe(0);
});
}));
it('close should close dialog', async(() => {
testComponent.touch = true;
fixture.detectChanges();
testComponent.datepicker.open();
fixture.detectChanges();
expect(document.querySelector('md-dialog-container')).not.toBeNull();
testComponent.datepicker.close();
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(document.querySelector('md-dialog-container')).toBeNull();
});
}));
it('setting selected should update input and close calendar', async(() => {
testComponent.touch = true;
fixture.detectChanges();
testComponent.datepicker.open();
fixture.detectChanges();
expect(document.querySelector('md-dialog-container')).not.toBeNull();
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 1));
let cells = document.querySelectorAll('.mat-calendar-body-cell');
dispatchMouseEvent(cells[1], 'click');
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(document.querySelector('md-dialog-container')).toBeNull();
expect(testComponent.datepickerInput.value).toEqual(new Date(2020, JAN, 2));
});
}));
it('startAt should fallback to input value', () => {
expect(testComponent.datepicker.startAt).toEqual(new Date(2020, JAN, 1));
});
it('should attach popup to native input', () => {
let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef();
expect(attachToRef.nativeElement.tagName.toLowerCase())
.toBe('input', 'popup should be attached to native input');
});
});
describe('datepicker with too many inputs', () => {
it('should throw when multiple inputs registered', async(() => {
let fixture = TestBed.createComponent(MultiInputDatepicker);
expect(() => fixture.detectChanges()).toThrow();
}));
});
describe('datepicker with no inputs', () => {
let fixture: ComponentFixture<NoInputDatepicker>;
let testComponent: NoInputDatepicker;
beforeEach(async(() => {
fixture = TestBed.createComponent(NoInputDatepicker);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('should throw when opened with no registered inputs', async(() => {
expect(() => testComponent.datepicker.open()).toThrow();
}));
});
describe('datepicker with startAt', () => {
let fixture: ComponentFixture<DatepickerWithStartAt>;
let testComponent: DatepickerWithStartAt;
beforeEach(async(() => {
fixture = TestBed.createComponent(DatepickerWithStartAt);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('explicit startAt should override input value', () => {
expect(testComponent.datepicker.startAt).toEqual(new Date(2010, JAN, 1));
});
});
describe('datepicker with ngModel', () => {
let fixture: ComponentFixture<DatepickerWithNgModel>;
let testComponent: DatepickerWithNgModel;
beforeEach(async(() => {
fixture = TestBed.createComponent(DatepickerWithNgModel);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
testComponent = fixture.componentInstance;
});
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('should update datepicker when model changes', async(() => {
expect(testComponent.datepickerInput.value).toBeNull();
expect(testComponent.datepicker._selected).toBeNull();
let selected = new Date(2017, JAN, 1);
testComponent.selected = selected;
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(testComponent.datepickerInput.value).toEqual(selected);
expect(testComponent.datepicker._selected).toEqual(selected);
});
}));
it('should update model when date is selected', async(() => {
expect(testComponent.selected).toBeNull();
expect(testComponent.datepickerInput.value).toBeNull();
let selected = new Date(2017, JAN, 1);
testComponent.datepicker._selectAndClose(selected);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(testComponent.selected).toEqual(selected);
expect(testComponent.datepickerInput.value).toEqual(selected);
});
}));
it('should mark input dirty after input event', () => {
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
expect(inputEl.classList).toContain('ng-pristine');
dispatchFakeEvent(inputEl, 'input');
fixture.detectChanges();
expect(inputEl.classList).toContain('ng-dirty');
});
it('should mark input dirty after date selected', async(() => {
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
expect(inputEl.classList).toContain('ng-pristine');
testComponent.datepicker._selectAndClose(new Date(2017, JAN, 1));
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(inputEl.classList).toContain('ng-dirty');
});
}));
it('should not mark dirty after model change', async(() => {
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
expect(inputEl.classList).toContain('ng-pristine');
testComponent.selected = new Date(2017, JAN, 1);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(inputEl.classList).toContain('ng-pristine');
});
}));
it('should mark input touched on blur', () => {
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
expect(inputEl.classList).toContain('ng-untouched');
dispatchFakeEvent(inputEl, 'focus');
fixture.detectChanges();
expect(inputEl.classList).toContain('ng-untouched');
dispatchFakeEvent(inputEl, 'blur');
fixture.detectChanges();
expect(inputEl.classList).toContain('ng-touched');
});
});
describe('datepicker with formControl', () => {
let fixture: ComponentFixture<DatepickerWithFormControl>;
let testComponent: DatepickerWithFormControl;
beforeEach(async(() => {
fixture = TestBed.createComponent(DatepickerWithFormControl);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('should update datepicker when formControl changes', () => {
expect(testComponent.datepickerInput.value).toBeNull();
expect(testComponent.datepicker._selected).toBeNull();
let selected = new Date(2017, JAN, 1);
testComponent.formControl.setValue(selected);
fixture.detectChanges();
expect(testComponent.datepickerInput.value).toEqual(selected);
expect(testComponent.datepicker._selected).toEqual(selected);
});
it('should update formControl when date is selected', () => {
expect(testComponent.formControl.value).toBeNull();
expect(testComponent.datepickerInput.value).toBeNull();
let selected = new Date(2017, JAN, 1);
testComponent.datepicker._selectAndClose(selected);
fixture.detectChanges();
expect(testComponent.formControl.value).toEqual(selected);
expect(testComponent.datepickerInput.value).toEqual(selected);
});
it('should disable input when form control disabled', () => {
let inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
expect(inputEl.disabled).toBe(false);
testComponent.formControl.disable();
fixture.detectChanges();
expect(inputEl.disabled).toBe(true);
});
});
describe('datepicker with mdDatepickerToggle', () => {
let fixture: ComponentFixture<DatepickerWithToggle>;
let testComponent: DatepickerWithToggle;
beforeEach(async(() => {
fixture = TestBed.createComponent(DatepickerWithToggle);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('should open calendar when toggle clicked', async(() => {
expect(document.querySelector('md-dialog-container')).toBeNull();
let toggle = fixture.debugElement.query(By.css('button'));
dispatchMouseEvent(toggle.nativeElement, 'click');
fixture.detectChanges();
expect(document.querySelector('md-dialog-container')).not.toBeNull();
}));
});
describe('datepicker inside input-container', () => {
let fixture: ComponentFixture<InputContainerDatepicker>;
let testComponent: InputContainerDatepicker;
beforeEach(async(() => {
fixture = TestBed.createComponent(InputContainerDatepicker);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('should attach popup to input-container underline', () => {
let attachToRef = testComponent.datepickerInput.getPopupConnectionElementRef();
expect(attachToRef.nativeElement.classList.contains('mat-input-underline'))
.toBe(true, 'popup should be attached to input-container underline');
});
});
describe('datepicker with min and max dates', () => {
let fixture: ComponentFixture<DatepickerWithMinAndMax>;
let testComponent: DatepickerWithMinAndMax;
beforeEach(async(() => {
fixture = TestBed.createComponent(DatepickerWithMinAndMax);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('should use min and max dates specified by the input', () => {
expect(testComponent.datepicker._minDate).toEqual(new Date(2010, JAN, 1));
expect(testComponent.datepicker._maxDate).toEqual(new Date(2020, JAN, 1));
});
});
describe('datepicker with filter and validation', () => {
let fixture: ComponentFixture<DatepickerWithFilterAndValidation>;
let testComponent: DatepickerWithFilterAndValidation;
beforeEach(async(() => {
fixture = TestBed.createComponent(DatepickerWithFilterAndValidation);
fixture.detectChanges();
testComponent = fixture.componentInstance;
}));
afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));
it('should mark input invalid', async(() => {
testComponent.date = new Date(2017, JAN, 1);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('input')).nativeElement.classList)
.toContain('ng-invalid');
testComponent.date = new Date(2017, JAN, 2);
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('input')).nativeElement.classList)
.not.toContain('ng-invalid');
});
});
}));
it('should disable filtered calendar cells', () => {
fixture.detectChanges();
testComponent.datepicker.open();
fixture.detectChanges();
expect(document.querySelector('md-dialog-container')).not.toBeNull();
let cells = document.querySelectorAll('.mat-calendar-body-cell');
expect(cells[0].classList).toContain('mat-calendar-body-disabled');
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
});
});
});
describe('with missing DateAdapter and MD_DATE_FORMATS', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
MdDatepickerModule,
MdInputModule,
NoopAnimationsModule,
ReactiveFormsModule,
],
declarations: [StandardDatepicker],
});
TestBed.compileComponents();
}));
it('should throw when created', () => {
expect(() => TestBed.createComponent(StandardDatepicker))
.toThrowError(/MdDatepicker: No provider found for .*/);
});
});
});
@Component({
template: `
<input [mdDatepicker]="d" [value]="date">
<md-datepicker #d [touchUi]="touch"></md-datepicker>
`,
})
class StandardDatepicker {
touch = false;
date = new Date(2020, JAN, 1);
@ViewChild('d') datepicker: MdDatepicker<Date>;
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
}
@Component({
template: `
<input [mdDatepicker]="d"><input [mdDatepicker]="d"><md-datepicker #d></md-datepicker>
`,
})
class MultiInputDatepicker {}
@Component({
template: `<md-datepicker #d></md-datepicker>`,
})
class NoInputDatepicker {
@ViewChild('d') datepicker: MdDatepicker<Date>;
}
@Component({
template: `
<input [mdDatepicker]="d" [value]="date">
<md-datepicker #d [startAt]="startDate"></md-datepicker>
`,
})
class DatepickerWithStartAt {
date = new Date(2020, JAN, 1);
startDate = new Date(2010, JAN, 1);
@ViewChild('d') datepicker: MdDatepicker<Date>;
}
@Component({
template: `<input [(ngModel)]="selected" [mdDatepicker]="d"><md-datepicker #d></md-datepicker>`,
})
class DatepickerWithNgModel {
selected: Date = null;
@ViewChild('d') datepicker: MdDatepicker<Date>;
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
}
@Component({
template: `
<input [formControl]="formControl" [mdDatepicker]="d">
<md-datepicker #d></md-datepicker>
`,
})
class DatepickerWithFormControl {
formControl = new FormControl();
@ViewChild('d') datepicker: MdDatepicker<Date>;
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
}
@Component({
template: `
<input [mdDatepicker]="d">
<button [mdDatepickerToggle]="d"></button>
<md-datepicker #d [touchUi]="true"></md-datepicker>
`,
})
class DatepickerWithToggle {
@ViewChild('d') datepicker: MdDatepicker<Date>;
}
@Component({
template: `
<md-input-container>
<input mdInput [mdDatepicker]="d">
<md-datepicker #d></md-datepicker>
</md-input-container>
`,
})
class InputContainerDatepicker {
@ViewChild('d') datepicker: MdDatepicker<Date>;
@ViewChild(MdDatepickerInput) datepickerInput: MdDatepickerInput<Date>;
}
@Component({
template: `
<input [mdDatepicker]="d" [min]="minDate" [max]="maxDate">
<md-datepicker #d></md-datepicker>
`,
})
class DatepickerWithMinAndMax {
minDate = new Date(2010, JAN, 1);
maxDate = new Date(2020, JAN, 1);
@ViewChild('d') datepicker: MdDatepicker<Date>;
}
@Component({
template: `
<input [mdDatepicker]="d" [(ngModel)]="date" [mdDatepickerFilter]="filter">
<button [mdDatepickerToggle]="d"></button>
<md-datepicker #d [touchUi]="true"></md-datepicker>
`,
})
class DatepickerWithFilterAndValidation {
@ViewChild('d') datepicker: MdDatepicker<Date>;
date: Date;
filter = (date: Date) => date.getDate() != 1;
}
+280
View File
@@ -0,0 +1,280 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ComponentRef,
EventEmitter,
Input,
OnDestroy,
Optional,
Output,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
import {Overlay} from '../core/overlay/overlay';
import {OverlayRef} from '../core/overlay/overlay-ref';
import {ComponentPortal} from '../core/portal/portal';
import {OverlayState} from '../core/overlay/overlay-state';
import {Dir} from '../core/rtl/dir';
import {MdError} from '../core/errors/error';
import {MdDialog} from '../dialog/dialog';
import {MdDialogRef} from '../dialog/dialog-ref';
import {PositionStrategy} from '../core/overlay/position/position-strategy';
import {
OriginConnectionPosition,
OverlayConnectionPosition
} from '../core/overlay/position/connected-position';
import {MdDatepickerInput} from './datepicker-input';
import 'rxjs/add/operator/first';
import {Subscription} from 'rxjs/Subscription';
import {MdDialogConfig} from '../dialog/dialog-config';
import {DateAdapter} from '../core/datetime/index';
import {createMissingDateImplError} from './datepicker-errors';
import {ESCAPE} from '../core/keyboard/keycodes';
import {MdCalendar} from './calendar';
/** Used to generate a unique ID for each datepicker instance. */
let datepickerUid = 0;
/**
* Component used as the content for the datepicker dialog and popup. We use this instead of using
* MdCalendar directly as the content so we can control the initial focus. This also gives us a
* place to put additional features of the popup that are not part of the calendar itself in the
* future. (e.g. confirmation buttons).
* @docs-internal
*/
@Component({
moduleId: module.id,
selector: 'md-datepicker-content',
templateUrl: 'datepicker-content.html',
styleUrls: ['datepicker-content.css'],
host: {
'class': 'mat-datepicker-content',
'[class.mat-datepicker-content-touch]': 'datepicker.touchUi',
'(keydown)': '_handleKeydown($event)',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdDatepickerContent<D> implements AfterContentInit {
datepicker: MdDatepicker<D>;
@ViewChild(MdCalendar) _calendar: MdCalendar<D>;
ngAfterContentInit() {
this._calendar._focusActiveCell();
}
/**
* Handles keydown event on datepicker content.
* @param event The event.
*/
_handleKeydown(event: KeyboardEvent): void {
switch (event.keyCode) {
case ESCAPE:
this.datepicker.close();
break;
default:
// Return so that we don't preventDefault on keys that are not explicitly handled.
return;
}
event.preventDefault();
}
}
// TODO(mmalerba): We use a component instead of a directive here so the user can use implicit
// template reference variables (e.g. #d vs #d="mdDatepicker"). We can change this to a directive if
// angular adds support for `exportAs: '$implicit'` on directives.
/** Component responsible for managing the datepicker popup/dialog. */
@Component({
moduleId: module.id,
selector: 'md-datepicker, mat-datepicker',
template: '',
})
export class MdDatepicker<D> implements OnDestroy {
/** The date to open the calendar to initially. */
@Input()
get startAt(): D {
// If an explicit startAt is set we start there, otherwise we start at whatever the currently
// selected value is.
return this._startAt || (this._datepickerInput ? this._datepickerInput.value : null);
}
set startAt(date: D) { this._startAt = date; }
private _startAt: D;
/** The view that the calendar should start in. */
@Input() startView: 'month' | 'year' = 'month';
/**
* Whether the calendar UI is in touch mode. In touch mode the calendar opens in a dialog rather
* than a popup and elements have more padding to allow for bigger touch targets.
*/
@Input() touchUi = false;
/** Emits new selected date when selected date changes. */
@Output() selectedChanged = new EventEmitter<D>();
/** Whether the calendar is open. */
opened = false;
/** The id for the datepicker calendar. */
id = `md-datepicker-${datepickerUid++}`;
/** The currently selected date. */
_selected: D = null;
/** The minimum selectable date. */
get _minDate(): D {
return this._datepickerInput && this._datepickerInput.min;
}
/** The maximum selectable date. */
get _maxDate(): D {
return this._datepickerInput && this._datepickerInput.max;
}
get _dateFilter(): (date: D | null) => boolean {
return this._datepickerInput && this._datepickerInput._dateFilter;
}
/** A reference to the overlay when the calendar is opened as a popup. */
private _popupRef: OverlayRef;
/** A reference to the dialog when the calendar is opened as a dialog. */
private _dialogRef: MdDialogRef<any>;
/** A portal containing the calendar for this datepicker. */
private _calendarPortal: ComponentPortal<MdDatepickerContent<D>>;
/** The input element this datepicker is associated with. */
private _datepickerInput: MdDatepickerInput<D>;
private _inputSubscription: Subscription;
constructor(private _dialog: MdDialog, private _overlay: Overlay,
private _viewContainerRef: ViewContainerRef,
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() private _dir: Dir) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
}
ngOnDestroy() {
this.close();
if (this._popupRef) {
this._popupRef.dispose();
}
if (this._inputSubscription) {
this._inputSubscription.unsubscribe();
}
}
/** Selects the given date and closes the currently open popup or dialog. */
_selectAndClose(date: D): void {
let oldValue = this._selected;
this._selected = date;
if (!this._dateAdapter.sameDate(oldValue, this._selected)) {
this.selectedChanged.emit(date);
}
this.close();
}
/**
* Register an input with this datepicker.
* @param input The datepicker input to register with this datepicker.
*/
_registerInput(input: MdDatepickerInput<D>): void {
if (this._datepickerInput) {
throw new MdError('An MdDatepicker can only be associated with a single input.');
}
this._datepickerInput = input;
this._inputSubscription =
this._datepickerInput._valueChange.subscribe((value: D) => this._selected = value);
}
/** Open the calendar. */
open(): void {
if (this.opened) {
return;
}
if (!this._datepickerInput) {
throw new MdError('Attempted to open an MdDatepicker with no associated input.');
}
this.touchUi ? this._openAsDialog() : this._openAsPopup();
this.opened = true;
}
/** Close the calendar. */
close(): void {
if (!this.opened) {
return;
}
if (this._popupRef && this._popupRef.hasAttached()) {
this._popupRef.detach();
}
if (this._dialogRef) {
this._dialogRef.close();
this._dialogRef = null;
}
if (this._calendarPortal && this._calendarPortal.isAttached) {
this._calendarPortal.detach();
}
this.opened = false;
}
/** Open the calendar as a dialog. */
private _openAsDialog(): void {
let config = new MdDialogConfig();
config.viewContainerRef = this._viewContainerRef;
this._dialogRef = this._dialog.open(MdDatepickerContent, config);
this._dialogRef.afterClosed().first().subscribe(() => this.close());
this._dialogRef.componentInstance.datepicker = this;
}
/** Open the calendar as a popup. */
private _openAsPopup(): void {
if (!this._calendarPortal) {
this._calendarPortal = new ComponentPortal(MdDatepickerContent, this._viewContainerRef);
}
if (!this._popupRef) {
this._createPopup();
}
if (!this._popupRef.hasAttached()) {
let componentRef: ComponentRef<MdDatepickerContent<D>> =
this._popupRef.attach(this._calendarPortal);
componentRef.instance.datepicker = this;
}
this._popupRef.backdropClick().first().subscribe(() => this.close());
}
/** Create the popup. */
private _createPopup(): void {
const overlayState = new OverlayState();
overlayState.positionStrategy = this._createPopupPositionStrategy();
overlayState.hasBackdrop = true;
overlayState.backdropClass = 'md-overlay-transparent-backdrop';
overlayState.direction = this._dir ? this._dir.value : 'ltr';
this._popupRef = this._overlay.create(overlayState);
}
/** Create the popup PositionStrategy. */
private _createPopupPositionStrategy(): PositionStrategy {
let origin = {originX: 'start', originY: 'bottom'} as OriginConnectionPosition;
let overlay = {overlayX: 'start', overlayY: 'top'} as OverlayConnectionPosition;
return this._overlay.position().connectedTo(
this._datepickerInput.getPopupConnectionElementRef(), origin, overlay);
}
}
+58
View File
@@ -0,0 +1,58 @@
import {NgModule} from '@angular/core';
import {MdMonthView} from './month-view';
import {CommonModule} from '@angular/common';
import {MdCalendarBody} from './calendar-body';
import {MdYearView} from './year-view';
import {OverlayModule} from '../core/overlay/overlay-directives';
import {MdDatepicker, MdDatepickerContent} from './datepicker';
import {MdDatepickerInput} from './datepicker-input';
import {MdDialogModule} from '../dialog/index';
import {MdCalendar} from './calendar';
import {MdDatepickerToggle} from './datepicker-toggle';
import {StyleModule} from '../core/style/index';
import {MdButtonModule} from '../button/index';
import {MdDatepickerIntl} from './datepicker-intl';
export * from './calendar';
export * from './calendar-body';
export * from './datepicker';
export * from './datepicker-input';
export * from './datepicker-intl';
export * from './datepicker-toggle';
export * from './month-view';
export * from './year-view';
@NgModule({
imports: [
CommonModule,
MdButtonModule,
MdDialogModule,
OverlayModule,
StyleModule,
],
exports: [
MdDatepicker,
MdDatepickerContent,
MdDatepickerInput,
MdDatepickerToggle,
],
declarations: [
MdCalendar,
MdCalendarBody,
MdDatepicker,
MdDatepickerContent,
MdDatepickerInput,
MdDatepickerToggle,
MdMonthView,
MdYearView,
],
providers: [
MdDatepickerIntl,
],
entryComponents: [
MdDatepickerContent,
]
})
export class MdDatepickerModule {}
+16
View File
@@ -0,0 +1,16 @@
<table class="mat-calendar-table">
<thead class="mat-calendar-table-header">
<tr><th *ngFor="let day of _weekdays" [attr.aria-label]="day.long">{{day.narrow}}</th></tr>
<tr><th class="mat-calendar-table-header-divider" colspan="7" aria-hidden="true"></th></tr>
</thead>
<tbody md-calendar-body
role="grid"
[label]="_monthLabel"
[rows]="_weeks"
[todayValue]="_todayDate"
[selectedValue]="_selectedDate"
[labelMinRequiredCells]="3"
[activeCell]="_dateAdapter.getDate(activeDate) - 1"
(selectedValueChange)="_dateSelected($event)">
</tbody>
</table>
+127
View File
@@ -0,0 +1,127 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdMonthView} from './month-view';
import {MdCalendarBody} from './calendar-body';
import {MdNativeDateModule} from '../core/datetime/index';
// When constructing a Date, the month is zero-based. This can be confusing, since people are
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
NOV = 10, DEC = 11;
describe('MdMonthView', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MdNativeDateModule,
],
declarations: [
MdCalendarBody,
MdMonthView,
// Test components.
StandardMonthView,
MonthViewWithDateFilter,
],
});
TestBed.compileComponents();
}));
describe('standard month view', () => {
let fixture: ComponentFixture<StandardMonthView>;
let testComponent: StandardMonthView;
let monthViewNativeElement: Element;
beforeEach(() => {
fixture = TestBed.createComponent(StandardMonthView);
fixture.detectChanges();
let monthViewDebugElement = fixture.debugElement.query(By.directive(MdMonthView));
monthViewNativeElement = monthViewDebugElement.nativeElement;
testComponent = fixture.componentInstance;
});
it('has correct month label', () => {
let labelEl = monthViewNativeElement.querySelector('.mat-calendar-body-label');
expect(labelEl.innerHTML.trim()).toBe('JAN');
});
it('has 31 days', () => {
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect(cellEls.length).toBe(31);
});
it('shows selected date if in same month', () => {
let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected');
expect(selectedEl.innerHTML.trim()).toBe('10');
});
it('does not show selected date if in different month', () => {
testComponent.selected = new Date(2017, MAR, 10);
fixture.detectChanges();
let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected');
expect(selectedEl).toBeNull();
});
it('fires selected change event on cell clicked', () => {
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
(cellEls[cellEls.length - 1] as HTMLElement).click();
fixture.detectChanges();
let selectedEl = monthViewNativeElement.querySelector('.mat-calendar-body-selected');
expect(selectedEl.innerHTML.trim()).toBe('31');
});
it('should mark active date', () => {
let cellEls = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect((cellEls[4] as HTMLElement).innerText.trim()).toBe('5');
expect(cellEls[4].classList).toContain('mat-calendar-body-active');
});
});
describe('month view with date filter', () => {
let fixture: ComponentFixture<MonthViewWithDateFilter>;
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 disable filtered dates', () => {
let cells = monthViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect(cells[0].classList).toContain('mat-calendar-body-disabled');
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
});
});
});
@Component({
template: `<md-month-view [activeDate]="date" [(selected)]="selected"></md-month-view>`,
})
class StandardMonthView {
date = new Date(2017, JAN, 5);
selected = new Date(2017, JAN, 10);
}
@Component({
template: `<md-month-view [activeDate]="activeDate" [dateFilter]="dateFilter"></md-month-view>`
})
class MonthViewWithDateFilter {
activeDate = new Date(2017, JAN, 1);
dateFilter(date: Date) {
return date.getDate() % 2 == 0;
}
}
+171
View File
@@ -0,0 +1,171 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
EventEmitter,
Inject,
Input,
Optional,
Output,
ViewEncapsulation
} from '@angular/core';
import {MdCalendarCell} from './calendar-body';
import {DateAdapter} from '../core/datetime/index';
import {createMissingDateImplError} from './datepicker-errors';
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
const DAYS_PER_WEEK = 7;
/**
* An internal component used to display a single month in the datepicker.
* @docs-private
*/
@Component({
moduleId: module.id,
selector: 'md-month-view',
templateUrl: 'month-view.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdMonthView<D> implements AfterContentInit {
/**
* The date to display in this month view (everything other than the month and year is ignored).
*/
@Input()
get activeDate(): D { return this._activeDate; }
set activeDate(value: D) {
let oldActiveDate = this._activeDate;
this._activeDate = value || this._dateAdapter.today();
if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) {
this._init();
}
}
private _activeDate: D;
/** The currently selected date. */
@Input()
get selected(): D { return this._selected; }
set selected(value: D) {
this._selected = value;
this._selectedDate = this._getDateInCurrentMonth(this.selected);
}
private _selected: D;
/** A function used to filter which dates are selectable. */
@Input() dateFilter: (date: D) => boolean;
/** Emits when a new date is selected. */
@Output() selectedChange = new EventEmitter<D>();
/** The label for this month (e.g. "January 2017"). */
_monthLabel: string;
/** Grid of calendar cells representing the dates of the month. */
_weeks: MdCalendarCell[][];
/** The number of blank cells in the first row before the 1st of the month. */
_firstWeekOffset: number;
/**
* The date of the month that the currently selected Date falls on.
* Null if the currently selected Date is in another month.
*/
_selectedDate: number;
/** The date of the month that today falls on. Null if today is in another month. */
_todayDate: number;
/** The names of the weekdays. */
_weekdays: {long: string, narrow: string}[];
constructor(@Optional() public _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
if (!this._dateFormats) {
throw createMissingDateImplError('MD_DATE_FORMATS');
}
const firstDayOfWeek = this._dateAdapter.getFirstDayOfWeek();
const narrowWeekdays = this._dateAdapter.getDayOfWeekNames('narrow');
const longWeekdays = this._dateAdapter.getDayOfWeekNames('long');
// Rotate the labels for days of the week based on the configured first day of the week.
let weekdays = longWeekdays.map((long, i) => {
return {long, narrow: narrowWeekdays[i]};
});
this._weekdays = weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));
this._activeDate = this._dateAdapter.today();
}
ngAfterContentInit(): void {
this._init();
}
/** Handles when a new date is selected. */
_dateSelected(date: number) {
if (this._selectedDate == date) {
return;
}
this.selectedChange.emit(this._dateAdapter.createDate(
this._dateAdapter.getYear(this.activeDate), this._dateAdapter.getMonth(this.activeDate),
date));
}
/** Initializes this month view. */
private _init() {
this._selectedDate = this._getDateInCurrentMonth(this.selected);
this._todayDate = this._getDateInCurrentMonth(this._dateAdapter.today());
this._monthLabel =
this._dateAdapter.getMonthNames('short')[this._dateAdapter.getMonth(this.activeDate)]
.toLocaleUpperCase();
let firstOfMonth = this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate),
this._dateAdapter.getMonth(this.activeDate), 1);
this._firstWeekOffset =
(DAYS_PER_WEEK + this._dateAdapter.getDayOfWeek(firstOfMonth) -
this._dateAdapter.getFirstDayOfWeek()) % DAYS_PER_WEEK;
this._createWeekCells();
}
/** Creates MdCalendarCells for the dates in this month. */
private _createWeekCells() {
let daysInMonth = this._dateAdapter.getNumDaysInMonth(this.activeDate);
let dateNames = this._dateAdapter.getDateNames();
this._weeks = [[]];
for (let i = 0, cell = this._firstWeekOffset; i < daysInMonth; i++, cell++) {
if (cell == DAYS_PER_WEEK) {
this._weeks.push([]);
cell = 0;
}
let date = this._dateAdapter.createDate(
this._dateAdapter.getYear(this.activeDate),
this._dateAdapter.getMonth(this.activeDate), i + 1);
let enabled = !this.dateFilter ||
this.dateFilter(date);
let ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel);
this._weeks[this._weeks.length - 1]
.push(new MdCalendarCell(i + 1, dateNames[i], ariaLabel, enabled));
}
}
/**
* Gets the date in this month that the given Date falls on.
* Returns null if the given Date is in another month.
*/
private _getDateInCurrentMonth(date: D): number {
return this._hasSameMonthAndYear(date, this.activeDate) ?
this._dateAdapter.getDate(date) : null;
}
/** Checks whether the 2 dates are non-null and fall within the same month of the same year. */
private _hasSameMonthAndYear(d1: D, d2: D): boolean {
return !!(d1 && d2 && this._dateAdapter.getMonth(d1) == this._dateAdapter.getMonth(d2) &&
this._dateAdapter.getYear(d1) == this._dateAdapter.getYear(d2));
}
}
+16
View File
@@ -0,0 +1,16 @@
<table class="mat-calendar-table">
<thead class="mat-calendar-table-header">
<tr><th class="mat-calendar-table-header-divider" colspan="7"></th></tr>
</thead>
<tbody md-calendar-body
role="grid"
allowDisabledSelection="true"
[label]="_yearLabel"
[rows]="_months"
[todayValue]="_todayMonth"
[selectedValue]="_selectedMonth"
[labelMinRequiredCells]="2"
[activeCell]="_dateAdapter.getMonth(activeDate)"
(selectedValueChange)="_monthSelected($event)">
</tbody>
</table>
+134
View File
@@ -0,0 +1,134 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdYearView} from './year-view';
import {MdCalendarBody} from './calendar-body';
import {MdNativeDateModule} from '../core/datetime/index';
// When constructing a Date, the month is zero-based. This can be confusing, since people are
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
NOV = 10, DEC = 11;
describe('MdYearView', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MdNativeDateModule,
],
declarations: [
MdCalendarBody,
MdYearView,
// Test components.
StandardYearView,
YearViewWithDateFilter,
],
});
TestBed.compileComponents();
}));
describe('standard year view', () => {
let fixture: ComponentFixture<StandardYearView>;
let testComponent: StandardYearView;
let yearViewNativeElement: Element;
beforeEach(() => {
fixture = TestBed.createComponent(StandardYearView);
fixture.detectChanges();
let yearViewDebugElement = fixture.debugElement.query(By.directive(MdYearView));
yearViewNativeElement = yearViewDebugElement.nativeElement;
testComponent = fixture.componentInstance;
});
it('has correct year label', () => {
let labelEl = yearViewNativeElement.querySelector('.mat-calendar-body-label');
expect(labelEl.innerHTML.trim()).toBe('2017');
});
it('has 12 months', () => {
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect(cellEls.length).toBe(12);
});
it('shows selected month if in same year', () => {
let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected');
expect(selectedEl.innerHTML.trim()).toBe('MAR');
});
it('does not show selected month if in different year', () => {
testComponent.selected = new Date(2016, MAR, 10);
fixture.detectChanges();
let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected');
expect(selectedEl).toBeNull();
});
it('fires selected change event on cell clicked', () => {
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
(cellEls[cellEls.length - 1] as HTMLElement).click();
fixture.detectChanges();
let selectedEl = yearViewNativeElement.querySelector('.mat-calendar-body-selected');
expect(selectedEl.innerHTML.trim()).toBe('DEC');
});
it('should mark active date', () => {
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('JAN');
expect(cellEls[0].classList).toContain('mat-calendar-body-active');
});
});
describe('year view with date filter', () => {
let fixture: ComponentFixture<YearViewWithDateFilter>;
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-body-cell');
expect(cells[0].classList).not.toContain('mat-calendar-body-disabled');
expect(cells[1].classList).toContain('mat-calendar-body-disabled');
});
});
});
@Component({
template: `
<md-year-view [activeDate]="date" [(selected)]="selected"></md-year-view>`,
})
class StandardYearView {
date = new Date(2017, JAN, 5);
selected = new Date(2017, MAR, 10);
}
@Component({
template: `<md-year-view [activeDate]="activeDate" [dateFilter]="dateFilter"></md-year-view>`
})
class YearViewWithDateFilter {
activeDate = new Date(2017, JAN, 1);
dateFilter(date: Date) {
if (date.getMonth() == JAN) {
return date.getDate() == 10;
}
if (date.getMonth() == FEB) {
return false;
}
return true;
}
}
+144
View File
@@ -0,0 +1,144 @@
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
EventEmitter,
Inject,
Input,
Optional,
Output,
ViewEncapsulation
} from '@angular/core';
import {MdCalendarCell} from './calendar-body';
import {DateAdapter} from '../core/datetime/index';
import {createMissingDateImplError} from './datepicker-errors';
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';
/**
* An internal component used to display a single year in the datepicker.
* @docs-private
*/
@Component({
moduleId: module.id,
selector: 'md-year-view',
templateUrl: 'year-view.html',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdYearView<D> implements AfterContentInit {
/** The date to display in this year view (everything other than the year is ignored). */
@Input()
get activeDate(): D { return this._activeDate; }
set activeDate(value: D) {
let oldActiveDate = this._activeDate;
this._activeDate = value || this._dateAdapter.today();
if (this._dateAdapter.getYear(oldActiveDate) != this._dateAdapter.getYear(this._activeDate)) {
this._init();
}
}
private _activeDate: D;
/** The currently selected date. */
@Input()
get selected(): D { return this._selected; }
set selected(value: D) {
this._selected = value;
this._selectedMonth = this._getMonthInCurrentYear(this.selected);
}
private _selected: D;
/** A function used to filter which dates are selectable. */
@Input() dateFilter: (date: D) => boolean;
/** Emits when a new month is selected. */
@Output() selectedChange = new EventEmitter<D>();
/** Grid of calendar cells representing the months of the year. */
_months: MdCalendarCell[][];
/** The label for this year (e.g. "2017"). */
_yearLabel: string;
/** The month in this year that today falls on. Null if today is in a different year. */
_todayMonth: number;
/**
* The month in this year that the selected Date falls on.
* Null if the selected Date is in a different year.
*/
_selectedMonth: number;
constructor(@Optional() public _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
if (!this._dateFormats) {
throw createMissingDateImplError('MD_DATE_FORMATS');
}
this._activeDate = this._dateAdapter.today();
}
ngAfterContentInit() {
this._init();
}
/** Handles when a new month is selected. */
_monthSelected(month: number) {
this.selectedChange.emit(this._dateAdapter.createDate(
this._dateAdapter.getYear(this.activeDate), month,
this._dateAdapter.getDate(this.activeDate)));
}
/** Initializes this month view. */
private _init() {
this._selectedMonth = this._getMonthInCurrentYear(this.selected);
this._todayMonth = this._getMonthInCurrentYear(this._dateAdapter.today());
this._yearLabel = this._dateAdapter.getYearName(this.activeDate);
let monthNames = this._dateAdapter.getMonthNames('short');
// 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, monthNames[month])));
}
/**
* Gets the month in this year that the given Date falls on.
* Returns null if the given Date is in another year.
*/
private _getMonthInCurrentYear(date: D) {
return date && this._dateAdapter.getYear(date) == this._dateAdapter.getYear(this.activeDate) ?
this._dateAdapter.getMonth(date) : null;
}
/** Creates an MdCalendarCell for the given month. */
private _createCellForMonth(month: number, monthName: string) {
let ariaLabel = this._dateAdapter.format(
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1),
this._dateFormats.display.monthYearA11yLabel);
return new MdCalendarCell(
month, monthName.toLocaleUpperCase(), ariaLabel, this._isMonthEnabled(month));
}
/** Whether the given month is enabled. */
private _isMonthEnabled(month: number) {
if (!this.dateFilter) {
return true;
}
let firstOfMonth = this._dateAdapter.createDate(
this._dateAdapter.getYear(this.activeDate), month, 1);
// If any date in the month is enabled count the month as enabled.
for (let date = firstOfMonth; this._dateAdapter.getMonth(date) == month;
date = this._dateAdapter.addCalendarDays(date, 1)) {
if (this.dateFilter(date)) {
return true;
}
}
return false;
}
}
+1 -1
View File
@@ -29,7 +29,7 @@
</div>
</div>
<div class="mat-input-underline"
<div class="mat-input-underline" #underline
[class.mat-disabled]="_mdInputChild.disabled">
<span class="mat-input-ripple"
[class.mat-accent]="color == 'accent'"
+4 -3
View File
@@ -32,9 +32,10 @@ $mat-input-underline-disabled-background-image:
}
// Allow icons in a prefix/suffix/hint/etc to adapt to the correct size.
& .mat-icon {
width: auto;
height: auto;
& .mat-icon,
& .mat-datepicker-toggle {
width: 1em;
height: 1em;
font-size: 100%;
vertical-align: top;
}
+3
View File
@@ -14,6 +14,7 @@ import {
QueryList,
Renderer2,
Self,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {animate, state, style, transition, trigger} from '@angular/animations';
@@ -334,6 +335,8 @@ export class MdInputContainer implements AfterViewInit, AfterContentInit {
}
private _floatPlaceholder: FloatPlaceholderType = 'auto';
@ViewChild('underline') underlineRef: ElementRef;
@ContentChild(MdInputDirective) _mdInputChild: MdInputDirective;
@ContentChild(MdPlaceholder) _placeholderChild: MdPlaceholder;
+3 -1
View File
@@ -35,6 +35,7 @@ import {MdDialogModule} from './dialog/index';
import {PlatformModule} from './core/platform/index';
import {MdAutocompleteModule} from './autocomplete/index';
import {StyleModule} from './core/style/index';
import {MdDatepickerModule} from './datepicker/index';
const MATERIAL_MODULES = [
MdAutocompleteModule,
@@ -43,6 +44,7 @@ const MATERIAL_MODULES = [
MdCardModule,
MdChipsModule,
MdCheckboxModule,
MdDatepickerModule,
MdDialogModule,
MdGridListModule,
MdIconModule,
@@ -68,7 +70,7 @@ const MATERIAL_MODULES = [
A11yModule,
PlatformModule,
MdCommonModule,
ObserveContentModule
ObserveContentModule,
];
/** @deprecated */
+1
View File
@@ -13,6 +13,7 @@ export * from './button-toggle/index';
export * from './card/index';
export * from './chips/index';
export * from './checkbox/index';
export * from './datepicker/index';
export * from './dialog/index';
export * from './grid-list/index';
export * from './icon/index';
+1 -1
View File
@@ -29,7 +29,7 @@
"unit-case": "lower",
"unit-no-unknown": true,
"unit-whitelist": ["px", "%", "deg", "ms", "em", "vh", "vw"],
"unit-whitelist": ["px", "%", "deg", "ms", "em", "vh", "vw", "vmin"],
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never",