好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

vue实现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 = `&nbsp@${item.name}&nbsp`
  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人文本输入框的资料请关注其它相关文章!

查看更多关于vue实现At人文本输入框示例详解的详细内容...

  阅读:37次