(鉴于本站好久没有发技术文了,所以正经一下)HTML5 的contenteditable 属性可以用来实现一个简单的富文本输入框。在项目的某个需求中有用到这个特征,以下是相关实现记录及踩坑备忘。
需求实现要求如下:
左侧为输入框,右侧为预览框。
左侧如果是 textarea,那么与之前类似的需求(欢迎语、群发消息)并无区别。然而,“插入昵称”功能的引入,使得无法直接用 textarea——“插入昵称”要求能在任意地方的光标处插入,需要支持按块删除,需要支持复制粘贴不丢失“昵称”状态等等。
于此,左侧的 textarea改为 Div+contenteditable 的结构。需求于是变成了在满足上面的特殊点的前提下,尽可能让Div+contenteditable 无限对齐textarea 的行为表现。嗯,还有考虑浏览器兼容性。
实现
这里的实现仅针对关键实现点进行说明。
“插入昵称”功能
左侧原先 textarea 修改为如下结构:
为 input “插入框”绑定点击事件,执行一个“在contenteditable div 的光标处插入相关 DOM”的功能。这个功能需要借助Selection API来实现。
这里其实有很多坑,比如你点击“插入框”的时候,光标其实是已经移动到“插入框”上面了,而不是输入框的光标。这里直接后人乘凉,给出关键有效的函数。
function insertHtmlAtCaretNew(html) {
var sel, range;
if (window.getSelection) {
// IE9 and non-IE
sel = window.getSelection();
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0);
range.deleteContents();
// Range.createContextualFragment() would be useful here but is
// non-standard and not supported in all browsers (IE9, for one)
var el = document.createElement('div');
el.innerHTML = html;
var frag = document.createDocumentFragment(),
node, lastNode;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
// Preserve the selection
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
} else if (document.selection && document.selection.type !== 'Control') {
// IE
点击“插入框”后执行:
textareaDom.focus();
insertHtmlAtCaretNew(' ');
在输入框中,“客户昵称”是一个img
标签的结构,不用span+样式的实现有如下原因:
1)span+样式会有很多潜在的问题:比如插入光标有可能触达到样式,新增文字就可能一直落在 span 内部,这种情况下处理起来巨麻烦;
2)img
标签的结构让整块删除变得容易且兼容性好;
3)img
标签里面有一个alt
属性,里面就是我定义的关键词“#客户昵称#”,复制带这个块的时候的文段,浏览器自动解析为纯文本,恰到好处的实现!
改写粘贴事件
contenteditable 的输入框粘贴事件需要承载至少两个功能:一是解析带“#客户昵称#”关键词的文段;二是粘贴的时候过滤带样式内容。因此需要改写默认的粘贴事件。
页面初始化完成后绑定自定义的粘贴事件。
$(document).on('paste', '#js_csMessage_create_ta', onPaste);
重写的粘贴事件如下:
function onPaste(e) {
e.preventDefault();
var text = '';
if (window.clipboardData && window.clipboardData.getData) {
// IE
text = window.clipboardData.getData('Text');
text = T.htmlEscape(text);
// ie 中直接粘贴纯文本
if (window.getSelection) window.getSelection().getRangeAt(0).insertNode(document.createTextNode(text));
return false;
} else if (e.originalEvent.clipboardData) {
// text = e.clipboardData.getData('text/plain');
text = (e.originalEvent || e).clipboardData.getData('text/plain');
text = T.htmlEscape(text);
}
// 换行等处理
text = text.replace(/\r\n/gi, '
').replace(/\n/gi, '
');
// 包含关键词的就得转化下
if (text.indexOf(NICKNAME_KEYWORD) > -1) {
var array = text.split(NICKNAME_KEYWORD);
document.execCommand('insertHTML', false, array.join(' '));
} else {
document.execCommand('insertHTML', false, text);
}
}
输入框内容的解析与保存
输入框里面本质是 HTML 代码,右侧的预览图包括最终存储到后台的数据都是纯文本,就涉及到“HTML代码转纯文本数据”的实现。因为“昵称”的引入,这里固然不是简单的$(id).text()
的实现。
我是用html-parse-stringify这个库将 HTML 解析为 AST 然后取我需要的内容。理论上编辑器里面的内容对我有效的只有三种类型:文本、img
标签、换行br
标签。后续的预览展示与保存,跟后台数据存储相关,这里就不展开了。
踩坑记录
支持 placeholder
模拟支持 textarea 的placeholder 特征:
[contentEditable=true]:empty:not(:focus):before {
color: #eeee;
content: attr(placeholder)
}
注意:在写HTML 时候,contenteditable Div 一定要闭合,不要留空或换行。
处理Chrome 下contenteditable div 自动产生多余结构
Chrome 下的编辑器编辑器在进行一些换行等操作会自动在contenteditable div 生成多余的 div、p、span等标签。
网络上的解决方式其实很简单,把块弄为 inline-block
;
处理 IE 系contenteditable div 下换行自动产生的多余结构
与上面的类似,IE/Edge 中换行也会产生多余的 div 结构,解决方法是通过$(textarea).html()
获取到的 html 结构稍微二次处理下:
if (document.documentMode || /Edge/.test(navigator.userAgent)) {
valueHtml = valueHtml.replace(//gim, '
');
valueHtml = valueHtml.replace(//gim, '');
}
解析的问题
当在 div 的最末尾添加一个空格,预览时候会展示成HTML 格式的
,即这种时候通过$(textareaid).text()
获取到的 value,空格会自动转义为
查了下资料为 chrome 的 bug,这里修复下:
value = value.replace(/ /gi, ' ');
换行的解析
换行这里在保存及读取,复制粘贴时候均有遇到,合适的时机 replace 下就好。
text = text.replace(/\r\n/gi, '
').replace(/\n/gi, '
');
Safari 中光标过大的问题
Safari存在初次点击输入框光标占据整个块,重新点时候正常。一图描述这种现象:
解决方式:前端 DMO 结构弄成两个,
一个为外层不可编辑,一个为 contentable ,实质就是为 textarea 的作用,然后外层限高,里层自增(height:auto
);
Paste 事件多次触发
两种处理,双管齐下:
1) 页面销毁时候取消监听 $(document).off('paste')
;
2) 节流:
$(document).on('paste', '#js_csMessage_create_ta', _.throttle(function (e) {
onPaste(e);
}, 100));
IE中图片元素resize
在 IE 中,contenteditable div 下的图片能被鼠标放大缩小,用如下样式进行处理:
img {
resize: none;
pointer-events: none;
}
更多坑点陆续更新中...
本文头图来自pixabay。