简介layui 富文本编辑器 layedit.js 字体与颜色功能包面向使用 Layui 框架的前端开发者解决网页内容编辑中字体类型、字号、文字颜色与背景颜色等格式控制需求。压缩包内共 126 个文件包含 29 个 js 脚本、6 个 css 样式表、75 个 gif 工具图标以及 html 示例页面、iconfont 字体文件和少量 png、woff、ttf 等资源整体大小约 849KB。其中 js 负责编辑器核心逻辑css 控制界面样式gif 用于工具按钮图标html 演示初始化与调用方法结构清晰便于直接引入工程或按需裁剪。已有 262 人学习使用。开发者可获得完整的 layedit 相关文件快速实现段落排版、超链接、视频插入、表格、批量图片上传等丰富编辑功能同时重点支持字体大小、颜色和背景色的灵活调整满足突出显示、内容分区等场景。配套示例页更是降低了入门门槛适合需要快速集成富文本能力的后台管理系统或内容发布平台。1. 先弄清 layedit 到底缺什么默认工具栏与扩展点用 layui 做后台管理系统时富文本是刚需layedit 作为内置编辑器开箱即用确实省事。但很多开发者在第一版交付时才发现一个尴尬问题想把一段标题文字调大、换个颜色工具栏上竟然没有字号和颜色按钮默认只有加粗、斜体、下划线、删除线、链接、表情和图片。设计稿上的多级字号和强调色根本落不了地只能退回纯文本或者额外引一个别的编辑器反而把页面风格搞乱了。要解决这个需求不需要换编辑器也不需要引第三方富文本直接在 layedit 的扩展机制上补功能就行字体大小、字体颜色、背景颜色这三个按钮能在工具栏上正常显示、能作用于选中文本、能正常同步回 textarea 提交到后端。本文就按这个目标一步步拆适合正在做后台系统、被 layedit 默认工具栏卡住的前端开发者也适合想弄懂 layedit 内部事件绑定逻辑的人。2. 扩展工具栏前必须知道的渲染机制把 layedit 的工具按钮拆开看2.1 工具按钮不是写死的表单项而是一段动态渲染逻辑很多开发者以为 layedit 的工具栏是静态 HTML直接在页面上加几个select或button改一改就能用。实际不是。layedit 初始化时会读取你传入的tool配置数组再调用内部渲染函数把每个工具项拼成 DOM最后统一绑定事件。也就是说工具按钮长什么样、点下去干什么全由这段渲染逻辑决定。打开layedit.js会看到默认配置大概是这样的// layedit.js 内部默认配置2.x 常见写法 this.config { tool: [ strong, italic, underline, del , |, link, unlink , |, face, image ] , hideTool: [] // 需要隐藏的工具 , font: [宋体, 黑体, 仿宋, 楷体] // 2.x 版本部分内置字体的配置 };这段代码说明了两件事工具条完全由tool数组控制如果你传入的数组里有 layedit 不认识的名字它基本不会渲染出对应按钮也不会报错静默忽略。所以要加字体大小和颜色得先让 layedit 认识这些新工具项或者绕开它的渲染机制自己挂按钮。还有一个容易忽略的点layedit 是往页面上插入一个iframe所有富文本编辑操作都在 iframe 内部的document上执行。你在外层页面写的document.execCommand对编辑器内容基本无效必须拿到 iframe 的内容文档再操作。这个后面第 3 章会详细说但第 2 章选方案时就要把这条路线定下来。2.2 改源码还是初始化传参两种方案与适用边界在动手之前先定实现路线。网上常见的做法有三种改 layedit.js 源码、初始化时传自定义tool数组搭配外部事件绑定、在layedit.build返回后手动往工具栏插 DOM。先说改源码。这种方式最彻底打开layedit.js找到工具栏渲染的each遍历在item image这种分支后面追加item fontsize、item color的处理分别渲染一个下拉框和一个取色按钮同时在统一的事件分发函数里补上对应分支。优点是按钮外观和 layedit 自身的风格完全一致缺点是一旦 layui 升级改动很容易被覆盖每次都要重新对比 diff。初始化传参是更轻量的方式。layedit.init支持你传入一个自己的tool数组其中完全可以不包含 layedit 内置的工具名比如layedit.build(demo, { tool: [ strong, italic, underline , fontsize // 自定义工具名layedit 本身不识别 , color // 自定义工具名 , |, link, image ] });这里fontsize和color虽然写进了配置但 layedit 默认不认识它们直接这样初始化工具栏上什么都不会多出来。正确的做法是在build返回编辑器实例后利用返回的索引去操作工具栏容器自己渲染这两个按钮并绑定事件。我一般推荐这条路线因为不动源码升级框架时只需要保证自定义按钮的 DOM 挂着独立事件不受 layedit 内部逻辑影响。二者怎么选我的判断标准是如果只在一个项目里用且以后不会升 layui 版本改源码最省事如果项目里多个页面复用或者要长期维护走自定义渲染更稳。下面这节就按自定义渲染路线展开。2.3 字号下拉框的完整实现让字体大小真正作用到选中文本明确一下最终效果工具栏上出现一个字号下拉框选项从 12px 到 36px选择某个字号后编辑器内选中的文字立即变成对应大小。实现分三步第一步把下拉框插到工具栏第二步监听 select 的 change 事件第三步在 iframe 文档里执行字号命令。先看插入下拉框的代码var editIndex layedit.build(content, { tool: [strong, italic, underline, |, link, image] }); // build 返回的是编辑器索引layedit 内部用这个索引管理实例 var container layedit.getContainer(editIndex); // getContainer 返回的是包含 textarea 和 iframe 的外层容器 // layedit 的工具条 DOM 结构容器内的第一个 div.layui-unselect.layedit-tool var toolBar container.querySelector(.layedit-tool); if (!toolBar) return; // 在工具栏末尾追加字号下拉框 var fontSizeBox document.createElement(select); fontSizeBox.className layedit-fontsize-select; fontSizeBox.style.cssText height:28px;margin:4px 2px;border:1px solid #e2e2e2;; [12, 14, 16, 18, 20, 24, 28, 32, 36].forEach(function (size) { var opt document.createElement(option); opt.value size; opt.text size px; fontSizeBox.appendChild(opt); }); toolBar.appendChild(fontSizeBox);这段代码的逻辑在于拿到了 layedit 渲染好的工具栏容器toolBar然后把自建的 select 追加进去。getContainer(editIndex)是 layedit 提供的公开方法它返回的是外层容器工具栏就在里面第一个带layedit-tool类的 div 上这个类名在 2.x 各版本里基本稳定但保险起见建议打印一下 container 的 innerHTML 确认结构。接着说事件绑定。字号命令必须作用在 iframe 内的选中文本上直接在外层页面用document.execCommand(fontSize, false, size)是无效的因为焦点在 iframe 文档里外层 document 并没有选中内容。正确写法如下fontSizeBox.addEventListener(change, function () { var size this.value; // layedit 内部约定iframe 的 id 为 layedit-索引可通过容器查找 var iframe container.querySelector(iframe); if (!iframe) return; // 进入 iframe 的内容文档执行 fontSize 命令 var win iframe.contentWindow; var doc iframe.contentDocument || win.document; doc.execCommand(fontSize, false, 7); // fontSize 命令只接受 1-77 对应最大字号 // 把 7 号字转换成实际像素对选中元素设置 font-size var selected doc.queryCommandValue(fontSize); if (selected) { // execCommand 之后用 font 标签包裹了选中文本直接改 font 标签的 size 属性不直观 // 更可靠的做法是通过围绕选中内容设置 style doc.execCommand(styleWithCSS, false, true); doc.execCommand(fontSize, false, 7); var fonts doc.querySelectorAll(font[size7]); fonts.forEach(function (font) { font.style.fontSize size px; font.removeAttribute(size); }); } });这段代码里有一个关键坑点execCommand(fontSize)的参数不是像素值而是 1 到 7 的 HTML 字号等级传入size px会直接被忽略或报错。常见做法是先用7这个最大等级触发浏览器的包裹行为之后再用styleWithCSS把 CSS 样式落到font标签上最后换成实际的像素值。这里要说明一下querySelectorAll(font[size7])是拿刚才被 execCommand 包裹的内容不同浏览器生成的标签结构不完全一样有的是font size7有的是带 style 的 span。稳妥的方式是在执行命令前先用window.getSelection()记录选中范围执行后再遍历范围内的 font 标签。但那样代码会复杂不少实际项目中大多数浏览器都支持这种 font 包裹方式先用这个方案遇到兼容性问题再走遍历选区。3. 字体颜色与背景颜色从内置颜色按钮到自定义取色器3.1 layedit 自带的颜色能力到底覆盖了什么在这个方案里字体颜色和背景颜色的实现思路与字号不同。字号可以用浏览器内置的fontSize命令颜色也有对应的foreColor和backColor命令但 layedit 的工具栏渲染逻辑里并没有默认暴露这些按钮。有些版本把face表情按钮和图片按钮内置了颜色按钮并不在默认工具列表里。先确认一个基本事实layedit 的 iframe 文档支持标准的document.execCommand(foreColor)与document.execCommand(backColor)命令。这意味着我们不需要自己写选区处理逻辑只需要一个能拿到用户所选颜色的入口然后把颜色值喂给这两个命令就行。颜色入口有两种做法引入一个第三方取色器组件或者自己写一个轻量的颜色面板。在后台管理系统里为了不引入额外依赖我一般建议自己写一个简单面板一个 input 的typecolor其实就能满足基础需求它弹出的原生取色器在主流浏览器都支持代码只有一行。!-- 最简单的取色入口原生支持透明度和取色 -- input typecolor idfontColorPicker value#ff0000 stylewidth:30px;height:28px;border:none;padding:0;cursor:pointer;用原生取色器做基础功能是够用的但有一个体验问题它无法和工具栏整体风格融合。想要更好的视觉效果可以在 input 外面包一层模仿 layedit 按钮样式的 div点击时让 input 触发 click 事件弹出取色器这样工具栏上看起来就是一个正常的图标按钮实际点击的是隐藏的取色器。3.2 自定义颜色按钮的完整接入流程接入流程和字号下拉框类似也需要在工具栏上追加自己的 DOM 元素代码逻辑分两步先渲染按钮再监听取色器事件并执行命令。下面这段代码实现了字体颜色的完整接入// 追加一个字体颜色按钮区域内部包含一个隐藏的取色器 input var colorBtnWrap document.createElement(div); colorBtnWrap.style.cssText display:inline-block;position:relative;margin:4px 2px;; var colorIcon document.createElement(span); colorIcon.style.cssText display:inline-block;width:24px;height:24px;line-height:24px;text-align:center;cursor:pointer;color:#666;; colorIcon.title 字体颜色; colorIcon.textContent A; var colorInput document.createElement(input); colorInput.type color; colorInput.value #ff0000; colorInput.style.cssText position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer;; colorInput.addEventListener(change, function () { // 将取到的颜色值作用到 iframe 内选中文本 var iframe container.querySelector(iframe); var doc iframe.contentDocument || iframe.contentWindow.document; doc.execCommand(styleWithCSS, false, true); // 优先输出内联 style而不是 font 标签 doc.execCommand(foreColor, false, this.value); // 命令执行完毕后保留 iframe 焦点方便连续操作 iframe.contentWindow.focus(); }); colorBtnWrap.appendChild(colorIcon); colorBtnWrap.appendChild(colorInput); toolBar.appendChild(colorBtnWrap);这段代码的核心逻辑是透明度为 0 的取色 input 覆盖在整个按钮上方用户点击的是按钮实际触发的是取色器。execCommand(styleWithCSS, false, true)这个设置要放在foreColor命令之前它决定浏览器是用font color...标签还是内联stylecolor:...在后台系统里内联样式更可控因为后端富文本过滤规则通常是按 style 白名单来处理的。背景颜色的实现和字体颜色几乎一样区别只在命令名和执行对象上背景色还要注意选中区域是否包含换行和块级元素这个在第 4 章踩坑部分展开。// 背景颜色完整流程同上这里只列出差异部分 colorInput.addEventListener(change, function () { var iframe container.querySelector(iframe); var doc iframe.contentDocument || iframe.contentWindow.document; doc.execCommand(styleWithCSS, false, true); // backColor 在部分浏览器中作用于选中文本的底色在极少数浏览器中作用于整个段落背景 doc.execCommand(backColor, false, this.value); iframe.contentWindow.focus(); });这里要特别注意backColor命令在各浏览器中的表现不一致。在 Chrome 和 Edge 中它作用在选中文字的高亮底色上符合“背景颜色”的直觉但在 Firefox 的某些版本中backColor会作用于包含选区的块级元素背景变成一整段的背景色视觉上差异很大。如果项目主要面向 Firefox 用户建议改用hiliteColor这个命令在 Firefox 中的表现更接近文字底色。具体用哪个需要跑一遍实测来决定这也是我反复强调要先按浏览器列表验证的原因。3.3 取色器与 iframe 焦点切换的细节处理接入取色器后会遇到一个非常影响体验的问题点击取色器弹出色板时iframe 内的光标和选区可能会丢失等选完颜色回到编辑器时之前选中的文字已经变成未选中状态颜色命令执行后没有任何效果。原因在于原生颜色选择器打开时会触发失焦事件iframe 的contentWindow失去了焦点选区被浏览器清空。解决办法是在打开取色器之前先保存当前选区取色完成后恢复选区再执行颜色命令。以下是一个经过验证的保存与恢复写法// 在打开取色器之前调用记录 iframe 内的选区状态 function saveEditorSelection(iframe) { var win iframe.contentWindow; var sel win.getSelection(); if (sel.rangeCount 0) { var range sel.getRangeAt(0).cloneRange(); // 把选区存在全局变量或闭包变量中 return range; } return null; } // 在取色完成后恢复选区 function restoreEditorSelection(iframe, range) { if (!range) return; var win iframe.contentWindow; var sel win.getSelection(); sel.removeAllRanges(); sel.addRange(range); // 恢复选区后立即执行颜色命令焦点会重新进入 iframe var doc iframe.contentDocument; doc.execCommand(styleWithCSS, false, true); doc.execCommand(foreColor, false, currentColor); }我在实际项目里操作时会额外监听window上的blur事件来触发saveEditorSelection而不仅仅是点击取色器前调用。因为用户在编辑器里选中文字后可能做了其他操作再回来取色时机不可控。监听blur还有个好处如果用户在取色器上按下 ESC 取消选择那时选区已经丢失至少还保留了一份打开取色器前的记录。注意恢复选区之后不要立即再次取色器打开而是直接执行颜色命令。因为在addRange之后iframe 还没有完全恢复焦点立刻再次弹取色器会导致选区再次丢失这就是很多人遇到的“第一次取色有效第二次无效”的玄学现象。4. 避坑与排查layedit 工具栏扩展最常见的四个翻车现场4.1 事件绑定完成后点击按钮完全没反应现象工具栏上已经渲染出了字号下拉框和颜色按钮外观正常但点击下拉框选择字号后编辑器内的文字纹丝不动。原因layedit 的工具栏容器在 iframe 加载过程中可能会被重新渲染。layedit.build返回索引后编辑器内部还在初始化 iframe 的 contentDocument如果你立刻向工具栏追加 DOM 并绑定事件之后 layedit 内部的初始化流程可能把工具栏 DOM 整体替换一次你绑定事件的元素被移除了 DOM 树事件自然失效。解决在layedit.build回调之后用setTimeout把渲染和绑定的代码延迟执行或者使用事件委托把事件绑定到外层容器上// 更稳妥的方式把 change 事件委托给整个工具栏容器 toolBar.addEventListener(change, function (e) { if (e.target e.target.classList.contains(layedit-fontsize-select)) { var size e.target.value; // 执行字号命令逻辑同第 2 章 } });我一般会直接用事件委托因为 layedit 内部动态操作工具栏 DOM 的可能性很大每一条新增按钮都用委托可以避免这种“元素被重建”导致的事件丢失问题。事件委托还能顺便覆盖多个编辑器实例的情况不需要为每个实例单独绑定。4.2 字号命令执行后文字变成了难看的默认大字现象选择了 36px结果编辑器里的文字变成了浏览器默认的最大字号查看 HTML 结构发现font size7标签被写进去了但没有任何像素样式。原因execCommand(fontSize, false, 7)只是触发浏览器生成font size7标签这个标签在 HTML5 中默认解析为font-size: xx-large左右并不是你要求的 36px。如果你没有执行后续的querySelectorAll(font[size7])替换步骤效果就只是变大了但不受控制。解决把 fontSize 命令的后续处理补全在替换像素值之前先判断浏览器生成的是font标签还是span标签两种都处理一下。这里可以用一个递归遍历选区的方式比全局querySelectorAll更精确但全局方式在绝大多数情况下已经够用。关键是要在替换时给font标签设置font-family继承否则部分浏览器会把字体也重置掉。4.3 背景颜色命令把整个段落都染了色现象只选中了一行文字的一部分执行背景颜色后整个段落甚至整行都变成了背景色不是选中的文字底色。原因execCommand(backColor)在 Firefox 和部分旧版内核中会作用于选区的块级父元素背景而不是内联文字背景。这是浏览器对命令语义的差异化实现不是代码写错。解决针对 Firefox 使用hiliteColor并做浏览器判断。用navigator.userAgent.includes(Firefox)判断后切换命令名或者在初始化时直接统一使用hiliteColor因为这个命令在所有主流浏览器中都能产生文字背景色的效果而backColor在 Chrome 中其实是通用别名。我自己的项目里直接统一用hiliteColor代替backColor很少再出现整段染色问题。如果遇到特殊场景需要整段背景色则直接操作 iframe 内的 DOM给块级元素设style.backgroundColor不走 execCommand。4.4 关闭编辑器后重新打开自定义按钮事件重复触发现象页面里先调用了layedit.build(content)然后某个操作把它关闭再次build同一个 textarea此时字号下拉框的 change 事件被触发多次一次选择执行了多次字号命令内容变得异常。原因每次build都会往工具栏追加一次自定义 DOM 并绑定事件旧实例虽然被关闭但它的事件监听器没有解绑关闭操作只是隐藏了容器。第二次 build 之后旧监听器依然还在且因为 DOM 结构复用新监听器也挂到了同一个或相近的元素上导致一次 change 触发多个回调。解决在build之前先解绑或清理旧的自定义元素。手动维护一个数组记录所有追加的自定义元素在重新初始化前移除// 记录工具栏扩展元素便于清理 window._layeditExtras window._layeditExtras || []; function cleanupLayeditExtras() { (window._layeditExtras || []).forEach(function (el) { if (el.parentNode) el.parentNode.removeChild(el); }); window._layeditExtras []; } // build 前先清理 cleanupLayeditExtras(); var editIndex layedit.build(content, { tool: [...] }); // ... 追加元素后 push 进 _layeditExtras这个坑在单页应用里尤其常见因为页面切换不会重置 window 级变量。每次重新创建编辑器实例之前强制走一次清理是最省心的后悔药。如果编辑器实例是通过弹窗动态创建销毁的也建议在弹窗关闭事件里主动调用layedit的del相关方法查看 layedit.js 里导出的 API通常有对应的方法可以移除实例。5. 进阶把整套扩展封装成可复用的初始化函数有了前面几个章节的原理和踩坑记录最后把它收拢成一个函数。以后每次新建编辑器只需要一行调用就能得到带字号、字体颜色、背景颜色的工具栏不用重复写踩坑代码。/** * 扩展 layedit 编辑器追加字号、字体颜色、背景颜色工具 * param {number} editIndex - layedit.build 返回的索引 */ function extendLayeditToolbar(editIndex) { var container layedit.getContainer(editIndex); var toolBar container.querySelector(.layedit-tool); var iframe container.querySelector(iframe); if (!toolBar || !iframe) return; var doc iframe.contentDocument || iframe.contentWindow.document; // ---- 字号下拉框 ---- var fontSelect document.createElement(select); fontSelect.className layedit-fontsize-select; [12, 14, 16, 18, 20, 24, 28, 32, 36].forEach(function (size) { var opt document.createElement(option); opt.value size; opt.text size px; fontSelect.appendChild(opt); }); fontSelect.addEventListener(change, function () { var size /^\d$/.test(this.value) ? this.value : 14; // 关键size 值做一次安全校验 doc.execCommand(styleWithCSS, false, true); doc.execCommand(fontSize, false, 7); var target doc.querySelectorAll(font[size7]); target.forEach(function (el) { el.style.fontSize size px; el.removeAttribute(size); }); iframe.contentWindow.focus(); }); // ---- 字体颜色与背景颜色 ---- function makeColorBtn(title, command) { var wrap document.createElement(div); wrap.style.cssText display:inline-block;position:relative;margin:4px 2px;; var icon document.createElement(span); icon.style.cssText display:inline-block;width:24px;height:24px;line-height:24px;text-align:center;cursor:pointer;color:#666;; icon.textContent title 字体颜色 ? A : ⭫; icon.title title; var input document.createElement(input); input.type color; input.value #ff0000; input.style.cssText position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;cursor:pointer;; input.addEventListener(change, function () { // 保存选区避免取色器导致焦点丢失 var sel iframe.contentWindow.getSelection(); var savedRange sel.rangeCount ? sel.getRangeAt(0).cloneRange() : null; doc.execCommand(styleWithCSS, false, true); // 背景色统一用 hiliteColor规避 Firefox 下 backColor 整段染色的问题 var cmd command backColor /Firefox/.test(navigator.userAgent) ? hiliteColor : command; doc.execCommand(cmd, false, input.value); // 若选区已丢失尝试恢复 if (savedRange sel.rangeCount 0) { sel.removeAllRanges(); sel.addRange(savedRange); doc.execCommand(cmd, false, input.value); } iframe.contentWindow.focus(); }); wrap.appendChild(icon); wrap.appendChild(input); return wrap; } toolBar.appendChild(fontSelect); toolBar.appendChild(makeColorBtn(字体颜色, foreColor)); toolBar.appendChild(makeColorBtn(背景颜色, backColor)); }使用方式是在每次layedit.build之后立刻调用同时记得在重新初始化之前做一次第 4.4 节的清理// 初始化时完整调用示例 cleanupLayeditExtras(); var index layedit.build(content, { tool: [strong, italic, underline, |, link, image] }); extendLayeditToolbar(index);这个函数里我留了一个安全校验/^\d$/.test(this.value)专门处理某些浏览器或修改过的 layedit 版本里 select 的value可能被插入非数字字符的情况。曾经有同事反馈字号选择后没反应排查到最后是 select 里被某个自己写的过滤器塞进了px后缀字符串数值校验挡掉了这一类问题。另外我把背景色命令的 Firefox 兼容分支也内置了这就是我能稳定复现的最终版本。从那以后我每次给系统加富文本编辑需求都会强制走一遍先确认 layedit 版本再跑一次默认clear配置测试最后才挂extendLayeditToolbar。这套流程虽然多花几分钟但能挡住大多数隐藏的踩坑点。希望帮到你。本文还有配套的精品资源点击获取