(鉴于本站好久没有发技术文了,所以正经一下)HTML5 的contenteditable 属性可以用来实现一个简单的富文本输入框。在项目的某个需求中有用到这个特征,以下是相关实现记录及踩坑备忘。
需求实现要求如下:
左侧为输入框,右侧为预览框。
左侧如果是 textarea,那么与之前类似的需求(欢迎语、群发消息)并无区别。然而,“插入昵称”功能的引入,使得无法直接用 textarea——“插入昵称”要求能在任意地方的光标处插入,需要支持按块删除,需要支持复制粘贴不丢失“昵称”状态等等。
于此,左侧的 textarea改为 Div+contenteditable 的结构。需求于是变成了在满足上面的特殊点的前提下,尽可能让Div+contenteditable 无限对齐textarea 的行为表现。嗯,还有考虑浏览器兼容性。
实现
这里的实现仅针对关键实现点进行说明。
“插入昵称”功能
左侧原先 textarea 修改为如下结构:
<div contenteditable="true" class="" maxlength="3000" id="" placeholder="如:你好,欢迎加入..."></div> <input type="button" value="插入客户昵称" class=""> |
为 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 < 9 document.selection.createRange().pasteHTML(html); } } |
点击“插入框”后执行:
textareaDom.focus(); insertHtmlAtCaretNew('<img src="name.svg" alt="#客户昵称#">'); |
在输入框中,“客户昵称”是一个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, '<br/>').replace(//n/gi, '<br/>'); // 包含关键词的就得转化下 if (text.indexOf(NICKNAME_KEYWORD) > -1) { var array = text.split(NICKNAME_KEYWORD); document.execCommand('insertHTML', false, array.join('<img src="' + NICKNAME_IMG_SRC + '" alt="' + NICKNAME_KEYWORD + '">')); } else { document.execCommand('insertHTML', false, 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, ‘<br/>’).replace(//n/gi, ‘<br/>’);
// 包含关键词的就得转化下
if (text.indexOf(NICKNAME_KEYWORD) > -1) {
var array = text.split(NICKNAME_KEYWORD);
document.execCommand(‘insertHTML’, false, array.join(‘<img src="’ + NICKNAME_IMG_SRC + ‘" alt="’ + NICKNAME_KEYWORD + ‘">’));
} 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 一定要闭合,不要留空或换行。
<div contenteditable="true"></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(/<//div>/gim, '<br/>'); valueHtml = valueHtml.replace(/<div>/gim, ''); } |
解析的问题
当在 div 的最末尾添加一个空格,预览时候会展示成HTML 格式的
,即这种时候通过$(textareaid).text()
获取到的 value,空格会自动转义为
查了下资料为 chrome 的 bug,这里修复下:
value = value.replace(/&nbsp;/gi, ' '); |
换行的解析
换行这里在保存及读取,复制粘贴时候均有遇到,合适的时机 replace 下就好。
text = text.replace(//r/n/gi, '<br/>').replace(//n/gi, '<br/>'); |
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。
原创文章,作者:506227337,如若转载,请注明出处:https://blog.ytso.com/243478.html