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

React.js实现ContentEditable中用户输入文本的正则转HTML功能

React实现文本变量格式化与预定义变量功能

原代码核心问题分析

你的代码没能达到预期效果主要有3个原因:

  1. 丢失结构信息:用textContent获取内容会直接忽略已渲染的<span>标签,只保留纯文本,导致每次编辑都会把之前的变量格式清零。
  2. 光标混乱:每次用dangerouslySetInnerHTML重新渲染内容,会让光标跳转到输入框开头,严重影响编辑体验。
  3. 缺乏预定义变量支持:没有提供便捷的变量插入入口,用户只能手动输入格式。

优雅实现方案

下面是一套兼顾编辑体验、状态安全和功能需求的实现方案:

1. 核心思路

  • 用状态存储原始文本内容(包含($变量名)格式的文本),而非渲染后的HTML,便于直接发送到后端。
  • 渲染时将原始文本解析为普通文本+变量节点(<span class="variable">),避免使用dangerouslySetInnerHTML。
  • 监听编辑事件,将DOM内容还原为原始文本格式,同步更新状态。
  • 实现预定义变量一键插入功能,提升用户体验。

2. 完整代码实现

import { useState, useRef } from 'react';

// 预定义变量列表
const PREDEFINED_VARS = ['username', 'orderId', 'totalAmount', 'shippingAddress'];

// 解析原始文本为JSX节点
const parseContent = (content) => {
  // 正则匹配($变量名)格式
  const regex = /\(\$(.*?)\)/g;
  const parts = content.split(regex);
  const result = [];

  for (let i = 0; i < parts.length; i++) {
    if (i % 2 === 0) {
      // 普通文本节点
      parts[i] && result.push(parts[i]);
    } else {
      // 变量节点,添加样式类
      result.push(
        <span key={i} className="variable">
          ${parts[i]}
        </span>
      );
    }
  }
  return result;
};

function CreateTemplate() {
  const [rawContent, setRawContent] = useState('');
  const editableRef = useRef(null);

  // 处理编辑事件,将DOM内容还原为原始文本格式
  const handleInput = () => {
    const element = editableRef.current;
    if (!element) return;

    let text = '';
    const nodes = element.childNodes;

    for (const node of nodes) {
      if (node.nodeType === Node.TEXT_NODE) {
        // 普通文本直接追加
        text += node.textContent;
      } else if (node.nodeName === 'SPAN' && node.classList.contains('variable')) {
        // 变量节点还原为($变量名)格式
        const varName = node.textContent.replace(/^\$/, '');
        text += `($${varName})`;
      } else {
        // 其他未知节点,直接取文本内容
        text += node.textContent;
      }
    }

    setRawContent(text);
  };

  // 插入预定义变量到光标位置
  const insertVariable = (varName) => {
    const element = editableRef.current;
    if (!element) return;

    const selection = window.getSelection();
    if (selection.rangeCount === 0) {
      // 无选中内容,直接追加到末尾
      setRawContent(prev => prev + `($${varName})`);
      return;
    }

    const range = selection.getRangeAt(0);
    // 删除选中内容
    range.deleteContents();
    // 插入变量文本
    const textNode = document.createTextNode(`($${varName})`);
    range.insertNode(textNode);
    // 移动光标到变量后方
    range.setStartAfter(textNode);
    range.setEndAfter(textNode);
    selection.removeAllRanges();
    selection.addRange(range);
    // 手动触发input事件更新状态
    element.dispatchEvent(new Event('input'));
  };

  // 发送到后端的逻辑,直接使用rawContent即可
  const handleSubmit = () => {
    console.log('发送到后端的内容:', rawContent);
    // fetch('/api/submit', { method: 'POST', body: JSON.stringify({ content: rawContent }) });
  };

  return (
    <div className="template-editor">
      {/* 预定义变量按钮组 */}
      <div className="var-buttons">
        {PREDEFINED_VARS.map(varName => (
          <button
            key={varName}
            onClick={() => insertVariable(varName)}
            className="var-button"
          >
            ${varName}
          </button>
        ))}
      </div>

      {/* 可编辑区域 */}
      <div
        ref={editableRef}
        contentEditable
        onInput={handleInput}
        className="editable-area"
      >
        {parseContent(rawContent)}
      </div>

      {/* 提交按钮 */}
      <button onClick={handleSubmit} className="submit-btn">
        提交模板
      </button>

      {/* 样式 */}
      <style jsx>{`
        .template-editor {
          max-width: 800px;
          margin: 20px auto;
          padding: 0 20px;
        }
        .var-buttons {
          margin-bottom: 10px;
          display: flex;
          gap: 8px;
          flex-wrap: wrap;
        }
        .var-button {
          padding: 4px 12px;
          background: #e5f3ff;
          border: 1px solid #2196f3;
          border-radius: 4px;
          cursor: pointer;
          color: #1976d2;
        }
        .editable-area {
          min-height: 150px;
          padding: 12px;
          border: 1px solid #ddd;
          border-radius: 4px;
          outline: none;
        }
        .variable {
          background: #fff3cd;
          padding: 2px 6px;
          border-radius: 3px;
          border: 1px solid #ffeeba;
          font-family: monospace;
          cursor: default;
        }
        .submit-btn {
          margin-top: 10px;
          padding: 8px 16px;
          background: #2196f3;
          color: white;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
      `}</style>
    </div>
  );
}

export default CreateTemplate;

3. 关键细节说明

  • 状态管理:rawContent存储用户输入的原始文本,包含($变量名)格式,直接用于后端提交,无需额外转换。
  • 内容解析:parseContent函数将原始文本拆分为普通文本和变量节点,用React的JSX安全渲染,避免XSS风险。
  • 编辑同步:handleInput遍历DOM节点,将变量节点还原为($变量名)格式,确保状态与编辑内容一致。
  • 变量插入:insertVariable利用window.getSelection定位光标,精准插入变量文本,同时更新光标位置,保持编辑连贯性。

4. 样式优化

给变量节点添加背景色、边框等样式,让用户清晰区分普通文本和变量,提升视觉体验。

内容的提问来源于stack exchange,提问作者Fares Galal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 15:47:10