vue实现At人文本输入框示例详解

目录
  • 知识前置
    • 需求分析
  • 实现
    • 创建能够输入文本的文本框
    • 添加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人文本输入框的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue自定义数字输入框组件

    最近自己在练习组件开发,做了一个简单的数字输入框加减的组件,效果图如下: 组件可以传入三个参数,value是初始化值,max是可输入的最大值,min是可输入最小值,当然参数可以按需求继续扩展的. 组件代码如下: <template>     <div style="text-align: center;margin-top: 20px;">         <input type="text" v-model="current

  • Vue实现数值型输入框并限制长度

    目录 vue数值型输入框并限制长度 描述 代码 vue输入框限制各种输入格式 1.限制只能输入数字.带小数点的数字 2.限制输入的只能为11位手机号 3.电子邮箱正则 4.身份证正则 vue数值型输入框并限制长度 描述 原有的 <el-input type="number" /> 个人觉得存在问题,maxlength 属性无法生效,其次 max 属性虽然能够限制,但是无法阻止用户手动输入,因此依然存在 bug 代码 改为正则表达式方式 <el-input v-mode

  • vue中输入框事件的使用及数值校验方式

    目录 vue输入框事件使用及数值校验 一.@input(或者是v-on:input) 二.@change 三.@keyup.enter 四.@blur(失焦) 提示和注释 vue中常用表单校验规则整理 这里整理了一些高频率用到的校验方法 vue输入框事件使用及数值校验 最近做项目,用到vue去监听输入框当中值,并且去校验值的正确性, 我们都知道 vue 当中 主要监听输入框的方法有四个:input  change blur  keyup.enter 他们都可以使用@+xxxx="在vue当中定义

  • Vue使用antd中input组件去验证输入框输入内容(rules 案例)

    目录 效果图 示例代码 效果图 示例代码 <a-form-model-item prop="phone"> <a-input v-model="form.phone" :maxLength="11" placeholder="手机号" /> </a-form-model-item> <a-form-model-item prop="code"> <a-i

  • vue实现页面内容禁止选中功能,仅输入框和文本域可选

    上网上翻了翻,共找到两种方式 CSS样式控制,只需将下面代码复制到 vue应用下,index.html文件中的body标签上 *{ -webkit-touch-callout:none; /*系统默认菜单被禁用*/ -webkit-user-select:none; /*webkit浏览器*/ -khtml-user-select:none; /*早期浏览器*/ -moz-user-select:none;/*火狐*/ -ms-user-select:none; /*IE10*/ user-se

  • Vue实现输入框回车发送和粘贴文本与图片功能

    目录 实现回车发送 粘贴文本 粘贴图片 总结 上一篇中,我们初步新建了一个可“双向绑定”的聊天输入框,结合实际使用的场景,如果仅仅只是实现了“双向绑定”还不够,还必须处理粘贴文字.图片等功能.本文就在此基础上,继续探讨如何实现回车发送.粘贴文本(包括HTML).粘贴图片等功能.话不多说,咱们开整.contenteditable的元素如何实现“双向绑定”? 实现回车发送 在一个可编辑元素中,默认回车就是换行.但是如果我们要实现回车是“发送”的功能,需要怎么处理呢? 思路:监听键盘事件,如果判断用户

  • vue实现At人文本输入框示例详解

    目录 知识前置 需求分析 实现 创建能够输入文本的文本框 添加at功能 后记 知识前置 基于vue手把手教你实现一个拥有@人功能的文本编辑器(其实就是微信群聊的输入框) Selection 对象,表示用户选择的文本范围或插入符号的当前 developer.mozilla.org/zh-CN/docs/… contenteditable 是一个枚举属性,表示元素是否可被用户编辑. developer.mozilla.org/zh-CN/docs/… 需求分析 文本框能够输入文本(太简单了) 能够a

  • vue组件生命周期钩子使用示例详解

    目录 组件生命周期图 组件生命周期钩子 1.beforeCreate 2.created 3.beforeMount 4.mounted 5.beforeUpdate 6.updated 7.activated 8.deactivated 9.beforeDestroy 10.destroyed 11.errorCaptured 组件生命周期图 组件生命周期钩子 所有的生命周期钩子自动绑定 一.组件的生命周期:一个组件从创建到销毁的整个过程 二.生命周期钩子:在一个组件生命周期中,会有很多特殊的

  • Vue.js实现watch属性的示例详解

    目录 1.写在前面 2.watch的实现原理 3.立即执行的watch与回调执行时机 立即执行的回调函数 回调函数的执行时机 4.过期的副作用函数和cleanup 5.写在最后 1.写在前面 在上篇文章中,我们讨论了compted的实现原理,就是利用effect和options参数进行封装.同样的,watch也是基于此进行封装的,当然watch还可以传递第三参数去清理过期的副作用函数.不仅可以利用副作用函数的调度性,去实现回调函数的立即执行,也可以控制回调函数的执行时机. 2.watch的实现原

  • Vue transx组件切换动画库示例详解

    目录 来个介绍 安装 使用 支持参数 支持事件 支持API 支持的动画类型 说明 来个介绍 先奉上组件库的名称:transx github地址:github.com/tnfe/transx npm参考: www.npmjs.com/package/tra… 示例地址:codesanbox 安装 npm install transx or yarn add transx 使用 <!-- 包裹动画元素 --> <trans-x :time="time" :delay=&q

  • js 实现验证码输入框示例详解

    目录 前言 思路 遇到的问题 HTML CSS JS 前言 验证码输入框应该是很常见的需求吧,不知道各位小伙伴在在遇到的时候是选择找一找插件还是自己动手撸一个呢?我花了点时间撸了一个出来,实际体验还不错,分享给大家. 思路 我在实现之前肯定也是上网搜了一下的,网上的方式大多是使用多个input标签来实现,但我觉得不太优雅,就自己想了一个方法.使用了6个div标签和一个input标签.验证码长度一般是4位或6位,我这里用6位做演示. 先将6个div使用flex布局平铺. 再将input使用绝对定位

  • vue electron实现无边框窗口示例详解

    目录 一.前言 二.实现方案 1.创建无边框窗口 2.创建windows窗口控件组件 三.后记 一.前言 无边框窗口是不带外壳(包括窗口边框.工具栏等),只含有网页内容的窗口.对于一个产品来讲,桌面应用带边框的很少,因为丑(我们的UI觉得--与我无关-.-).因此我们就来展开说下,在做无边框窗口时候需要注意的事项以及我踩过的坑. 二.实现方案 1.创建无边框窗口 要创建无边框窗口,只需在 BrowserWindow的 options 中将 frame 设置为 false: const { Bro

  • Python实现全自动输入文本的示例详解

    目录 1. 效果图 2. 示例代码 3. 代码解释 1. 效果图 该Python脚本可以实现自动用Notepad++打开文本文件,然后自动输入文本,最后保存并关闭文件,从而实现全面自动化处理文本. 2. 示例代码 Python脚本源码如下,主要使用了win32gui.win32con和win32api模块 import win32gui import win32con import win32api import time ret = win32api.ShellExecute(1, 'open

  • vue整合项目中百度API示例详解

    目录 官网介绍 申请密钥 官方示例 项目实战 创建地图 获取经纬度 创建Map实例 两个坐标点之间的距离 查询地点信息 Vue项目中整合百度API获取地理位置的方法 组件中使用 vue-baidu-map 百度地图官方vue组件 官网介绍 百度地图 JavaScript API 是一套由 JavaScript 语言编写的应用程序接口 可帮助您在网站中,构建功能丰富交互性强的地图应用 支持PC端和移动端,基于浏览器的地图应用开发,且支持HTML5特性的地图开发 官网传送门 百度地图JavaScri

  • Springboot Vue可配置调度任务实现示例详解

    目录 正文 1.表结构: 2.接口: 3.业务层: 4.Mapper 5.前端(Vue): 正文 Springboot + Vue,定时任务调度的全套实现方案. 这里用了quartz这个框架,实现分布式调度任务很不错,关于quarz的使用方式改天补一篇.相当简单. 1.表结构: sys_job 列名 数据类型 长度 是否可空 是否主键 说明 job_id bigint 否 是 任务ID job_name varchar 64 否 是 任务名称 job_group varchar 64 否 是 任

  • vue的ssr服务端渲染示例详解

    为什么使用服务器端渲染 (SSR) 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面. 请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引.在这里,同步是关键.如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容.也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题. 更快的内容到达时间 (ti

随机推荐