license import { Directionality } from '@angular/cdk/bidi' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { ESCAPE , UP_ARROW } from '@angular/cdk/keycodes' ; import { Overlay , OverlayConfig , OverlayRef , PositionStrategy , ScrollStrategy , } from '@angular/cdk/overlay' ; import { ComponentPortal , ComponentType } from '@angular/cdk/portal' ; import { DOCUMENT } from '@angular/common' ; import { AfterViewInit , ChangeDetectionStrategy , Component , ComponentRef , ElementRef , EventEmitter , Inject , InjectionToken , Input , NgZone , OnDestroy , Optional , Output , ViewChild , ViewContainerRef , ViewEncapsulation , } from '@angular/core' ; import { CanColor , CanColorCtor , DateAdapter , mixinColor , ThemePalette , } from '@angular/material/core' ; import { MatDialog , MatDialogRef } from '@angular/material/dialog' ; import { merge , Subject , Subscription } from 'rxjs' ; import { filter , take } from 'rxjs/operators' ; import { MatCalendar } from './calendar' ; import { matDatepickerAnimations } from './datepicker-animations' ; import { createMissingDateImplError } from './datepicker-errors' ; import { MatDatepickerInput } from './datepicker-input' ; import { MatCalendarCellCssClasses } from './calendar-body' ; /** Used to generate a unique ID for each datepicker instance. */ let datepickerUid = 0 ; /** Injection token that determines the scroll handling while the calendar is open. */ export const MAT_DATEPICKER_SCROLL_STRATEGY = new InjectionToken < ( ) => ScrollStrategy > ( 'mat-datepicker-scroll-strategy' ) ; docs-private export function MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY ( overlay ) { return ( ) => overlay . scrollStrategies . reposition ( ) ; } docs-private export const MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER = { provide : MAT_DATEPICKER_SCROLL_STRATEGY , deps : [ Overlay ] , useFactory : MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY , } ; docs-private export class MatDatepickerContentBase { constructor ( public _elementRef ) { } } export const _MatDatepickerContentMixinBase = mixinColor ( MatDatepickerContentBase ) ; docs-private @ Component ( { moduleId : module . id , selector : 'mat-datepicker-content' , templateUrl : 'datepicker-content.html' , styleUrls : [ 'datepicker-content.css' ] , host : { 'class' : 'mat-datepicker-content' , '[@transformPanel]' : '"enter"' , '[class.mat-datepicker-content-touch]' : 'datepicker.touchUi' , } , animations : [ matDatepickerAnimations . transformPanel , matDatepickerAnimations . fadeInCalendar , ] , exportAs : 'matDatepickerContent' , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , inputs : [ 'color' ] , } ) export class MatDatepickerContent < D > extends _MatDatepickerContentMixinBase implements AfterViewInit , CanColor { /** Reference to the internal calendar component. */ @ ViewChild ( MatCalendar ) _calendar : MatCalendar < D > ; /** Reference to the datepicker that created the overlay. */ datepicker : MatDatepicker < D > ; /** Whether the datepicker is above or below the input. */ _isAbove : boolean ; constructor ( elementRef ) { super ( elementRef ) ; } ngAfterViewInit ( ) { this . _calendar . focusActiveCell ( ) ; } } /** Component responsible for managing the datepicker popup/dialog. */ @ Component ( { moduleId : module . id , selector : 'mat-datepicker' , template : '' , exportAs : 'matDatepicker' , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , } ) export class MatDatepicker < D > implements OnDestroy , CanColor { private _scrollStrategy : ( ) => ScrollStrategy ; /** An input indicating the type of the custom header component for the calendar, if set. */ @ Input ( ) calendarHeaderComponent : ComponentType < any > ; /** The date to open the calendar to initially. */ @ Input ( ) get startAt ( ) : D | null { return this . _startAt || ( this . _datepickerInput ? this . _datepickerInput . value : null ) ; } set startAt ( value ) { this . _startAt = this . _getValidDateOrNull ( this . _dateAdapter . deserialize ( value ) ) ; } private _startAt : D | null ; /** The view that the calendar should start in. */ @ Input ( ) startView : 'month' | 'year' | 'multi-year' = 'month' ; /** Color palette to use on the datepicker's calendar. */ @ Input ( ) get color ( ) : ThemePalette { return this . _color || ( this . _datepickerInput ? this . _datepickerInput . _getThemePalette ( ) : undefined ) ; } set color ( value ) { this . _color = value ; } _color : ThemePalette ; /**
     * 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 ( ) get touchUi ( ) : boolean { return this . _touchUi ; } set touchUi ( value ) { this . _touchUi = coerceBooleanProperty ( value ) ; } private _touchUi = false ; /** Whether the datepicker pop-up should be disabled. */ @ Input ( ) get disabled ( ) : boolean { return this . _disabled === undefined && this . _datepickerInput ? this . _datepickerInput . disabled : ! ! this . _disabled ; } set disabled ( value ) { const newValue = coerceBooleanProperty ( value ) ; if ( newValue !== this . _disabled ) { this . _disabled = newValue ; this . _disabledChange . next ( newValue ) ; } } private _disabled : boolean ; /**
     * Emits selected year in multiyear view.
     * This doesn't imply a change on the selected date.
     */ @ Output ( ) readonly yearSelected : EventEmitter < D > = new EventEmitter < D > ( ) ; /**
     * Emits selected month in year view.
     * This doesn't imply a change on the selected date.
     */ @ Output ( ) readonly monthSelected : EventEmitter < D > = new EventEmitter < D > ( ) ; /** Classes to be passed to the date picker panel. Supports the same syntax as `ngClass`. */ @ Input ( ) panelClass : string | string [ ] ; /** Function that can be used to add custom CSS classes to dates. */ @ Input ( ) dateClass : ( date : D ) => MatCalendarCellCssClasses ; /** Emits when the datepicker has been opened. */ @ Output ( 'opened' ) openedStream : EventEmitter < void > = new EventEmitter < void > ( ) ; /** Emits when the datepicker has been closed. */ @ Output ( 'closed' ) closedStream : EventEmitter < void > = new EventEmitter < void > ( ) ; /** Whether the calendar is open. */ @ Input ( ) get opened ( ) : boolean { return this . _opened ; } set opened ( value ) { value ? this . open ( ) : this . close ( ) ; } private _opened = false ; /** The id for the datepicker calendar. */ id : string = `mat-datepicker-${ datepickerUid ++ }` ; /** The currently selected date. */ get _selected ( ) : D | null { return this . _validSelected ; } set _selected ( value ) { this . _validSelected = value ; } private _validSelected : D | null = null ; /** The minimum selectable date. */ get _minDate ( ) : D | null { return this . _datepickerInput && this . _datepickerInput . min ; } /** The maximum selectable date. */ get _maxDate ( ) : D | null { 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. */ _popupRef : OverlayRef ; /** A reference to the dialog when the calendar is opened as a dialog. */ private _dialogRef : MatDialogRef < MatDatepickerContent < D > > | null ; /** A portal containing the calendar for this datepicker. */ private _calendarPortal : ComponentPortal < MatDatepickerContent < D > > ; /** Reference to the component instantiated in popup mode. */ private _popupComponentRef : ComponentRef < MatDatepickerContent < D > > | null ; /** The element that was focused before the datepicker was opened. */ private _focusedElementBeforeOpen : HTMLElement | null = null ; /** Subscription to value changes in the associated input element. */ private _inputSubscription = Subscription . EMPTY ; /** The input element this datepicker is associated with. */ _datepickerInput : MatDatepickerInput < D > ; /** Emits when the datepicker is disabled. */ readonly _disabledChange = new Subject < boolean > ( ) ; /** Emits new selected date when selected date changes. */ readonly _selectedChanged = new Subject < D > ( ) ; constructor ( private _dialog , private _overlay , private _ngZone , private _viewContainerRef , @ Inject ( MAT_DATEPICKER_SCROLL_STRATEGY ) scrollStrategy , @ Optional ( ) private _dateAdapter , @ Optional ( ) private _dir , @ Optional ( ) @ Inject ( DOCUMENT ) private _document ) { if ( ! this . _dateAdapter ) { throw createMissingDateImplError ( 'DateAdapter' ) ; } this . _scrollStrategy = scrollStrategy ; } ngOnDestroy ( ) { this . close ( ) ; this . _inputSubscription . unsubscribe ( ) ; this . _disabledChange . complete ( ) ; if ( this . _popupRef ) { this . _popupRef . dispose ( ) ; this . _popupComponentRef = null ; } } /** Selects the given date */ select ( date ) { let oldValue = this . _selected ; this . _selected = date ; if ( ! this . _dateAdapter . sameDate ( oldValue , this . _selected ) ) { this . _selectedChanged . next ( date ) ; } } /** Emits the selected year in multiyear view */ _selectYear ( normalizedYear ) { this . yearSelected . emit ( normalizedYear ) ; } /** Emits selected month in year view */ _selectMonth ( normalizedMonth ) { this . monthSelected . emit ( normalizedMonth ) ; } param input _registerInput ( input ) { if ( this . _datepickerInput ) { throw Error ( 'A MatDatepicker can only be associated with a single input.' ) ; } this . _datepickerInput = input ; this . _inputSubscription = this . _datepickerInput . _valueChange . subscribe ( ( value ) => this . _selected = value ) ; } /** Open the calendar. */ open ( ) { if ( this . _opened || this . disabled ) { return ; } if ( ! this . _datepickerInput ) { throw Error ( 'Attempted to open an MatDatepicker with no associated input.' ) ; } if ( this . _document ) { this . _focusedElementBeforeOpen = this . _document . activeElement ; } this . touchUi ? this . _openAsDialog ( ) : this . _openAsPopup ( ) ; this . _opened = true ; this . openedStream . emit ( ) ; } /** Close the calendar. */ close ( ) { 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 ( ) ; } const completeClose = ( ) => { if ( this . _opened ) { this . _opened = false ; this . closedStream . emit ( ) ; this . _focusedElementBeforeOpen = null ; } } ; if ( this . _focusedElementBeforeOpen && typeof this . _focusedElementBeforeOpen . focus === 'function' ) { this . _focusedElementBeforeOpen . focus ( ) ; setTimeout ( completeClose ) ; } else { completeClose ( ) ; } } /** Open the calendar as a dialog. */ private _openAsDialog ( ) { if ( this . _dialogRef ) { this . _dialogRef . close ( ) ; } this . _dialogRef = this . _dialog . open < MatDatepickerContent < D > > ( MatDatepickerContent , { direction : this . _dir ? this . _dir . value : 'ltr' , viewContainerRef : this . _viewContainerRef , panelClass : 'mat-datepicker-dialog' , } ) ; this . _dialogRef . afterClosed ( ) . subscribe ( ( ) => this . close ( ) ) ; this . _dialogRef . componentInstance . datepicker = this ; this . _setColor ( ) ; } /** Open the calendar as a popup. */ private _openAsPopup ( ) { if ( ! this . _calendarPortal ) { this . _calendarPortal = new ComponentPortal < MatDatepickerContent < D > > ( MatDatepickerContent , this . _viewContainerRef ) ; } if ( ! this . _popupRef ) { this . _createPopup ( ) ; } if ( ! this . _popupRef . hasAttached ( ) ) { this . _popupComponentRef = this . _popupRef . attach ( this . _calendarPortal ) ; this . _popupComponentRef . instance . datepicker = this ; this . _setColor ( ) ; this . _ngZone . onStable . asObservable ( ) . pipe ( take ( 1 ) ) . subscribe ( ( ) => { this . _popupRef . updatePosition ( ) ; } ) ; } } /** Create the popup. */ private _createPopup ( ) { const overlayConfig = new OverlayConfig ( { positionStrategy : this . _createPopupPositionStrategy ( ) , hasBackdrop : true , backdropClass : 'mat-overlay-transparent-backdrop' , direction : this . _dir , scrollStrategy : this . _scrollStrategy ( ) , panelClass : 'mat-datepicker-popup' , } ) ; this . _popupRef = this . _overlay . create ( overlayConfig ) ; this . _popupRef . overlayElement . setAttribute ( 'role' , 'dialog' ) ; merge ( this . _popupRef . backdropClick ( ) , this . _popupRef . detachments ( ) , this . _popupRef . keydownEvents ( ) . pipe ( filter ( event => { return event . keyCode === ESCAPE || ( this . _datepickerInput && event . altKey && event . keyCode === UP_ARROW ) ; } ) ) ) . subscribe ( ( ) => this . close ( ) ) ; } /** Create the popup PositionStrategy. */ private _createPopupPositionStrategy ( ) { return this . _overlay . position ( ) . flexibleConnectedTo ( this . _datepickerInput . getConnectedOverlayOrigin ( ) ) . withTransformOriginOn ( '.mat-datepicker-content' ) . withFlexibleDimensions ( false ) . withViewportMargin ( 8 ) . withLockedPosition ( ) . withPositions ( [ { originX : 'start' , originY : 'bottom' , overlayX : 'start' , overlayY : 'top' } , { originX : 'start' , originY : 'top' , overlayX : 'start' , overlayY : 'bottom' } , { originX : 'end' , originY : 'bottom' , overlayX : 'end' , overlayY : 'top' } , { originX : 'end' , originY : 'top' , overlayX : 'end' , overlayY : 'bottom' } ] ) ; } param obj returns private _getValidDateOrNull ( obj ) { return ( this . _dateAdapter . isDateInstance ( obj ) && this . _dateAdapter . isValid ( obj ) ) ? obj : null ; } /** Passes the current theme color along to the calendar overlay. */ private _setColor ( ) { const color = this . color ; if ( this . _popupComponentRef ) { this . _popupComponentRef . instance . color = color ; } if ( this . _dialogRef ) { this . _dialogRef . componentInstance . color = color ; } } }