import { Directive , Input , ComponentRef , ElementRef , ViewContainerRef , ChangeDetectorRef , ComponentFactoryResolver , forwardRef , EventEmitter , Output , SimpleChanges , OnChanges , HostListener , OnDestroy } from "@angular/core" ; import { AbstractControl , ControlValueAccessor , NG_VALIDATORS , NG_VALUE_ACCESSOR , Validator } from "@angular/forms" ; import { IMyDate , IMyOptions , IMyDateModel , IMyCalendarViewChanged , IMyInputFieldChanged , IMySelectorPosition } from "./interfaces/index" ; import { NgxMyDatePicker } from "./ngx-my-date-picker.component" ; import { UtilService } from "./services/ngx-my-date-picker.util.service" ; import { NgxMyDatePickerConfig } from "./services/ngx-my-date-picker.config" ; import { CalToggle } from "./enums/cal-toggle.enum" ; import { Year } from "./enums/year.enum" ; import { KeyCode } from "./enums/key-code.enum" ; const NGX_DP_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => NgxMyDatePickerDirective ) , multi : true } ; const NGX_DP_VALIDATORS = { provide : NG_VALIDATORS , useExisting : forwardRef ( ( ) => NgxMyDatePickerDirective ) , multi : true } ; @ Directive ( { selector : "[ngx-mydatepicker]" , exportAs : "ngx-mydatepicker" , providers : [ UtilService , NGX_DP_VALUE_ACCESSOR , NGX_DP_VALIDATORS ] } ) export class NgxMyDatePickerDirective implements OnChanges , OnDestroy , ControlValueAccessor , Validator { @ Input ( ) options : IMyOptions ; @ Input ( ) defaultMonth : string ; @ Output ( ) dateChanged : EventEmitter < IMyDateModel > = new EventEmitter < IMyDateModel > ( ) ; @ Output ( ) inputFieldChanged : EventEmitter < IMyInputFieldChanged > = new EventEmitter < IMyInputFieldChanged > ( ) ; @ Output ( ) calendarViewChanged : EventEmitter < IMyCalendarViewChanged > = new EventEmitter < IMyCalendarViewChanged > ( ) ; @ Output ( ) calendarToggle : EventEmitter < number > = new EventEmitter < number > ( ) ; private cRef : ComponentRef < NgxMyDatePicker > = null ; private inputText : string = "" ; private preventClose : boolean = false ; private disabled = false ; private opts : IMyOptions ; onChangeCb : ( _ : any ) => void = ( ) => { } ; onTouchedCb : ( ) => void = ( ) => { } ; constructor ( private utilService , private vcRef , private cfr , private cdr , private elem , private config ) { this . opts = Object . assign ( { } , config ) ; this . parseOptions ( config ) ; } @ HostListener ( "keyup" , [ "$event" ] ) onKeyUp ( evt ) { if ( this . ignoreKeyPress ( evt . keyCode ) ) { return ; } else if ( evt . keyCode === KeyCode . esc ) { this . closeSelector ( CalToggle . CloseByEsc ) ; } else { let date = this . utilService . isDateValid ( this . elem . nativeElement . value , this . opts . dateFormat , this . opts . minYear , this . opts . maxYear , this . opts . disableUntil , this . opts . disableSince , this . opts . disableWeekends , this . opts . disableDates , this . opts . disableDateRanges , this . opts . disableWeekdays , this . opts . monthLabels , this . opts . enableDates ) ; if ( this . utilService . isInitializedDate ( date ) ) { let dateModel = this . utilService . getDateModel ( date , this . opts . dateFormat , this . opts . monthLabels , this . elem . nativeElement . value ) ; this . emitDateChanged ( dateModel ) ; this . updateModel ( dateModel ) ; this . emitInputFieldChanged ( dateModel . formatted , true ) ; if ( this . opts . closeSelectorOnDateSelect ) { this . closeSelector ( CalToggle . CloseByDateSel ) ; } else if ( this . cRef !== null ) { this . cRef . instance . setCalendarView ( date ) ; } } else { if ( this . inputText !== this . elem . nativeElement . value ) { if ( this . elem . nativeElement . value === "" ) { this . clearDate ( ) ; } else { this . onChangeCb ( null ) ; this . emitInputFieldChanged ( this . elem . nativeElement . value , false ) ; } } } this . inputText = this . elem . nativeElement . value ; } } @ HostListener ( "blur" ) onBlur ( ) { this . onTouchedCb ( ) ; } private onClickWrapper = ( ev ) => { this . onClick ( ev ) ; } ; onClick ( evt ) { if ( this . opts . closeSelectorOnDocumentClick && ! this . preventClose && evt . target && this . cRef !== null && this . elem . nativeElement !== evt . target && ! this . cRef . location . nativeElement . contains ( evt . target ) && ! this . disabled ) { this . closeSelector ( CalToggle . CloseByOutClick ) ; } } public ngOnChanges ( changes ) { if ( changes . hasOwnProperty ( "options" ) ) { this . parseOptions ( changes [ "options" ] . currentValue ) ; } if ( changes . hasOwnProperty ( "defaultMonth" ) ) { let dm = changes [ "defaultMonth" ] . currentValue ; if ( typeof dm === "object" ) { dm = dm . defMonth ; } this . defaultMonth = dm ; } } public ngOnDestroy ( ) { this . closeCalendar ( ) ; document . removeEventListener ( "click" , this . onClickWrapper ) ; } public parseOptions ( opts ) { if ( opts !== undefined ) { Object . keys ( opts ) . forEach ( ( k ) => { ( < IMyOptions > this . opts ) [ k ] = opts [ k ] ; } ) ; } if ( this . opts . minYear < Year . min ) { this . opts . minYear = Year . min ; } if ( this . opts . maxYear > Year . max ) { this . opts . maxYear = Year . max ; } } public writeValue ( value ) { if ( ! this . disabled ) { if ( value && ( value [ "date" ] || value [ "jsdate" ] ) ) { let formatted = this . utilService . formatDate ( value [ "date" ] ? value [ "date" ] : this . jsDateToMyDate ( value [ "jsdate" ] ) , this . opts . dateFormat , this . opts . monthLabels ) ; let date = this . utilService . isDateValid ( formatted , this . opts . dateFormat , this . opts . minYear , this . opts . maxYear , this . opts . disableUntil , this . opts . disableSince , this . opts . disableWeekends , this . opts . disableDates , this . opts . disableDateRanges , this . opts . disableWeekdays , this . opts . monthLabels , this . opts . enableDates ) ; this . setInputValue ( formatted ) ; this . emitInputFieldChanged ( formatted , this . utilService . isInitializedDate ( date ) ) ; } else if ( value === null || value === "" ) { this . setInputValue ( "" ) ; this . emitInputFieldChanged ( "" , false ) ; } } } public registerOnChange ( fn ) { this . onChangeCb = fn ; } public registerOnTouched ( fn ) { this . onTouchedCb = fn ; } public setDisabledState ( isDisabled ) { this . disabled = isDisabled ; const { nativeElement } = this . elem ; if ( isDisabled ) { nativeElement . setAttribute ( "disabled" , "" ) ; } else { nativeElement . removeAttribute ( "disabled" ) ; } if ( isDisabled ) { this . closeCalendar ( ) ; } } public validate ( c ) { if ( this . elem . nativeElement . value === null || this . elem . nativeElement . value === "" ) { return null ; } let date = this . utilService . isDateValid ( this . elem . nativeElement . value , this . opts . dateFormat , this . opts . minYear , this . opts . maxYear , this . opts . disableUntil , this . opts . disableSince , this . opts . disableWeekends , this . opts . disableDates , this . opts . disableDateRanges , this . opts . disableWeekdays , this . opts . monthLabels , this . opts . enableDates ) ; if ( ! this . utilService . isInitializedDate ( date ) ) { return { invalidDateFormat : true } ; } return null ; } public openCalendar ( ) { if ( this . disabled ) { return ; } this . preventClose = true ; this . cdr . detectChanges ( ) ; if ( this . cRef === null ) { this . cRef = this . vcRef . createComponent ( this . cfr . resolveComponentFactory ( NgxMyDatePicker ) ) ; this . appendSelector ( this . cRef . location . nativeElement ) ; this . cRef . instance . initialize ( this . opts , this . defaultMonth , this . getSelectorPosition ( this . elem . nativeElement ) , this . elem . nativeElement . value , ( dm , close ) => { this . focusToInput ( ) ; this . emitDateChanged ( dm ) ; this . emitInputFieldChanged ( dm . formatted , true ) ; this . updateModel ( dm ) ; if ( close ) { this . closeSelector ( CalToggle . CloseByDateSel ) ; } } , ( cvc ) => { this . emitCalendarChanged ( cvc ) ; } , ( ) => { this . closeSelector ( CalToggle . CloseByEsc ) ; } ) ; this . emitCalendarToggle ( CalToggle . Open ) ; document . addEventListener ( "click" , this . onClickWrapper ) ; } setTimeout ( ( ) => { this . preventClose = false ; } , 50 ) ; } public closeCalendar ( ) { this . closeSelector ( CalToggle . CloseByCalBtn ) ; } public toggleCalendar ( ) { if ( this . disabled ) { return ; } if ( this . cRef === null ) { this . openCalendar ( ) ; } else { this . closeSelector ( CalToggle . CloseByCalBtn ) ; } } public clearDate ( ) { if ( this . disabled ) { return ; } this . setInputValue ( "" ) ; this . emitInputFieldChanged ( "" , false ) ; this . emitDateChanged ( { date : { year : 0 , month : 0 , day : 0 } , jsdate : null , formatted : "" , epoc : 0 } ) ; this . onChangeCb ( null ) ; this . onTouchedCb ( ) ; this . closeSelector ( CalToggle . CloseByCalBtn ) ; } public isDateValid ( ) { if ( this . elem . nativeElement . value !== "" ) { let date = this . utilService . isDateValid ( this . elem . nativeElement . value , this . opts . dateFormat , this . opts . minYear , this . opts . maxYear , this . opts . disableUntil , this . opts . disableSince , this . opts . disableWeekends , this . opts . disableDates , this . opts . disableDateRanges , this . opts . disableWeekdays , this . opts . monthLabels , this . opts . enableDates ) ; if ( this . utilService . isInitializedDate ( date ) ) { this . emitInputFieldChanged ( this . elem . nativeElement . value , true ) ; return true ; } } this . emitInputFieldChanged ( this . elem . nativeElement . value , false ) ; return false ; } private ignoreKeyPress ( keyCode ) { return keyCode === KeyCode . leftArrow || keyCode === KeyCode . rightArrow || keyCode === KeyCode . upArrow || keyCode === KeyCode . downArrow || keyCode === KeyCode . tab || keyCode === KeyCode . shift ; } private closeSelector ( reason ) { if ( this . cRef !== null ) { this . vcRef . remove ( this . vcRef . indexOf ( this . cRef . hostView ) ) ; this . cRef = null ; this . emitCalendarToggle ( reason ) ; document . removeEventListener ( "click" , this . onClickWrapper ) ; } } private updateModel ( model ) { this . setInputValue ( model . formatted ) ; this . onChangeCb ( model ) ; this . onTouchedCb ( ) ; } private setInputValue ( value ) { this . inputText = value ; this . elem . nativeElement . setAttribute ( "value" , value ) ; } private focusToInput ( ) { if ( this . opts . focusInputOnDateSelect ) { setTimeout ( ( ) => { this . elem . nativeElement . focus ( ) ; } ) ; } } private emitDateChanged ( dateModel ) { this . dateChanged . emit ( dateModel ) ; } private emitInputFieldChanged ( value , valid ) { this . inputFieldChanged . emit ( { value : value , dateFormat : this . opts . dateFormat , valid : valid } ) ; } private emitCalendarChanged ( cvc ) { this . calendarViewChanged . emit ( cvc ) ; } private emitCalendarToggle ( reason ) { this . calendarToggle . emit ( reason ) ; } private jsDateToMyDate ( date ) { return { year : date . getFullYear ( ) , month : date . getMonth ( ) + 1 , day : date . getDate ( ) } ; } private appendSelector ( elem ) { if ( this . opts . appendSelectorToBody ) { document . querySelector ( "body" ) . appendChild ( elem ) ; } } private getSelectorPosition ( elem ) { let top = 0 ; let left = 0 ; if ( this . opts . appendSelectorToBody ) { let b = document . body . getBoundingClientRect ( ) ; let e = elem . getBoundingClientRect ( ) ; top = e . top - b . top ; left = e . left - b . left ; } if ( this . opts . openSelectorTopOfInput ) { top = top - this . getSelectorDimension ( this . opts . selectorHeight ) - 2 ; } else { top = top + elem . offsetHeight + ( this . opts . showSelectorArrow ? 12 : 2 ) ; } if ( this . opts . alignSelectorRight ) { left = left + elem . offsetWidth - this . getSelectorDimension ( this . opts . selectorWidth ) ; } return { top : top + "px" , left : left + "px" } ; } private getSelectorDimension ( value ) { return Number ( value . replace ( "px" , "" ) ) ; } }