基于 React 和 CodeMirror 实现自定义占位符编辑器

npm

git

在前端开发中,我们经常需要实现各种复杂的编辑器功能,比如代码编辑器、富文本编辑器等。本文将介绍如何基于 React 和 CodeMirror 实现一个带有自定义占位符功能的编辑器,这种编辑器在模板系统、表单设计器等场景中非常有用。
在这里插入图片描述

一、需求分析

我们需要实现一个编辑器,具有以下功能:

  1. 支持在文本中插入特定格式的占位符,如 [left:0]内容[right:0]
  2. 占位符区域有特殊的样式,与普通文本区分开
  3. 占位符区域内的内容可编辑
  4. 当占位符内容为空时,显示默认提示文本
  5. 占位符的边界不可编辑,需要保持完整性

二、技术选型

  • React:用于构建用户界面
  • CodeMirror:强大的代码编辑器库,提供了丰富的扩展机制
  • TypeScript:提供类型检查,增强代码可维护性

CodeMirror 是一个功能强大的编辑器库,它提供了丰富的扩展机制,可以通过自定义插件来实现各种复杂的编辑功能。在本项目中,我们将利用 CodeMirror 的 ViewPlugin、Decoration 等 API 来实现占位符功能。

三、设计思路

1. 数据结构设计

首先,我们需要定义几个关键的数据结构:

// 占位符配置
interface PlaceholderConfig {
  id: string;        // 唯一标识
  begin: string;     // 开始标记
  end: string;       // 结束标记
  placeholder: string; // 默认提示文本
}

// 区域信息
interface SlotRegion {
  id: string;        // 对应的配置ID
  beginPos: number;  // 开始位置
  endPos: number;    // 结束位置
}

2. 核心组件设计

我们需要设计几个核心组件:

  • SlotLeftWidget:左侧边界小部件,用于显示占位符的左边界
  • SlotRightWidget:右侧边界小部件,用于显示占位符的右边界
  • SlotPlaceholderWidget:占位符小部件,用于在内容为空时显示提示文本
  • createPlaceholderPlugin:创建 CodeMirror 插件,处理占位符的渲染和交互

3. 工作流程

  1. 解析文本,找出所有符合格式的占位符区域
  2. 为每个占位符区域创建装饰器(Decoration)
  3. 处理用户输入,确保占位符边界不被破坏
  4. 当用户删除占位符边界时,删除整个占位符区域

四、实现细节

1. 小部件实现

首先,我们实现三个小部件类,用于显示占位符的不同部分:

// 左侧边界小部件
class SlotLeftWidget extends WidgetType {
  constructor(private id: string) {
    super();
  }

  eq(other: SlotLeftWidget): boolean {
    return this.id === other.id;
  }

  toDOM(): HTMLElement {
    const span = document.createElement("span");
    span.className = "slot-side-left";
    span.contentEditable = "false";
    span.setAttribute("data-slot-id", this.id);
    return span;
  }
}

// 右侧边界小部件
class SlotRightWidget extends WidgetType {
  // 类似实现...
}

// 占位符小部件
class SlotPlaceholderWidget extends WidgetType {
  // 类似实现...
}

2. 插件实现

接下来,我们实现 CodeMirror 插件,处理占位符的渲染和交互:

function createPlaceholderPlugin(config: PlaceholderConfig[] = []) {
  let slotRegions: SlotRegion[] = [];
  
  return ViewPlugin.fromClass(
    class {
      decorations;
      
      constructor(view: EditorView) {
        this.decorations = this.createDecorations(view);
      }
      
      // 创建装饰器
      createDecorations(view: EditorView) {
        // 解析文本,找出所有占位符区域
        // 为每个区域创建装饰器
      }
      
      // 处理更新
      update(update: ViewUpdate) {
        // 处理文档变化
        // 处理光标移动
        // 处理视口变化
      }
    },
    {
      decorations: v => v.decorations,
      provide: plugin => EditorView.atomicRanges.of(view => {
        // 确保边界标记是原子的
      })
    }
  );
}

3. 主组件实现

最后,我们实现主组件,将插件集成到 CodeMirror 中:

const CodeEditor: React.FC<CodeEditorProps> = ({ 
  value, 
  onChange,
  placeholderConfig = [
    { id: '0', begin: '[left:0]', end: '[right:0]', placeholder: '请输入内容' },
    // 更多默认配置...
  ] 
}) => {
  const extensions = React.useMemo(() => {
    return [
      createPlaceholderPlugin(placeholderConfig),
      EditorView.lineWrapping,
      EditorView.theme({
        // 样式定义
      }),
    ];
  }, [placeholderConfig]);

  return (
    <CodeMirror
      value={value}
      onChange={onChange}
      extensions={extensions}
    />
  );
};

五、遇到的问题及解决方案

在实现过程中,我遇到了一些挑战性的问题,下面分享一下解决方案:

1. 删除操作导致的更新冲突

问题:当用户删除占位符边界时,我们需要删除整个占位符区域,但直接在 update 方法中修改文档会导致错误:Error: Calls to EditorView.update are not allowed while an update is in progress

解决方案:使用 setTimeout 延迟执行删除操作,避免在更新过程中触发新的更新:

setTimeout(() => {
  try {
    const transaction = update.view.state.update({
      changes: {from: beginPos, to: toB, insert: ''},
      scrollIntoView: false
    });
    update.view.dispatch(transaction);
  } catch (e) {
    console.error('延迟删除区域时出错:', e);
  } finally {
    globalIsDeleting = false;
  }
}, 0);

2. 占位符内容包含换行符的处理

问题:当占位符内容包含换行符时,简单的装饰器无法正确处理多行内容。

解决方案:分段处理每一行,为每一行和换行符单独添加装饰器:

if (contentText.includes('\n')) {
  // 如果包含换行符,分段处理每一行
  let lineStart = contentStartPos;
  
  for (let i = 0; i < contentText.length; i++) {
    const char = contentText[i];
    const currentPos = contentStartPos + i;
    
    if (char === '\n') {
      // 为当前行添加样式
      // 换行符单独处理
      // 更新行起始位置
    }
  }
  
  // 处理最后一行
}

3. begin 与 end 邻接时占位符无法显示

问题:当 begin 与 end 标记直接相邻时(即 contentStartPos === contentEndPos),占位符无法正常显示。

解决方案:使用 Decoration.widget 而不是 Decoration.replace,在单个位置插入内容:

if (contentStartPos === contentEndPos) {
  // 处理 begin 与 end 邻接的情况
  decorations.push(
    Decoration.widget({
      widget: new SlotPlaceholderWidget(placeholder, id),
      side: 1, // 放置在位置的右侧
      block: false
    }).range(contentStartPos)
  );
}

4. 光标位置修正

问题:用户可能会将光标放在占位符边界内部,这会导致边界被破坏。

解决方案:实现光标位置修正方法,检测并修正光标位置:

correctCursorPosition(view: EditorView, pos: number): number {
  // 检查光标是否在任何占位符区域内
  for (const region of slotRegions) {
    // 如果光标在开始标记内部,将其移到标记之后
    // 如果光标在结束标记内部,将其移到标记之前
  }
  
  return pos;
}

六、优化与改进

在基本功能实现后,我们进行了一些优化:

1. 样式优化

为了提升用户体验,我们对占位符的样式进行了优化:

'.slot-placeholder': {
  display: 'inline-block',
  backgroundColor: 'rgba(186, 192, 255, .2)',
  color: 'rgba(148, 152, 247, .7)',
  padding: '0 4px',
  lineHeight: '20px',
  minHeight: '20px',
  verticalAlign: 'middle',
  cursor: 'text',
  border: '1px solid rgba(148, 152, 247, .3)',
  borderLeft: 'none',
  borderRight: 'none',
  whiteSpace: 'nowrap',
  overflow: 'hidden',
  textOverflow: 'ellipsis',
  maxWidth: '150px',
}

2. 性能优化

为了提高性能,我们对代码进行了一些优化:

  1. 使用 React.useMemo 缓存扩展配置,避免不必要的重新计算
  2. 对装饰器进行排序和过滤,确保它们有效且有序
  3. 使用模块级变量来避免状态丢失

3. 错误处理

为了提高代码的健壮性,我们添加了全面的错误处理:

try {
  // 操作代码
} catch (e) {
  console.error('错误信息:', e);
  // 恢复状态
} finally {
  // 重置标志
}

七、使用示例

下面是一个简单的使用示例:

function App() {
  const [code, setCode] = useState(`这是示例0:[left:0][right:0] [left:0][right:0]   [left:1]有内容[right:1]

    这是示例1: 
 ssssss  [left:2] [right:2]  
    这是示例3: [left:3]这是多行内容
    第二行内容[right:3]
    `);
    
  useEffect(() => {
    console.log('code', code)
  }, [code])
  
  return (
    <div>
      <h2 style={{ textAlign: "center" }}>代码编辑器</h2>
      <CodeEditor
        value={code}
        onChange={setCode}
      />
    </div>
  );
}

八、总结与展望

通过本文,我们实现了一个基于 React 和 CodeMirror 的自定义占位符编辑器。这个编辑器可以在各种场景中使用,如模板系统、表单设计器等。

未来可以考虑的改进方向:

  1. 支持更多类型的占位符,如下拉选择、日期选择等
  2. 添加占位符的拖拽功能,方便用户调整位置
  3. 实现占位符的嵌套支持
  4. 添加撤销/重做功能的特殊处理
  5. 优化移动端的支持

希望本文对你实现类似功能有所帮助!如有问题,欢迎在评论区讨论。

参考资源

  1. CodeMirror 官方文档
  2. React 官方文档
  3. TypeScript 官方文档
首先,我们需要安装 `react-codemirror2` `codemirror` 包: ``` npm install react-codemirror2 codemirror ``` 然后,我们可以在 React 组件中使用 `CodeMirror` 组件来实现一个 Python 编辑器。以下是一个示例代码: ```jsx import React, { useState } from 'react'; import { Controlled as CodeMirror } from 'react-codemirror2'; import 'codemirror/lib/codemirror.css'; import 'codemirror/theme/material.css'; import 'codemirror/mode/python/python'; function PythonEditor() { const [code, setCode] = useState(''); const handleChange = (editor, data, value) => { setCode(value); }; return ( <CodeMirror value={code} onBeforeChange={handleChange} options={{ mode: 'python', theme: 'material', lineNumbers: true, }} /> ); } export default PythonEditor; ``` 在上面的代码中,我们使用 `useState` Hook 来创建一个 `code` 状态变量,用于保存用户输入的 Python 代码。我们还定义了一个 `handleChange` 函数,用于在用户输入时更新 `code` 变量的值。 在 `return` 中,我们使用 `CodeMirror` 组件来显示代码编辑器。我们将 `value` 属性设置为 `code` 变量的值,将 `onBeforeChange` 属性设置为 `handleChange` 函数,这样当用户输入时,`handleChange` 函数将被调用。我们还使用 `options` 属性来设置编辑器的一些选项,包括语言模式、主题行号显示。 最后,我们将 `PythonEditor` 组件导出供其他组件使用。 希望这个示例能够帮助你实现一个 Python 编辑器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也想MK代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值