import { Directive, Input, ComponentRef, ElementRef, ViewContainerRef, Renderer2, ChangeDetectorRef, ComponentFactoryResolver, forwardRef, EventEmitter, Output, SimpleChanges, OnChanges, HostListener, OnDestroy, NgZone } from '@angular/core'; import { AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; import { CalendarComponent } from './components/calendar/calendar.component'; import { IMyOptions, IMyDate, IMyDateModel, IMyDateRange, IMyRangeDateSelection, IMyCalendarViewChanged, IMyInputFieldChanged, IMySelectorPosition } from './interfaces/public-api'; import { UtilService, DatepickerLocaleService, DefaultConfigService } from './services/public-api'; import { KeyCode, CalToggle, Year, ResetDateType } from './enums/public-api'; import { KEYUP, BLUR, EMPTY_STR, DISABLED, CLICK, BODY, VALUE, OPTIONS, DEFAULT_MONTH, LOCALE, OBJECT, PX } from './constants/constants'; const NGX_DP_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FarrisDatePickerDirective), multi: true }; const NGX_DP_VALIDATORS = { provide: NG_VALIDATORS, useExisting: forwardRef(() => FarrisDatePickerDirective), multi: true }; @Directive({ selector: '[farris-datepicker]', exportAs: 'farris-datepicker', providers: [ UtilService, DatepickerLocaleService, DefaultConfigService, NGX_DP_VALUE_ACCESSOR, NGX_DP_VALIDATORS ] }) export class FarrisDatePickerDirective implements OnChanges, OnDestroy, ControlValueAccessor, Validator { @Input() options: IMyOptions; @Input() locale: string; @Input() defaultMonth: string; @Output() dateChanged: EventEmitter = new EventEmitter< IMyDateModel >(); @Output() inputFieldChanged: EventEmitter< IMyInputFieldChanged > = new EventEmitter(); @Output() calendarViewChanged: EventEmitter< IMyCalendarViewChanged > = new EventEmitter(); @Output() calendarToggle: EventEmitter = new EventEmitter(); @Output() rangeDateSelection: EventEmitter< IMyRangeDateSelection > = new EventEmitter(); private cRef: ComponentRef = null; private inputText = ''; private preventClose = false; private disabled = false; private opts: IMyOptions; // scrollFn: any = (e) => { // console.log((e.target as any).scrollTop); // }; onChangeCb: (_: any) => void = () => {}; onTouchedCb: () => void = () => {}; constructor( public localeService: DatepickerLocaleService, private utilService: UtilService, private vcRef: ViewContainerRef, private cfr: ComponentFactoryResolver, private renderer: Renderer2, private cdr: ChangeDetectorRef, private elem: ElementRef, private config: DefaultConfigService, private zone: NgZone ) { this.opts = this.config.getDefaultConfig(); this.parseOptions(this.opts); } @HostListener(KEYUP, ['$event']) onKeyUp(event: KeyboardEvent) { const keyCode: number = this.utilService.getKeyCodeFromEvent(event); if (this.ignoreKeyPress(keyCode)) { return; } else if (keyCode === KeyCode.esc) { this.closeSelector(CalToggle.CloseByEsc); } else { const { value } = this.elem.nativeElement; let valid = false; if (!this.opts.dateRange) { const date: IMyDate = this.utilService.isDateValid( value, this.opts ); valid = this.utilService.isInitializedDate(date); } else { const { begin, end } = this.utilService.isDateValidDateRange( value, this.opts ); valid = this.utilService.isInitializedDate(begin) && this.utilService.isInitializedDate(end); } this.emitInputFieldChanged(value, valid); } } @HostListener(BLUR) onBlur() { const { inputFieldValidation, dateRange } = this.opts; if (inputFieldValidation) { const { value } = this.elem.nativeElement; let valid = false; if (!dateRange) { const date: IMyDate = this.utilService.isDateValid( value, this.opts ); valid = this.utilService.isInitializedDate(date); } else { const _dateRange: IMyDateRange = this.utilService.isDateValidDateRange( value, this.opts ); const { begin, end } = _dateRange; valid = this.utilService.isInitializedDate(begin) && this.utilService.isInitializedDate(end); } if (!valid && this.inputText !== value) { this.clearDate(); } this.onChangeCb(value); } this.onTouchedCb(); } // wrapper with arrow function to preserve the use of 'this' word private onClickWrapper = (ev: MouseEvent) => { this.onClick(ev); } onClick(evt: MouseEvent) { 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: SimpleChanges): void { if (changes.hasOwnProperty(LOCALE)) { this.setLocaleOptions(); } if (changes.hasOwnProperty(DEFAULT_MONTH)) { let dm: any = changes[DEFAULT_MONTH].currentValue; if (typeof dm === OBJECT) { dm = dm.defMonth; } this.defaultMonth = dm; } if (changes.hasOwnProperty(OPTIONS)) { this.parseOptions(changes[OPTIONS].currentValue); } } public ngOnDestroy(): void { this.closeCalendar(); } setLocaleOptions(): void { const opts: IMyOptions = this.localeService.getLocaleOptions( this.locale ); Object.keys(opts).forEach(k => { (this.opts as IMyOptions)[k] = opts[k]; }); } public parseOptions(opts: IMyOptions): void { if (opts !== undefined) { Object.keys(opts).forEach(k => { if (opts[k] !== undefined && opts[k] !== '') { (this.opts as IMyOptions)[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; } } formatDate(value: any) { const { dateRange, dateFormat, monthLabels, dateRangeDatesDelimiter, returnFormat } = this.opts; if (!dateRange) { // Single date mode const date: IMyDate = this.utilService.isDateValid( value, this.opts ); return this.utilService.getDateModel( date, null, dateFormat, monthLabels, dateRangeDatesDelimiter, returnFormat ); } else { // Date range mode const _dateRange = this.utilService.isDateValidDateRange( value, this.opts ); return this.utilService.getDateModel( null, _dateRange, dateFormat, monthLabels, dateRangeDatesDelimiter, returnFormat ); } } public writeValue(value: any): void { if (this.disabled) { return; } if (value) { value = this.formatDate(value); } if (!value) { this.setInputValue(EMPTY_STR); this.emitInputFieldChanged(EMPTY_STR, false); if (this.cRef !== null) { this.cRef.instance.resetDateValue(ResetDateType.both); } } else if (value.isRange === false && value.singleDate) { // single date const { date, formatted } = value.singleDate; const valid: boolean = this.utilService.isInitializedDate(date); if (valid) { this.setInputValue(formatted); this.emitInputFieldChanged(formatted, valid); if (this.cRef !== null) { this.cRef.instance.resetDateValue(ResetDateType.dateRange); } } } else if (value.isRange === true && value.dateRange) { // date range const { beginDate, endDate, formatted } = value.dateRange; if (beginDate && endDate) { const valid: boolean = this.utilService.isInitializedDate(beginDate) && this.utilService.isInitializedDate(endDate); if (valid) { this.setInputValue(formatted); this.emitInputFieldChanged(formatted, valid); if (this.cRef !== null) { this.cRef.instance.resetDateValue( ResetDateType.singleDate ); } } } } } public registerOnChange(fn: any): void { this.onChangeCb = fn; } public registerOnTouched(fn: any): void { this.onTouchedCb = fn; } public setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; this.renderer.setProperty( this.elem.nativeElement, DISABLED, isDisabled ); if (isDisabled) { this.closeCalendar(); } } public validate(c: AbstractControl): { [p: string]: any } { return null; } public openCalendar(): void { if (this.disabled) { return; } this.preventClose = true; this.cdr.detectChanges(); if (this.cRef === null) { this.cRef = this.vcRef.createComponent( this.cfr.resolveComponentFactory(CalendarComponent) ); this.appendSelector(this.cRef.location.nativeElement); this.cRef.instance.initialize( this.opts, this.defaultMonth, this.getSelectorPosition(this.elem.nativeElement), this.elem.nativeElement.value, (dm: IMyDateModel, close: boolean) => { this.focusToInput(); this.updateModel(dm); if (close) { this.closeSelector(CalToggle.CloseByDateSel); } this.emitDateChanged(dm); this.emitInputFieldChanged( this.utilService.getFormattedDate(dm), true ); }, (cvc: IMyCalendarViewChanged) => { this.emitCalendarChanged(cvc); }, (rds: IMyRangeDateSelection) => { this.emitRangeDateSelection(rds); }, () => { this.closeSelector(CalToggle.CloseByEsc); } ); this.emitCalendarToggle(CalToggle.Open); this.cdr.detectChanges(); } else { this.appendSelector(this.cRef.location.nativeElement); } this.preventClose = false; } public closeCalendar(): void { this.closeSelector(CalToggle.CloseByCalBtn); } public toggleCalendar(event: MouseEvent): void { event.stopPropagation(); if (this.disabled) { return; } if (this.cRef === null) { document.addEventListener(CLICK, this.onClickWrapper, true); this.openCalendar(); const container = document.querySelector('.date-overlay-container'); this.renderer.setStyle(container, 'pointer-events', 'auto'); } else { let flag = true; const container = document.querySelector('.date-overlay-container'); if (container && this.cRef.location) { if (container.hasChildNodes()) { container.childNodes.forEach(el => { if (el === this.cRef.location.nativeElement) { flag = false; } }); } } if (flag) { this.renderer.setStyle(container, 'pointer-events', 'auto'); this.openCalendar(); } else { document.removeEventListener(CLICK, this.onClickWrapper); this.closeSelector(CalToggle.CloseByCalBtn); this.renderer.setStyle(container, 'pointer-events', ''); } } } public clearDate(): void { if (this.disabled) { return; } this.setInputValue(EMPTY_STR); this.emitDateChanged({ isRange: this.opts.dateRange, singleDate: { date: this.utilService.resetDate(), jsDate: null, formatted: EMPTY_STR, epoc: 0 }, dateRange: { beginDate: this.utilService.resetDate(), beginJsDate: null, beginEpoc: 0, endDate: this.utilService.resetDate(), endJsDate: null, endEpoc: 0, formatted: EMPTY_STR } }); this.onChangeCb(null); this.onTouchedCb(); this.closeSelector(CalToggle.CloseByCalBtn); } public isDateValid(): boolean { const { value } = this.elem.nativeElement; if (value !== EMPTY_STR) { const date: IMyDate = this.utilService.isDateValid( value, this.opts ); if (this.utilService.isInitializedDate(date)) { this.emitInputFieldChanged(value, true); return true; } } this.emitInputFieldChanged(value, false); return false; } private ignoreKeyPress(keyCode: number): boolean { // keyCode === KeyCode.tab || return ( keyCode === KeyCode.leftArrow || keyCode === KeyCode.rightArrow || keyCode === KeyCode.upArrow || keyCode === KeyCode.downArrow || keyCode === KeyCode.shift ); } private closeSelector(reason: number): void { if (this.cRef !== null) { this.vcRef.remove(this.vcRef.indexOf(this.cRef.hostView)); this.cRef = null; this.cdr.detectChanges(); this.emitCalendarToggle(reason); const container = document.querySelector('.date-overlay-container'); this.renderer.setStyle(container, 'pointer-events', ''); } } private updateModel(model: IMyDateModel): void { this.setInputValue(this.utilService.getFormattedDate(model)); this.onChangeCb(this.utilService.getFormattedDate(model)); this.onTouchedCb(); } private setInputValue(value: string): void { this.inputText = value; this.renderer.setProperty(this.elem.nativeElement, VALUE, value); } private focusToInput(): void { if (this.opts.focusInputOnDateSelect) { this.elem.nativeElement.focus(); } } private emitDateChanged(dateModel: IMyDateModel): void { this.dateChanged.emit(dateModel); } private emitInputFieldChanged(value: string, valid: boolean): void { this.inputFieldChanged.emit({ value, dateFormat: this.opts.dateFormat, valid }); } private emitCalendarChanged(cvc: IMyCalendarViewChanged) { this.calendarViewChanged.emit(cvc); } private emitRangeDateSelection(rds: IMyRangeDateSelection) { this.rangeDateSelection.emit(rds); } private emitCalendarToggle(reason: number): void { this.calendarToggle.emit(reason); } private jsDateToMyDate(date: Date): IMyDate { return { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() }; } private appendSelector(elem: any): void { let container = document.querySelector('.date-overlay-container'); if (container) { if (container.hasChildNodes()) { container.childNodes.forEach(el => { if (el !== elem) { container.removeChild(el); } }); } } else { container = document.createElement('div'); container.classList.add('date-overlay-container'); container.classList.add('overlay-container'); document.body.appendChild(container); } container.appendChild(elem); } private getSelectorPosition(elem: any): IMySelectorPosition { let top = 0; let left = 0; let _selectorHeight = 0; let _selectorWidth = 0; const { selectorHeight, selectorWidth, showTime, dateRange } = this.opts; const b: any = document.body.getBoundingClientRect(); const e: any = elem.getBoundingClientRect(); top = e.top - b.top; left = e.left - b.left; let position = 'bottom'; if (dateRange) { _selectorWidth = this.getSelectorDimension(selectorWidth) * 2; } else { _selectorWidth = this.getSelectorDimension(selectorWidth); } if (showTime) { _selectorHeight = this.getSelectorDimension(selectorHeight) + 36; } else { _selectorHeight = this.getSelectorDimension(selectorHeight); } if (top + elem.offsetHeight + _selectorHeight > window.innerHeight && top - _selectorHeight - 2 > 0) { top = top - _selectorHeight - 2; position = 'top'; } else { top = top + elem.offsetHeight + 2; } if (left + _selectorWidth > b.width) { left = b.width - _selectorWidth - 15; } left = left > 0 ? left : 0; return { top , left, position }; } private getSelectorDimension(value: string): number { return Number(value.replace(PX, EMPTY_STR)); } }