div可编辑属性contenteditable实现富文本编辑器
使用div标签的可编辑属性contenteditable实现可插入链接、表情包、其他变量的富文本编辑器,因为在我使用这个功能是在19年项目中需求中有涉及,最近被问到一些关于该功能的问题,就做一下总结.
说明:
1、技术栈: vue@^2.7.14, element-ui@^2.13.1, emoji@^0.3.2, js, html, css
2、div可编辑属性,change事件失效,可通过监听input事件来时时得到输入内容的变化
3、开发此功能是为了实现用微信公众号向用户推送客服消息时,创建文本消息内容开发的,微信开放平台对于文本消息("msgtype":"text",)内容的格式有限制:文本中只支持a标签
去微信开放平台
完成后的页面效果
可插入emoji表情,可插入a链接,可插入小程序链接,还可以插入一些自定义的变量
公众号发送到用户看到的效果
一、富文本实现
1、输入功能
div标签可编辑
这一步比较简单,只需要给div标签添加contenteditable为true即可;
<div contenteditable="true" style="height:100px; border: 1px solid red; padding:2px;" id="editor" ref="editor">
</div>
通过监听input事件,时时关注内容的变化并获取输入内容
//let editor = document.getElementById('editor')
//editor.addEventListener('input', (item) => { console.log(item) })this.$refs.editor.addEventListener('input', this.changeContentValue);
自动获取焦点
// let editor = document.getElementById('editor')
// editor.focus();this.$refs.editor.focus();
往光标处插入html片段
// 往光标位置插入HTML片段
function insertHtmlAtCaret(html) {
if (window.getSelection) {
// IE9 and non-IE
if (this.sel.getRangeAt && this.sel.rangeCount) {
var el = document.createElement('div');
el.innerHTML = html;
var frag = document.createDocumentFragment();
var node;
var lastNode;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
this.range.insertNode(frag);
if (lastNode) {
this.range = this.range.cloneRange();
this.range.setStartAfter(lastNode);
this.range.collapse(true);
this.sel.removeAllRanges();
this.sel.addRange(this.range);
}
}
}
else if (document.selection && document.selection.type !== 'Control') {
// IE < 9 document.selection.createRange().pasteHTML(html);
}
},
2、插入a链接功能
点击插入链接按钮可出现弹窗插入或者修改内容
在点击插入链接按钮(也就是输入框失去焦点)的时候获取光标所在的位置
this.sel = window.getSelection();
this.range = this.sel.getRangeAt(0);
this.taget = this.sel.focusNode.parentElement;
const { sel, taget } = this;选中一部分内容,或者点解已插入链接的内容
第一次添加链接或者多次修改链接内容
this.selectContents = sel.toString(); // 当选中未添加链接的内容时,选中内容复制给链接的文字字段显示弹窗,对弹窗的文本与链接进行修改
const { selectContents, selectUrl} = this;
this.$set(this.textForm, 'text', selectContents);
this.$set(this.textForm, 'url', selectUrl);完成后点击确定,以新内容替换旧内容
const { text, url } = this.textForm;
if (text && url) {
this.range && this.range.deleteContents(); // 删除输入框原有的文本内容
const { selectContents, selectUrl, taget } = this;
if (selectContents && selectUrl && taget) {
Array.from(this.$refs.editor.childNodes).forEach((item) => {
if (item === taget) {
this.$refs.editor.removeChild(taget); // 删除输入框原有的文本链接内容 }
else if (taget.parentNode === item) {
item.removeChild(taget); // 当村子a链接内有插入了一次a标签的情况处理
}
});
}插入到输入框
this.insertHtmlAtCaret(`<a href='${url}' style="color:#5392ff">${text}</a>`); }
this.textForm = { url: '', text: '' }; // 重置
效果图
3、插入小程序链接同上
4、插入表情包功能
封装emoji 组件
引入emoji组件
import Emoji from './emoji';
const emoji = require('emoji');
components: {
Emoji
}
html部分
<el-popover
ref="popover-click"
placement="bottom-start"
width="390"
trigger="click"
@show="mountedEmoji = true"
>
<Emoji
@emoji = "selectEmoji">
</Emoji>
</el-popover>插入表情
function selectEmoji(emoji) {
this.insertHtmlAtCaret(emoji);
},
5、输入字数统计功能
div的可编辑属性,获取到的内容格式如下,如果统计输入字数需要对其进行处理
从获取到的输入内容可得出的结论是
(1) shift+回车换行会在当前操作的这一行后生成<br/>标签,用来与下一行内容分开
(2) 回车直接换行会生成<div><br/></div>形式, 输入内容后,输入的内容替换div标签中的br
(3)当使用了直接回车换行,再使用shift+回车换行,则shift+回车换行这行内容会被直接回车换行生成的div包裹
(4) 光标处于0位置的时候禁止换行
针对以上需求处理方法是,对div中输入的内容进行过滤
function getDomValue(elem) {
var res = '';
let arr = Array.from(elem.childNodes);
arr.forEach((child) => {
if (child.nodeName === '#text') {
res += child.nodeValue;
} else if (child.nodeName === 'BR') {
res += '\n';
} else if (child.nodeName === 'P') {
res += '\n' + getDomValue(child);
} else if (child.nodeName === 'SPAN') {
res += getDomValue(child);
} else if (child.nodeName === 'BUTTON') {
res += getDomValue(child);
} else if (child.nodeName === 'IMG') {
res += child.alt;
} else if (child.nodeName === 'DIV') {
const s = Array.from(child.childNodes);
if (s.length === 1 && s[0].nodeName === 'BR' || child.previousSibling && child.previousSibling.nodeName === 'BR') {
// 处理shift+回车与直接回车混用导致多处来换行的情况
res += getDomValue(child); }
else {
res += '\n' + getDomValue(child);
}
)else if (child.nodeName === 'A') {
if (child.href !== null) {
const innerHTML = child.innerHTML.replace(/<br>/g, '')
.replace(/<span (.*?)>/gi, '').replace(/<\/span>/gi, '');
res += `<a href='${child.href}'>${innerHTML}</a>`;
}
}
}统计字数
function getDomValuelength(elem) {
var reg = /<a[^>]+?href=["']?([^"']+)["']?[^>]*>([^<]+)<\/a>/gi;
var data = elem.toLowerCase().replace(reg, function ($1, $2, $3) {
return $3;
});
return data.length;
}
6、我的源码git地址:https://github.com/wangAlisa/div-follow-input