import "./input.scss" ; import React , { DOMAttributes , InputHTMLAttributes , TextareaHTMLAttributes } from "react" ; import { cssNames , debouncePromise } from "../../utils" ; import { Icon } from "../icon" ; import { conditionalValidators , Validator } from "./input.validators" ; import isString from "lodash/isString" ; import isFunction from "lodash/isFunction" ; import isBoolean from "lodash/isBoolean" ; import uniqueId from "lodash/uniqueId" ; type InputElement = HTMLInputElement | HTMLTextAreaElement ; type InputElementProps = InputHTMLAttributes < InputElement > & TextareaHTMLAttributes < InputElement > & DOMAttributes < InputElement > ; export type InputProps < T = any > = Omit < InputElementProps , "onChange" > & { className ? : string ; value ? : T ; multiLine ? : boolean ; maxRows ? : number ; dirty ? : boolean ; showErrors ? : boolean ; showValidationLine ? : boolean ; restoreInvalidOnBlur ? : boolean ; iconLeft ? : string | React . ReactNode ; iconRight ? : string | React . ReactNode ; validators ? : Validator | Validator [ ] ; labelContent ? : React . ReactNode ; infoContent ? : React . ReactNode ; onChange ? ( value , evt ) : void ; onBlurChanged ? ( data , evt ) : void ; } ; export interface OnBlurChangedData { valid : boolean ; focusValue : string ; value : string ; } interface State { focused ? : boolean ; dirty ? : boolean ; dirtyOnBlur ? : boolean ; valid ? : boolean ; validating ? : boolean ; errors ? : React . ReactNode [ ] ; } const defaultProps = { rows : 1 , maxRows : 10000 , showErrors : true , showValidationLine : true , restoreInvalidOnBlur : true , validators : [ ] , } ; export class Input extends React . Component < InputProps , State > { static defaultProps = defaultProps as object ; public input : InputElement ; public validators : Validator [ ] = [ ] ; public focusValue : string ; public state : State = { dirty : ! ! this . props . dirty , valid : true , errors : [ ] , } ; setValue ( value ) { if ( value != this . getValue ( ) ) { var nativeInputValueSetter = Object . getOwnPropertyDescriptor ( this . input . constructor . prototype , "value" ) . set ; nativeInputValueSetter . call ( this . input , value ) ; var evt = new Event ( "input" , { bubbles : true } ) ; this . input . dispatchEvent ( evt ) ; } } getValue ( ) { var { value , defaultValue = "" } = this . props ; if ( value !== undefined ) return value ; if ( this . input ) return this . input . value ; return defaultValue as string ; } focus ( ) { this . input . focus ( ) ; } blur ( ) { this . input . blur ( ) ; } select ( ) { this . input . select ( ) ; } private autoFitHeight ( ) { var { multiLine , rows , maxRows } = this . props ; if ( ! multiLine ) { return ; } var textArea = this . input ; var lineHeight = parseFloat ( window . getComputedStyle ( textArea ) . lineHeight ) ; var rowsCount = ( this . getValue ( ) . match ( /\n/g ) || [ ] ) . length + 1 ; var height = lineHeight * Math . min ( Math . max ( rowsCount , rows ) , maxRows ) ; textArea . style . height = height + "px" ; } private validationId : string ; async validate ( value = this . getValue ( ) ) { var validationId = ( this . validationId = "" ) ; var asyncValidators = [ ] ; var errors = [ ] ; for ( let validator of this . validators ) { if ( errors . length ) { break ; } var result = validator . validate ( value , this . props ) ; if ( isBoolean ( result ) && ! result ) { errors . push ( this . getValidatorError ( value , validator ) ) ; } else if ( result instanceof Promise ) { if ( ! validationId ) { this . validationId = validationId = uniqueId ( "validation_id_" ) ; } asyncValidators . push ( result . then ( ( ) => null , error => this . getValidatorError ( value , validator ) || error ) ) ; } } this . setValidation ( errors ) ; if ( asyncValidators . length > 0 ) { this . setState ( { validating : true , valid : false , } ) ; var asyncErrors = await Promise . all ( asyncValidators ) ; var isLastValidationCheck = this . validationId === validationId ; if ( isLastValidationCheck ) { errors = this . state . errors . concat ( asyncErrors . filter ( err => err ) ) ; this . setValidation ( errors ) ; } } this . input . setCustomValidity ( errors . length ? errors [ 0 ] . toString ( ) : "" ) ; } setValidation ( errors ) { this . setState ( { validating : false , valid : ! errors . length , errors : errors , } ) ; } private getValidatorError ( value , { message } ) { if ( isFunction ( message ) ) return message ( value , this . props ) ; return message || "" ; } private setupValidators ( ) { this . validators = conditionalValidators . filter ( validator => validator . condition ( this . props ) ) . concat ( this . props . validators ) . map ( ( { debounce , ... validator } ) => { if ( debounce ) validator . validate = debouncePromise ( validator . validate , debounce ) ; return validator ; } ) ; this . validate ( ) ; } setDirty ( dirty = true ) { if ( this . state . dirty === dirty ) return ; this . setState ( { dirty } ) ; } onFocus = ( evt ) => { var { onFocus , onBlurChanged , restoreInvalidOnBlur } = this . props ; if ( onBlurChanged || restoreInvalidOnBlur ) { this . focusValue = this . getValue ( ) ; } if ( onFocus ) { onFocus ( evt ) ; } this . setState ( { focused : true } ) ; } ; onBlur = ( evt ) => { var { onBlurChanged , onBlur , restoreInvalidOnBlur } = this . props ; var focusValue = this . focusValue ; var value = this . getValue ( ) ; var { valid , dirtyOnBlur } = this . state ; if ( restoreInvalidOnBlur && ! valid ) { value = focusValue ; this . setValue ( focusValue ) ; } if ( onBlur ) { onBlur ( evt ) ; } if ( onBlurChanged && focusValue != value ) { onBlurChanged ( { valid , value , focusValue } , evt ) ; } if ( dirtyOnBlur ) { this . setState ( { dirty : true , dirtyOnBlur : false } ) ; } this . setState ( { focused : false } ) ; } ; onChange = ( evt ) => { if ( this . props . onChange ) { this . props . onChange ( evt . currentTarget . value , evt ) ; } this . validate ( ) ; this . autoFitHeight ( ) ; if ( ! this . state . dirty ) { this . setState ( { dirtyOnBlur : true } ) ; } if ( this . isUncontrolled && this . showMaxLenIndicator ) { this . forceUpdate ( ) ; } } ; get showMaxLenIndicator ( ) { var { maxLength , multiLine } = this . props ; return maxLength && multiLine ; } get isUncontrolled ( ) { return this . props . value === undefined ; } componentDidMount ( ) { this . setupValidators ( ) ; this . autoFitHeight ( ) ; } componentDidUpdate ( prevProps ) { var { defaultValue , value , dirty , validators } = this . props ; if ( prevProps . value !== value || defaultValue !== prevProps . defaultValue ) { this . validate ( ) ; this . autoFitHeight ( ) ; } if ( prevProps . dirty !== dirty ) { this . setDirty ( dirty ) ; } if ( prevProps . validators !== validators ) { this . setupValidators ( ) ; } } bindRef = ( elem ) => { this . input = elem ; } ; render ( ) { var { className , iconLeft , iconRight , multiLine , dirty , showValidationLine , showErrors , validators , maxRows , children , restoreInvalidOnBlur , onBlurChanged , labelContent , infoContent , ... inputProps } = this . props ; var { maxLength , rows , disabled } = this . props ; var { focused , dirty , valid , validating , errors } = this . state ; className = cssNames ( "Input" , className , { focused : focused , disabled : disabled , invalid : ! valid , dirty : dirty , validating : validating , validatingLine : validating && showValidationLine , } ) ; if ( isString ( iconLeft ) ) iconLeft = < Icon material = { iconLeft } />; if ( isString ( iconRight ) ) iconRight = < Icon material = { iconRight } />; Object . assign ( inputProps , { className : "input box grow" , onFocus : this . onFocus , onBlur : this . onBlur , onChange : this . onChange , rows : multiLine ? ( rows || 1 ) : null , ref : this . bindRef , } ) ; return ( < div className = { className } > < label className = "input-area flex gaps align-center" > { iconLeft } { multiLine ? < textarea { ... inputProps as any } / > : < input { ... inputProps as any } / > } { iconRight } { labelContent } < /label> < div className = "input-info flex gaps" > { infoContent } { showErrors && ! valid && dirty && ( < div className = "errors box grow" > { errors . map ( ( error , i ) => < p key = { i } > { error } < /p>)} < /div>)} { this . showMaxLenIndicator && ( < div className = "maxLengthIndicator box right" > { this . getValue ( ) . length } / { maxLength } < /div>)} < /div> < /div>); } }