div+contenteditable 实现富文本发布框的小结
效果图和实现的功能
实现的效果图如下,主要实现的功能有
- 表情的插入
- 插入话题之后部分文字选中
- 文本框高度自适应
- 发送消息,获取发送框中的纯文本内容
- placeholder的实现
- 输入文字的计数面板
代码地址传送门 代码实现Vue

灵感来源
“你想做的一定有人做了,你一定不是第一个遇到这个问题的人”——这句话对80%(二八分布)的人是有效的,我也从中获益不少。
我的第一份参考案例是qq空间的说说发布框&&webqq的消息发送框,从中的收获有以下几点

实现的一些细节
contenteditable
关于contenteditable
属性的特性可以去上面的两个链接中查阅,总之是会把设置了这个属性的标签和里面的子标签都设置为可编辑属性。
palceholder的实现
像input
和textarea
这些标签自带placeholder
属性,但是div
没有,要实现就需要通过JS和CSS来模拟。
通过两层div
实现,在外层通过监听编辑框中是否存在文字来选择是否展示placeholder
,placeholder
通过伪元素和绝对定位实现,脱离标准文档流浮于编辑框之上
<div
class="edit-panel"
:class="{'show-placeholder' : showPlaceholder}"
:placeholder="placeholder"
>
<div contenteditable="true" ref="editor" class="editor"></div>
<span class="count" :class="{'font-red':textCount < 0}">{{ textCount }}</span>
</div>
.edit-panel {
position: relative;
width: 100%;
height: auto;
font-size: 14px;
line-height: 20px;
border: 1px solid;
}
.show-placeholder::before {
content: attr(placeholder);
position: absolute;
top: 4px;
left: 8px;
color: #555;
pointer-events: none;
}
面板计数
于上面placeholder
实现异曲同工
.edit-panel .count {
position: absolute;
color: #555;
right: 1rem;
bottom: 0.5rem;
user-select: none;
pointer-events: none;
}
文本框高度自适应
这里只需要设置min-height
和max-height
就行
插入表情
这里需要注意,只要操作DOM的方式不对,光标位置就会错位,良好的用户体验就是光标位置保持不变
function insertHtmlAtCaret (html) {
var sel, range, frag
if (window.getSelection) {
sel = window.getSelection()
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0)
range.deleteContents()
var el = document.createElement('div')
el.innerHTML = html
frag = document.createDocumentFragment()
var node
var lastNode
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node)
}
range.insertNode(frag)
if (lastNode) {
range = range.cloneRange()
range.setStartAfter(lastNode)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
}
}
}
}
插入话题之后部分文字选中
这里需要去理解一下range
对象中四个重要的属性startContainer
、startOffset
、endContainer
、endOffset
。在不同情况下指代的意思是不一样的,我这里就是轻描淡写的提一下,我理解的不是很透彻就不误导大家了。
addTopic (event) {
this.$refs.editor.focus()
insertHtmlAtCaret('#')
insertHtmlAtCaret('请输入一个话题')
insertHtmlAtCaret('#')
var range = window.getSelection().getRangeAt(0)
console.log(range)
range.selectNodeContents(range.startContainer.childNodes[range.startOffset - 2])
}
获取纯文本内容
直接使用textContent
是不行的,这样获取不到img
标签中的内容,加上之后会用button
或者input[type=button]
去实现一些高亮功能,这里需要自己去定义一个获取纯文本内容的方法。我实现的比较简单
function getDomValue (elem) {
var res = ''
Array.from(elem.childNodes).forEach((child) => {
if (child.nodeName === '#text') {
res += child.nodeValue
} else if (child.nodeName === 'BR') {
res += '\n'
} else if (child.nodeName === 'BUTTON') {
res += getDomValue(child)
} else if (child.nodeName === 'IMG') {
res += child.alt
} else if (child.nodeName === 'DIV') {
res += '\n' + getDomValue(child)
}
})
return res
}
注意点和展望
- 富文本编辑框需要考虑很多XSS的问题,赋值粘贴时标签的过滤等
- 浏览器兼容性的问题,
range
对象操作的不同,contenteditable
属性表现出来的问题 - 统一插入文本的样式,否则输入文字的时候会沿用前面的样式
- @用户高亮显示的浏览器兼容问题
感谢
转载自:https://juejin.cn/post/6844903793864212494