You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React JS如何为可编辑文本域内的特定词汇设置不同颜色?

可编辑文本域%包裹词汇高亮实现方案

核心方案采用contentEditable可编辑容器+实时分词重渲染逻辑,既可以保留统一输入域的连续编辑体验,也能动态匹配输入内容给特殊词汇加样式,完全避开拆分多输入框、原生textarea不支持局部样式的问题。

具体实现逻辑

  • 放弃原生<textarea>和拆分多input的实现思路:原生textarea不支持内部部分文本单独设置样式,拆分多input的方案会天然破坏输入连续性,光标移动、跨片段选中文本、连续输入的体验都无法和统一输入域对齐,直接使用设置了contentEditable="true"的div作为输入载体即可。
  • 绑定实时输入监听:监听可编辑div的input事件,每次内容变化时将当前纯文本内容存入组件state,使用正则/%[^%]+%/g做全局匹配,把整段文本拆成「普通文本片段」和「%包裹的特殊词汇片段」两类,按文本原有顺序排列,避免分词错位。
  • 差异化渲染片段:遍历分词结果,普通文本直接渲染为文本节点,匹配到的特殊词汇包裹在内联样式的<span>标签中,设置绿色字体样式即可,注意不要给高亮span加contentEditable="false"属性,否则会导致这部分文本无法正常编辑、删除。
  • 修复光标跳位问题:可编辑容器重渲染后默认会把光标重置到输入域开头,需要在每次重渲染前记录当前光标位置,重渲染完成后通过浏览器原生Selection和Range API把光标还原到原有位置,保证输入流畅。

核心实现代码

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

export default function HighlightTextarea() {
  const [plainText, setPlainText] = useState('');
  const editorRef = useRef(null);
  const cursorPosition = useRef(0);

  // 记录重渲染前的光标位置
  const saveCursorPosition = () => {
    const selection = window.getSelection();
    if (selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);
      cursorPosition.current = range.startOffset;
    }
  };

  // 重渲染后还原光标位置
  const restoreCursorPosition = () => {
    const selection = window.getSelection();
    const range = document.createRange();
    if (!editorRef.current.childNodes.length) return;
    
    let currentOffset = 0;
    for (let i = 0; i < editorRef.current.childNodes.length; i++) {
      const node = editorRef.current.childNodes[i];
      const nodeLength = node.textContent.length;
      if (cursorPosition.current <= currentOffset + nodeLength) {
        const targetNode = node.firstChild || node;
        range.setStart(targetNode, cursorPosition.current - currentOffset);
        range.collapse(true);
        selection.removeAllRanges();
        selection.addRange(range);
        break;
      }
      currentOffset += nodeLength;
    }
  };

  // 文本分词逻辑
  const parseTextToFragments = (text) => {
    const matchReg = /%[^%]+%/g;
    const fragments = [];
    let lastMatchEnd = 0;
    let matchResult;

    while ((matchResult = matchReg.exec(text)) !== null) {
      // 插入匹配项前的普通文本
      if (matchResult.index > lastMatchEnd) {
        fragments.push(text.slice(lastMatchEnd, matchResult.index));
      }
      // 插入高亮特殊词汇
      fragments.push(
        <span key={matchResult.index} style={{ color: '#16a34a' }}>
          {matchResult[0]}
        </span>
      );
      lastMatchEnd = matchReg.lastIndex;
    }
    // 插入最后一段普通文本
    if (lastMatchEnd < text.length) {
      fragments.push(text.slice(lastMatchEnd));
    }
    return fragments;
  };

  const handleInput = (e) => {
    saveCursorPosition();
    setPlainText(e.currentTarget.textContent);
  };

  useEffect(() => {
    restoreCursorPosition();
  }, [plainText]);

  return (
    <div
      ref={editorRef}
      contentEditable="true"
      onInput={handleInput}
      style={{
        width: '100%',
        minHeight: '120px',
        padding: '8px 12px',
        border: '1px solid #d4d4d4',
        borderRadius: '6px',
        outline: 'none',
        lineHeight: 1.5
      }}
    >
      {parseTextToFragments(plainText)}
    </div>
  );
}

原有方案问题说明

前两种拆分标签通过map渲染的方案,本质是把连续的文本流拆成了独立的表单控件,控件之间天然存在交互边界,无法实现单输入域的连续编辑体验;第三种固定字符串匹配的方案,没有把匹配逻辑和输入事件、state更新绑定,仅在组件初始化时执行了一次匹配,自然无法响应实时输入的内容变化更新高亮。

使用注意事项

  • 提交、校验输入内容时,直接使用state中存储的plainText纯文本变量即可,不要读取可编辑div的innerHTML,避免混入span标签字符串。
  • 如果需要调整特殊词汇的匹配规则,直接修改parseTextToFragments方法里的正则即可,支持动态匹配任意规则的关键词。

内容的提问来源于stack exchange,提问作者Anh Hoang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 08:39:20