资讯动态

基于contentEditable实现可插入标签的富文本编辑器

发布时间:2026/10/27 8:15:49 来源:尧图企业网站定制
1. 为什么我们需要可插入标签的输入框在内容管理后台的开发过程中我经常遇到这样的需求运营人员需要在文章正文中插入一些动态标签比如{username}、{date}或者{product_name}。传统的解决方案要么是让用户手动输入这些标签容易出错要么是在输入框旁边放一堆按钮占用界面空间。contentEditable这个HTML属性给了我新的思路。它允许我们将普通的div元素变成可编辑区域就像textarea一样但功能更强大。通过它我们可以实现在任意位置插入不可编辑的标签块保持标签的整体性用户不能只删除标签的一部分自定义标签的视觉样式比如加上背景色和边框支持富文本编辑的同时保护标签结构最近一个电商项目就验证了这个方案的实用性。商品详情页需要插入20多种动态参数从简单的价格标签到复杂的库存状态组合。使用传统textarea时客服人员经常把{price}误写成{prise}导致页面显示异常。而改造后的标签化编辑器完全解决了这个问题。2. 核心实现方案设计2.1 基础结构搭建我们从一个最简单的contentEditable div开始div ideditor contenteditabletrue stylemin-height: 100px; border: 1px solid #ccc; padding: 8px; /div关键点在于如何处理标签插入。我的经验是使用span元素包裹标签内容并添加特定classfunction insertTag(tagName) { const editor document.getElementById(editor); const tag document.createElement(span); tag.className custom-tag; tag.dataset.tagName tagName; tag.contentEditable false; tag.textContent {${tagName}}; // 插入到当前光标位置 const selection window.getSelection(); const range selection.getRangeAt(0); range.insertNode(tag); // 插入后移动光标到标签后面 const newRange document.createRange(); newRange.setStartAfter(tag); selection.removeAllRanges(); selection.addRange(newRange); }2.2 标签的不可编辑特性这里有几个技术细节需要注意contentEditablefalse让标签本身不可编辑但需要配合CSS防止误操作.custom-tag { background-color: #e6f3ff; border-radius: 3px; padding: 2px 4px; margin: 0 2px; user-select: none; }处理退格键行为当光标紧跟在标签后面时按退格键应该直接删除整个标签而不是进入标签内部。这需要监听keydown事件editor.addEventListener(keydown, (e) { if (e.key Backspace) { const selection window.getSelection(); if (selection.rangeCount 0) return; const range selection.getRangeAt(0); const startContainer range.startContainer; // 检查光标是否紧跟在标签后面 if (startContainer.nodeType Node.TEXT_NODE) { const tag startContainer.previousSibling; if (tag tag.classList.contains(custom-tag)) { tag.remove(); e.preventDefault(); } } } });3. 进阶功能实现3.1 标签选择器设计在实际项目中我们通常需要提供一个可视化界面让用户选择要插入的标签。我推荐两种方案方案A下拉菜单式// 标签数据 const tags [username, email, order_date, product_name]; // 创建下拉菜单 const dropdown document.createElement(select); tags.forEach(tag { const option document.createElement(option); option.value tag; option.textContent {${tag}}; dropdown.appendChild(option); }); dropdown.addEventListener(change, () { if (dropdown.value) { insertTag(dropdown.value); dropdown.value ; } });方案B按钮面板式更适合标签数量少10个的场景div classtag-panel button onclickinsertTag(username){username}/button button onclickinsertTag(email){email}/button !-- 更多标签按钮 -- /div style .tag-panel { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; } .tag-panel button { padding: 4px 8px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; } /style3.2 数据提取与验证当用户提交表单时我们需要从编辑器中提取两种内容普通文本标签标记这里有个坑要注意通过innerHTML直接获取内容时标签的span元素会被序列化可能包含不需要的HTML属性。我的解决方案是function getEditorContent() { const editor document.getElementById(editor); const walker document.createTreeWalker( editor, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT ); let content ; let node; while (node walker.nextNode()) { if (node.nodeType Node.TEXT_NODE) { content node.textContent; } else if (node.classList.contains(custom-tag)) { content {${node.dataset.tagName}}; } } return content; }4. 实际项目中的经验教训4.1 移动端适配问题在iOS Safari上contentEditable的行为有些特殊点击插入的标签时虚拟键盘可能不会自动弹出长按标签有时会触发系统默认的文本选择菜单解决方案是添加以下CSS.custom-tag { -webkit-user-select: none; -webkit-touch-callout: none; }4.2 粘贴内容处理用户可能会从其他来源复制富文本内容粘贴到编辑器中。我们需要处理两种情况粘贴纯文本editor.addEventListener(paste, (e) { e.preventDefault(); const text (e.clipboardData || window.clipboardData).getData(text/plain); document.execCommand(insertText, false, text); });粘贴HTML内容时过滤危险标签editor.addEventListener(paste, (e) { e.preventDefault(); const html (e.clipboardData || window.clipboardData).getData(text/html); // 使用DOMPurify等库过滤HTML const cleanHTML DOMPurify.sanitize(html, { ALLOWED_TAGS: [b, i, u, br, p, span] }); // 插入清理后的内容 const range window.getSelection().getRangeAt(0); const fragment range.createContextualFragment(cleanHTML); range.deleteContents(); range.insertNode(fragment); });4.3 性能优化技巧当编辑器内容变得很长比如超过500个节点时可能会遇到性能问题。我的优化方案使用MutationObserver监听内容变化而不是频繁的DOM操作const observer new MutationObserver((mutations) { // 处理变化 }); observer.observe(editor, { childList: true, subtree: true, characterData: true });对于大量标签插入使用DocumentFragment批量操作function insertMultipleTags(tagNames) { const fragment document.createDocumentFragment(); tagNames.forEach(name { const tag document.createElement(span); // ...创建标签逻辑 fragment.appendChild(tag); }); const range window.getSelection().getRangeAt(0); range.insertNode(fragment); }5. 与现有富文本编辑器集成如果你已经在使用Quill、TinyMCE等富文本编辑器可以通过它们的API实现类似功能。以Quill为例const quill new Quill(#editor, { modules: { toolbar: [ // 常规工具栏项... [insertTag] // 自定义按钮 ] }, theme: snow }); // 自定义标签插入功能 const toolbar quill.getModule(toolbar); toolbar.addHandler(insertTag, function() { const tagName prompt(输入标签名); if (tagName) { const range quill.getSelection(); quill.insertEmbed(range.index, tag, { name: tagName }, user); quill.setSelection(range.index 1); } }); // 注册标签类型 Quill.register(formats/tag, { blotName: tag, tagName: span, className: custom-tag, create(value) { const node super.create(); node.textContent {${value.name}}; node.dataset.tagName value.name; node.contentEditable false; return node; } });这种方案的优点是能直接利用富文本编辑器现有的功能如撤销/重做、格式保持等缺点是定制灵活性稍低。6. 完整实现示例下面是一个可直接集成到项目中的完整实现!DOCTYPE html html head title标签化文本编辑器/title style #editor { min-height: 150px; border: 1px solid #ddd; padding: 10px; margin: 10px 0; outline: none; } .custom-tag { background-color: #e6f3ff; border: 1px solid #b8daff; border-radius: 3px; padding: 2px 4px; margin: 0 2px; display: inline-block; color: #004085; user-select: none; -webkit-user-select: none; } .tag-panel { display: flex; gap: 8px; margin: 10px 0; flex-wrap: wrap; } .tag-panel button { padding: 4px 8px; background: #f8f9fa; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-size: 14px; } .tag-panel button:hover { background: #e9ecef; } /style /head body h2标签化文本编辑器/h2 div classtag-panel button>

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑