| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  | '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' | 
					
						
							| 
									
										
										
										
											2023-10-12 23:14:28 +08:00
										 |  |  | import Toast from '../toast' | 
					
						
							| 
									
										
										
										
											2024-07-09 15:05:40 +08:00
										 |  |  | import classNames from '@/utils/classnames' | 
					
						
							| 
									
										
										
										
											2023-06-01 23:19:36 +08:00
										 |  |  | import { checkKeys } from '@/utils/var' | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | // 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
 | 
					
						
							| 
									
										
										
										
											2023-06-06 10:52:02 +08:00
										 |  |  |   readonly?: boolean | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |   onConfirm?: (value: string, keys: string[]) => void | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const BlockInput: FC<IBlockInputProps> = ({ | 
					
						
							|  |  |  |   value = '', | 
					
						
							|  |  |  |   className, | 
					
						
							| 
									
										
										
										
											2023-06-06 10:52:02 +08:00
										 |  |  |   readonly = false, | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |   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) { | 
					
						
							| 
									
										
										
										
											2024-09-08 12:14:11 +07:00
										 |  |  |       // TODO: Focus at the click position
 | 
					
						
							| 
									
										
										
										
											2023-06-01 23:19:36 +08:00
										 |  |  |       if (currentValue) | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |         contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length) | 
					
						
							| 
									
										
										
										
											2023-06-01 23:19:36 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |       contentEditableRef.current.focus() | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }, [isEditing]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const style = classNames({ | 
					
						
							| 
									
										
										
										
											2023-10-12 23:14:28 +08:00
										 |  |  |     'block px-4 py-2 w-full h-full text-sm text-gray-900 outline-0 border-0 break-all': true, | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |     'block-input--editing': isEditing, | 
					
						
							|  |  |  |   }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const coloredContent = (currentValue || '') | 
					
						
							| 
									
										
										
										
											2023-05-25 18:42:42 +08:00
										 |  |  |     .replace(/</g, '<') | 
					
						
							|  |  |  |     .replace(/>/g, '>') | 
					
						
							| 
									
										
										
										
											2023-05-25 23:38:06 +08:00
										 |  |  |     .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
 | 
					
						
							|  |  |  |     .replace(/\n/g, '<br />') | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   // Not use useCallback. That will cause out callback get old data.
 | 
					
						
							| 
									
										
										
										
											2023-10-12 23:14:28 +08:00
										 |  |  |   const handleSubmit = (value: string) => { | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |     if (onConfirm) { | 
					
						
							|  |  |  |       const keys = getInputKeys(value) | 
					
						
							|  |  |  |       const { isValid, errorKey, errorMessageKey } = checkKeys(keys) | 
					
						
							|  |  |  |       if (!isValid) { | 
					
						
							|  |  |  |         Toast.notify({ | 
					
						
							|  |  |  |           type: 'error', | 
					
						
							| 
									
										
										
										
											2023-06-01 23:19:36 +08:00
										 |  |  |           message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |         }) | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       onConfirm(value, keys) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => { | 
					
						
							| 
									
										
										
										
											2023-10-12 23:14:28 +08:00
										 |  |  |     const value = e.target.value | 
					
						
							|  |  |  |     setCurrentValue(value) | 
					
						
							|  |  |  |     handleSubmit(value) | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |   }, []) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // 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 = ( | 
					
						
							| 
									
										
										
										
											2023-06-06 10:52:02 +08:00
										 |  |  |     <div className={classNames(readonly ? 'max-h-[180px] pb-5' : 'h-[180px]', ' overflow-y-auto')} onClick={() => !readonly && setIsEditing(true)}> | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |       {isEditing | 
					
						
							| 
									
										
										
										
											2023-10-12 23:14:28 +08:00
										 |  |  |         ? <div className='h-full px-4 py-2'> | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |           <textarea | 
					
						
							|  |  |  |             ref={contentEditableRef} | 
					
						
							| 
									
										
										
										
											2023-10-12 23:14:28 +08:00
										 |  |  |             className={classNames(editAreaClassName, 'block w-full h-full resize-none')} | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |             placeholder={placeholder} | 
					
						
							|  |  |  |             onChange={onValueChange} | 
					
						
							|  |  |  |             value={currentValue} | 
					
						
							|  |  |  |             onBlur={() => { | 
					
						
							|  |  |  |               blur() | 
					
						
							| 
									
										
										
										
											2023-10-12 23:14:28 +08:00
										 |  |  |               setIsEditing(false) | 
					
						
							| 
									
										
										
										
											2024-09-08 12:14:11 +07:00
										 |  |  |               // click confirm also make blur. Then outer value is change. So below code has problem.
 | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |               // setTimeout(() => {
 | 
					
						
							|  |  |  |               //   handleCancel()
 | 
					
						
							|  |  |  |               // }, 1000)
 | 
					
						
							|  |  |  |             }} | 
					
						
							|  |  |  |           /> | 
					
						
							|  |  |  |         </div> | 
					
						
							|  |  |  |         : <TextAreaContentView />} | 
					
						
							|  |  |  |     </div>) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							| 
									
										
										
										
											2023-10-12 23:14:28 +08:00
										 |  |  |     <div className={classNames('block-input w-full overflow-y-auto bg-white border-none rounded-xl')}> | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  |       {textAreaContent} | 
					
						
							|  |  |  |       {/* footer */} | 
					
						
							| 
									
										
										
										
											2023-06-06 10:52:02 +08:00
										 |  |  |       {!readonly && ( | 
					
						
							| 
									
										
										
										
											2025-03-21 17:41:03 +08:00
										 |  |  |         <div className='flex pb-2 pl-4'> | 
					
						
							|  |  |  |           <div className="h-[18px] rounded-md bg-gray-100 px-1 text-xs leading-[18px] text-gray-500">{currentValue?.length}</div> | 
					
						
							| 
									
										
										
										
											2023-06-06 10:52:02 +08:00
										 |  |  |         </div> | 
					
						
							|  |  |  |       )} | 
					
						
							| 
									
										
										
										
											2023-05-15 08:51:32 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     </div> | 
					
						
							|  |  |  |   ) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default React.memo(BlockInput) |