微信小程序纯文本实现@功能


Posted in Javascript onApril 08, 2020

前言

大家肯定对@功能不陌生,在如今的各大社交软件中它是一种不可或缺的功能。实现@人的功能并不复杂,只需将@人员的id传给后端,后端下发通知即可。主要的复杂点在于一键删除功能与变色功能,web端可以使用现成库 caret.js 或者 At.js 来实现。但笔者需要在小程序中实现这个功能,而且在 textarea 标签里实现,当然@人名的变色功能自然而然就砍掉了。

准备工作

怎么来实现一键删除呢?首先想到对@人名前后用特殊符号标记+正则来实现,但结果不是很理想,扩展性也比较差,如果还要匹配话题之类的就得多写一套代码,所以就试着找其他方法解决。发现 wx.getSelectedTextRange 可以获取文本框聚焦时的光标,这样就可以将@人员插入文本指定位置。文本框事件 @input 的可以获取到变化的数据与位置,那就可以根据变化的位置与变化的数据来判断是否命中@人员,@人员的位置可以通过计算获取。

// bindinput事件返回值 
// value为变化后的值 cursor为变化的位置 keyCode为触发的键值 
const {value, cursor, keyCode} = event.detail 
// 获取光标位置,聚焦时生效 
wx.getSelectedTextRange({
  complete: res => {
    console.log('光标位置:', res.start, res.end)
  }
})

准备工作做好了就进入实践环节,毕竟实践是检验真理的唯一标准。设计图呈现:通过点击@按钮到人员列表页面,选择人员后返回,具体如下图。这里涉及页面之间的通信问题,可以通过状态管理器、数据缓存、获取页面栈设置数据等来实现,本例中使用数据缓存。

微信小程序纯文本实现@功能 

数据组装

从人员列表返回用 wx.navigateBack ,会触发 onShow 这个生命周期,所以需要在 onShow 里组装@数据。获取到的@人员根据光标位置对文本进行字符串截取组装,若未获取到光标位置则直接将@人员添加到文本末尾。然后对@人员数据、文本数据等进行备份,用于后续的计算。

initAtFn() {
    // 获取@人员数据
    const me = this
    const initMemberList = wx.getStorageSync('atMemberList')
    const atMemberArr = initMemberList ? initMemberList : []
    // 赋值后清除@人员数据
    wx.removeStorageSync('atMemberList')
    // 获取上一次光标的位置
    const preCursor = wx.getStorageSync('blurCursor') ? parseInt(wx.getStorageSync('blurCursor')) : me.content.length
    // 将 @人员数据 并入内容区域
    if (atMemberArr.length > 0) {
     // 获取人员名称
     const atMemberName = `@${atMemberArr[0].name}`
     // 如果上次光标有记录 就根据光标分割字符串 并入@人员名称
     if (preCursor.toString().length !== me.content.length) {
      const start = me.content.substring(0, preCursor)
      const end = me.content.substring(preCursor)
      me.content = `${start}${atMemberName}${end}`
     } else {
      me.content += `${atMemberName}`
     }
     me.atArr = me.atArr.concat(atMemberArr) // 合并人员
     wx.setStorageSync('blurCursor', preCursor + atMemberName.length)
    }else {
     wx.setStorageSync('blurCursor', me.content.length)
    }
    me.focus = true
    me.copyContent = me.content
    me.executeArr = me.getAtMemberPosFn() // 获取@人员位置
   }

计算@人员位置

对@人员数组进行遍历,计算@人员在文本中的位置区间。通过indexOf来获取起点(这里有一个缺陷,也是需要优化的点,当手动输入的内容中有和@人员名字相同的字段时,那么位置靠前的那一个将会生效),终点为起点+名字长度。这里有个问题:如果重复@相同的人员,删除时怎么区分呢?笔者想当然的使用了时间戳,结果发现在遍历中使用时间戳并不准确,只有规规矩矩生成唯一值。

计算时收集了人员位置的最值区间,在这个范围之外增减文本不会影响@人员的完整性。下面是代码:

getAtMemberPosFn() {
    const me = this
    const [tipArr, left, right] = [ [], [], [] ]
    // 根据@人员的数组来匹配计算所处位置
    me.atArr.map(item => {
     const name = item.name
     const userId = item.userId
     // 此处有一个缺陷 如果手输入的和获取的@人名字相同 第一个会生效 第二个不会生效
     let start = me.copyContent.indexOf(name)
     
     if (tipArr.length > 0) {
      const _arr = tipArr.filter(v => v.name.includes(name))
      if (_arr.length > 0) {
       start = me.copyContent.indexOf(name, _arr[_arr.length - 1].end)
      }
     }
     
     const end = name.length + start // end
     left.push(start)
     right.push(end)
     // 获取唯一标识 是用于重复@的区分
     const guid = me.createGuidFn()
     const tipObj = {
      start: start - 1, // @ - 1
      end,
      name,
      atName: `@${name}`,
      type: item.userId, 
      userId: userId,
      code: guid
     }
     tipArr.push(tipObj)
    })
    
    // 获取区间左右最值
    right.length > 0 ? me.maxAt = Math.max(...right) : me.maxAt = 0
    left.length > 0 ? me.minAt = Math.min(...left) : me.minAt = 0
    me.atArr = tipArr
    return tipArr
   }

一键删除功能

@人员的位置区间已经计算出来了,接下来监听输入框的内容变化实现一键删除功能,当输入框文本内容变化,会触发 @input 事件,它会返回变化后的值 value ,变化的位置 cursor ,我们将利用这两个数据作为是否 命中@人员的判断依据 。将情况分为以下几种:

变化后的value为空,即清空了输入框。

数据变化的光标位置大于@人员位置最值区间的最大值,即不影响@人员位置。

当数据变化影响@人员时,这里对增加减少内容做了区分处理:

增加时,如果增加位置小于最值的最小值,则直接重新计算位置。如果增加值的位置命中@人员位置,则过滤掉失效人员,再重新计算。这里需要注意,移动端输入法会有一次性输入多个字符,变化的位置不再是返回的光标位置,而是以光标位置减去变化前后数据的差值。

删除时,获取删除的起始位置 (A,B) ,然后与@人员位置 (start, end) 作比较。 当 !(A < start || B > end) 时,则为命中,将命中的@人员过滤掉即可。

changeFn(txt) {
    const me = this
    const { value, cursor, keyCode } = txt.detail // 改变后的值,改变的位置,按键
     
    // 如果改变后的值为'', 就直接返回
    if(!value) {
     me.content = value
     me.copyContent = value
     me.atArr = []
     return false
    }
    
    // 判断值改变的增减
    const changeLen = value.length - me.copyContent.length
    // 值改变的光标位置 不影响@人员的则不管
    if (cursor > me.maxAt) {
     me.copyContent = me.content
     return false
    }
  
    // 判断为 增加值
    if (changeLen > 0) {
     const addCursor = cursor - changeLen // 重新计算增加位置 防止移动端一次性粘贴导致失效问题
     me.copyContent = me.content
     // 增加值的位置 小于左区间最值 则重新计算位置
     if(addCursor < me.minAt) {
      me.executeArr = me.getAtMemberPosFn()
      return false
     }
     
     me.executeArr.map(item => {
      const { start, end, name, code } = item
      if (addCursor < end && addCursor > start) {
       // 删除命中人员,则该人员失效
       me.atArr = me.atArr.filter(v => v.code !== code)
      }
     })
     
     // 需要重新计算位置
     me.executeArr = me.getAtMemberPosFn()
    } else {
     let replaceStr = '' // 应被删除的字段
     const left = [] // 删除左值集合
     const right = [] // 删除右值集合
     const delLen = cursor - changeLen // 本身删除的长度
     const deleteString = me.copyContent.substring(cursor, delLen) // 本身删除的字段 [cursor, changeLen)
     // 获取应被删除的左右位置
     function pushArrEvent(s, e) {
      left.push(s)
      right.push(e)
     }
  
     me.executeArr.map(item => {
      let { start, end, name, code } = item
      // D大 <= B小 || D小 >= B大
      // 命中部分为 删除部分与@人员的交集
      if (!(delLen <= start || cursor >= end)) {
       // 命中判定,命中位置在名字区间 左边/右边/之间/或者多选中删除的
       if (delLen <= end && cursor >= start) {
        pushArrEvent(start, end)
       } else {
        if (cursor > start) {
         if (delLen > end) {
          pushArrEvent(start, delLen)
         } else {
          pushArrEvent(start, end)
         }
        } else if (cursor < start) {
         if (delLen > end) {
          pushArrEvent(cursor, delLen)
         } else {
          pushArrEvent(cursor, end)
         }
        } else {
         pushArrEvent(cursor, delLen)
        }
       }
  
       // 获取一键删除区间 
       const del_left = Math.min(...left)
       const del_right = Math.max(...right)
       // 根据区间获取一键删除字段
       replaceStr = me.copyContent.substring(del_left, del_right)
       // 删除后的赋值
       me.content = me.copyContent.substring(0, del_left) + me.copyContent.substring(del_right)
       
       // @人员数组生成
       me.atArr = me.atArr.filter(v => v.code !== code)
      }
     })
     // 执行完后 重新赋值计算
     me.copyContent = me.content
     me.executeArr = me.getAtMemberPosFn()
    }
   }

添加标签

我们还差最后一步,那就是给@人名添加标签,用于显示时与一般文本做区分。这里踩了一个坑,用正则替换时,如果名字与名字之间存在包含关系,则会失效,所以用记录位置的方式来对文本进行截取组装。

submitTxtFn() {
    const copyTxt = this.content
    const arr = JSON.parse(JSON.stringify(this.atArr))
    const atUserIds = [...new Set(arr.map(v=>v.userId))] // 获取@人员id
    let targetContent = ''
    let count = 0
    // 给@人员添加wxml标签,此处用了jyf-Parser富文本解析插件,href里面的值用于点击传参
    if(arr.length > 0) {
     arr.forEach((item, index)=>{
      let _tip = ''
      const txt = copyTxt.substring(count, item.start)
      // 加空格
      _tip = `${txt}<a class="link" href="${item.name}" rel="external nofollow" >${item.atName} </a>`
      targetContent += _tip
      // 处理最后一个标签后面的文本
      if(index + 1 === arr.length) {
       if(item.end < copyTxt.length) {
        targetContent += copyTxt.substring(item.end)
       }
      }
      count = item.end
     })
    }else {
     targetContent = this.content
    }
    // 目标数据
    const targetObj = {
     content: targetContent,
     atIds: atUserIds
    }
    this.submitData = targetObj
    return targetObj
   }

以上就实现了纯文本的@功能,通过计算位置来实现的优点是具有扩展性,比如一套代码可以实现#话题功能和@功能共存,只需加个type作为区分即可。缺点是一键删除时体验不是很好,并且删除后不能控制光标位置,不能实现人员名称变色等。虽然功能比较ZZ,但也比较有趣,所以就分享给大家,如果大家有更好的解决方案,评论区有请。

完整代码请移步 语雀

总结

到此这篇关于微信小程序纯文本实现@功能的文章就介绍到这了,更多相关小程序@功能内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
[原创]图片分页查看
Aug 28 Javascript
jQuery用unbind方法去掉hover事件及其他方法介绍
Mar 18 Javascript
JavaScript自定义方法实现trim()、Ltrim()、Rtrim()的功能
Nov 03 Javascript
jQuery中的val()示例应用
Feb 26 Javascript
深入理解javascript作用域和闭包
Sep 23 Javascript
深入理解JavaScript系列(26):设计模式之构造函数模式详解
Mar 03 Javascript
Javascript中使用parseInt函数需要注意的问题
Apr 02 Javascript
jQuery插件select2利用ajax高效查询大数据列表(可搜索、可分页)
May 19 jQuery
详解ElementUI之表单验证、数据绑定、路由跳转
Jun 21 Javascript
jQuery中的类名选择器(.class)用法简单示例
May 14 jQuery
基于javascript实现贪吃蛇小游戏
Nov 25 Javascript
ES6学习笔记之let与const用法实例分析
Jan 22 Javascript
JavaScript 俄罗斯方块游戏实现方法与代码解释
Apr 08 #Javascript
vue与iframe之间的信息交互的实现
Apr 08 #Javascript
Javascript摸拟自由落体与上抛运动原理与实现方法详解
Apr 08 #Javascript
antd-mobile ListView长列表的数据更新遇到的坑
Apr 08 #Javascript
详解element上传组件before-remove钩子问题解决
Apr 08 #Javascript
javascript 设计模式之享元模式原理与应用详解
Apr 08 #Javascript
javascript 设计模式之组合模式原理与应用详解
Apr 08 #Javascript
You might like
php实现遍历多维数组的方法
2015/11/25 PHP
PHP7.1方括号数组符号多值复制及指定键值赋值用法分析
2016/09/26 PHP
详谈php ip2long 出现负数的原因及解决方法
2017/04/05 PHP
JQuery中$之选择器用法介绍
2011/04/05 Javascript
jq实现酷炫的鼠标经过图片翻滚效果
2014/03/12 Javascript
学习JavaScript设计模式之中介者模式
2016/01/14 Javascript
功能强大的Bootstrap效果展示(二)
2016/08/03 Javascript
jQuery替换节点用法示例(使用replaceWith方法)
2016/09/08 Javascript
Node.js的环境安装配置(使用nvm方式)
2016/10/11 Javascript
浅谈js中几种实用的跨域方法原理详解
2016/12/02 Javascript
angularjs+bootstrap菜单的使用示例代码
2017/03/07 Javascript
原生js实现放大镜特效
2017/03/08 Javascript
JavaScript 函数的定义-调用、注意事项
2017/04/16 Javascript
js下载文件并修改文件名
2017/05/08 Javascript
JavaScript设计模式之单例模式原理与用法实例分析
2018/07/26 Javascript
深入理解与使用keep-alive(配合router-view缓存整个路由页面)
2018/09/25 Javascript
浅谈一个webpack构建速度优化误区
2019/06/24 Javascript
Vue3.0数据响应式原理详解
2019/10/09 Javascript
微信小程序 点击切换样式scroll-view实现代码实例
2019/10/11 Javascript
基于JQuery和DWR实现异步数据传递
2020/10/16 jQuery
PHP webshell检查工具 python实现代码
2009/09/15 Python
Python实现提取谷歌音乐搜索结果的方法
2015/07/10 Python
举例讲解Python中的死锁、可重入锁和互斥锁
2015/11/05 Python
Jupyter安装nbextensions,启动提示没有nbextensions库
2020/04/23 Python
对Python 检查文件名是否规范的实例详解
2019/06/10 Python
django实现支付宝支付实例讲解
2019/10/17 Python
pandas中遍历dataframe的每一个元素的实现
2019/10/23 Python
澳大利亚领先的折扣药房:Chemist Direct(有中文站)
2018/11/24 全球购物
应届生高等护理求职信
2013/10/12 职场文书
单位刻章介绍信范文
2014/01/11 职场文书
一体化教学实施方案
2014/05/10 职场文书
个人租房协议书样本
2014/10/01 职场文书
局机关干部群众路线个人对照检查材料思想汇报
2014/10/05 职场文书
解除同居协议书
2015/01/29 职场文书
毕业证明模板
2015/06/19 职场文书
水浒传读书笔记
2015/06/25 职场文书