import classNames from "classnames" ; import * as React from "react" ; import { polyfill } from "react-lifecycles-compat" ; import { AbstractPureComponent2 , Classes , Keys } from "../../common" ; import { DISPLAYNAME_PREFIX , IntentProps , Props } from "../../common/props" ; import { clamp } from "../../common/utils" ; import { Browser } from "../../compatibility" ; export type EditableTextProps = IEditableTextProps ; deprecated export interface IEditableTextProps extends IntentProps , Props { default alwaysRenderInput ? : boolean ; default confirmOnEnterKey ? : boolean ; /** Default text value of uncontrolled input. */ defaultValue ? : string ; default disabled ? : boolean ; /** Whether the component is currently being edited. */ isEditing ? : boolean ; /** Maximum number of characters allowed. Unlimited by default. */ maxLength ? : number ; /** Minimum width in pixels of the input, when not `multiline`. */ minWidth ? : number ; default multiline ? : boolean ; /**
     * Maximum number of lines before scrolling begins, when `multiline`.
     */ maxLines ? : number ; default minLines ? : number ; default placeholder ? : string ; default selectAllOnFocus ? : boolean ; /**
     * The type of input that should be shown, when not `multiline`.
     */ type ? : string ; /** Text value of controlled input. */ value ? : string ; /** ID attribute to pass to the underlying element that contains the text contents. This allows for referencing via aria attributes */ contentId ? : string ; /** Callback invoked when user cancels input with the `esc` key. Receives last confirmed value. */ onCancel ? ( value ) : void ; /** Callback invoked when user changes input in any way. */ onChange ? ( value ) : void ; /** Callback invoked when user confirms value with `enter` key or by blurring input. */ onConfirm ? ( value ) : void ; /** Callback invoked after the user enters edit mode. */ onEdit ? ( value ) : void ; } export interface IEditableTextState { /** Pixel height of the input, measured from span size */ inputHeight ? : number ; /** Pixel width of the input, measured from span size */ inputWidth ? : number ; /** Whether the value is currently being edited */ isEditing ? : boolean ; /** The last confirmed value */ lastValue ? : string ; /** The controlled input value, may be different from prop during editing */ value ? : string ; } const BUFFER_WIDTH_DEFAULT = 5 ; const BUFFER_WIDTH_IE = 30 ; @ polyfill export class EditableText extends AbstractPureComponent2 < EditableTextProps , IEditableTextState > { public static displayName = `${ DISPLAYNAME_PREFIX }.EditableText` ; public static defaultProps : EditableTextProps = { alwaysRenderInput : false , confirmOnEnterKey : false , defaultValue : "" , disabled : false , maxLines : Infinity , minLines : 1 , minWidth : 80 , multiline : false , placeholder : "Click to Edit" , type : "text" , } ; private inputElement : HTMLInputElement | HTMLTextAreaElement | null = null ; private valueElement : HTMLSpanElement | null = null ; private refHandlers = { content : ( spanElement ) => { this . valueElement = spanElement ; } , input : ( input ) => { if ( input != null ) { this . inputElement = input ; if ( ! this . props . alwaysRenderInput ) { this . inputElement . focus ( ) ; } if ( this . state != null && this . state . isEditing ) { const supportsSelection = inputSupportsSelection ( input ) ; if ( supportsSelection ) { const { length } = input . value ; input . setSelectionRange ( this . props . selectAllOnFocus ? 0 : length , length ) ; } if ( ! supportsSelection || ! this . props . selectAllOnFocus ) { input . scrollLeft = input . scrollWidth ; } } } } , } ; public constructor ( props , context ? ) { super ( props , context ) ; const value = props . value == null ? props . defaultValue : props . value ; this . state = { inputHeight : 0 , inputWidth : 0 , isEditing : props . isEditing === true && props . disabled === false , lastValue : value , value , } ; } public render ( ) { const { alwaysRenderInput , disabled , multiline , contentId } = this . props ; const value = this . props . value ?? this . state . value ; const hasValue = value != null && value !== "" ; const classes = classNames ( Classes . EDITABLE_TEXT , Classes . intentClass ( this . props . intent ) , { [ Classes . DISABLED ] : disabled , [ Classes . EDITABLE_TEXT_EDITING ] : this . state . isEditing , [ Classes . EDITABLE_TEXT_PLACEHOLDER ] : ! hasValue , [ Classes . MULTILINE ] : multiline , } , this . props . className ) ; let contentStyle ; if ( multiline ) { contentStyle = { height : ! this . state . isEditing ? this . state . inputHeight : undefined } ; } else { contentStyle = { height : this . state . inputHeight , lineHeight : this . state . inputHeight != null ? `${ this . state . inputHeight }px` : undefined , minWidth : this . props . minWidth , } ; } const tabIndex = alwaysRenderInput || this . state . isEditing || disabled ? undefined : 0 ; const shouldHideContents = alwaysRenderInput && ! this . state . isEditing ; const spanProps = contentId != null ? { id : contentId } : { } ; return ( < div className = { classes } onFocus = { this . handleFocus } tabIndex = { tabIndex } > { alwaysRenderInput || this . state . isEditing ? this . renderInput ( value ) : undefined } { shouldHideContents ? undefined : ( < span { ... spanProps } className = { Classes . EDITABLE_TEXT_CONTENT } ref = { this . refHandlers . content } style = { contentStyle } > { hasValue ? value : this . props . placeholder } < /span>)} < /div>); } public componentDidMount ( ) { this . updateInputDimensions ( ) ; } public componentDidUpdate ( prevProps , prevState ) { const newState = { } ; if ( this . props . value !== prevProps . value && ( prevProps . value != null || this . props . value != null ) ) { newState . value = this . props . value ; } if ( this . props . isEditing != null && this . props . isEditing !== prevProps . isEditing ) { newState . isEditing = this . props . isEditing ; } if ( this . props . disabled || ( this . props . disabled == null && prevProps . disabled ) ) { newState . isEditing = false ; } this . setState ( newState ) ; if ( this . state . isEditing && ! prevState . isEditing ) { this . props . onEdit ?. ( this . state . value ) ; } if ( this . state . value !== prevState . value || this . props . alwaysRenderInput !== prevProps . alwaysRenderInput || this . props . maxLines !== prevProps . maxLines || this . props . minLines !== prevProps . minLines || this . props . minWidth !== prevProps . minWidth || this . props . multiline !== prevProps . multiline ) { this . updateInputDimensions ( ) ; } } public cancelEditing = ( ) => { const { lastValue , value } = this . state ; this . setState ( { isEditing : false , value : lastValue } ) ; if ( value !== lastValue ) { this . props . onChange ?. ( lastValue ! ) ; } this . props . onCancel ?. ( lastValue ! ) ; } ; public toggleEditing = ( ) => { if ( this . state . isEditing ) { const { value } = this . state ; this . setState ( { isEditing : false , lastValue : value } ) ; this . props . onConfirm ?. ( value ! ) ; } else if ( ! this . props . disabled ) { this . setState ( { isEditing : true } ) ; } } ; private handleFocus = ( ) => { const { alwaysRenderInput , disabled , selectAllOnFocus } = this . props ; if ( ! disabled ) { this . setState ( { isEditing : true } ) ; } if ( alwaysRenderInput && selectAllOnFocus && this . inputElement != null ) { const { length } = this . inputElement . value ; this . inputElement . setSelectionRange ( 0 , length ) ; } } ; private handleTextChange = ( event ) => { const value = ( event . target as HTMLInputElement ) . value ; if ( this . props . value == null ) { this . setState ( { value } ) ; } this . props . onChange ?. ( value ) ; } ; private handleKeyEvent = ( event ) => { const { altKey , ctrlKey , metaKey , shiftKey , which } = event ; if ( which === Keys . ESCAPE ) { this . cancelEditing ( ) ; return ; } const hasModifierKey = altKey || ctrlKey || metaKey || shiftKey ; if ( which === Keys . ENTER ) { if ( altKey || shiftKey ) { event . preventDefault ( ) ; } if ( this . props . confirmOnEnterKey && this . props . multiline ) { if ( event . target != null && hasModifierKey ) { insertAtCaret ( event . target as HTMLTextAreaElement , "\n" ) ; this . handleTextChange ( event ) ; } else { this . toggleEditing ( ) ; } } else if ( ! this . props . multiline || hasModifierKey ) { this . toggleEditing ( ) ; } } } ; private renderInput ( value ) { const { disabled , maxLength , multiline , type , placeholder } = this . props ; const props = { className : Classes . EDITABLE_TEXT_INPUT , disabled , maxLength , onBlur : this . toggleEditing , onChange : this . handleTextChange , onKeyDown : this . handleKeyEvent , placeholder , value , } ; const { inputHeight , inputWidth } = this . state ; if ( inputHeight !== 0 && inputWidth !== 0 ) { props . style = { height : inputHeight , lineHeight : ! multiline && inputHeight != null ? `${ inputHeight }px` : undefined , width : multiline ? "100%" : inputWidth , } ; } return multiline ? ( < textarea ref = { this . refHandlers . input } { ... props } />) : (<input ref={this.refHandlers.input} type={type} {...props}/ > ) ; } private updateInputDimensions ( ) { if ( this . valueElement != null ) { const { maxLines , minLines , minWidth , multiline } = this . props ; const { parentElement , textContent } = this . valueElement ; let { scrollHeight , scrollWidth } = this . valueElement ; const lineHeight = getLineHeight ( this . valueElement ) ; if ( multiline && this . state . isEditing && /\n$/ . test ( textContent ?? "" ) ) { scrollHeight += lineHeight ; } if ( lineHeight > 0 ) { scrollHeight = clamp ( scrollHeight , minLines ! * lineHeight , maxLines ! * lineHeight ) ; } scrollHeight = Math . max ( scrollHeight , getFontSize ( this . valueElement ) + 1 , getLineHeight ( parentElement ! ) ) ; scrollWidth += Browser . isInternetExplorer ( ) ? BUFFER_WIDTH_IE : BUFFER_WIDTH_DEFAULT ; this . setState ( { inputHeight : scrollHeight , inputWidth : Math . max ( scrollWidth , minWidth ! ) , } ) ; if ( multiline && this . state . isEditing ) { this . setTimeout ( ( ) => ( parentElement ! . style . height = `${ scrollHeight }px` ) ) ; } } } } function getFontSize ( element ) { const fontSize = getComputedStyle ( element ) . fontSize ; return fontSize === "" ? 0 : parseInt ( fontSize . slice ( 0 , - 2 ) , 10 ) ; } function getLineHeight ( element ) { let lineHeight = parseInt ( getComputedStyle ( element ) . lineHeight . slice ( 0 , - 2 ) , 10 ) ; if ( isNaN ( lineHeight ) ) { const line = document . createElement ( "span" ) ; line . innerHTML = "<br>" ; element . appendChild ( line ) ; const singleLineHeight = element . offsetHeight ; line . innerHTML = "<br><br>" ; const doubleLineHeight = element . offsetHeight ; element . removeChild ( line ) ; lineHeight = doubleLineHeight - singleLineHeight ; } return lineHeight ; } function insertAtCaret ( el , text ) { const { selectionEnd , selectionStart , value } = el ; if ( selectionStart >= 0 ) { const before = value . substring ( 0 , selectionStart ) ; const after = value . substring ( selectionEnd , value . length ) ; const len = text . length ; el . value = `${ before }${ text }${ after }` ; el . selectionStart = selectionStart + len ; el . selectionEnd = selectionStart + len ; } } function inputSupportsSelection ( input ) { switch ( input . type ) { case "textarea" : return true ; case "text" : case "search" : case "tel" : case "url" : case "password" : return true ; default : return false ; } }