知识前置
需求分析
实现
创建能够输入文本的文本框
添加at功能
后记
知识前置基于vue手把手教你实现一个拥有@人功能的文本编辑器(其实就是微信群聊的输入框)
Selection
对象,表示用户选择的文本范围或插入符号的当前
developer.mozilla.org/zh-CN/docs/…
contenteditable
是一个枚举属性,表示元素是否可被用户编辑。
developer.mozilla.org/zh-CN/docs/…
需求分析文本框能够输入文本(太简单了)
能够at人
实现 创建能够输入文本的文本框在这里主要利用 contenteditable
属性,让创建的 div
能够编辑
利用input事件监听数据变化,将数据同步出去
<!--main.vue!-->
<template>
<div>
<Editor v-model="value"/>
</div>
</template>
<!--editor.vue!-->
<template>
<div>
<div
class="editor"
contenteditable="true"
@input="input"
/>
</div>
</template>
<script>
export default {
computed: {
editor() {
return this.$refs.editor || {}
}
},
methods: {
input(e) {
this.$emit('input', this.getEditorHtml())
},
getEditorHtml() {
return this.editor.innerHTML || ''
}
}
}
</script>
<style lang="less" scoped>
.editor{
overflow-y: auto;
background: #F4F6FB;
border-radius: 4px;
border: 1px solid transparent;
min-height: 40px;
max-height:200px;
padding: 14px 9px;
line-height: 20px;
&:empty{
&::before{
content:'输入你想对他/她说的话,然后@她!';
color: #999;
}
}
&:focus{
outline: none;
border-color: #3656C6;
border-radius: 4px;
}
}
</style>
效果如下图所示
这个时候我们就实现了一个能够绑定数据的文本输入框,第一个需求完美实现,接下来实现第二个需求(开始折磨)
添加at功能这里的需求主要分四步走
当用户输入@字符时,弹出用户选择列表
当用户点击@的人时,收回@列表
将@的人嵌入到文本框中
删除@的人时,要直接整块删除
首先我们先实现一个用户选择的列表,这里主要涉及到的都是界面的编辑和动画的设置,不展开描述,直接上效果图**(完整代码会在文末给出)**
接着我们要改造input函数,检测当用户输入为@
符号时,弹出选择框
input(e) {
if (e.data === '@') {
// 弹出用户选择框
this.$refs.UserList.show()
// 失去焦点,退出手机的软体键盘
this.editor.blur()
}
this.$emit('input', this.getEditorHtml())
},
当用户点击要@的人时,关闭选择列表,同时将@人的人插入到文本框中
userItemClick(item) {
const dom = this.createAtDom(item)
this.$refs.editor.innerHTML = this.$refs.editor.innerHTML + dom.outerHTML
this.$refs.UserList.close()
},
createAtDom(item) {
const dom = document.createElement('span')
dom.classList.add('active-text')
// 这里的contenteditable属性设置为false,删除时可以整块删除
dom.setAttribute('contenteditable', 'false')
// 将id存储在dom元素的标签上,便于后续数据处理
dom.setAttribute('data-id', item.id)
dom.innerHTML = ` @${item.name} `
return dom
},
效果入下图所示
相信有不少朋友已经发现问题了,这种方式只能怪将@
的人添加到文本的最末尾,但如果我编辑文本的时候,光标的位置不是在文本的最后,而是在文本之间的某个位置,那此时我们这么添加@
的人就会有点反直觉。
所以我们在弹出选择列表的时候,要把当前光标所处的位置标记下来,插入时,就插入到对应的位置上。所以此时就要抛出我们本文最重要的一个对象
Selection
对象
我们要利用 Selection
对象的 anchorOffset 属性去获取当前焦点的位置,此时我们改造input函数,添加 saveIndex
方法,在弹出文本框失焦之前,保存当前焦点的位置 。
//改造input函数
input(e) {
if (e.data === '@') {
// 保存焦点位置
this.saveIndex()
// 弹出用户选择框
this.$refs.UserList.show()
// 失去焦点,退出手机的软体键盘
this.editor.blur()
}
this.$emit('input', this.getEditorHtml())
},
// 添加saveIndex方法
async saveIndex() {
// 获取selection对象
const selection = getSelection()
// 保存当前焦点的位置
this.selectionIndex = selection.anchorOffset
},
// 改造userItemClick函数
userItemClick(item) {
const dom = this.createAtDom(item)
this.addData(item)
this.$refs.UserList.close()
},
// 添加dom节点到指定位置
addData(item){
const html = this.editor.innerHTML
const leftInnerHtml = html.substring(0, this.selectionIndex - 1)
const dom = this.createAtDom(item)
const rightInnerHtml = html.substring(this.selectionIndex, html.length)
this.editor.innerHTML = leftInnerHtml + dom.outerHTML + rightInnerHtml
}
这个时候我们就可以把@
的人添加到我们之前光标的位置了,效果如下如所示
但在某天,你突发奇想,想同时对很多个女神发出邀请,这个时候你发现,@
多人的时候,出现问题了
我们插入的@
人的节点被硬生生拆成了字符串,这很明显跟我们的预期有差别呀,这个时候我们应该分析一下我们编辑时的dom结构,如下图所示
为了便于理解我画了个简单的图
我们在插入dom节点
之前,文本框的所有内容都是属于editor节点
下唯一一个textNode节点,插入dom节点
之后,editor节点
新增了一个子节点,而 Selection.anchorOffset 这个属性获取到的焦点位置,实际上是相对于当前所处node节点
而言的(←理解这个概念,非常重要)
也就是说
我们第一次插入dom节点
,焦点位置是相对于当前节点,也就是editor节点
下的唯一一个textNode节点计算
第二次插入dom节点
,焦点位置是相对于当前节点,也就是当前textNode节点
计算
后续插入的dom节点
,焦点位置计算方式同上
所以当我们有如下需求的时候
Selection.anchorOffset
的返回值是5,而我们的addData
方法,实际上是从editor.innerHtml
的第一个位置开始算,第五个位置刚好插到了span节点
的里面,所以就出现了上文乱码的问题。
所以我们解决的方案,就是在保存焦点位置的时候,同时保存当前编辑的那个textNode节点
,那我们怎么找到当前正在编辑的那个textNode节点
呢?
Selection
对象提供了一个方法 Selection.containsNode()
mdn文档是这么描述的:判断指定的节点是否包含在 Selection 中 (是否被选中)
在我们这个场景中,通俗点讲就是,我这个节点到底是不是编辑的节点?是你就返回true
,不是就false
所以我们可以在弹出用户选择框之前,遍历一下editor节点
的子节点,找出我们当前编辑的那个textNode节点
// 改造一下saveIndex
async saveIndex() {
const selection = getSelection()
this.selectionIndex = selection.anchorOffset
const nodeList = this.editor.childNodes
// 保存当前编辑的dom节点
for (const [index, value] of nodeList.entries()) {
// 这里第二个参数要配置成true,没配置有其他的一些小bug,这里不展开讲,详细可以看文档
if (selection.containsNode(value, true)) {
this.dom = value
this.domIndex = index
}
}
},
现在当前编辑的节点和编辑的位置都已经保存下来了,剩下的就是把@
人的节点插入到我们编辑的那个textNode
节点里面就完成了。
// 改造一下addData方法
addData(item) {
const html = this.dom.textContent
const leftText = html.substring(0, this.selectionIndex - 1)
const dom = this.createAtDom(item)
const rightText = html.substring(this.selectionIndex, html.length)
this.dom.textContent = leftText + dom.outerHTML + rightText
},
然而,当我们再次运行代码调试的时候,出现了我们预期外的结果
是我们代码有问题吗?说是其实不算是,说不是,其实也算是(废话)
其实是因为我们编辑的是textNode节点
,而textNode节点
就算包含了dom结构
,他也是把结构当成文本输出到页面上,所以在这里
我们应该创建一个新的结构,也就是我们的文档片段DocumentFragment
然后把我们的节点结构插入到DocumentFragment
中
接着利用Node.insertBefore()方法,把DocumentFragment
插入到原来编辑的textNode节点
之前,再用Node.removeChild()方法把原来编辑的textNode节点
删除
这样就可以实现正常的插入
为了方便理解,可以看一下流程图
addData(item) {
const text = document.createDocumentFragment()
const span = document.createElement('span')
const html = this.dom.textContent
// 左边的节点
const textLeft = document.createTextNode(html.substring(0, this.selectionIndex - 1) + '')
// 这里如果textLeft是个空的文本节点,会导致@用户无法删除,这里添加一个判断,如果是空,则插入一个空的span节点
text.appendChild(textLeft.textContent ? textLeft : span)
// 加入@人的节点
text.appendChild(this.createAtDom(item))
// 右边的节点
const textRight = document.createTextNode(html.substring(this.selectionIndex, html.length))
textRight.textContent && text.appendChild(textRight)
this.editor.insertBefore(text, this.dom)
this.editor.removeChild(this.dom)
},
当我们处理到这里时,就可以多次at想要at的人,效果如图
后续我们要将数据提取出来,可以根据v-model绑定的value进行解析,把插在标签里的数据提取出来,也可以根据自己的业务插入一些数据,这里不是重点,也不展开讲
后记基本上文本编辑器的核心逻辑到这里就讲完了,但是这个demo在做的过程中,有好几个地方做了优化,特别是针对移动端软体键盘的进入和离开,还有焦点的对焦和失焦,都做了一些处理,但是在文章里头没有展开讲
想要详细了解的大佬们可以到我github仓库下载源码 github.com/adouni1996/…
以上就是vue实现At人文本输入框示例详解的详细内容,更多关于vue At人文本输入框的资料请关注软件开发网其它相关文章!