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


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 相关文章推荐
共享自己写一个框架DreamScript
Jan 20 Javascript
jQuery DIV弹出效果实现代码
Jul 03 Javascript
用JQuery 实现AJAX加载XML并解析的脚本
Jul 25 Javascript
javascript基础知识大集锦(二) 推荐收藏
Jan 13 Javascript
JavaScript多线程详解
Aug 12 Javascript
Javascript在IE和Firefox浏览器常见兼容性问题总结
Aug 03 Javascript
jQuery Ajax实现跨域请求
Jan 21 Javascript
Angular6 写一个简单的Select组件示例
Aug 20 Javascript
js类的继承定义与用法分析
Jun 21 Javascript
javascript实现扫雷简易版
Aug 18 Javascript
JavaScript动态生成表格的示例
Nov 02 Javascript
vue动态绑定style样式
Apr 20 Vue.js
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数组函数序列之rsort() - 对数组的元素值进行降序排序
2011/11/02 PHP
php银联网页支付实现方法
2015/03/04 PHP
PHP自动生成表单代码分享
2015/06/19 PHP
php生成带logo二维码方法小结
2016/04/08 PHP
PHP JWT初识及其简单示例
2018/10/10 PHP
不常用但很实用的PHP预定义变量分析
2019/06/25 PHP
juqery 学习之四 筛选过滤
2010/11/30 Javascript
javascript白色简洁计算器
2015/05/04 Javascript
深入理解Vue官方文档梳理之全局API
2017/11/22 Javascript
使用Vue开发一个实时性时间转换指令
2018/01/17 Javascript
angularjs实现的购物金额计算工具示例
2018/05/08 Javascript
vue2中使用sass并配置全局的sass样式变量的方法
2018/09/04 Javascript
微信小程序实现简易table表格
2020/06/19 Javascript
细说Vue组件的服务器端渲染的过程
2019/05/30 Javascript
no-vnc和node.js实现web远程桌面的完整步骤
2019/08/11 Javascript
JS判断浏览器类型与操作系统的方法分析
2020/04/30 Javascript
javascript实现的图片预览和上传功能示例【兼容IE 9】
2020/05/01 Javascript
python实现的一只从百度开始不断搜索的小爬虫
2013/08/13 Python
Python中类的定义、继承及使用对象实例详解
2015/04/30 Python
深入解析Python中的WSGI接口
2015/05/11 Python
Python处理json字符串转化为字典的简单实现
2016/07/07 Python
Python实现读取邮箱中的邮件功能示例【含文本及附件】
2017/08/05 Python
Python实现重建二叉树的三种方法详解
2018/06/23 Python
Python中几种属性访问的区别与用法详解
2018/10/10 Python
详解如何设置Python环境变量?
2019/05/13 Python
python字符串和常用数据结构知识总结
2019/05/21 Python
Python绘制股票移动均线的实例
2019/08/24 Python
django haystack实现全文检索的示例代码
2020/06/24 Python
canvas绘制表情包的示例代码
2018/07/09 HTML / CSS
Interhome丹麦:在线预订度假屋和公寓
2019/07/18 全球购物
shell程序中如何注释
2012/01/28 面试题
金融专业个人求职信
2013/09/22 职场文书
2014领导班子“四风问题”对照检查材料思想汇报(执法局)
2014/09/21 职场文书
学生会个人总结范文
2015/02/15 职场文书
酒店工程部的岗位职责汇总大全
2019/10/23 职场文书
Python基础之Socket通信原理
2021/04/22 Python