npm
git
在前端开发中,我们经常需要实现各种复杂的编辑器功能,比如代码编辑器、富文本编辑器等。本文将介绍如何基于 React 和 CodeMirror 实现一个带有自定义占位符功能的编辑器,这种编辑器在模板系统、表单设计器等场景中非常有用。
一、需求分析
我们需要实现一个编辑器,具有以下功能:
- 支持在文本中插入特定格式的占位符,如
[left:0]内容[right:0]
- 占位符区域有特殊的样式,与普通文本区分开
- 占位符区域内的内容可编辑
- 当占位符内容为空时,显示默认提示文本
- 占位符的边界不可编辑,需要保持完整性
二、技术选型
- 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. 工作流程
- 解析文本,找出所有符合格式的占位符区域
- 为每个占位符区域创建装饰器(Decoration)
- 处理用户输入,确保占位符边界不被破坏
- 当用户删除占位符边界时,删除整个占位符区域
四、实现细节
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. 性能优化
为了提高性能,我们对代码进行了一些优化:
- 使用
React.useMemo
缓存扩展配置,避免不必要的重新计算 - 对装饰器进行排序和过滤,确保它们有效且有序
- 使用模块级变量来避免状态丢失
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 的自定义占位符编辑器。这个编辑器可以在各种场景中使用,如模板系统、表单设计器等。
未来可以考虑的改进方向:
- 支持更多类型的占位符,如下拉选择、日期选择等
- 添加占位符的拖拽功能,方便用户调整位置
- 实现占位符的嵌套支持
- 添加撤销/重做功能的特殊处理
- 优化移动端的支持
希望本文对你实现类似功能有所帮助!如有问题,欢迎在评论区讨论。