一:前言

近期需要接到一个需求,需要在输入框中实现@通知用户的功能,这个功能现在也有很多应用都有,像我们常用的QQ空间,微博这些,开始看到这个需求,心里一阵惊恐,没做过啊~~

二:思路

我们大体的思路就是:当监听到用户输入@的时候我们弹出人员选择器,这时候我们需要记住现在光标所在的位置,当用户选择人员完毕之后,我们创建一个span标签在插入到我们刚刚记录光标的位置,并且把我们输入的@删除,将光标放在这个节点的最后。

三:需求拆解

按住shift + @ 的时候,弹出人员选择器

人员选择器要跟随光标的位置出现

选择时 @的用户标签插入当前的光标位置中

生成@的用户标签的规则是:高亮、携带用户ID、一键删除信息、不可以编辑。

文本框要随内容自适应高度

用户点击生成的标签或移动键盘方向键也不能聚焦进@的标签,光标需自定移到当前标签最后

输入@后连续输入的非空内容作为搜索关键词

四:准备工作

普通文本输入框实现不了这个功能,这里利用了wangEditor的富文本编辑器功能作为基础载体,

wangeditor的官方文档:www.wangeditor.com/doc/

1:wangeditor安装:

 npm i wangeditor --save

2:使用

 <div ref="editor"id="editor"@keyup='enterEvUp($event)'@keydown="enterEv($event)"></div>

引入

import E from 'wangeditor'

3:初始化编辑器

这里通过各种属性来设置编辑器的基础功能

 // 初始换编辑器initEditor() {const { placeholder, content } = thisconst editor = new E(this.$refs.editor)editor.config.placeholder = placeholdereditor.config.menus = [] // 显示菜单按钮editor.config.showFullScreen = false // 不显示全屏按钮editor.config.pasteIgnoreImg = true // 如果复制的内容有图片又有文字,则只粘贴文字,不粘贴图片。editor.config.height = '100'editor.config.zIndex = 2 // 编辑器 z-index 默认为 10000editor.config.focus = false // 取消自动 focuseditor.config.onchange = html => {this.onchange(html)}// 事件绑定editor.txt.eventHooks.clickEvents.push(this.clickEvents) // 点击事件editor.txt.eventHooks.pasteEvents.push(this.pasteEvents) // 粘贴事件editor.create()editor.txt.html(content) // 设置编辑器内容this.editor = editor// 销毁编辑器this.$once('hook:beforeDestroy', () => {this.editor.destroy()this.editor = null})},

五:@功能的实现

@基础功能实现

编辑器基本环境好了后我们正式开始实现@功能,首先我们监听键盘事件:按住shift + @ 的时候,弹出人员选择框,这里监听的触发的时候需要注意的点是不同输入模式下,键盘上@符号的keyCode数字不一样,在英文模式下keyCode的值是50,而中文输入法下标点符号keyCode都是一样的:229,这里需要注意。

 // keydown触发事件(按下键盘时候触发)enterEv(e) {const { keyCode, code } = e// 英文code是 50, 判断是否按住shift + @键// 中文输入法下标点符号keyCode都是一样的:229,推荐使用event.code或event.key作为@的判断。const isCode =((keyCode === 229 && e.key === '@') ||(keyCode === 229 && e.code === 'Digit2') ||keyCode === 50) &&e.shiftKeyif (isCode) {this.getPosition()this.getWord = truethis.showFlag = 'visible'this.userName = ''} else if (code === 'Backspace' || e.key === 'Backspace') {// 删除键this.deleteKey()} else if (code === 'Enter' || +keyCode === 13) {// 回车键const { getWord, spinShow, contactList, listIndex, stopInput } = thisif (getWord && !spinShow && contactList.length > 0 && !stopInput) {this.selectPerson(contactList[listIndex])e.preventDefault ? e.preventDefault() : (e.returnValue = false)}} else {// 当用户中文输入法下,没有选择输入内容时候就敲击回车,这时候选取内容即可其他不做处理this.stopInput = this.showFlag === 'visible' && !this.spinShow}this.isDelete = code === 'Backspace' || e.key === 'Backspace'},

记录光标的位置

这里记录光标的位置是为了再触发@的时候,下拉框定位到当期@出现的位置,因此,当我们出入@触发的时候,就记录当前光标所在的坐标位置,以及当前光标所在的为本位置。这里光标的像素位置我们用的插件 caret-pos 来获取,caret-pos插件使用也很简单,直接npm安装即可这里不详细介绍。

记录光标的坐标

这里下拉框出现的位置会受到页面高度的影响,因此,如果整体的页面如果有滚动条,我们就需要通过计算滚动条的位置来设置弹窗出现的位置,这里的细节就是当输入的文字靠近输入框最右侧的时候,我们需要把下拉框定位到光标的左侧显示,这样页面就不会被挤压变形。

import { position } from 'caret-pos'
​// 获取@位置设置下拉框出现位置getPosition() {// 滚动条滚动高度const scrollTop =document.body.scrollTop || document.documentElement.scrollTopconst width = this.$refs.editor.clientWidthconst ele = this.editor.$textElem.elems[0]const pos = position(ele)this.left = pos.left + 20// 当靠近最右边的时候,输入框在光标左边显示,300是下拉框的默认宽度,其他数字就是调整页面位置if (width - pos.left <= 300) {this.left = pos.left - 280}this.top = pos.top + 20 - scrollTop},

保存当前光标的文本位置

我们还需要记录光标在文本的位置,因为我们选中人员的时候,需要将内容填充当刚才光标的位置,保存方式也很简单,我们只需要获取当前光标所在的选区,里面包含了光标所在的各种信息,用一个全局变量保存即可。

 const range = getSelection().getRangeAt(0)const textNode = range.startContainerconst pos = this.getCursortPosition(textNode)this.cursorPos = pos

getSelection()表示用户选择的文本范围或光标的当前位置

getRangeAt(0)表示获取当前的第一个选区

这里面有很多SelectionRange的详细介绍以及使用可以参考文档:developer.mozilla.org/zh-CN/docs/…

@的功能的监听

键盘的@字符英文的code是50,还有判断同时是否按住shift + @键,而这里需要注意的点是,中文输入法下,标点符号的keyCode都是一样的,都是229,这里最好使用event.code或者event.key来作为输入是否是@的判断条件

 // 英文code是 50, 判断是否按住shift + @键// 中文输入法下标点符号keyCode都是一样的:229,推荐使用event.code或event.key作为@的判断。const isCode =((e.keyCode === 229 && e.key === '@') ||(e.keyCode === 229 && e.code === 'Digit2') ||e.keyCode === 50) &&e.shiftKey

生成 @的标签,并且高亮、携带用户ID。

生成@的用户标签的规则是:高亮、携带用户id跟userCode、一键删除信息、不可以编辑。生成逻辑也很简单,就是创建一个span标签,插入到光标的位置,然后删除用户输入的文本内容。

 // 选人回填数据selectPerson(data) {const { userCode, userId } = data
​const selection = this.position.selectionconst range = this.position.range
​// 生成需要显示的内容,包括一个 span 和前后各一个空格。const spanNode1 = document.createElement('span')const spanNode2 = document.createElement('span')const spanNode3 = document.createElement('span')
​spanNode1.className = 'at-text'spanNode1.innerHTML = `@${data.userName}` // @的文本信息spanNode1.dataset.userId = userId // 用户ID、为后续解析富文本提供spanNode1.dataset.userCode = userCode // 用户userCode// spanNode1.contentEditable = false // 当设置为false时,富文本会把成功文本视为一个节点。spanNode2.innerHTML = '&nbsp;'spanNode3.innerHTML = '&nbsp;'// 将生成内容打包放在 Fragment 中,并获取生成内容的最后一个节点,也就是空格。const frag = document.createDocumentFragment()
​let node, lastNodefrag.appendChild(spanNode3)frag.appendChild(spanNode1)frag.appendChild(spanNode2)
​// 如果是键盘触发的默认删除面前的@以及@搜索的内容const textNode = range.startContainerconst { userName } = thisconst num = userName.lengthrange.setStart(textNode, range.endOffset - 1)range.setEnd(textNode, range.endOffset + num)range.deleteContents()
​// 将 Fragment 中的内容放入 range 中,并将光标放在空格之后。while ((node = spanNode2.firstChild)) {lastNode = frag.appendChild(node)}range.insertNode(frag)// 设置光标位置selection.collapse(lastNode, 1)// 判断是否有文本、是否有坐标if (this.editor.txt.text() && this.position && range) {range.insertNode(frag)} else {// 如果没有内容一开始就插入数据特别处理this.editor.txt.append(`<span class='at-text' data-userId="${userId}" data-userCode="${userCode}">&nbsp;@${userName}&nbsp;</span>`)}this.close()},

@内容搜索

在@触发后,用户还可以继续输入搜索内容,输入空格或者回车关闭选择框,实现方式就是监听文本内容,当我们触发@的时候getWord标识为true,然后截取用户输入的内容作为搜索关键词,而当用户输入空格或者tab键的时候我们关闭选择器,用户敲击回车的时候我们默认取搜索结果的第一条数据。

 // 内容改变监听onchange(html) {const { getWord, isDelete } = thisconst str = this.editor.txt.text()// 输入内容空格替换const text = str.replace(/&nbsp;/gi, ' ').trim()// 替换空标签const regex = /<span[^<>]*></span>/gmconst content = html.replace(regex, '')// @触发后的输入处理if (getWord && str) {const range = getSelection().getRangeAt(0)const index = this.getCursortPosition(range.startContainer)// 用户在输入回车,换行时不记录if (range.startContainer.innerText === '\n') {this.close()return}const arr = range.startContainer.data.substring(0, index).split('@')const value = arr[arr.length - 1]const isSpaceStr = value.substring(value.length - 1, value.length)const isSpace = this.isSpaceReg(isSpaceStr)if (value === '') {// 保存光标位置,在@生成时候光标的位置,const selection = getSelection()this.position = {range: selection.getRangeAt(0),selection: selection}}if (isSpace) {// 空格或者tab键时关闭人员选择器this.close()return}this.userName = valuethis.spinShow = truethis.search()}// 删除spanif (isDelete) {this.deleteAtSpan()}const data = {preview: text,html: content}this.text = textthis.$emit('change', data)},

删除整块带有@内容标签,

因为我们在生成@内容的时候contentEditable属性我们没有设置成false,所以此时的整块标签其实是可以编辑的,这样显然不行,我们删除的时候要删除一整块,实现的方法就是当我们删除的内容包含到@内容的时候,我们需要手动取删除整块标签。

删除的方法就是在我们删除内容的时候,取获取当前光标的位置,判断删除的内容是否是我们设置的标识className,如果是,我们就将整个选区扩大,包含整个@内容,然后删除其节点。range.deleteContents()方法是删除节点的文本内容,我们通过range.cloneContents()这个方法获取节点,然后将其节点也删除。

 // 删除整块带有@内容标签deleteAtSpan() {const selection = window.getSelection() // 获取当前选中区域const range = selection.getRangeAt(0)const { startOffset, endOffset } = rangeconst textNodeStar = range.startContainerconst textNodeEnd = range.endContainer// 获取节点const selectNode = range.cloneContents()const className =textNodeStar.parentNode.className ||textNodeEnd.parentNode.className ||''if (className === 'at-text' && (+endOffset !== 0 || +startOffset !== 0)) {range.selectNodeContents(textNodeStar)range.selectNodeContents(textNodeEnd)range.deleteContents()if (selectNode.firstChild) {// 删除节点selectNode.removeChild(selectNode.firstChild)}}},

移动光标位置

当我们讲鼠标放到@的内容上时,我们光标要不可聚焦上去,要定位在当前点击的@内容的末尾处,这个功能就是移动光标位置,当我们点击@内容的时候,我们需要判断光标在此节点文本中的位置,以及这个节点的文本有多长,由此可以计算,我们需要向左或者向右移动多少个单位。

selection.modify(‘move’, ‘left’, ‘character’)方法就是移动光标的位置,这个方法接受三个参数,第一个参数是移动还是扩大选区

传入"move"来移动光标位置,或者``"extend"来扩展当前选区。

第二个是移动的方向,调整选区的方向。你可以传入"forward"或``"backward"来根据选区内容的语言书写方向来调整。或者使用"left"或"right"来指明一个明确的调整方向。

第三个参数是单位,调整的距离颗粒度。可选值有"character"、``"word"、``"sentence"、``"line"、``"paragraph"、``"lineboundary"、``"sentenceboundary"、``"paragraphboundary"、``"documentboundary"。

我们这里选择的是以字符来移动。

 // 移动光标moveCursor(direction) {try {const selection = window.getSelection() // 获取当前选中区域const range = getSelection().getRangeAt(0)const textNode = range.startContainerconst pos = this.getCursortPosition(textNode)
​// 左移光标if (direction === 'left') {for (let i = 0; i < pos; i++) {selection.modify('move', 'left', 'character')}// 移动完成后再次检查,光标是否还在@所在标签} else {// 右移光标 多移动一个空格for (let i = 0; i < textNode.length - pos + 1; i++) {selection.modify('move', 'right', 'character')}}if (textNode) {this.moveDirection()}} catch (e) {// console.log(e)}},

判断空格

这里需要注意的是,判断是否输入的是空格我们不能单单使用一个空格取判断,我们在生成的时候空格是用的是&nbsp来生成的,这里我们调试发现,跟普通的空格是有区别的,普通空格的ASCII码是32,这里富文本的空格ASCII码是160 (不间断空格:就是页面上的 ‘& nbsp’ 所产生的空格。)。 下面这个方法就是判断空格

 // 判断是否是空格isSpaceReg(str) {// 判断是否是空格  普通空格的ASCII码是32,这里富文本的空格ASCII码是160 (不间断空格:就是页面上的&nbsp;所产生的空格。)。// 不间断空格有个问题,就是它无法被trim()所裁剪,也无法被正则表达式的\s所匹配,// 也无法被StringUtils的isBlank()所识别,也就是说,无法像裁剪寻常空格那样移除这个不间断空格。// 利用不间断空格的Unicode编码来移除它,其编码为\u00A0。const regu = '^[\u00A0 ]+$'const re = new RegExp(regu)return re.test(str)},

粘贴除去样式:

原本这里wangEditor编辑器有个可以控制粘贴样式的过滤。但是测试过后这个属性并不能生效,因此我们需要自己定义粘贴事件。

 editor.config.pasteFilterStyle = false // 关闭粘贴样式的过滤----无效

这里编辑器在粘贴的时候会触发一个粘贴事件,里面会传递给我们粘贴的内容,我只需将内容的标签样式剔除,获取到里面的文本即可。

 // 自定义去除粘贴样式   不适用于 IEeditor.config.pasteTextHandle = function (content) {if (content === '' && !content) return ''let str = content// 去除粘贴样式str = str.replace(/<xml>[\s\S]*?</xml>/gi, '')str = str.replace(/<style>[\s\S]*?</style>/gi, '')str = str.replace(/</?[^>]*>/g, '')str = str.replace(/[ | ]*\n/g, '')str = str.replace(/&nbsp;/gi, '')return str}

六:总结

1、在生成@的标签时,记录光标位置的时机要在键盘抬起时候记录,这时候@已经生成,如果在键盘按下瞬间去记录会导致最终@标签回填的位置总是相差一个单位。

2、普通空格跟‘&nbsp’的ASCII码不一致,导致调试期间,判断是否为空格的时候出错,普通键盘输入的空格是ASCII32,而& nbsp’ 生成的空格ASCII码是160 ,也叫不间断空格。

3、键盘的@字符英文的code是50,中文输入法下,标点符号的keyCode都是一样的,都是229,这里在触发@的条件时候容易忽略,这里最好使用event.code或者event.key来作为输入是否是@的判断条件。

4、PC端由于发帖跟页面列表是在同一个页面,因此在我们切换页面的时候,要记得弹出框的关闭(方案:监听路由),存储光标选区信息的时候不能跨页面缓存,这样会搞垮整个页面。不然两个页面之间的编辑器会互相干扰。

4、多看文档!!!还要看官方文档,百度的有时候不靠谱,光标相关的事件以及api要熟悉,开始的时候删除光标的文本一直找不到方法,多看文档后发现有办法实现,range.cloneContents()可以拿到光标选区的节点(这里其实也只是复制克隆的节点)。还有就是移动光标位置的方法: selection.modify(‘move’, ‘right’, ‘character’)(上文有介绍使用),当时看别百度的介绍使用的时候一脸懵,感觉好难啊!但是看官方文档后豁然开朗,So easy!

5、有信心!!!这里无疑就是文本的增删改查,事件也都具备,只是考虑的场景比较多,但的绝大部分场景在熟悉SelectionRange后外加思考,结合一些原生的dom事件都能解决,可能时间上花的多一点!

Vue——@功能PC端实现总结相关推荐

  1. vue适配PC端屏幕自适应

    vue适配PC端屏幕自适应 1.下载postcss-px2rem和px2rem-loader npm i postcss-px2rem px2rem-loader 2.src目录下新建utils文件夹 ...

  2. vue 调用pc端本地摄像头、麦克风实现拍照、录视频、录音 并上传到服务器指定树文件夹

    vue 调用pc端本地摄像头.麦克风实现拍照.录视频.录音 并上传 自己写blog只是为了下次方便使用 过程确实很烦 ,自己摸索加各大网站cv查看 可以直接使用 1.调用摄像头拍照 录屏 首先是npm ...

  3. vue 实现pc端调取本地摄像头拍照生成base64数据 navigator.userAgent 功能

    文章目录 1. 写在前面 2. demo摄像头拍照实现效果 3. https 方式实现摄像头拍照生成base64数据的 4. 配置浏览器的目标位置 实现摄像头拍照功能 5. pc 端实现调用本地摄像头 ...

  4. 基于SpringBoot+VUE(PC端+小程序端)的智能在线考试系统毕业设计

    作者主页:编程千纸鹤 作者简介:Java.前端.Python开发多年,做过高程,项目经理,架构师 主要内容:Java项目开发.毕业设计开发.面试技术整理.最新技术分享 收藏点赞不迷路  关注作者有好处 ...

  5. VUE调用pc端摄像头

    VUE项目调用pc端摄像头功能 (摄像头只可以用localhost启用项目访问,或者修改浏览器配置,底部有方法) 代码如下: <template> <!-- 原生摄像头-->& ...

  6. Vue.js——PC端和移动端样式适配方案

    此方案整合了断点响应式样式,和移动端样式重分配. 前言 最近在学习Vue的项目架构,查询了很多移动端样式适配,整合了一下我自己的适配方案做一个记录,可能不是最好的,但我自己用着还蛮顺手的~欢迎大家补充 ...

  7. vue构建pc端项目(ElementUI)、vue入门小应用

    Webpack+Vue-router的架构方式 Vue-cli安装省略(vue-cli搭建) ElementUI库(pc端)的引用(见下文) 打包(项目完成后打包放服务器) 在项目目录下运行 npm ...

  8. vue的PC端和移动端分辨率适配

    使用lib-flexible和px2rem实现移动端和PC端界面适配 注释:lib-flexbles是由阿里团队很早提出解决屏幕分辨率适配的问题.现已不被推荐(因为目前比较主流的适配方案是使用vw和v ...

  9. vue实现pc端扫码登录

    vue pc端微信扫码登录 文章目录 vue pc端微信扫码登录 效果图 方式一:npm 安装并引入插件 参数说明 使用 自定义样式 方式二:通过js 引用js文件 vue 使用 注意 效果图 方式一 ...

最新文章

  1. php如何获取当前时间 格式化,PHP获取当前日期和时间格式化步骤
  2. Project Server的页面如何修改Text
  3. NEERC 17 Problem I. Interactive Sort
  4. ARMA模型性质之平稳AR模型得统计性质
  5. mysql-聚合函数
  6. opencv-api drawKeypoints drawMatches
  7. 牛客 2021年度训练联盟热身训练赛第二场 E题NIH Budget
  8. python指数运算是不是有问题_为什么在Python 3中复指数运算如此之快?
  9. idea中如何查看一个类的方法被那些类调用了,显示方法对应的调用树
  10. [开源]STM32F103RBT6最小系统,LEDx2,KEYx4
  11. centos7 Samba服务安装和配置
  12. 【基音频率】基音matlab基音频率计算【含Matlab源码 1384期】
  13. 问题解决模型ORID
  14. RHEL7.3 已经GA了.
  15. Invalid argument: Subshape must have computed start >= end since stride is negative, but is 0 and 2
  16. python ui界面设计(二)
  17. 关于python中的模块的定义、使用、优点及其使用cpy文件的介绍 简单易懂
  18. php 用count 变量,countif函数的使用方法 PHP的可变变量名的使用方法分享
  19. Python int基本用法
  20. JAVA简单手写数字识别

热门文章

  1. mysql查询成绩大于89分_查询每门课程成绩都大于80分学生的姓名
  2. 智慧楼宇系统:解决园区/写字楼90%的管理问题
  3. 试用Unity3D体验(三):添加Loading页面
  4. 日语机种依赖文字问题探析之一问题描述
  5. Python自然语言处理 第一章 课后习题答案
  6. 如何戒掉短视频?2个方法适合职场人,从未失败过
  7. 疯狂英语脱口而出900句
  8. asm 编写 wasm 对比原生性能
  9. 计算机毕业设计ssm餐饮外卖系统v22fo系统+程序+源码+lw+远程部署
  10. 苹果手机调试(ios)