Spaces:
Build error
Build error
'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' | |
// regex to match the {{}} and replace it with a span | |
const regex = /\{\{([^}]+)\}\}/g | |
export const getInputKeys = (value: string) => { | |
const keys = value.match(regex)?.map((item) => { | |
return item.replace('{{', '').replace('}}', '') | |
}) || [] | |
const keyObj: Record<string, boolean> = {} | |
// remove duplicate keys | |
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 // wrapper class | |
highLightClassName?: string // class for the highlighted text default is text-blue-500 | |
readonly?: boolean | |
onConfirm?: (value: string, keys: string[]) => void | |
} | |
const BlockInput: FC<IBlockInputProps> = ({ | |
value = '', | |
className, | |
readonly = false, | |
onConfirm, | |
}) => { | |
const { t } = useTranslation() | |
// current is used to store the current value of the contentEditable element | |
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) { | |
// TODO: Focus at the click position | |
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' })) // `<span class="${highLightClassName}">{{$1}}</span>` | |
.replace(/\n/g, '<br />') | |
// Not use useCallback. That will cause out callback get old data. | |
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) | |
}, []) | |
// Prevent rerendering caused cursor to jump to the start of the contentEditable element | |
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) | |