import { Component , Input , Output , EventEmitter , ViewEncapsulation , forwardRef , ViewChild , TemplateRef , OnDestroy , ElementRef , ChangeDetectionStrategy , ChangeDetectorRef , HostBinding , OnChanges , SimpleChanges } from '@angular/core' ; import { ControlValueAccessor , FormControl , NG_VALIDATORS , NG_VALUE_ACCESSOR , ValidationErrors , Validator } from '@angular/forms' ; import momentTimezone from 'moment-timezone' ; import { Clipboard } from '@angular/cdk/clipboard' ; import { DialogService } from '../dialog/dialog.service' ; import { DateTimeType } from './date-time-type.enum' ; import { Datelike } from './date-like.type' ; import { InputComponent } from '../input/input.component' ; import { NotificationService } from '../notification/notification.service' ; import { NotificationStyleType } from '../notification/notification-style-type.enum' ; import { CoerceBooleanProperty } from '../../utils/coerce/coerce-boolean' ; import { CoerceNumberProperty } from '../../utils/coerce/coerce-number' ; import { Size } from '../../mixins/size/size.enum' ; import { Appearance } from '../../mixins/appearance/appearance.enum' ; import { DATE_DISPLAY_FORMATS , DATE_DISPLAY_INPUT_FORMATS , DATE_DISPLAY_TYPES } from '../../enums/date-formats.enum' ; import { KeyboardKeys } from '../../enums/keyboard-keys.enum' ; import { CalendarComponent } from '../calendar/calendar.component' ; import { defaultDisplayFormat , defaultInputFormat } from '../../utils/date-formats/default-formats' ; let nextId = 0 ; const MIN_WIDTH = 60 ; const DATE_TIME_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => DateTimeComponent ) , multi : true } ; const DATE_TIME_VALIDATORS = { provide : NG_VALIDATORS , useExisting : forwardRef ( ( ) => DateTimeComponent ) , multi : true } ; const guessTimeZone = momentTimezone . tz . guess ( ) ; @ Component ( { exportAs : 'ngxDateTime' , selector : 'ngx-date-time' , templateUrl : './date-time.component.html' , styleUrls : [ './date-time.component.scss' ] , providers : [ DATE_TIME_VALUE_ACCESSOR , DATE_TIME_VALIDATORS ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , host : { class : 'ngx-date-time' , '[class.legacy]' : 'appearance === "legacy"' , '[class.fill]' : 'appearance === "fill"' , '[class.sm]' : 'size === "sm"' , '[class.md]' : 'size === "md"' , '[class.lg]' : 'size === "lg"' , '[class.autosize]' : 'autosize' , '[class.marginless]' : '!withMargin' , '[class.no-label]' : '!label' } } ) export class DateTimeComponent implements OnDestroy , OnChanges , ControlValueAccessor , Validator { @ Input ( ) id = `datetime-${ ++ nextId }` ; @ Input ( ) name : string ; @ Input ( ) label : string ; @ Input ( ) hint : string ; @ Input ( ) placeholder = '' ; @ Input ( ) size : Size = Size . Small ; @ Input ( ) appearance : Appearance = Appearance . Legacy ; @ Input ( ) withMargin = true ; @ Input ( ) precision : moment . unitOfTime . StartOf ; @ Input ( ) timezone : string ; @ Input ( ) inputFormats : any [ ] = DATE_DISPLAY_INPUT_FORMATS ; @ Input ( ) set value ( val ) { if ( typeof val === 'string' ) { val = val . trim ( ) ; } if ( ! val && ! this . _value ) { val = this . _value = null ; } let isSame = val === this . _value ; if ( isSame ) return ; let isDate = false ; const date = this . parseDate ( val ) ; if ( val && date . isValid ( ) ) { isDate = true ; if ( this . _value instanceof Date ) { const sameDiff = this . precision ? this . precision : this . inputType === DateTimeType . date ? 'day' : 'second' ; isSame = this . _value ? date . isSame ( this . _value , sameDiff ) : false ; } } this . _value = isDate ? date . toDate ( ) : val ; if ( ! isSame ) { this . update ( ) ; if ( ! this . dateInvalid ) { this . change . emit ( this . _value ) ; } this . onChangeCallback ( this . _value ) ; this . valueChange . emit ( val ) ; } } get value ( ) : Date | string { return this . _value ; } @ Input ( ) @ CoerceBooleanProperty ( ) disabled = false ; @ CoerceNumberProperty ( ) minWidth : number = MIN_WIDTH ; @ Input ( ) @ CoerceNumberProperty ( ) tabindex : number ; @ Input ( ) @ CoerceBooleanProperty ( ) autofocus = false ; @ Input ( ) set inputType ( val ) { this . _inputType = val ; } get inputType ( ) : string { if ( this . _inputType ) return this . _inputType ; if ( this . precision === 'hour' || this . precision === 'minute' ) return DateTimeType . datetime ; return DateTimeType . date ; } /**
     * Display mode for date/time
     * 'timezone' - display date/time with a timezone
     * 'local' - display date/time without timezone
     *
     * Defaults to LOCAL unless timezone is set
     */ @ Input ( ) set displayMode ( val ) { this . _displayMode = val ; } get displayMode ( ) : DATE_DISPLAY_TYPES { if ( typeof this . _displayMode === 'string' ) { return this . _displayMode ; } return this . timezone ? DATE_DISPLAY_TYPES . TIMEZONE : DATE_DISPLAY_TYPES . LOCAL ; } /**
     * Display format for date/time
     * Considers if mode is user (has timezone), local (no timezone)
     */ @ Input ( ) set format ( val ) { this . _format = val ; } get format ( ) : string { if ( this . _format ) return DATE_DISPLAY_FORMATS [ this . _format ] || this . _format ; return defaultInputFormat ( this . displayMode , this . inputType as DateTimeType , this . precision ) ; } @ Input ( ) set tooltipFormat ( val ) { this . _tooltipFormat = val ; } get tooltipFormat ( ) : string { if ( this . _tooltipFormat ) return DATE_DISPLAY_FORMATS [ this . _tooltipFormat ] || this . _tooltipFormat ; if ( this . _format ) return DATE_DISPLAY_FORMATS [ this . _format ] || this . _format ; return defaultDisplayFormat ( this . displayMode , this . inputType as DateTimeType , this . precision ) ; } @ Input ( ) @ CoerceBooleanProperty ( ) tooltipDisabled = false ; @ HostBinding ( 'class.ngx-date-time--has-popup' ) get hasPopup ( ) { return ! ! this . value && ! this . dateInvalid && DATE_DISPLAY_TYPES . LOCAL !== this . displayMode ; } @ HostBinding ( 'class.ngx-date-time--date-invalid' ) dateInvalid = false ; @ HostBinding ( 'class.ngx-date-time--date-out-of-range' ) dateOutOfRange = false ; /**
     * Used to display date in other timezones
     *
     * Only used if displayMode is 'user' or timezone is set
     */ @ Input ( ) timezones : Record < string , string > = { UTC : 'Etc/UTC' , Local : '' } ; @ Input ( ) tooltipCssClass = 'date-tip-tooltip' ; @ Input ( ) set clipFormat ( val ) { this . _clipFormat = val ; } get clipFormat ( ) : string { if ( this . _clipFormat ) return DATE_DISPLAY_FORMATS [ this . _clipFormat ] || this . _clipFormat ; return this . format ; } @ Input ( ) requiredIndicator : string | boolean = '*' ; @ Input ( ) @ CoerceBooleanProperty ( ) required = false ; get displayValue ( ) : string { return this . _displayValue ; } set displayValue ( value ) { this . _displayValue = value ; this . cdr . markForCheck ( ) ; } @ Input ( ) @ CoerceBooleanProperty ( ) autosize = false ; @ Input ( ) minDate : Date | string ; @ Input ( ) maxDate : Date | string ; /**
     * this output will emit only when the input value is valid or cleared.
     */ @ Output ( ) change = new EventEmitter < string | Date | undefined | null > ( ) ; /**
     * this output will emit anytime the value changes regardless of validity.
     */ @ Output ( ) valueChange = new EventEmitter < string | Date | undefined | null > ( ) ; /**
     * this output will emit anytime the value changes in the input, regardless of validity.
     */ @ Output ( ) inputChange = new EventEmitter < string | Date | undefined | null > ( ) ; /**
     * this output will emit a date is selected from the calendar
     */ @ Output ( ) dateTimeSelected = new EventEmitter < Date | string > ( ) ; @ Output ( ) blur = new EventEmitter < Event > ( ) ; @ ViewChild ( 'dialogTpl' , { static : true } ) readonly calendarTpl : TemplateRef < ElementRef > ; @ ViewChild ( 'input' , { static : true } ) readonly input : InputComponent ; @ ViewChild ( CalendarComponent , { static : false } ) readonly calendar : CalendarComponent ; dialog : any ; dialogModel : moment . Moment ; hour : number ; minute : string ; amPmVal : string ; modes = [ 'millisecond' , 'second' , 'minute' , 'hour' , 'date' , 'month' , 'year' ] ; timeValues = { } ; private _value : Date | string ; private _displayValue = '' ; private _format : string ; private _tooltipFormat : string ; private _inputType : string ; private _displayMode : DATE_DISPLAY_TYPES ; private _clipFormat : string ; constructor ( private readonly dialogService , private readonly cdr , private readonly clipboard , private readonly notificationService ) { } ngOnDestroy ( ) { this . close ( ) ; } ngOnChanges ( changes ) { if ( 'value' in changes && ! changes . value . firstChange ) return ; this . update ( ) ; } writeValue ( val ) { this . value = val ; } onBlur ( _event ? ) { this . onTouchedCallback ( ) ; this . update ( ) ; if ( ! this . dateInvalid && this . input . value !== this . displayValue ) { this . input . value = this . displayValue ; } this . blur . emit ( event ) ; } open ( ) { const value = momentTimezone ( this . _value ) ; const isValid = value . isValid ( ) ; this . setDialogDate ( isValid ? value : new Date ( ) ) ; this . dialog = this . dialogService . create ( { cssClass : 'ngx-date-time-dialog' , template : this . calendarTpl , closeButton : false } ) ; } apply ( ) { this . value = this . dialogModel . toDate ( ) ; this . update ( ) ; this . dateTimeSelected . emit ( this . value ) ; this . close ( ) ; } setDialogDate ( date ) { this . dialogModel = this . createMoment ( date ) ; this . hour = + this . dialogModel . format ( 'hh' ) ; this . minute = this . dialogModel . format ( 'mm' ) ; this . amPmVal = this . dialogModel . format ( 'A' ) ; } minuteChanged ( newVal ) { this . dialogModel = this . dialogModel . clone ( ) . minute ( newVal ) ; this . minute = this . dialogModel . format ( 'mm' ) ; } hourChanged ( newVal ) { newVal = + newVal % 12 ; if ( this . amPmVal === 'PM' ) { newVal = 12 + newVal ; } this . dialogModel = this . dialogModel . clone ( ) . hour ( newVal ) ; this . hour = + this . dialogModel . format ( 'hh' ) ; } selectCurrent ( ) { this . setDialogDate ( new Date ( ) ) ; } isCurrent ( ) { const now = this . createMoment ( new Date ( ) ) ; if ( this . inputType === 'time' ) { return now . hour ( ) === this . dialogModel . hour ( ) && now . minute ( ) === this . dialogModel . minute ( ) ; } return now . isSame ( this . dialogModel , 'minute' ) ; } clear ( ) { this . value = undefined ; this . update ( ) ; this . dateTimeSelected . emit ( this . value ) ; this . close ( ) ; } onAmPmChange ( newVal ) { const clone = this . dialogModel . clone ( ) ; if ( newVal === 'AM' && this . amPmVal === 'PM' ) { this . dialogModel = clone . subtract ( 12 , 'h' ) ; } else if ( newVal === 'PM' && this . amPmVal === 'AM' ) { this . dialogModel = clone . add ( 12 , 'h' ) ; } this . amPmVal = this . dialogModel . format ( 'A' ) ; } getDayDisabled ( date ) { if ( ! date ) return false ; const isBeforeMin = this . minDate && date . isBefore ( this . parseDate ( this . minDate ) ) ; const isAfterMax = this . maxDate && date . isAfter ( this . parseDate ( this . maxDate ) ) ; return isBeforeMin || isAfterMax ; } isTimeDisabled ( mode ) { return this . modes . indexOf ( `${ this . precision }` ) > this . modes . indexOf ( `${ mode }` ) ; } inputChanged ( val ) { this . value = val ; this . inputChange . emit ( val ) ; this . displayValue = val ; } close ( ) { if ( ! this . dialog ) return ; this . dialogService . destroy ( this . dialog ) ; this . update ( ) ; } registerOnChange ( fn ) { this . onChangeCallback = fn ; } registerOnTouched ( fn ) { this . onTouchedCallback = fn ; } onClick ( item ) { this . clipboard . copy ( item . value . clip ) ; this . notificationService . create ( { body : `${ item . key } date copied to clipboard` , styleType : NotificationStyleType . success , timeout : 3000 } ) ; } validate ( c ) { if ( ! c . value ) return null ; return { ... ( this . dateInvalid ? { invalid : true } : null ) , ... ( this . dateOutOfRange ? { outOfRange : true } : null ) } ; } setDisabledState ( isDisabled ) { this . disabled = isDisabled ; } onTouchedCallback : ( ) => void = ( ) => { } ; onChangeCallback : ( _ : any ) => void = ( ) => { } ; onInputKeyDown ( event ) { if ( event . code === KeyboardKeys . ARROW_DOWN ) { this . open ( ) ; setTimeout ( ( ) => { this . calendar . focus ( ) ; } , 200 ) ; } else if ( event . code === KeyboardKeys . ESCAPE ) { this . close ( ) ; } } private roundTo ( val , key ) { if ( ! key || ! val ) { return val ; } val = val . clone ( ) ; const idx = this . modes . indexOf ( key ) ; if ( idx > 0 ) { this . modes . forEach ( ( mode , index ) => { if ( index < idx ) { val = val [ mode ] ( mode === 'date' ? 1 : 0 ) ; } } ) ; } return val ; } private parseDate ( date ) { if ( date instanceof Date ) { date = isNaN ( date . getTime ( ) ) ? date . toString ( ) : date . toISOString ( ) ; } const inputFormats = [ ... this . inputFormats ] ; if ( this . format && ! inputFormats . includes ( this . format ) ) { inputFormats . unshift ( this . format ) ; } const timezone = this . timezone || ( this . displayMode === DATE_DISPLAY_TYPES . TIMEZONE ? guessTimeZone : undefined ) ; let m = timezone ? momentTimezone . tz ( date , inputFormats , timezone ) : momentTimezone ( date , inputFormats ) ; m = this . precision ? this . roundTo ( m , this . precision ) : m ; return m ; } private createMoment ( date ) { let m = momentTimezone ( date ) . clone ( ) ; const timezone = this . timezone || ( this . displayMode === DATE_DISPLAY_TYPES . TIMEZONE ? guessTimeZone : undefined ) ; m = timezone ? m . tz ( timezone ) : m ; m = this . precision ? this . roundTo ( m , this . precision ) : m ; return m ; } private update ( ) { const isDate = this . value instanceof Date ; this . dateInvalid = ! ! this . value && ! isDate ; this . displayValue = ! this . value ? '' : String ( this . value ) ; this . dateOutOfRange = false ; this . timeValues = { } ; if ( ! isDate ) return ; const mdate = this . createMoment ( this . value ) ; this . displayValue = mdate . format ( this . format ) ; this . dateOutOfRange = ! this . dateInvalid && this . getDayDisabled ( mdate ) ; if ( ! this . hasPopup ) return ; for ( const key in this . timezones ) { const tz = this . timezones [ key ] || guessTimeZone ; const date = mdate . clone ( ) . tz ( tz ) ; const clip = date . format ( this . clipFormat ) ; const display = date . format ( this . tooltipFormat ) ; this . timeValues [ key ] = { key , clip , display } ; } } }