掘金文章归档 · 第 69 篇 React 生态 5/5 contentEditable · AI 输入框

contentEditable 实现可编辑区域

模拟 placeholder、空态判断、受控组件封装,React 实现 AI 编程输入框

原文作者:随意_(掘金) | juejin.cn/post/7531849749932113970 | 发布于 2025-07-28 | 阅读 329 · 约 1 分钟

0背景

最近看豆包的时候,发现他的代码写 AI 编程模块如下:

截图占位:豆包 AI 编程模块的可编辑输入区域(contentEditable 实现)

借助的是 contentEditable 来实现的,操作比较友好,可以随意输入可编辑数据,有类似于 placeholder 等属性。

如果我们开发大模型,免不了借鉴一下,下面基于 react 来封装一个使用 contentEditable 来实现的可编辑区域组件。

1实现重点

📌 核心思路:用 CSS 伪元素 after 的 content 读取 data-placeholder 属性来模拟占位文案,用 isEmpty 状态控制是否启用占位样式。

2完整组件代码

import React, { useState, useRef, useEffect } from 'react'

interface ContentEditablePromptProps {
  placeholder?: string
  className?: string
  value?: string
  onChange?: (value: string) => void
}

const ContentEditablePrompt: React.FC<ContentEditablePromptProps> = ({
  placeholder = '请输入',
  className = '',
  value = '',
  onChange
}) => {
  const contentEditableRef = useRef<HTMLDivElement>(null)
  const [isEmpty, setIsEmpty] = useState(true)
  const [internalValue, setInternalValue] = useState(value)

  useEffect(() => {
    if (contentEditableRef.current) {
      contentEditableRef.current.textContent = value
      setIsEmpty(value.trim() === '')
    }
  }, [])

  const updateContent = (newValue: string) => {
    if (!contentEditableRef.current) return

    setInternalValue(newValue)

    if (onChange) {
      onChange(newValue)
    }

    const isNowEmpty = newValue.trim() === ''
    setIsEmpty(isNowEmpty)

    if (isNowEmpty && contentEditableRef.current.innerHTML === '<br>') {
      contentEditableRef.current.innerHTML = ''
    }
  }

  const handleInput = () => {
    if (!contentEditableRef.current) return
    updateContent(contentEditableRef.current.textContent || '')
  }

  const handleBlur = () => {
    if (!contentEditableRef.current) return

    const newValue = contentEditableRef.current.textContent || ''
    updateContent(newValue)

    if (contentEditableRef.current.textContent !== internalValue) {
      contentEditableRef.current.textContent = internalValue
    }
  }

  useEffect(() => {
    if (!contentEditableRef.current) return
    if (value !== internalValue) {
      setInternalValue(value)
      contentEditableRef.current.textContent = value
      setIsEmpty(value.trim() === '')
    }
  }, [value])

  return (
    <div
      ref={contentEditableRef}
      className={`p-2 px-3 w-fit rounded-2xl flex items-center ${
        isEmpty
          ? 'after:content-[attr(data-placeholder)] after:tracking-[1px] bg-[#EEF6FF] font-bold text-[#007DFA] word-spacing-2'
          : 'bg-[#EEF6FF] font-bold text-[#007DFA] word-spacing-2'
      } ${className}`}
      contentEditable
      data-placeholder={placeholder}
      onInput={handleInput}
      onBlur={handleBlur}
      suppressContentEditableWarning
    />
  )
}

export default ContentEditablePrompt

组件逻辑拆解

要点实现方式
模拟 placeholderdata-placeholder 存占位文案,isEmpty 时用 after 伪元素渲染
空态判断textContent.trim() === '' 判定为空,同步 isEmpty 状态
清空 <br>空内容且 innerHTML 为 <br> 时置空,避免占位样式残留
受控同步useEffect 监听外部 value,变化时回写 textContent 与 isEmpty
失焦回滚handleBlur 中比较 textContent 与 internalValue,不一致时回写内部值

3使用示例

<EditableArea placeholder='预设提取项' value='' onChange={onChangeValue}></EditableArea>
⚠️ contentEditable 为空时会自动插入 <br>,组件里已做了兜底清理;受控时外部 value 变化会通过 useEffect 同步回 DOM,注意不要在输入中直接 setState 覆盖内部值造成光标跳动。

4结束

以上即基于 contentEditable 的可编辑区域组件完整实现,可借鉴用于大模型产品的输入模块。