@@ -27,20 +27,21 @@ import {
2727 Inject ,
2828 Input ,
2929 NgZone ,
30+ OnChanges ,
3031 OnDestroy ,
3132 Optional ,
3233 Output ,
33- ViewEncapsulation ,
34- ViewChild ,
35- OnChanges ,
3634 SimpleChanges ,
35+ ViewChild ,
36+ ViewEncapsulation ,
3737} from '@angular/core' ;
3838import { DateAdapter , MAT_DATE_FORMATS , MatDateFormats } from '@angular/material/core' ;
3939import { take } from 'rxjs/operators/take' ;
4040import { Subscription } from 'rxjs/Subscription' ;
4141import { createMissingDateImplError } from './datepicker-errors' ;
4242import { MatDatepickerIntl } from './datepicker-intl' ;
4343import { MatMonthView } from './month-view' ;
44+ import { MatMultiYearView , yearsPerPage , yearsPerRow } from './multi-year-view' ;
4445import { MatYearView } from './year-view' ;
4546
4647
@@ -73,7 +74,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
7374 private _startAt : D | null ;
7475
7576 /** Whether the calendar should be started in month or year view. */
76- @Input ( ) startView : 'month' | 'year' = 'month' ;
77+ @Input ( ) startView : 'month' | 'year' | 'multi-year' = 'month' ;
7778
7879 /** The currently selected date. */
7980 @Input ( )
@@ -114,7 +115,10 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
114115 /** Reference to the current year view component. */
115116 @ViewChild ( MatYearView ) yearView : MatYearView < D > ;
116117
117- /** Date filter for the month and year views. */
118+ /** Reference to the current multi-year view component. */
119+ @ViewChild ( MatMultiYearView ) multiYearView : MatMultiYearView < D > ;
120+
121+ /** Date filter for the month, year, and multi-year views. */
118122 _dateFilterForViews = ( date : D ) => {
119123 return ! ! date &&
120124 ( ! this . dateFilter || this . dateFilter ( date ) ) &&
@@ -133,28 +137,46 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
133137 private _clampedActiveDate : D ;
134138
135139 /** Whether the calendar is in month view. */
136- _monthView : boolean ;
140+ _currentView : 'month' | 'year' | 'multi-year' ;
137141
138142 /** The label for the current calendar view. */
139143 get _periodButtonText ( ) : string {
140- return this . _monthView ?
141- this . _dateAdapter . format ( this . _activeDate , this . _dateFormats . display . monthYearLabel )
142- . toLocaleUpperCase ( ) :
143- this . _dateAdapter . getYearName ( this . _activeDate ) ;
144+ if ( this . _currentView == 'month' ) {
145+ return this . _dateAdapter . format ( this . _activeDate , this . _dateFormats . display . monthYearLabel )
146+ . toLocaleUpperCase ( ) ;
147+ }
148+ if ( this . _currentView == 'year' ) {
149+ return this . _dateAdapter . getYearName ( this . _activeDate ) ;
150+ }
151+ let curYear = this . _dateAdapter . getYear ( this . _activeDate ) ;
152+ let firstYear = this . _dateAdapter . getYearName (
153+ this . _dateAdapter . createDate ( curYear - curYear % 24 , 0 , 1 ) ) ;
154+ let lastYear = this . _dateAdapter . getYearName (
155+ this . _dateAdapter . createDate ( curYear + yearsPerPage - 1 - curYear % 24 , 0 , 1 ) ) ;
156+ return `${ firstYear } \u2013 ${ lastYear } ` ;
144157 }
145158
146159 get _periodButtonLabel ( ) : string {
147- return this . _monthView ? this . _intl . switchToYearViewLabel : this . _intl . switchToMonthViewLabel ;
160+ return this . _currentView == 'month' ?
161+ this . _intl . switchToMultiYearViewLabel : this . _intl . switchToMonthViewLabel ;
148162 }
149163
150164 /** The label for the the previous button. */
151165 get _prevButtonLabel ( ) : string {
152- return this . _monthView ? this . _intl . prevMonthLabel : this . _intl . prevYearLabel ;
166+ return {
167+ 'month' : this . _intl . prevMonthLabel ,
168+ 'year' : this . _intl . prevYearLabel ,
169+ 'multi-year' : this . _intl . prevMultiYearLabel
170+ } [ this . _currentView ] ;
153171 }
154172
155173 /** The label for the the next button. */
156174 get _nextButtonLabel ( ) : string {
157- return this . _monthView ? this . _intl . nextMonthLabel : this . _intl . nextYearLabel ;
175+ return {
176+ 'month' : this . _intl . nextMonthLabel ,
177+ 'year' : this . _intl . nextYearLabel ,
178+ 'multi-year' : this . _intl . nextMultiYearLabel
179+ } [ this . _currentView ] ;
158180 }
159181
160182 constructor ( private _elementRef : ElementRef ,
@@ -178,7 +200,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
178200 ngAfterContentInit ( ) {
179201 this . _activeDate = this . startAt || this . _dateAdapter . today ( ) ;
180202 this . _focusActiveCell ( ) ;
181- this . _monthView = this . startView != 'year' ;
203+ this . _currentView = this . startView ;
182204 }
183205
184206 ngOnDestroy ( ) {
@@ -189,7 +211,7 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
189211 const change = changes . minDate || changes . maxDate || changes . dateFilter ;
190212
191213 if ( change && ! change . firstChange ) {
192- const view = this . monthView || this . yearView ;
214+ const view = this . monthView || this . yearView || this . multiYearView ;
193215
194216 if ( view ) {
195217 view . _init ( ) ;
@@ -208,29 +230,37 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
208230 this . _userSelection . emit ( ) ;
209231 }
210232
233+ /** Handles month selection in the multi-year view. */
234+ _yearSelected ( year : D ) : void {
235+ this . _activeDate = year ;
236+ this . _currentView = 'year' ;
237+ }
238+
211239 /** Handles month selection in the year view. */
212240 _monthSelected ( month : D ) : void {
213241 this . _activeDate = month ;
214- this . _monthView = true ;
242+ this . _currentView = 'month' ;
215243 }
216244
217245 /** Handles user clicks on the period label. */
218246 _currentPeriodClicked ( ) : void {
219- this . _monthView = ! this . _monthView ;
247+ this . _currentView = this . _currentView == 'month' ? 'multi-year' : 'month' ;
220248 }
221249
222250 /** Handles user clicks on the previous button. */
223251 _previousClicked ( ) : void {
224- this . _activeDate = this . _monthView ?
252+ this . _activeDate = this . _currentView == 'month' ?
225253 this . _dateAdapter . addCalendarMonths ( this . _activeDate , - 1 ) :
226- this . _dateAdapter . addCalendarYears ( this . _activeDate , - 1 ) ;
254+ this . _dateAdapter . addCalendarYears (
255+ this . _activeDate , this . _currentView == 'year' ? - 1 : - yearsPerPage ) ;
227256 }
228257
229258 /** Handles user clicks on the next button. */
230259 _nextClicked ( ) : void {
231- this . _activeDate = this . _monthView ?
260+ this . _activeDate = this . _currentView == 'month' ?
232261 this . _dateAdapter . addCalendarMonths ( this . _activeDate , 1 ) :
233- this . _dateAdapter . addCalendarYears ( this . _activeDate , 1 ) ;
262+ this . _dateAdapter . addCalendarYears (
263+ this . _activeDate , this . _currentView == 'year' ? 1 : yearsPerPage ) ;
234264 }
235265
236266 /** Whether the previous period button is enabled. */
@@ -251,10 +281,12 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
251281 // TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent
252282 // disabled ones from being selected. This may not be ideal, we should look into whether
253283 // navigation should skip over disabled dates, and if so, how to implement that efficiently.
254- if ( this . _monthView ) {
284+ if ( this . _currentView == 'month' ) {
255285 this . _handleCalendarBodyKeydownInMonthView ( event ) ;
256- } else {
286+ } else if ( this . _currentView == 'year' ) {
257287 this . _handleCalendarBodyKeydownInYearView ( event ) ;
288+ } else {
289+ this . _handleCalendarBodyKeydownInMultiYearView ( event ) ;
258290 }
259291 }
260292
@@ -269,10 +301,15 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
269301
270302 /** Whether the two dates represent the same view in the current view mode (month or year). */
271303 private _isSameView ( date1 : D , date2 : D ) : boolean {
272- return this . _monthView ?
273- this . _dateAdapter . getYear ( date1 ) == this . _dateAdapter . getYear ( date2 ) &&
274- this . _dateAdapter . getMonth ( date1 ) == this . _dateAdapter . getMonth ( date2 ) :
275- this . _dateAdapter . getYear ( date1 ) == this . _dateAdapter . getYear ( date2 ) ;
304+ if ( this . _currentView == 'month' ) {
305+ return this . _dateAdapter . getYear ( date1 ) == this . _dateAdapter . getYear ( date2 ) &&
306+ this . _dateAdapter . getMonth ( date1 ) == this . _dateAdapter . getMonth ( date2 )
307+ }
308+ if ( this . _currentView == 'year' ) {
309+ return this . _dateAdapter . getYear ( date1 ) == this . _dateAdapter . getYear ( date2 ) ;
310+ }
311+ return Math . floor ( this . _dateAdapter . getYear ( date1 ) / yearsPerPage ) ==
312+ Math . floor ( this . _dateAdapter . getYear ( date2 ) / yearsPerPage ) ;
276313 }
277314
278315 /** Handles keydown events on the calendar body when calendar is in month view. */
@@ -337,10 +374,10 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
337374 this . _activeDate = this . _dateAdapter . addCalendarMonths ( this . _activeDate , 1 ) ;
338375 break ;
339376 case UP_ARROW :
340- this . _activeDate = this . _prevMonthInSameCol ( this . _activeDate ) ;
377+ this . _activeDate = this . _dateAdapter . addCalendarMonths ( this . _activeDate , - 4 ) ;
341378 break ;
342379 case DOWN_ARROW :
343- this . _activeDate = this . _nextMonthInSameCol ( this . _activeDate ) ;
380+ this . _activeDate = this . _dateAdapter . addCalendarMonths ( this . _activeDate , 4 ) ;
344381 break ;
345382 case HOME :
346383 this . _activeDate = this . _dateAdapter . addCalendarMonths ( this . _activeDate ,
@@ -371,6 +408,52 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
371408 event . preventDefault ( ) ;
372409 }
373410
411+ /** Handles keydown events on the calendar body when calendar is in multi-year view. */
412+ private _handleCalendarBodyKeydownInMultiYearView ( event : KeyboardEvent ) : void {
413+ switch ( event . keyCode ) {
414+ case LEFT_ARROW :
415+ this . _activeDate = this . _dateAdapter . addCalendarYears ( this . _activeDate , - 1 ) ;
416+ break ;
417+ case RIGHT_ARROW :
418+ this . _activeDate = this . _dateAdapter . addCalendarYears ( this . _activeDate , 1 ) ;
419+ break ;
420+ case UP_ARROW :
421+ this . _activeDate = this . _dateAdapter . addCalendarYears ( this . _activeDate , - yearsPerRow ) ;
422+ break ;
423+ case DOWN_ARROW :
424+ this . _activeDate = this . _dateAdapter . addCalendarYears ( this . _activeDate , yearsPerRow ) ;
425+ break ;
426+ case HOME :
427+ this . _activeDate = this . _dateAdapter . addCalendarYears ( this . _activeDate ,
428+ - this . _dateAdapter . getYear ( this . _activeDate ) % yearsPerPage ) ;
429+ break ;
430+ case END :
431+ this . _activeDate = this . _dateAdapter . addCalendarYears ( this . _activeDate ,
432+ yearsPerPage - this . _dateAdapter . getYear ( this . _activeDate ) % yearsPerPage - 1 ) ;
433+ break ;
434+ case PAGE_UP :
435+ this . _activeDate =
436+ this . _dateAdapter . addCalendarYears (
437+ this . _activeDate , event . altKey ? - yearsPerPage * 10 : - yearsPerPage ) ;
438+ break ;
439+ case PAGE_DOWN :
440+ this . _activeDate =
441+ this . _dateAdapter . addCalendarYears (
442+ this . _activeDate , event . altKey ? yearsPerPage * 10 : yearsPerPage ) ;
443+ break ;
444+ case ENTER :
445+ this . _yearSelected ( this . _activeDate ) ;
446+ break ;
447+ default :
448+ // Don't prevent default or focus active cell on keys that we don't explicitly handle.
449+ return ;
450+ }
451+
452+ this . _focusActiveCell ( ) ;
453+ // Prevent unexpected default actions such as form submission.
454+ event . preventDefault ( ) ;
455+ }
456+
374457 /**
375458 * Determine the date for the month that comes before the given month in the same column in the
376459 * calendar table.
0 commit comments