File size: 4,626 Bytes
a8b3f00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
'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, '&lt;')
    .replace(/>/g, '&gt;')
    .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)