一、需求缘起
写文章时,经常想在正文里放一个提示框或小结卡片,把一段话单独“框”起来。希望它的外观可以随内容调整,具体是四项:
框体可配置项
背景色:框体底色
框体颜色:框内文字颜色
边框颜色:外描边颜色
框体宽度:百分比或像素
站点用的是 wangEditor V5,工具栏里并没有现成的“框体”。好在它提供了扩展能力,于是动手注册了一个自定义元素。
二、实现原理:三件套 + 一个菜单
wangEditor 通过 Boot.registerModule() 注册自定义元素。一个元素要在“编辑区显示、导出 HTML、再把 HTML 导回编辑”三个环节都正确,需要提供三个数组:
- renderElems:节点在编辑区如何渲染;
- elemsToHtml:节点如何序列化为 HTML;
- parseElemsHtml:已有 HTML 如何解析回节点。
工具栏按钮则通过 Boot.registerMenu() 单独注册,在它的 exec 里打开配置弹窗。
三、两个容易踩的坑
这部分最花时间,先记下来:
- 数组名是 renderElems / elemsToHtml / parseElemsHtml,网上一些旧文写的 customElems 在这个版本里并不存在。
- registerModule 是直接从模块顶层读取这些数组的。我最初把它们嵌套在 editor: {} 下,结果注册“静默失败”、框体毫无反应。
下面是各部分的实际代码。
四、定义元素:编辑区渲染
// 编辑器内渲染:把自定义节点渲染成带样式的 div
function renderContentBox(elem, children) {
var bg = elem.bgColor || BOX_DEFAULTS.bgColor;
var tc = elem.textColor || BOX_DEFAULTS.textColor;
var bc = elem.borderColor || BOX_DEFAULTS.borderColor;
var w = elem.width || BOX_DEFAULTS.width;
return {
sel: 'div',
data: {
attrs: { 'data-w-e-type': 'contentBox' },
style: {
boxSizing: 'border-box',
backgroundColor: bg,
color: tc,
border: '1px solid ' + bc,
width: w,
maxWidth: '100%',
margin: '14px auto',
borderRadius: '12px',
padding: '22px 26px',
textAlign: 'center'
}
},
children: children
};
}五、序列化为 HTML
// 序列化为 HTML:内联样式,保证前台 / 换主题也能显示
function contentBoxToHtml(elem, childrenHtml) {
var bg = elem.bgColor || BOX_DEFAULTS.bgColor;
var tc = elem.textColor || BOX_DEFAULTS.textColor;
var bc = elem.borderColor || BOX_DEFAULTS.borderColor;
var w = elem.width || BOX_DEFAULTS.width;
var style = 'box-sizing:border-box;background-color:' + bg + ';color:' + tc +
';border:1px solid ' + bc + ';width:' + w + ';max-width:100%;margin:18px auto;' +
'border-radius:12px;padding:22px 26px;text-align:center;';
return '' + childrenHtml + '';
}六、把 HTML 解析回编辑器
// 解析已有 HTML:编辑旧文 / 源代码模式切回时,还原成自定义节点
function parseContentBoxHtml(domElem, children) {
var raw = domElem.getAttribute('style') || '';
var s = ';' + raw.replace(/\s+/g, '');
function pick(prop) {
var re = new RegExp(';' + prop + ':([^;]+)', 'i');
var m = s.match(re);
return m ? m[1].trim() : '';
}
var bm = s.match(/border:[^;]*solid([^;]+)/i);
return {
type: 'box',
children: children,
bgColor: pick('background-color') || BOX_DEFAULTS.bgColor,
textColor: pick('color') || BOX_DEFAULTS.textColor,
borderColor: bm ? bm[1].trim() : BOX_DEFAULTS.borderColor,
width: pick('width') || BOX_DEFAULTS.width
};
}七、注册模块与工具栏菜单
if (Boot) {
// 注意:三个数组必须写在模块“顶层”,不能再包一层 editor: {}
Boot.registerModule({
renderElems: [
{ type: 'box', renderElem: renderContentBox }
],
elemsToHtml: [
{ type: 'box', elemToHtml: contentBoxToHtml }
],
parseElemsHtml: [
{ selector: 'div.content-box', parseElemHtml: parseContentBoxHtml }
]
});
// 工具栏菜单
function ContentBoxMenu() {
this.title = '插入内容框体';
this.iconSvg = BOX_ICON_SVG;
this.tag = 'button';
}
ContentBoxMenu.prototype.getValue = function () { return ''; };
ContentBoxMenu.prototype.isActive = function () { return false; };
ContentBoxMenu.prototype.isDisabled = function () { return false; };
ContentBoxMenu.prototype.exec = function (editor) {
if (typeof window.__openContentBoxModal === 'function') {
window.__openContentBoxModal(editor); // 打开配置弹窗
}
};
Boot.registerMenu({
key: 'contentBox',
factory: function () { return new ContentBoxMenu(); }
});
}八、配置弹窗
弹窗里三个颜色字段,每个都由“取色器 + 十六进制输入 + 预设色块”组成;宽度支持百分比 / 像素两种单位,配滑块、数值和快捷预设,下方是实时预览。
光标若已在某个框体内,弹窗自动变为“编辑内容框体”并读回当前四项;否则按默认值插入新框。实时预览的刷新逻辑很简单:
// 任一项变化时刷新预览
function refresh() {
preview.style.backgroundColor = st.bgColor;
preview.style.color = st.textColor;
preview.style.border = '1px solid ' + st.borderColor;
preview.style.width = widthStr(); // 百分比 / 像素
}点击确定时,区分“更新已有框”和“插入新框”两种情况:
okBtn.addEventListener('click', function () {
var props = {
bgColor: st.bgColor, textColor: st.textColor,
borderColor: st.borderColor, width: widthStr()
};
if (st.mode === 'edit' && curRef) {
// 光标在已有框体内:原地更新
activeEditor.setNodes(props, { at: curRef.path });
} else {
// 否则插入一个含空段落的新框体
var box = { type: 'box',
children: [ { type: 'paragraph', children: [ { text: '' } ] } ] };
Object.assign(box, props);
activeEditor.insertNode(box);
activeEditor.focus();
}
close();
});九、前台样式
框体的颜色和宽度都以内联样式随正文保存,本身即可移植、换主题也不丢。前台 CSS 主要做两件事:把框内段落的首行缩进清零(正文段落默认缩进两字符),再补一层轻投影。
/* 前台:框体已带内联样式,这里主要重置段落首行缩进、加轻投影 */
.article-content .content-box,
.page-content .content-box {
box-sizing: border-box;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
text-indent: 0;
}
.article-content .content-box p,
.page-content .content-box p {
text-indent: 0;
margin-bottom: 12px;
}
.article-content .content-box p:last-child,
.page-content .content-box p:last-child {
margin-bottom: 0;
}十、实际效果
下面两个框体就是用该功能生成的,一个默认样式、一个自定义了颜色与宽度:
这是默认样式的内容框体,文字居中。
适合放小结、提示,或一段想被强调的话。
自定义底色、边框与宽度后的效果。
宽度 80%,整体更暖一些。
十一、使用方式
- 编辑文章时,点工具栏的“框体”图标;
- 在弹窗里设置背景、文字、边框颜色与宽度,可实时预览;
- 确定即插入;光标放在已有框体内再点按钮,则是编辑该框。
一个可配置的小元素,本质上就是把“渲染、序列化、解析”三条链路补齐,再用一个菜单把配置弹窗串起来。理清这三个数组的位置之后,剩下的都是界面功夫了。
发表评论
登录后即可发表评论