import classNames from "classnames" ; import * as React from "react" ; import { Classes as CoreClasses , DISPLAYNAME_PREFIX , HTMLSelect , Icon , Intent , Props , Keys } from "@blueprintjs/core" ; import * as Classes from "./common/classes" ; import * as DateUtils from "./common/dateUtils" ; import { getDefaultMaxTime , getDefaultMinTime , getTimeUnit , getTimeUnitClassName , isTimeUnitValid , setTimeUnit , TimeUnit , wrapTimeAtUnit , } from "./common/timeUnit" ; import * as Utils from "./common/utils" ; export const TimePrecision = { MILLISECOND : "millisecond" as "millisecond" , MINUTE : "minute" as "minute" , SECOND : "second" as "second" , } ; export type TimePrecision = typeof TimePrecision [ keyof typeof TimePrecision ] ; export type TimePickerProps = ITimePickerProps ; deprecated export interface ITimePickerProps extends Props { default autoFocus ? : boolean ; /**
     * Initial time the `TimePicker` will display.
     * This should not be set if `value` is set.
     */ defaultValue ? : Date ; default disabled ? : boolean ; /**
     * Callback invoked on blur event emitted by specific time unit input
     */ onBlur ? : ( event : React . FocusEvent < HTMLInputElement > , unit : TimeUnit ) => void ; /**
     * Callback invoked when the user changes the time.
     */ onChange ? : ( newTime : Date ) => void ; /**
     * Callback invoked on focus event emitted by specific time unit input
     */ onFocus ? : ( event : React . FocusEvent < HTMLInputElement > , unit : TimeUnit ) => void ; /**
     * Callback invoked on keydown event emitted by specific time unit input
     */ onKeyDown ? : ( event : React . KeyboardEvent < HTMLInputElement > , unit : TimeUnit ) => void ; /**
     * Callback invoked on keyup event emitted by specific time unit input
     */ onKeyUp ? : ( event : React . KeyboardEvent < HTMLInputElement > , unit : TimeUnit ) => void ; default precision ? : TimePrecision ; default selectAllOnFocus ? : boolean ; default showArrowButtons ? : boolean ; default useAmPm ? : boolean ; /**
     * The latest time the user can select. The year, month, and day parts of the `Date` object are ignored.
     * While the `maxTime` will be later than the `minTime` in the basic case,
     * it is also allowed to be earlier than the `minTime`.
     * This is useful, for example, to express a time range that extends before and after midnight.
     * If the `maxTime` and `minTime` are equal, then the valid time range is constrained to only that one value.
     */ maxTime ? : Date ; /**
     * The earliest time the user can select. The year, month, and day parts of the `Date` object are ignored.
     * While the `minTime` will be earlier than the `maxTime` in the basic case,
     * it is also allowed to be later than the `maxTime`.
     * This is useful, for example, to express a time range that extends before and after midnight.
     * If the `maxTime` and `minTime` are equal, then the valid time range is constrained to only that one value.
     */ minTime ? : Date ; /**
     * The currently set time.
     * If this prop is provided, the component acts in a controlled manner.
     */ value ? : Date | null ; } export interface ITimePickerState { hourText ? : string ; minuteText ? : string ; secondText ? : string ; millisecondText ? : string ; value ? : Date ; isPm ? : boolean ; } export class TimePicker extends React . Component < TimePickerProps , ITimePickerState > { public static defaultProps : TimePickerProps = { autoFocus : false , disabled : false , maxTime : getDefaultMaxTime ( ) , minTime : getDefaultMinTime ( ) , precision : TimePrecision . MINUTE , selectAllOnFocus : false , showArrowButtons : false , useAmPm : false , } ; public static displayName = `${ DISPLAYNAME_PREFIX }.TimePicker` ; public constructor ( props ? , context ? ) { super ( props , context ) ; this . state = this . getFullStateFromValue ( this . getInitialValue ( ) , props . useAmPm ) ; } public render ( ) { const shouldRenderMilliseconds = this . props . precision === TimePrecision . MILLISECOND ; const shouldRenderSeconds = shouldRenderMilliseconds || this . props . precision === TimePrecision . SECOND ; const hourUnit = this . props . useAmPm ? TimeUnit . HOUR_12 : TimeUnit . HOUR_24 ; const classes = classNames ( Classes . TIMEPICKER , this . props . className , { [ CoreClasses . DISABLED ] : this . props . disabled , } ) ; return ( < div className = { classes } > < div className = { Classes . TIMEPICKER_ARROW_ROW } > { this . maybeRenderArrowButton ( true , hourUnit ) } { this . maybeRenderArrowButton ( true , TimeUnit . MINUTE ) } { shouldRenderSeconds && this . maybeRenderArrowButton ( true , TimeUnit . SECOND ) } { shouldRenderMilliseconds && this . maybeRenderArrowButton ( true , TimeUnit . MS ) } < /div> < div className = { Classes . TIMEPICKER_INPUT_ROW } > { this . renderInput ( Classes . TIMEPICKER_HOUR , hourUnit , this . state . hourText ) } { this . renderDivider ( ) } { this . renderInput ( Classes . TIMEPICKER_MINUTE , TimeUnit . MINUTE , this . state . minuteText ) } { shouldRenderSeconds && this . renderDivider ( ) } { shouldRenderSeconds && this . renderInput ( Classes . TIMEPICKER_SECOND , TimeUnit . SECOND , this . state . secondText ) } { shouldRenderMilliseconds && this . renderDivider ( "." ) } { shouldRenderMilliseconds && this . renderInput ( Classes . TIMEPICKER_MILLISECOND , TimeUnit . MS , this . state . millisecondText ) } < /div> { this . maybeRenderAmPm ( ) } < div className = { Classes . TIMEPICKER_ARROW_ROW } > { this . maybeRenderArrowButton ( false , hourUnit ) } { this . maybeRenderArrowButton ( false , TimeUnit . MINUTE ) } { shouldRenderSeconds && this . maybeRenderArrowButton ( false , TimeUnit . SECOND ) } { shouldRenderMilliseconds && this . maybeRenderArrowButton ( false , TimeUnit . MS ) } < /div> < /div>); } public componentDidUpdate ( prevProps ) { const didMinTimeChange = prevProps . minTime !== this . props . minTime ; const didMaxTimeChange = prevProps . maxTime !== this . props . maxTime ; const didBoundsChange = didMinTimeChange || didMaxTimeChange ; const didPropValueChange = prevProps . value !== this . props . value ; const shouldStateUpdate = didBoundsChange || didPropValueChange ; let value = this . state . value ; if ( this . props . value == null ) { value = this . getInitialValue ( ) ; } if ( didBoundsChange ) { value = DateUtils . getTimeInRange ( this . state . value , this . props . minTime , this . props . maxTime ) ; } if ( this . props . value != null && ! DateUtils . areSameTime ( this . props . value , prevProps . value ) ) { value = this . props . value ; } if ( shouldStateUpdate ) { this . setState ( this . getFullStateFromValue ( value , this . props . useAmPm ) ) ; } } private maybeRenderArrowButton ( isDirectionUp , timeUnit ) { if ( ! this . props . showArrowButtons ) { return null ; } const classes = classNames ( Classes . TIMEPICKER_ARROW_BUTTON , getTimeUnitClassName ( timeUnit ) ) ; const onClick = ( ) => ( isDirectionUp ? this . incrementTime : this . decrementTime ) ( timeUnit ) ; return ( < span tabIndex = { - 1 } className = { classes } onClick = { onClick } > < Icon icon = { isDirectionUp ? "chevron-up" : "chevron-down" } title = { isDirectionUp ? "Increase" : "Decrease" } / > < /span>); } private renderDivider ( text = ":" ) { return < span className = { Classes . TIMEPICKER_DIVIDER_TEXT } > { text } < /span>; } private renderInput ( className , unit , value ) { const isValid = isTimeUnitValid ( unit , parseInt ( value , 10 ) ) ; const isHour = unit === TimeUnit . HOUR_12 || unit === TimeUnit . HOUR_24 ; return ( < input className = { classNames ( Classes . TIMEPICKER_INPUT , { [ CoreClasses . intentClass ( Intent . DANGER ) ] : ! isValid } , className ) } onBlur = { this . getInputBlurHandler ( unit ) } onChange = { this . getInputChangeHandler ( unit ) } onFocus = { this . getInputFocusHandler ( unit ) } onKeyDown = { this . getInputKeyDownHandler ( unit ) } onKeyUp = { this . getInputKeyUpHandler ( unit ) } value = { value } disabled = { this . props . disabled } autoFocus = { isHour && this . props . autoFocus } />); } private maybeRenderAmPm ( ) { if ( ! this . props . useAmPm ) { return null ; } return ( < HTMLSelect className = { Classes . TIMEPICKER_AMPM_SELECT } disabled = { this . props . disabled } onChange = { this . handleAmPmChange } value = { this . state . isPm ? "pm" : "am" } > < option value = "am" > AM < /option> < option value = "pm" > PM < /option> < /HTMLSelect>); } private getInputChangeHandler = ( unit ) => ( e ) => { const text = getStringValueFromInputEvent ( e ) ; switch ( unit ) { case TimeUnit . HOUR_12 : case TimeUnit . HOUR_24 : this . setState ( { hourText : text } ) ; break ; case TimeUnit . MINUTE : this . setState ( { minuteText : text } ) ; break ; case TimeUnit . SECOND : this . setState ( { secondText : text } ) ; break ; case TimeUnit . MS : this . setState ( { millisecondText : text } ) ; break ; } } ; private getInputBlurHandler = ( unit ) => ( e ) => { const text = getStringValueFromInputEvent ( e ) ; this . updateTime ( parseInt ( text , 10 ) , unit ) ; this . props . onBlur ?. ( e , unit ) ; } ; private getInputFocusHandler = ( unit ) => ( e ) => { if ( this . props . selectAllOnFocus ) { e . currentTarget . select ( ) ; } this . props . onFocus ?. ( e , unit ) ; } ; private getInputKeyDownHandler = ( unit ) => ( e ) => { handleKeyEvent ( e , { [ Keys . ARROW_UP ] : ( ) => this . incrementTime ( unit ) , [ Keys . ARROW_DOWN ] : ( ) => this . decrementTime ( unit ) , [ Keys . ENTER ] : ( ) => { ( e . currentTarget as HTMLInputElement ) . blur ( ) ; } , } ) ; this . props . onKeyDown ?. ( e , unit ) ; } ; private getInputKeyUpHandler = ( unit ) => ( e ) => { this . props . onKeyUp ?. ( e , unit ) ; } ; private handleAmPmChange = ( e ) => { const isNextPm = e . currentTarget . value === "pm" ; if ( isNextPm !== this . state . isPm ) { const hour = DateUtils . convert24HourMeridiem ( this . state . value . getHours ( ) , isNextPm ) ; this . setState ( { isPm : isNextPm } , ( ) => this . updateTime ( hour , TimeUnit . HOUR_24 ) ) ; } } ; private getFullStateFromValue ( value , useAmPm ) { const timeInRange = DateUtils . getTimeInRange ( value , this . props . minTime , this . props . maxTime ) ; const hourUnit = useAmPm ? TimeUnit . HOUR_12 : TimeUnit . HOUR_24 ; return { hourText : formatTime ( timeInRange . getHours ( ) , hourUnit ) , minuteText : formatTime ( timeInRange . getMinutes ( ) , TimeUnit . MINUTE ) , secondText : formatTime ( timeInRange . getSeconds ( ) , TimeUnit . SECOND ) , millisecondText : formatTime ( timeInRange . getMilliseconds ( ) , TimeUnit . MS ) , value : timeInRange , isPm : DateUtils . getIsPmFrom24Hour ( timeInRange . getHours ( ) ) , } ; } private incrementTime = ( unit ) => this . shiftTime ( unit , 1 ) ; private decrementTime = ( unit ) => this . shiftTime ( unit , - 1 ) ; private shiftTime ( unit , amount ) { if ( this . props . disabled ) { return ; } const newTime = getTimeUnit ( unit , this . state . value ) + amount ; this . updateTime ( wrapTimeAtUnit ( unit , newTime ) , unit ) ; } private updateTime ( time , unit ) { const newValue = DateUtils . clone ( this . state . value ) ; if ( isTimeUnitValid ( unit , time ) ) { setTimeUnit ( unit , time , newValue , this . state . isPm ) ; if ( DateUtils . isTimeInRange ( newValue , this . props . minTime , this . props . maxTime ) ) { this . updateState ( { value : newValue } ) ; } else { this . updateState ( this . getFullStateFromValue ( this . state . value , this . props . useAmPm ) ) ; } } else { this . updateState ( this . getFullStateFromValue ( this . state . value , this . props . useAmPm ) ) ; } } private updateState ( state ) { let newState = state ; const hasNewValue = newState . value != null && ! DateUtils . areSameTime ( newState . value , this . state . value ) ; if ( this . props . value == null ) { if ( hasNewValue ) { newState = this . getFullStateFromValue ( newState . value , this . props . useAmPm ) ; } this . setState ( newState ) ; } else { if ( hasNewValue ) { this . setState ( this . getFullStateFromValue ( this . state . value , this . props . useAmPm ) ) ; } else { this . setState ( { ... newState , value : DateUtils . clone ( this . state . value ) } ) ; } } if ( hasNewValue ) { this . props . onChange ?. ( newState . value ) ; } } private getInitialValue ( ) { let value = this . props . minTime ; if ( this . props . value != null ) { value = this . props . value ; } else if ( this . props . defaultValue != null ) { value = this . props . defaultValue ; } return value ; } } function formatTime ( time , unit ) { switch ( unit ) { case TimeUnit . HOUR_24 : return time . toString ( ) ; case TimeUnit . HOUR_12 : return DateUtils . get12HourFrom24Hour ( time ) . toString ( ) ; case TimeUnit . MINUTE : case TimeUnit . SECOND : return Utils . padWithZeroes ( time . toString ( ) , 2 ) ; case TimeUnit . MS : return Utils . padWithZeroes ( time . toString ( ) , 3 ) ; default : throw Error ( "Invalid TimeUnit" ) ; } } function getStringValueFromInputEvent ( e ) { return ( e . target as HTMLInputElement ) . value ; } interface IKeyEventMap { [ key ] : ( ) => void ; } function handleKeyEvent ( e , actions , preventDefault = true ) { for ( const k of Object . keys ( actions ) ) { const key = Number ( k ) ; if ( e . which === key ) { if ( preventDefault ) { e . preventDefault ( ) ; } actions [ key ] ( ) ; } } }