You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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