Vue——@功能PC端实现总结
一:前言
近期需要接到一个需求,需要在输入框中实现@通知用户的功能,这个功能现在也有很多应用都有,像我们常用的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)表示获取当前的第一个选区
这里面有很多Selection跟Range的详细介绍以及使用可以参考文档: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 = ' 'spanNode3.innerHTML = ' '// 将生成内容打包放在 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}"> @${userName} </span>`)}this.close()},
@内容搜索
在@触发后,用户还可以继续输入搜索内容,输入空格或者回车关闭选择框,实现方式就是监听文本内容,当我们触发@的时候getWord标识为true,然后截取用户输入的内容作为搜索关键词,而当用户输入空格或者tab键的时候我们关闭选择器,用户敲击回车的时候我们默认取搜索结果的第一条数据。
// 内容改变监听onchange(html) {const { getWord, isDelete } = thisconst str = this.editor.txt.text()// 输入内容空格替换const text = str.replace(/ /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)}},
判断空格
这里需要注意的是,判断是否输入的是空格我们不能单单使用一个空格取判断,我们在生成的时候空格是用的是 来生成的,这里我们调试发现,跟普通的空格是有区别的,普通空格的ASCII码是32,这里富文本的空格ASCII码是160 (不间断空格:就是页面上的 ‘& nbsp’ 所产生的空格。)。 下面这个方法就是判断空格
// 判断是否是空格isSpaceReg(str) {// 判断是否是空格 普通空格的ASCII码是32,这里富文本的空格ASCII码是160 (不间断空格:就是页面上的 所产生的空格。)。// 不间断空格有个问题,就是它无法被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(/ /gi, '')return str}
六:总结
1、在生成@的标签时,记录光标位置的时机要在键盘抬起时候记录,这时候@已经生成,如果在键盘按下瞬间去记录会导致最终@标签回填的位置总是相差一个单位。
2、普通空格跟‘ ’的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、有信心!!!这里无疑就是文本的增删改查,事件也都具备,只是考虑的场景比较多,但的绝大部分场景在熟悉Selection跟Range后外加思考,结合一些原生的dom事件都能解决,可能时间上花的多一点!
Vue——@功能PC端实现总结相关推荐
- vue适配PC端屏幕自适应
vue适配PC端屏幕自适应 1.下载postcss-px2rem和px2rem-loader npm i postcss-px2rem px2rem-loader 2.src目录下新建utils文件夹 ...
- vue 调用pc端本地摄像头、麦克风实现拍照、录视频、录音 并上传到服务器指定树文件夹
vue 调用pc端本地摄像头.麦克风实现拍照.录视频.录音 并上传 自己写blog只是为了下次方便使用 过程确实很烦 ,自己摸索加各大网站cv查看 可以直接使用 1.调用摄像头拍照 录屏 首先是npm ...
- vue 实现pc端调取本地摄像头拍照生成base64数据 navigator.userAgent 功能
文章目录 1. 写在前面 2. demo摄像头拍照实现效果 3. https 方式实现摄像头拍照生成base64数据的 4. 配置浏览器的目标位置 实现摄像头拍照功能 5. pc 端实现调用本地摄像头 ...
- 基于SpringBoot+VUE(PC端+小程序端)的智能在线考试系统毕业设计
作者主页:编程千纸鹤 作者简介:Java.前端.Python开发多年,做过高程,项目经理,架构师 主要内容:Java项目开发.毕业设计开发.面试技术整理.最新技术分享 收藏点赞不迷路 关注作者有好处 ...
- VUE调用pc端摄像头
VUE项目调用pc端摄像头功能 (摄像头只可以用localhost启用项目访问,或者修改浏览器配置,底部有方法) 代码如下: <template> <!-- 原生摄像头-->& ...
- Vue.js——PC端和移动端样式适配方案
此方案整合了断点响应式样式,和移动端样式重分配. 前言 最近在学习Vue的项目架构,查询了很多移动端样式适配,整合了一下我自己的适配方案做一个记录,可能不是最好的,但我自己用着还蛮顺手的~欢迎大家补充 ...
- vue构建pc端项目(ElementUI)、vue入门小应用
Webpack+Vue-router的架构方式 Vue-cli安装省略(vue-cli搭建) ElementUI库(pc端)的引用(见下文) 打包(项目完成后打包放服务器) 在项目目录下运行 npm ...
- vue的PC端和移动端分辨率适配
使用lib-flexible和px2rem实现移动端和PC端界面适配 注释:lib-flexbles是由阿里团队很早提出解决屏幕分辨率适配的问题.现已不被推荐(因为目前比较主流的适配方案是使用vw和v ...
- vue实现pc端扫码登录
vue pc端微信扫码登录 文章目录 vue pc端微信扫码登录 效果图 方式一:npm 安装并引入插件 参数说明 使用 自定义样式 方式二:通过js 引用js文件 vue 使用 注意 效果图 方式一 ...
最新文章
- php如何获取当前时间 格式化,PHP获取当前日期和时间格式化步骤
- Project Server的页面如何修改Text
- NEERC 17 Problem I. Interactive Sort
- ARMA模型性质之平稳AR模型得统计性质
- mysql-聚合函数
- opencv-api drawKeypoints drawMatches
- 牛客 2021年度训练联盟热身训练赛第二场 E题NIH Budget
- python指数运算是不是有问题_为什么在Python 3中复指数运算如此之快?
- idea中如何查看一个类的方法被那些类调用了,显示方法对应的调用树
- [开源]STM32F103RBT6最小系统,LEDx2,KEYx4
- centos7 Samba服务安装和配置
- 【基音频率】基音matlab基音频率计算【含Matlab源码 1384期】
- 问题解决模型ORID
- RHEL7.3 已经GA了.
- Invalid argument: Subshape must have computed start >= end since stride is negative, but is 0 and 2
- python ui界面设计(二)
- 关于python中的模块的定义、使用、优点及其使用cpy文件的介绍 简单易懂
- php 用count 变量,countif函数的使用方法 PHP的可变变量名的使用方法分享
- Python int基本用法
- JAVA简单手写数字识别