import { Component , Input , ChangeDetectionStrategy , ElementRef , Renderer2 , TemplateRef , forwardRef , ChangeDetectorRef , Output , EventEmitter , ViewChild , OnInit , Inject , OnChanges , SimpleChanges , OnDestroy , Optional , NgZone , LOCALE_ID } from '@angular/core' ; import { NG_VALUE_ACCESSOR , ControlValueAccessor , NG_VALIDATORS , Validator , AbstractControl , ValidationErrors } from '@angular/forms' ; import { CdkConnectedOverlay , ConnectionPositionPair } from '@angular/cdk/overlay' ; import { FocusTrapFactory , FocusTrap } from '@angular/cdk/a11y' ; import { DOWN_ARROW , UP_ARROW } from '@angular/cdk/keycodes' ; import { BehaviorSubject } from 'rxjs' ; import { take } from 'rxjs/operators' ; import { uniqueId } from '../../util/util' ; import { InputBoolean , toBoolean } from '../../util/convert' ; import { HostService } from '../../common/host/host.service' ; import { NglDateAdapter } from '../adapters/date-fns-adapter' ; import { NGL_DATEPICKER_CONFIG , NglDatepickerConfig } from '../config' ; import { DEFAULT_DROPDOWN_POSITIONS } from '../../util/overlay-position' ; import { parseDate , isDisabled } from '../util' ; import { IDatepickerInput } from './datepicker-input.interface' ; const NGL_DATEPICKER_INPUT_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => NglDatepickerInput ) , multi : true } ; const NGL_DATEPICKER_INPUT_VALIDATOR = { provide : NG_VALIDATORS , useExisting : forwardRef ( ( ) => NglDatepickerInput ) , multi : true } ; @ Component ( { selector : 'ngl-datepicker-input' , templateUrl : './datepicker-input.html' , changeDetection : ChangeDetectionStrategy . OnPush , providers : [ NGL_DATEPICKER_INPUT_VALUE_ACCESSOR , NGL_DATEPICKER_INPUT_VALIDATOR , HostService ] , } ) export class NglDatepickerInput implements ControlValueAccessor , Validator , OnInit , OnChanges , OnDestroy { /**
     * Label that appears above the input.
     */ @ Input ( ) label : string | TemplateRef < any > ; /**
     * Pre-defined format to use.
     */ @ Input ( ) format : 'big-endian' | 'little-endian' | 'middle-endian' ; /**
     * Delimiter to use on pre-defined formats.
     */ @ Input ( ) delimiter ; /**
     * Disable input and calendar.
     */ @ Input ( ) @ InputBoolean ( ) disabled : boolean ; /**
     * Aligns the right or left side of the dropdown menu with the respective side of the input.
     */ @ Input ( ) dropdownAlign : 'left' | 'right' ; /**
     * The date value.
     */ @ Input ( ) set value ( value ) { if ( value === this . _value ) { return ; } this . _value = value ; if ( this . value instanceof Date ) { this . date = this . value ; this . formatInputValue ( ) ; } else { this . updateInputValue ( < string > value || '' ) ; } } get value ( ) : Date | string | null { return this . _value ; } /**
     * Whether to open the datepicker when a mouse user clicks on the input.
     */ @ Input ( ) @ InputBoolean ( ) openOnInputClick : boolean ; /**
     * Emits when selected date changes.
     */ @ Output ( ) valueChange = new EventEmitter < Date | string | null > ( ) ; inputEl : IDatepickerInput ; @ ViewChild ( 'cdkOverlay' ) cdkOverlay : CdkConnectedOverlay ; /**
     * The minimum valid date.
     */ @ Input ( ) min : Date ; /**
     * The maximum valid date.
     */ @ Input ( ) max : Date ; @ Input ( ) set required ( required ) { this . isRequired = toBoolean ( required ) ; } isRequired : Boolean ; /**
     * Text for button to open calendar.
     */ @ Input ( ) selectDateLabel = 'Select a date' ; /**
     * Whether to use the accepted pattern as placeholder.
     */ @ Input ( ) @ InputBoolean ( ) patternPlaceholder : boolean ; /**
     * Datepicker inputs
     */ @ Input ( ) monthNames : ReadonlyArray < string > ; @ Input ( ) dayNamesShort : ReadonlyArray < string > ; @ Input ( ) dayNamesLong : ReadonlyArray < string > ; @ Input ( ) firstDayOfWeek : number ; @ Input ( ) @ InputBoolean ( ) showToday : boolean ; @ Input ( ) dateDisabled : ( date : Date ) => boolean | null = null ; @ Input ( ) relativeYearFrom : number ; @ Input ( ) relativeYearTo : number ; @ Input ( ) todayLabel : string ; @ Input ( ) previousMonthLabel : string ; @ Input ( ) nextMonthLabel : string ; date : Date ; uid = uniqueId ( 'datepicker-input' ) ; overlayPositions : ConnectionPositionPair [ ] ; set open ( open ) { this . _open . next ( open ) ; } get open ( ) { return this . _open . value ; } private _open = new BehaviorSubject ( false ) ; private _value : Date | string | null = null ; private pattern : string ; private config : NglDatepickerConfig ; private focusTrap : FocusTrap ; constructor ( @ Optional ( ) @ Inject ( NGL_DATEPICKER_CONFIG ) defaultConfig , @ Inject ( LOCALE_ID ) locale , private element , private renderer , private cd , private hostService , private ngZone , private focusTrapFactory , private adapter ) { this . renderer . addClass ( this . element . nativeElement , 'slds-form-element' ) ; this . renderer . addClass ( this . element . nativeElement , 'slds-dropdown-trigger' ) ; this . renderer . addClass ( this . element . nativeElement , 'slds-dropdown-trigger_click' ) ; this . config = { ... new NglDatepickerConfig ( locale ) , ... defaultConfig } ; this . format = this . config . format ; this . delimiter = this . config . delimiter ; this . setPositions ( this . config . dropdownAlign ) ; this . monthNames = this . config . monthNames ; this . dayNamesShort = this . config . dayNamesShort ; this . dayNamesLong = this . config . dayNamesLong ; this . firstDayOfWeek = this . config . firstDayOfWeek ; this . showToday = this . config . showToday ; this . relativeYearFrom = this . config . relativeYearFrom ; this . relativeYearTo = this . config . relativeYearTo ; this . openOnInputClick = this . config . openOnInputClick ; this . todayLabel = this . config . todayLabel ; this . previousMonthLabel = this . config . previousMonthLabel ; this . nextMonthLabel = this . config . nextMonthLabel ; this . patternPlaceholder = this . config . patternPlaceholder ; } onChange : Function | null = null ; onTouched = ( ) => { } ; validatorChange = ( ) => { } ; validate ( c ) { const value = c . value ; if ( ! value ) { return null ; } if ( ! ( this . value instanceof Date ) ) { return { 'nglDatepickerInput' : { invalid : c . value } } ; } const date = parseDate ( value ) ; if ( isDisabled ( date , this . dateDisabled , parseDate ( this . min ) , parseDate ( this . max ) ) ) { return { 'nglDatepickerInput' : { disabled : c . value } } ; } return null ; } writeValue ( value ) { this . value = value ; this . cd . markForCheck ( ) ; } registerOnChange ( fn ) { this . onChange = fn ; } registerOnTouched ( fn ) { this . onTouched = fn ; } registerOnValidatorChange ( fn ) { this . validatorChange = fn ; } setDisabledState ( disabled ) { this . disabled = disabled ; } onBlur ( ) { if ( this . value instanceof Date ) { this . updateInputValue ( ) ; } this . onTouched ( ) ; } ngOnInit ( ) { this . _open . subscribe ( ( ) => { this . setHostClass ( ) ; this . cd . markForCheck ( ) ; } ) ; } ngOnChanges ( changes ) { if ( changes . format || changes . delimiter ) { this . setPattern ( ) ; if ( this . value instanceof Date ) { this . updateInputValue ( ) ; } } if ( changes . dropdownAlign ) { this . setPositions ( this . dropdownAlign ) ; } if ( changes . min || changes . max ) { this . validatorChange ( ) ; } if ( ( changes . patternPlaceholder || changes . format || changes . delimiter ) && this . patternPlaceholder ) { this . inputEl . setPlaceholder ( this . getPattern ( ) . toLocaleUpperCase ( ) ) ; } if ( changes . disabled ) { this . inputEl . setDisabled ( this . disabled ) ; } } ngOnDestroy ( ) { this . closeCalendar ( false ) ; } onKeyboardInput ( evt ) { const keyCode = evt . keyCode ; if ( ! this . open && ( keyCode === DOWN_ARROW || keyCode === UP_ARROW ) ) { this . openCalendar ( ) ; } } onInputChange ( ) { const value = this . inputEl . element . nativeElement . value ; const date = this . dateParse ( value ) ; this . emitSelection ( date || value ) ; } openCalendar ( ) { this . open = true ; } onAttach ( ) { this . focusTrap = this . focusTrapFactory . create ( this . cdkOverlay . overlayRef . overlayElement ) ; } onDetach ( ) { if ( this . open ) { this . closeCalendar ( ) ; } } closeCalendar ( focusInput = true ) { this . open = false ; if ( this . focusTrap ) { this . focusTrap . destroy ( ) ; this . focusTrap = null ; } if ( focusInput ) { this . inputEl . element . nativeElement . focus ( ) ; } } onTriggerClick ( origin ) { if ( origin === 'input' && ! this . openOnInputClick ) { return ; } if ( ! this . open ) { this . openCalendar ( ) ; } else { this . closeCalendar ( false ) ; } } pickerSelection ( date ) { this . emitSelection ( date ) ; this . closeCalendar ( ) ; } updateDatepickerSize ( width , height ) { this . ngZone . onStable . asObservable ( ) . pipe ( take ( 1 ) ) . subscribe ( ( ) => { const { overlayRef } = this . cdkOverlay ; overlayRef . updateSize ( { minWidth : width , minHeight : height + 4 , } ) ; overlayRef . updatePosition ( ) ; } ) ; } private setPositions ( align ) { this . overlayPositions = [ ... DEFAULT_DROPDOWN_POSITIONS [ align ] ] ; } private formatInputValue ( ) { const inputValue = this . inputEl . element . nativeElement . value ; if ( ! inputValue ) { this . updateInputValue ( ) ; } else { const date = this . value as Date ; const dateNow = this . dateParse ( inputValue ) ; if ( ! dateNow || dateNow . getFullYear ( ) !== date . getFullYear ( ) || dateNow . getMonth ( ) !== date . getMonth ( ) || dateNow . getDate ( ) !== date . getDate ( ) ) { this . updateInputValue ( ) ; } } } private updateInputValue ( value = this . dateFormat ( < Date > this . value ) ) { this . renderer . setProperty ( this . inputEl . element . nativeElement , 'value' , value || '' ) ; } private dateParse ( value ) { return this . adapter . parse ( value , this . getPattern ( ) ) ; } private dateFormat ( date ) { return this . adapter . format ( date , this . getPattern ( ) ) ; } private getPattern ( ) { if ( ! this . pattern ) { this . setPattern ( ) ; } return this . pattern ; } private setPattern ( ) { this . pattern = this . adapter . pattern ( this . format || this . config . format , this . delimiter || this . config . delimiter ) ; } private emitSelection ( value ) { this . valueChange . emit ( value ) ; if ( this . onChange ) { this . value = value ; this . onChange ( value ) ; } } private setHostClass ( ) { this . hostService . updateClass ( this . element , { [ `slds-is-open` ] : this . open , } ) ; } }