import React, { useState, useEffect, useMemo, useCallback, useContext, } from "react"; import { EditableText as BlueprintEditableText, Classes as BlueprintClasses, } from "@blueprintjs/core"; import styled, { ThemeContext } from "styled-components"; import { noop } from "lodash"; import Text, { TextType } from "./Text"; import Spinner from "./Spinner"; import { CommonComponentProps } from "./common"; import Icon, { IconSize } from "./Icon"; export enum EditInteractionKind { SINGLE = "SINGLE", DOUBLE = "DOUBLE", } export enum SavingState { STARTED = "STARTED", NOT_STARTED = "NOT_STARTED", SUCCESS = "SUCCESS", ERROR = "ERROR", } export type EditableTextSubComponentProps = CommonComponentProps & { defaultValue: string; placeholder?: string; editInteractionKind: EditInteractionKind; defaultSavingState: SavingState; savingState: SavingState; setSavingState: typeof noop; onBlur?: (value: string) => void; onTextChanged?: (value: string) => void; valueTransform?: (value: string) => string; isEditingDefault?: boolean; isEditing: boolean; forceDefault?: boolean; updating?: boolean; setIsEditing: typeof noop; inputValidation?: (value: string) => string | boolean; isInvalid: string | boolean; setIsInvalid: typeof noop; hideEditIcon?: boolean; fill?: boolean; underline?: boolean; isError?: boolean; }; export const EditableTextWrapper = styled.div<{ filled: boolean; }>` ${(props) => !props.filled ? ` width: 243px; ` : ` width: 100%; flex: 1; `} .error-message { margin-left: ${(props) => props.theme.spaces[5]}px; color: ${(props) => props.theme.colors.danger.main}; } `; const editModeBgcolor = ( isInvalid: boolean, isEditing: boolean, savingState: SavingState, theme: any, ): string => { if ((isInvalid && isEditing) || savingState === SavingState.ERROR) { return theme.colors.editableText.dangerBg; } else if (!isInvalid && isEditing) { return theme.colors.editableText.bg; } else { return "transparent"; } }; const TextContainer = styled.div<{ isInvalid: boolean; isEditing: boolean; bgColor: string; underline?: boolean; }>` display: flex; align-items: center; .bp3-editable-text.bp3-editable-text-editing::before, .bp3-editable-text.bp3-disabled::before { display: none; } &&& .${BlueprintClasses.EDITABLE_TEXT_CONTENT}, &&& .${BlueprintClasses.EDITABLE_TEXT_INPUT} { font-size: ${(props) => props.theme.typography.p1.fontSize}px; line-height: ${(props) => props.theme.typography.p1.lineHeight}px; letter-spacing: ${(props) => props.theme.typography.p1.letterSpacing}px; font-weight: ${(props) => props.theme.typography.p1.fontWeight}; } &&& .${BlueprintClasses.EDITABLE_TEXT_CONTENT} { cursor: pointer; color: ${(props) => props.theme.colors.editableText.color}; overflow: hidden; text-overflow: ellipsis; ${(props) => (props.isEditing ? "display: none" : "display: block")}; width: fit-content !important; min-width: auto !important; } &&& .${BlueprintClasses.EDITABLE_TEXT_CONTENT}:hover { ${(props) => props.underline && !props.isEditing ? ` border-bottom-style: solid; border-bottom-width: 1px; width: fit-content; ` : null} } &&& .${BlueprintClasses.EDITABLE_TEXT_INPUT} { border: none; outline: none; height: ${(props) => props.theme.spaces[14] + 1}px; color: ${(props) => props.theme.colors.editableText.color}; min-width: 100%; border-radius: ${(props) => props.theme.spaces[0]}px; } &&& .${BlueprintClasses.EDITABLE_TEXT} { overflow: hidden; height: ${(props) => props.theme.spaces[14] + 1}px; padding: ${(props) => props.theme.spaces[4]}px ${(props) => props.theme.spaces[5]}px; width: calc(100% - 40px); background-color: ${(props) => props.bgColor}; } .icon-wrapper { background-color: ${(props) => props.bgColor}; } `; const IconWrapper = styled.div` width: ${(props) => props.theme.spaces[15]}px; padding-right: ${(props) => props.theme.spaces[5]}px; display: flex; align-items: center; justify-content: flex-end; `; export function EditableTextSubComponent(props: EditableTextSubComponentProps) { const { defaultValue, inputValidation, isEditing, isEditingDefault, isError, isInvalid, onBlur, onTextChanged, savingState, setIsEditing, setIsInvalid, setSavingState, valueTransform, } = props; const [value, setValue] = useState(defaultValue); const [lastValidValue, setLastValidValue] = useState(defaultValue); const [changeStarted, setChangeStarted] = useState(false); useEffect(() => { if (isError) { // if there is any error occurs while saving appname. // last saved app name will be shown to user. setValue(defaultValue); } }, [isError]); useEffect(() => { setSavingState(props.defaultSavingState); }, [props.defaultSavingState]); useEffect(() => { setValue(defaultValue); }, [defaultValue]); useEffect(() => { setIsEditing(!!isEditingDefault); }, [defaultValue, isEditingDefault]); useEffect(() => { if (props.forceDefault === true) setValue(defaultValue); }, [props.forceDefault, defaultValue]); const theme = useContext(ThemeContext); const bgColor = useMemo( () => editModeBgcolor(!!isInvalid, isEditing, savingState, theme), [isInvalid, isEditing, savingState, theme], ); const onConfirm = useCallback( (_value: string) => { const finalVal: string = _value.trim(); if (savingState === SavingState.ERROR || isInvalid || finalVal === "") { setValue(lastValidValue); onBlur && onBlur(lastValidValue); setSavingState(SavingState.NOT_STARTED); } if (changeStarted) { onTextChanged && onTextChanged(finalVal); } if (finalVal && finalVal !== defaultValue) { onBlur && onBlur(finalVal); } setIsEditing(false); setChangeStarted(false); }, [ changeStarted, savingState, isInvalid, lastValidValue, onBlur, onTextChanged, ], ); const onInputchange = useCallback( (_value: string) => { let finalVal: string = _value.indexOf(" ") === 0 ? _value.trim() : _value; if (valueTransform) { finalVal = valueTransform(finalVal); } const errorMessage = inputValidation && inputValidation(finalVal); const error = errorMessage ? errorMessage : false; if (!error && finalVal !== "") { setLastValidValue(finalVal); onTextChanged && onTextChanged(finalVal); } setValue(finalVal); setIsInvalid(error); setChangeStarted(true); }, [inputValidation, onTextChanged], ); const iconName = !isEditing && savingState === SavingState.NOT_STARTED && !props.hideEditIcon ? "edit" : !isEditing && savingState === SavingState.SUCCESS ? "success" : savingState === SavingState.ERROR || (isEditing && !!isInvalid) ? "error" : undefined; return ( <> {savingState === SavingState.STARTED ? ( ) : value && !props.hideEditIcon ? ( ) : null} {isEditing && !!isInvalid ? ( {isInvalid} ) : null} ); } export default EditableTextSubComponent;