index.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. 'use client'
  2. import type { ChangeEvent, FC } from 'react'
  3. import React, { useCallback, useEffect, useRef, useState } from 'react'
  4. import classNames from 'classnames'
  5. import { useTranslation } from 'react-i18next'
  6. import Toast from '../toast'
  7. import { varHighlightHTML } from '../../app/configuration/base/var-highlight'
  8. import Button from '@/app/components/base/button'
  9. import { checkKeys } from '@/utils/var'
  10. // regex to match the {{}} and replace it with a span
  11. const regex = /\{\{([^}]+)\}\}/g
  12. export const getInputKeys = (value: string) => {
  13. const keys = value.match(regex)?.map((item) => {
  14. return item.replace('{{', '').replace('}}', '')
  15. }) || []
  16. const keyObj: Record<string, boolean> = {}
  17. // remove duplicate keys
  18. const res: string[] = []
  19. keys.forEach((key) => {
  20. if (keyObj[key])
  21. return
  22. keyObj[key] = true
  23. res.push(key)
  24. })
  25. return res
  26. }
  27. export type IBlockInputProps = {
  28. value: string
  29. className?: string // wrapper class
  30. highLightClassName?: string // class for the highlighted text default is text-blue-500
  31. readonly?: boolean
  32. onConfirm?: (value: string, keys: string[]) => void
  33. }
  34. const BlockInput: FC<IBlockInputProps> = ({
  35. value = '',
  36. className,
  37. readonly = false,
  38. onConfirm,
  39. }) => {
  40. const { t } = useTranslation()
  41. // current is used to store the current value of the contentEditable element
  42. const [currentValue, setCurrentValue] = useState<string>(value)
  43. useEffect(() => {
  44. setCurrentValue(value)
  45. }, [value])
  46. const isContentChanged = value !== currentValue
  47. const contentEditableRef = useRef<HTMLTextAreaElement>(null)
  48. const [isEditing, setIsEditing] = useState<boolean>(false)
  49. useEffect(() => {
  50. if (isEditing && contentEditableRef.current) {
  51. // TODO: Focus at the click positon
  52. if (currentValue)
  53. contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length)
  54. contentEditableRef.current.focus()
  55. }
  56. }, [isEditing])
  57. const style = classNames({
  58. 'block px-4 py-1 w-full h-full text-sm text-gray-900 outline-0 border-0 break-all': true,
  59. 'block-input--editing': isEditing,
  60. })
  61. const coloredContent = (currentValue || '')
  62. .replace(/</g, '&lt;')
  63. .replace(/>/g, '&gt;')
  64. .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
  65. .replace(/\n/g, '<br />')
  66. // Not use useCallback. That will cause out callback get old data.
  67. const handleSubmit = () => {
  68. if (onConfirm) {
  69. const value = currentValue
  70. const keys = getInputKeys(value)
  71. const { isValid, errorKey, errorMessageKey } = checkKeys(keys)
  72. if (!isValid) {
  73. Toast.notify({
  74. type: 'error',
  75. message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
  76. })
  77. return
  78. }
  79. onConfirm(value, keys)
  80. setIsEditing(false)
  81. }
  82. }
  83. const handleCancel = useCallback(() => {
  84. setIsEditing(false)
  85. setCurrentValue(value)
  86. }, [value])
  87. const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
  88. setCurrentValue(e.target.value)
  89. }, [])
  90. // Prevent rerendering caused cursor to jump to the start of the contentEditable element
  91. const TextAreaContentView = () => {
  92. return <div
  93. className={classNames(style, className)}
  94. dangerouslySetInnerHTML={{ __html: coloredContent }}
  95. suppressContentEditableWarning={true}
  96. />
  97. }
  98. const placeholder = ''
  99. const editAreaClassName = 'focus:outline-none bg-transparent text-sm'
  100. const textAreaContent = (
  101. <div className={classNames(readonly ? 'max-h-[180px] pb-5' : 'h-[180px]', ' overflow-y-auto')} onClick={() => !readonly && setIsEditing(true)}>
  102. {isEditing
  103. ? <div className='h-full px-4 py-1'>
  104. <textarea
  105. ref={contentEditableRef}
  106. className={classNames(editAreaClassName, 'block w-full h-full absolut3e resize-none')}
  107. placeholder={placeholder}
  108. onChange={onValueChange}
  109. value={currentValue}
  110. onBlur={() => {
  111. blur()
  112. if (!isContentChanged)
  113. setIsEditing(false)
  114. // click confirm also make blur. Then outter value is change. So below code has problem.
  115. // setTimeout(() => {
  116. // handleCancel()
  117. // }, 1000)
  118. }}
  119. />
  120. </div>
  121. : <TextAreaContentView />}
  122. </div>)
  123. return (
  124. <div className={classNames('block-input w-full overflow-y-auto border-none rounded-lg')}>
  125. {textAreaContent}
  126. {/* footer */}
  127. {!readonly && (
  128. <div className='flex item-center h-14 px-4'>
  129. {isContentChanged
  130. ? (
  131. <div className='flex items-center justify-between w-full'>
  132. <div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue?.length}</div>
  133. <div className='flex space-x-2'>
  134. <Button
  135. onClick={handleCancel}
  136. className='w-20 !h-8 !text-[13px]'
  137. >
  138. {t('common.operation.cancel')}
  139. </Button>
  140. <Button
  141. onClick={handleSubmit}
  142. type="primary"
  143. className='w-20 !h-8 !text-[13px]'
  144. >
  145. {t('common.operation.confirm')}
  146. </Button>
  147. </div>
  148. </div>
  149. )
  150. : (
  151. <p className="leading-5 text-xs text-gray-500">
  152. {t('appDebug.promptTip')}
  153. </p>
  154. )}
  155. </div>
  156. )}
  157. </div>
  158. )
  159. }
  160. export default React.memo(BlockInput)