import { AfterContentInit , Component , ElementRef , EventEmitter , HostListener , Input , OnDestroy , Optional , Output , Renderer , ViewEncapsulation } from '@angular/core' ; import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ; import { Config } from '../../config/config' ; import { Picker } from '../picker/picker' ; import { PickerController } from '../picker/picker-controller' ; import { PickerColumn } from '../picker/picker-options' ; import { Form } from '../../util/form' ; import { BaseInput } from '../../util/base-input' ; import { Item } from '../item/item' ; import { assert , clamp , isArray , isBlank , isObject , isPresent , isString } from '../../util/util' ; import { DateTimeData , LocaleData , compareDates , convertDataToISO , convertFormatToKey , dateDataSortValue , dateSortValue , dateValueRange , daysInMonth , getValueFromFormat , parseDate , parseTemplate , renderDateTime , renderTextFormat , updateDate , } from '../../util/datetime-util' ; name description NgModule usage demo @ Component ( { selector : 'ion-datetime' , template : '<div *ngIf="!_text" class="datetime-text datetime-placeholder">{{placeholder}}</div>' + '<div *ngIf="_text" class="datetime-text">{{_text}}</div>' + '<button aria-haspopup="true" ' + 'type="button" ' + '[id]="id" ' + 'ion-button="item-cover" ' + '[attr.aria-labelledby]="_labelId" ' + '[attr.aria-disabled]="_disabled" ' + 'class="item-cover">' + '</button>' , host : { '[class.datetime-disabled]' : '_disabled' } , providers : [ { provide : NG_VALUE_ACCESSOR , useExisting : DateTime , multi : true } ] , encapsulation : ViewEncapsulation . None , } ) export class DateTime extends BaseInput < DateTimeData > implements AfterContentInit , ControlValueAccessor , OnDestroy { _text : string = '' ; _min : DateTimeData ; _max : DateTimeData ; _locale : LocaleData = { } ; _picker : Picker ; input @ Input ( ) min : string ; input @ Input ( ) max : string ; input @ Input ( ) displayFormat : string ; input @ Input ( ) initialValue : string ; input @ Input ( ) pickerFormat : string ; input @ Input ( ) cancelText : string = 'Cancel' ; input @ Input ( ) doneText : string = 'Done' ; input @ Input ( ) yearValues : any ; input @ Input ( ) monthValues : any ; input @ Input ( ) dayValues : any ; input @ Input ( ) hourValues : any ; input @ Input ( ) minuteValues : any ; input @ Input ( ) monthNames : any ; input @ Input ( ) monthShortNames : any ; input @ Input ( ) dayNames : any ; input @ Input ( ) dayShortNames : any ; input @ Input ( ) pickerOptions : any = { } ; input @ Input ( ) placeholder : string = '' ; output @ Output ( ) ionCancel : EventEmitter < any > = new EventEmitter ( ) ; constructor ( form , config , elementRef , renderer , @ Optional ( ) item , @ Optional ( ) private _pickerCtrl ) { super ( config , elementRef , renderer , 'datetime' , { } , form , item , null ) ; } hidden ngAfterContentInit ( ) { [ 'monthNames' , 'monthShortNames' , 'dayNames' , 'dayShortNames' ] . forEach ( type => { ( < any > this ) . _locale [ type ] = convertToArrayOfStrings ( isPresent ( ( < any > this ) [ type ] ) ? ( < any > this ) [ type ] : this . _config . get ( type ) , type ) ; } ) ; this . _initialize ( ) ; } hidden _inputNormalize ( val ) { updateDate ( this . _value , val ) ; return this . _value ; } hidden _inputUpdated ( ) { super . _inputUpdated ( ) ; this . updateText ( ) ; } hidden _inputShouldChange ( ) { return true ; } hidden _inputChangeEvent ( ) { return this . value ; } hidden _inputNgModelEvent ( ) { return convertDataToISO ( this . value ) ; } @ HostListener ( 'click' , [ '$event' ] ) _click ( ev ) { ev . preventDefault ( ) ; ev . stopPropagation ( ) ; this . open ( ) ; } @ HostListener ( 'keyup.space' ) _keyup ( ) { this . open ( ) ; } hidden open ( ) { if ( this . isFocus ( ) || this . _disabled ) { return ; } console . debug ( 'datetime, open picker' ) ; const pickerOptions = { ... this . pickerOptions } ; const defaultButtons = [ { text : this . cancelText , role : 'cancel' , handler : ( ) => this . ionCancel . emit ( this ) } , { text : this . doneText , handler : ( data ) => this . value = data , } ] ; pickerOptions . buttons = ( pickerOptions . buttons || [ ] ) . concat ( defaultButtons ) ; const picker = this . _picker = this . _pickerCtrl . create ( pickerOptions ) ; picker . ionChange . subscribe ( ( ) => { this . validate ( ) ; picker . refresh ( ) ; } ) ; this . generate ( ) ; this . validate ( ) ; this . _fireFocus ( ) ; picker . present ( pickerOptions ) ; picker . onDidDismiss ( ( ) => { this . _fireBlur ( ) ; } ) ; } hidden generate ( ) { const picker = this . _picker ; let template = this . pickerFormat || this . displayFormat || DEFAULT_FORMAT ; if ( isPresent ( template ) ) { this . calcMinMax ( ) ; template = template . replace ( 'DDDD' , '{~}' ) . replace ( 'DDD' , '{~}' ) ; if ( template . indexOf ( 'D' ) === - 1 ) { template = template . replace ( '{~}' , 'D' ) ; } template = template . replace ( /{~}/g , '' ) ; parseTemplate ( template ) . forEach ( format => { let key = convertFormatToKey ( format ) ; let values ; if ( isPresent ( ( < any > this ) [ key + 'Values' ] ) ) { values = convertToArrayOfNumbers ( ( < any > this ) [ key + 'Values' ] , key ) ; } else { values = dateValueRange ( format , this . _min , this . _max ) ; } const column = { name : key , selectedIndex : 0 , options : values . map ( val => { return { value : val , text : renderTextFormat ( format , val , null , this . _locale ) , } ; } ) } ; const optValue = getValueFromFormat ( this . getValueOrDefault ( ) , format ) ; const selectedIndex = column . options . findIndex ( opt => opt . value === optValue ) ; if ( selectedIndex >= 0 ) { column . selectedIndex = selectedIndex ; } picker . addColumn ( column ) ; } ) ; const min = < any > this . _min ; const max = < any > this . _max ; const columns = this . _picker . getColumns ( ) ; [ 'month' , 'day' , 'hour' , 'minute' ] . filter ( name => ! columns . find ( column => column . name === name ) ) . forEach ( name => { min [ name ] = 0 ; max [ name ] = 0 ; } ) ; this . divyColumns ( ) ; } } hidden validateColumn ( name , index , min , max , lowerBounds , upperBounds ) { assert ( lowerBounds . length === 5 , 'lowerBounds length must be 5' ) ; assert ( upperBounds . length === 5 , 'upperBounds length must be 5' ) ; const column = this . _picker . getColumn ( name ) ; if ( ! column ) { return 0 ; } const lb = lowerBounds . slice ( ) ; const ub = upperBounds . slice ( ) ; const options = column . options ; let indexMin = options . length - 1 ; let indexMax = 0 ; for ( var i = 0 ; i < options . length ; i ++ ) { var opt = options [ i ] ; var value = opt . value ; lb [ index ] = opt . value ; ub [ index ] = opt . value ; var disabled = opt . disabled = ( value < lowerBounds [ index ] || value > upperBounds [ index ] || dateSortValue ( ub [ 0 ] , ub [ 1 ] , ub [ 2 ] , ub [ 3 ] , ub [ 4 ] ) < min || dateSortValue ( lb [ 0 ] , lb [ 1 ] , lb [ 2 ] , lb [ 3 ] , lb [ 4 ] ) > max ) ; if ( ! disabled ) { indexMin = Math . min ( indexMin , i ) ; indexMax = Math . max ( indexMax , i ) ; } } let selectedIndex = column . selectedIndex = clamp ( indexMin , column . selectedIndex , indexMax ) ; opt = column . options [ selectedIndex ] ; if ( opt ) { return opt . value ; } return 0 ; } private validate ( ) { const today = new Date ( ) ; const minCompareVal = dateDataSortValue ( this . _min ) ; const maxCompareVal = dateDataSortValue ( this . _max ) ; const yearCol = this . _picker . getColumn ( 'year' ) ; assert ( minCompareVal <= maxCompareVal , 'invalid min/max value' ) ; let selectedYear = today . getFullYear ( ) ; if ( yearCol ) { if ( ! yearCol . options . find ( col => col . value === today . getFullYear ( ) ) ) { selectedYear = yearCol . options [ 0 ] . value ; } var yearOpt = yearCol . options [ yearCol . selectedIndex ] ; if ( yearOpt ) { selectedYear = yearOpt . value ; } } const selectedMonth = this . validateColumn ( 'month' , 1 , minCompareVal , maxCompareVal , [ selectedYear , 0 , 0 , 0 , 0 ] , [ selectedYear , 12 , 31 , 23 , 59 ] ) ; const numDaysInMonth = daysInMonth ( selectedMonth , selectedYear ) ; const selectedDay = this . validateColumn ( 'day' , 2 , minCompareVal , maxCompareVal , [ selectedYear , selectedMonth , 0 , 0 , 0 ] , [ selectedYear , selectedMonth , numDaysInMonth , 23 , 59 ] ) ; const selectedHour = this . validateColumn ( 'hour' , 3 , minCompareVal , maxCompareVal , [ selectedYear , selectedMonth , selectedDay , 0 , 0 ] , [ selectedYear , selectedMonth , selectedDay , 23 , 59 ] ) ; this . validateColumn ( 'minute' , 4 , minCompareVal , maxCompareVal , [ selectedYear , selectedMonth , selectedDay , selectedHour , 0 ] , [ selectedYear , selectedMonth , selectedDay , selectedHour , 59 ] ) ; } hidden divyColumns ( ) { const pickerColumns = this . _picker . getColumns ( ) ; let columnsWidth = [ ] ; let col ; let width ; for ( var i = 0 ; i < pickerColumns . length ; i ++ ) { col = pickerColumns [ i ] ; columnsWidth . push ( 0 ) ; for ( var j = 0 ; j < col . options . length ; j ++ ) { width = col . options [ j ] . text . length ; if ( width > columnsWidth [ i ] ) { columnsWidth [ i ] = width ; } } } if ( columnsWidth . length === 2 ) { width = Math . max ( columnsWidth [ 0 ] , columnsWidth [ 1 ] ) ; pickerColumns [ 0 ] . align = 'right' ; pickerColumns [ 1 ] . align = 'left' ; pickerColumns [ 0 ] . optionsWidth = pickerColumns [ 1 ] . optionsWidth = `${ width * 17 }px` ; } else if ( columnsWidth . length === 3 ) { width = Math . max ( columnsWidth [ 0 ] , columnsWidth [ 2 ] ) ; pickerColumns [ 0 ] . align = 'right' ; pickerColumns [ 1 ] . columnWidth = `${ columnsWidth [ 1 ] * 17 }px` ; pickerColumns [ 0 ] . optionsWidth = pickerColumns [ 2 ] . optionsWidth = `${ width * 17 }px` ; pickerColumns [ 2 ] . align = 'left' ; } } hidden updateText ( ) { const template = this . displayFormat || this . pickerFormat || DEFAULT_FORMAT ; this . _text = renderDateTime ( template , this . getValue ( ) , this . _locale ) ; } hidden getValue ( ) { return this . _value ; } hidden getValueOrDefault ( ) { if ( this . hasValue ( ) ) { return this . _value ; } const initialDateString = this . getDefaultValueDateString ( ) ; const _default = { } ; updateDate ( _default , initialDateString ) ; return _default ; } hidden getDefaultValueDateString ( ) { if ( this . initialValue ) { return this . initialValue ; } const nowString = ( new Date ) . toISOString ( ) ; if ( this . max ) { const now = parseDate ( nowString ) ; const max = parseDate ( this . max ) ; let v ; for ( let i in max ) { v = ( < any > max ) [ i ] ; if ( v === null ) { ( < any > max ) [ i ] = ( < any > now ) [ i ] ; } } const diff = compareDates ( now , max ) ; if ( diff > 0 ) { return this . max ; } } return nowString ; } hidden hasValue ( ) { const val = this . _value ; return isPresent ( val ) && isObject ( val ) && Object . keys ( val ) . length > 0 ; } hidden calcMinMax ( now ? ) { const todaysYear = ( now || new Date ( ) ) . getFullYear ( ) ; if ( isPresent ( this . yearValues ) ) { var years = convertToArrayOfNumbers ( this . yearValues , 'year' ) ; if ( isBlank ( this . min ) ) { this . min = Math . min . apply ( Math , years ) ; } if ( isBlank ( this . max ) ) { this . max = Math . max . apply ( Math , years ) ; } } else { if ( isBlank ( this . min ) ) { this . min = ( todaysYear - 100 ) . toString ( ) ; } if ( isBlank ( this . max ) ) { this . max = todaysYear . toString ( ) ; } } const min = this . _min = parseDate ( this . min ) ; const max = this . _max = parseDate ( this . max ) ; min . year = min . year || todaysYear ; max . year = max . year || todaysYear ; min . month = min . month || 1 ; max . month = max . month || 12 ; min . day = min . day || 1 ; max . day = max . day || 31 ; min . hour = min . hour || 0 ; max . hour = max . hour || 23 ; min . minute = min . minute || 0 ; max . minute = max . minute || 59 ; min . second = min . second || 0 ; max . second = max . second || 59 ; if ( min . year > max . year ) { console . error ( 'min.year > max.year' ) ; min . year = max . year - 100 ; } if ( min . year === max . year ) { if ( min . month > max . month ) { console . error ( 'min.month > max.month' ) ; min . month = 1 ; } else if ( min . month === max . month && min . day > max . day ) { console . error ( 'min.day > max.day' ) ; min . day = 1 ; } } } } hidden function convertToArrayOfNumbers ( input , type ) { if ( isString ( input ) ) { input = input . replace ( /\[|\]|\s/g , '' ) . split ( ',' ) ; } let values ; if ( isArray ( input ) ) { values = input . map ( ( num ) => parseInt ( num , 10 ) ) . filter ( isFinite ) ; } if ( ! values || ! values . length ) { console . warn ( `Invalid "${ type }Values". Must be an array of numbers, or a comma separated string of numbers.` ) ; } return values ; } hidden function convertToArrayOfStrings ( input , type ) { if ( isPresent ( input ) ) { if ( isString ( input ) ) { input = input . replace ( /\[|\]/g , '' ) . split ( ',' ) ; } var values ; if ( isArray ( input ) ) { values = input . map ( ( val ) => val . trim ( ) ) ; } if ( ! values || ! values . length ) { console . warn ( `Invalid "${ type }Names". Must be an array of strings, or a comma separated string.` ) ; } return values ; } } const DEFAULT_FORMAT = 'MMM D, YYYY' ;