|
'use client' |
|
|
|
import type { ChangeEvent, FC } from 'react' |
|
import React, { useCallback, useEffect, useRef, useState } from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import { varHighlightHTML } from '../../app/configuration/base/var-highlight' |
|
import Toast from '../toast' |
|
import classNames from '@/utils/classnames' |
|
import { checkKeys } from '@/utils/var' |
|
|
|
|
|
const regex = /\{\{([^}]+)\}\}/g |
|
|
|
export const getInputKeys = (value: string) => { |
|
const keys = value.match(regex)?.map((item) => { |
|
return item.replace('{{', '').replace('}}', '') |
|
}) || [] |
|
const keyObj: Record<string, boolean> = {} |
|
|
|
const res: string[] = [] |
|
keys.forEach((key) => { |
|
if (keyObj[key]) |
|
return |
|
|
|
keyObj[key] = true |
|
res.push(key) |
|
}) |
|
return res |
|
} |
|
|
|
export type IBlockInputProps = { |
|
value: string |
|
className?: string |
|
highLightClassName?: string |
|
readonly?: boolean |
|
onConfirm?: (value: string, keys: string[]) => void |
|
} |
|
|
|
const BlockInput: FC<IBlockInputProps> = ({ |
|
value = '', |
|
className, |
|
readonly = false, |
|
onConfirm, |
|
}) => { |
|
const { t } = useTranslation() |
|
|
|
const [currentValue, setCurrentValue] = useState<string>(value) |
|
useEffect(() => { |
|
setCurrentValue(value) |
|
}, [value]) |
|
|
|
const contentEditableRef = useRef<HTMLTextAreaElement>(null) |
|
const [isEditing, setIsEditing] = useState<boolean>(false) |
|
useEffect(() => { |
|
if (isEditing && contentEditableRef.current) { |
|
|
|
if (currentValue) |
|
contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length) |
|
|
|
contentEditableRef.current.focus() |
|
} |
|
}, [isEditing]) |
|
|
|
const style = classNames({ |
|
'block px-4 py-2 w-full h-full text-sm text-gray-900 outline-0 border-0 break-all': true, |
|
'block-input--editing': isEditing, |
|
}) |
|
|
|
const coloredContent = (currentValue || '') |
|
.replace(/</g, '<') |
|
.replace(/>/g, '>') |
|
.replace(regex, varHighlightHTML({ name: '$1' })) |
|
.replace(/\n/g, '<br />') |
|
|
|
|
|
const handleSubmit = (value: string) => { |
|
if (onConfirm) { |
|
const keys = getInputKeys(value) |
|
const { isValid, errorKey, errorMessageKey } = checkKeys(keys) |
|
if (!isValid) { |
|
Toast.notify({ |
|
type: 'error', |
|
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), |
|
}) |
|
return |
|
} |
|
onConfirm(value, keys) |
|
} |
|
} |
|
|
|
const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => { |
|
const value = e.target.value |
|
setCurrentValue(value) |
|
handleSubmit(value) |
|
}, []) |
|
|
|
|
|
const TextAreaContentView = () => { |
|
return <div |
|
className={classNames(style, className)} |
|
dangerouslySetInnerHTML={{ __html: coloredContent }} |
|
suppressContentEditableWarning={true} |
|
/> |
|
} |
|
|
|
const placeholder = '' |
|
const editAreaClassName = 'focus:outline-none bg-transparent text-sm' |
|
|
|
const textAreaContent = ( |
|
<div className={classNames(readonly ? 'max-h-[180px] pb-5' : 'h-[180px]', ' overflow-y-auto')} onClick={() => !readonly && setIsEditing(true)}> |
|
{isEditing |
|
? <div className='h-full px-4 py-2'> |
|
<textarea |
|
ref={contentEditableRef} |
|
className={classNames(editAreaClassName, 'block w-full h-full resize-none')} |
|
placeholder={placeholder} |
|
onChange={onValueChange} |
|
value={currentValue} |
|
onBlur={() => { |
|
blur() |
|
setIsEditing(false) |
|
// click confirm also make blur. Then outer value is change. So below code has problem. |
|
// setTimeout(() => { |
|
// handleCancel() |
|
// }, 1000) |
|
}} |
|
/> |
|
</div> |
|
: <TextAreaContentView />} |
|
</div>) |
|
|
|
return ( |
|
<div className={classNames('block-input w-full overflow-y-auto bg-white border-none rounded-xl')}> |
|
{textAreaContent} |
|
{/* footer */} |
|
{!readonly && ( |
|
<div className='pl-4 pb-2 flex'> |
|
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue?.length}</div> |
|
</div> |
|
)} |
|
|
|
</div> |
|
) |
|
} |
|
|
|
export default React.memo(BlockInput) |
|
|