layui layedit富文本编辑器自定义字号颜色实现与五大避坑指南

发布时间:2026/10/19 7:43:47
layui layedit富文本编辑器自定义字号颜色实现与五大避坑指南
简介Layui富文本编辑器layedit.js的完整资源包适合使用layui框架的前端开发者用于快速实现字体大小、字体颜色、背景颜色等文本样式定制也覆盖表格、图片批量上传、超链接、视频插入和全屏编辑等常见内容管理需求。压缩包仅849KB共126个文件其中29个js脚本、6个css样式、4个html示例页面构成核心代码与演示入口另配75个gif过程演示动画、字体图标和协议说明等辅助文件便于直接对照学习。当前已有262人学习下载适合需要集成或二次开发富文本编辑器的中级前端工程师。通过示例页面结合动态演示可以直观掌握layedit.js在字体与颜色设置、段落排版、锚点跳转等功能上的调用方式和配置细节有效减少在layui项目中接入与调试编辑器的时间。1. 天天跟layui富文本打交道迟早会撞上同一个尴尬一位天天跟layui富文本打交道的后端开发迟早会撞上同一个尴尬layedit.js这个官方内置编辑器居然连字体大小、字体颜色、背景颜色这三件基础事都办不利索。照着文档在tool数组里填上fontSize和colorPicker渲染出的按钮要么点了没反应要么弹层位置飘忽选完颜色回到编辑器刚才想改的那段文字已经不知道去哪了。这里就把这套内置机制拆开来看先讲清楚为什么默认按钮是半残的再给出可复现的自定义字号按钮、自定义色板实现最后补上五个反直觉的坑。适合正在用layui做后台、又不想为富文本单独引入重编辑器的团队。2. 先看内置实现layedit工具栏、iframe机制与失效根因2.1 内置tool配置能列出哪些键layedit的build方法是整个编辑器的入口第二个参数里的tool字段直接决定工具栏渲染哪些按钮。最常见的配置长这样layui.use([layedit], function () { var layedit layui.layedit; var index layedit.build(editor, { tool: [ strong, italic, underline, del, |, left, center, right, |, link, unlink, image ] }); });说明tool数组里每个字符串对应一个内部注册好的按钮处理器|负责渲染分隔条。strong、italic这些键对应加粗、斜体等基础命令走的是iframe内document.execCommandlink、image走的是layedit自己的弹层逻辑。注意点不同layui小版本对tool数组的容错不一样。你传一个没有对应实现的键进去部分版本会直接渲染出一个空span占位部分版本会静默忽略。所以判断当前版本有没有某个内置功能唯一可靠的标准是去翻当前layedit.js里toolList的注册表而不是只看文档示例。这也是fontSize和colorPicker在部分版本里看起来有配置、没效果的原因之一。2.2 iframe编辑区与隐藏textarea一套内容两份同步layedit把textarea隐藏之后真正承担编辑职责的是一个iframe。用户在iframe里输入时layedit通过监听内部body的输入事件把innerHTML同步到textarea里供表单提交。理解这个架构后面所有自定义操作都要围绕三件事来写// 1. 拿到iframe var $wrap $(#editor).next(.layui-layedit); // build后容器紧挨着textarea var $iframe $wrap.find(iframe); var iframe $iframe[0]; var doc iframe.contentDocument || iframe.contentWindow.document; var win iframe.contentWindow; // 2. 编辑完后把内容从iframe同步回textarea layedit.sync(index); // 3. 表单取值时用layedit.getContent(index) var html layedit.getContent(index);说明第一步的DOM查找方式依赖layedit默认的渲染结构——textarea被隐藏后紧随其后生成一个.layui-layedit容器里面再放工具栏和iframe。假如项目里给编辑器又包了一层div把选择器改成在容器内find(iframe)即可。参数说明doc是iframe内部文档所有execCommand都要作用在doc上而不是主文档上win用来实现focus和读取选中区。layedit.sync必须传build返回的indexgetContent同理。把这三件事记牢后面所有自定义逻辑都只是在这三个对象上做文章。2.3 字号设置失败的直接原因execCommand只认1-7内置fontSize按钮失效最直接的原因是底层execCommand对字号参数的限制。命令签名是doc.execCommand(fontSize, false, size)这里的size只接受1-7对应到浏览器再把这几个值映射成默认字号而不会出现你想要的16px。// 这是layedit内置字号按钮内部执行的等效代码 doc.execCommand(fontSize, false, selVal); // selVal来自下拉框问题就出在selVal有的版本下拉框的值是12px16px这类带单位字符串甚至直接是空串。execCommand收到非法值后什么都不做所以用户无论怎么选选中文字纹丝不动。就算给它一个合法值3生成的内容也是这个标签在内容提交后很容易被服务端的XSS过滤或样式重置干掉存进数据库再拿出来就只剩裸文本。所以结论很明确别指望内置按钮自定义字号要走先打大号标记、再替换成span加内联style这条路第3章会写完整实现。3. 自定义字体大小从按钮组到execCommand改造3.1 在工具栏注入一组字号按钮build完编辑器后工具栏DOM已经就位。我一般在构建参数里不写fontSize而是在build之后手动往工具栏追加一组按钮layui.use([layedit], function () { var layedit layui.layedit; var index layedit.build(editor, { tool: [strong, italic, underline, del, |, left, center, right] }); var $wrap $(#editor).next(.layui-layedit); var $tool $wrap.find(.layui-layedit-tool); var sizes [12, 14, 16, 18, 20, 24, 28, 36]; var html div classcustom-font-size-group; sizes.forEach(function (px) { html button typebutton>$tool.on(mousedown, .custom-font-size-group button, function (e) { e.preventDefault(); // 阻止按钮抢占焦点 }); $tool.on(click, .custom-font-size-group button, function () { var px parseInt($(this).data(size), 10); applyFontSize(index, px); });说明mousedown时preventDefault浏览器不会把焦点从iframe移到按钮上等click触发时iframe里的选中文字还挂在Selection上。这个细节是整个方案成立的前提也解决了设置完字号光标却落到按钮上的体验问题。参数说明applyFontSize在下一节实现第一个参数index用于后续sync第二个参数px是目标字号。3.3 字号真正写入fontSize传7再全局替换font标签execCommand不接受像素值那就用一个取巧但稳定的思路先执行doc.execCommand(fontSize, false, 7)让浏览器给选中文字包上然后立刻在iframe内部把所有这些font标签替换成带style的spanfunction applyFontSize(editorIndex, px) { var $wrap $(#editor).next(.layui-layedit); var $iframe $wrap.find(iframe); var iframe $iframe[0]; var doc iframe.contentDocument || iframe.contentWindow.document; var win iframe.contentWindow; win.focus(); // focus回到编辑区 doc.execCommand(fontSize, false, 7); var fonts doc.querySelectorAll(font[size7]); fonts.forEach(function (font) { var span doc.createElement(span); span.style.fontSize px px; span.innerHTML font.innerHTML; // 保留原有文本与内部标签 font.parentNode.replaceChild(span, font); }); layedit.sync(editorIndex); // 同步回隐藏textarea }说明execCommand只认1-7所以统一传7作为临时标记再用querySelectorAll把刚生成的全部捞出来替换。替换时保留innerHTML这样选中文字内部的加粗、斜体不会丢。最后layedit.sync把iframe里的新HTML同步给textarea防止表单提交拿到旧内容。参数说明px是要设置的像素字号来自按钮的data-size。sync方法必须传build返回的index传错会导致找不到对应编辑器。还有一个容易忽略的点如果用户选中了跨块级元素的内容execCommand生成的可能是多个分散的font标签所以必须用querySelectorAll遍历替换不能只处理第一个。3.4 验证效果与边界情况完成替换后往编辑器里输入一段文字全选点18px按钮然后打开控制台读iframe内部的bodyvar $wrap $(#editor).next(.layui-layedit); var doc $wrap.find(iframe)[0].contentDocument; console.log(doc.body.innerHTML);预期输出应该出现而不是。若还是font标签说明替换逻辑没执行或者选择器没匹配到先检查querySelectorAll的返回值。边界情况空选区时执行命令execCommand不会生成font标签替换循环空转页面不会报错这是可接受的跨段落选中时会出现多个font标签会被全部替换建议替换后保持原样不做合并减少DOM结构调整带来的意外副作用。4. 字体颜色与背景色预设色板与选区恢复4.1 内置colorPicker为什么难用内置colorPicker的难用分两层。第一层是弹层定位它把颜色面板渲染在顶层主文档的某个绝对定位容器里而触发按钮在iframe外层的工具栏里坐标算不准时面板就飘到左上角。第二层是焦点打开颜色面板的click事件一旦发生iframe立刻失焦Selection被重置选完颜色再回编辑器已经不知道刚才选的是哪段文字。所以做自定义颜色按钮时我完全绕开内置colorPicker自己维护一块预设色板用mousedown阶段先保存选区选完颜色再恢复。4.2 预设色板挂在body下按按钮坐标定位色板本质上是一个隐藏的div里面放色块与一个hex输入框。关键是要把它append到body下而不是追加到工具栏容器里否则容易被容器的overflow裁剪掉一半var colorList [ #ff0000, #ff7800, #ffff00, #00ff00, #00aaff, #0000ff, #ff00ff, #800000, #808000, #008080, #000080, #808080, #c0c0c0, #ffffff, #000000 ]; var $palette $(div classcustom-color-palette styledisplay:none;/div); colorList.forEach(function (color) { $(a hrefjavascript:void(0);/a) .css(background-color, color) .attr(data-color, color) .appendTo($palette); }); $palette.append(div classcustom-color-hexinput typetext placeholder#rrggbb maxlength7 //div); $(body).append($palette);说明色板用绝对定位样式点击按钮时根据按钮的offset坐标show出来。a标签的href用javascript:void(0)是为了兼容旧浏览器下的默认跳转行为每块色样都带data-color供后续读取。参数说明colorList按项目需要调整这里给了15个常用颜色深色、浅色、纯色都有前端展示场景基本够用。色板里的input用于输入自定义hex值后面会专门处理它的失焦问题。4.3 打开颜色面板前先保存选区点击颜色按钮时需要先保存当前iframe内的选区再显示面板。如果直接show面板iframe焦点丢失后面再恢复就无从谈起var savedRange null; function saveSelection(win) { var sel win.getSelection(); savedRange sel.rangeCount 0 ? sel.getRangeAt(0).cloneRange() : null; } function restoreSelection(win) { win.focus(); var sel win.getSelection(); sel.removeAllRanges(); if (savedRange) { sel.addRange(savedRange); } } // 颜色按钮 $tool.on(mousedown, .custom-color-btn, function (e) { e.preventDefault(); saveSelection(win); }); $tool.on(click, .custom-color-btn, function () { var offset $(this).offset(); $palette.css({ left: offset.left px, top: offset.top $(this).outerHeight() px }).show(); e.stopPropagation(); });说明saveSelection用cloneRange把Range对象存下来restore时先focus再addRange。mousedown阶段阻止焦点转移click再show面板此时选区已经被savedRange保管不会丢。参数说明win是iframe.contentWindow。savedRange是模块级变量单编辑器场景够用如果页面里多个编辑器实例共用这一套逻辑需要把savedRange收进闭包按实例隔离第5.1条会展开讲。点击色块时不需要再次save直接restore后执行颜色命令$palette.on(mousedown, a, function (e) { e.preventDefault(); }); $palette.on(click, a, function () { var color $(this).attr(data-color); restoreSelection(win); doc.execCommand(foreColor, false, color); layedit.sync(index); $palette.hide(); });说明色块上的mousedown也做preventDefault是防止面板内点击把焦点拉到a标签上。restoreSelection里先focus再恢复选区之后再执行execCommand才有对象可作用。4.4 foreColor与backColor的浏览器差异文字颜色用foreColor基本没争议背景色才是坑。Chrome下doc.execCommand(backColor, false, color)对行内选区会把整段的块背景填色而不是只给文字涂高亮效果就像给整行加了一个底色条不是想象中的mark高亮。function applyTextBgColor(doc, color) { // Chrome / Edge 走 hiliteColor只作用于文本 var ok doc.execCommand(hiliteColor, false, color); // Firefox 不支持 hiliteColor回退到 backColor if (!ok) { doc.execCommand(backColor, false, color); } }说明execCommand会返回布尔值用它做能力检测比UA判断可靠。hiliteColor在Firefox里会返回false于是接着执行backColor。Chrome里backColor也能执行成功但效果是块级背景所以不能反过来先试backColor否则就翻车了。hex输入框同样走mousedown保存、change应用到选中的段落$palette.on(mousedown, input, function (e) { e.preventDefault(); // 防止输入框抢走iframe焦点 }); $palette.on(change, input, function () { var color $(this).val().trim(); if (!/^#[0-9a-fA-F]{6}$/.test(color)) return; restoreSelection(win); doc.execCommand(foreColor, false, color); layedit.sync(index); $palette.hide(); });说明正则限定了#rrggbb格式。执行颜色命令前先restore确保选区在iframe里有效。输入非法值时直接return不给错误提示保持交互轻量。5. 避坑layedit自定义工具条最常见的五个翻车现场5.1 多实例串台工具栏事件绑到了所有编辑器现象页面放了两个layedit点第一个编辑器的字号按钮第二个编辑器的内容被改了。原因用了$(.layui-layedit-tool).on(click, ...)这种全局选择器绑事件事件会命中所有工具栏反向查找iframe时又用了类名首次匹配拿到的永远是第一个编辑器。解决用textarea的id做锚点先定位容器再找子元素function getLayeditFrame(editorId) { var $wrap $(# editorId).next(.layui-layedit); return $wrap.find(iframe)[0]; }每个编辑器的事件回调都通过闭包绑定各自index和iframe事件委托只挂在当前容器的$tool上。从那以后我接手的项目里所有layedit扩展函数第一行都是这个定位逻辑。5.2 getContent拿到空内容忘了sync现象页面用layedit.getContent(index)提交文章内容发现拿到的还是编辑器里最初的占位内容或者直接是空字符串。原因getContent读的是隐藏textarea的值而textarea只有在sync或内部输入监听触发时才被更新。自定义按钮直接操作iframe的body后并没有自动sync。解决所有自定义命令执行完统一调一次layedit.sync(index)。如果ajax提交前不放心可以再手动拉一次layedit.sync(index); var content layedit.getContent(index);这一步是幂等的多调用一次没有副作用但漏掉一次就是事故。5.3 字号按钮第一次点击没反应现象页面加载完立刻点字号按钮选中文字没变化但从第二次开始就正常。原因iframe里的body存在contentEditable初始化时序问题execCommand执行时机早于iframe内部document就绪。layedit.build是异步渲染的build返回index只代表textarea容器已创建iframe内部document不一定写完了。解决在build后用一个短轮询等待iframe的body ready(function waitReady() { var body iframe.contentDocument iframe.contentDocument.body; if (body body.isContentEditable) { return; } setTimeout(waitReady, 50); })();确保按钮绑定和后续操作都发生在ready之后。项目里如果反复遇到这种偶发现象优先排查是不是初始化时序问题别急着改execCommand的参数。5.4 背景色把整块都染了不是文字高亮现象选中一句话设置背景颜色结果整行甚至整段都被铺上底色看起来像表格单元格着色。原因execCommand(backColor)在Chrome中作用于块级背景只有hiliteColor才是文本高亮语义。解决按4.4的兼容编排先用hiliteColor失败再回退backColor。实际项目中用行内选区对比两种命令的效果差异非常明显选错API就是这个坑的直接原因。5.5 色板被截断或定位飘了现象点颜色按钮色板从页面左上角冒出来或者只露出一半。原因色板被append到了工具栏容器内部而工具栏或外层容器带有overflow属性或者色板用了position:absolute但祖先里有transform元素导致坐标系偏移。解决一律把色板append到body下定位用按钮的offset计算整体结构确定下来$palette.css({ position: absolute, left: btnOffset.left px, top: btnOffset.top btnHeight px, zIndex: 9999 });避免使用fixedfixed在iframe滚动时不会跟着动体验更差。定位坐标用触发按钮的offset加按钮自身高度计算能保证色板稳稳出现在按钮下方。6. 进阶把字号/颜色功能收拢成一段可复用的扩展函数做过三四个项目后我把第3章、第4章的细节收进了一个函数新项目里接编辑器只调一行。核心是参数化字号档位、色板和是否启用背景色function extendLayeditFontTool(editorId, layedit, index, options) { var opts $.extend({ sizes: [12, 14, 16, 18, 20, 24, 28, 36], colors: [#ff0000, #ff7800, #ffff00, #00ff00, #00aaff, #0000ff, #ff00ff, #800000, #808000, #008080, #000080, #808080, #c0c0c0, #ffffff, #000000] }, options || {}); var $wrap $(# editorId).next(.layui-layedit); var $tool $wrap.find(.layui-layedit-tool); var $iframe $wrap.find(iframe); var iframe $iframe[0]; var doc iframe.contentDocument || iframe.contentWindow.document; var win iframe.contentWindow; var savedRange null; // 第3章的字号按钮组、第4章的色板与选区恢复逻辑全部封装在这里 // 省略的具体实现直接复用前文代码按 editorId 定位后挂载事件 return { applySize: function (px) { /* ... */ }, applyColor: function (color) { /* ... */ }, applyBgColor: function (color) { /* ... */ } }; }参数说明editorId是textarea的idlayedit是全局layedit对象index是build返回的实例序号。options里的sizes控制字号档位colors控制色板颜色需要更多或更少的档位直接改数组即可不需要碰内部实现。使用示例放在layedit.build之后layedit.build(editor, { tool: [strong, italic, underline, del, |, left, center, right] }); var ext extendLayeditFontTool(editor, layedit, index, { sizes: [12, 14, 16, 18, 20, 24, 28, 36] }); // 后续通过 ext.applySize / ext.applyColor 调用这一层封装把按id定位编辑器、mousedown保选区、统一sync三板斧全部收住新项目里要做的只是保证tool配置里不出现fontSize这个半残键再把扩展函数挂上去。我从那以后每接一个layui后端项目都强制走一遍这套流程踩坑率低了很多希望帮到你。本文还有配套的精品资源点击获取