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


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 相关文章推荐
自适应图片大小的弹出窗口
Jul 27 Javascript
Jquery 获取表单text,areatext,radio,checkbox,select值的代码
Nov 12 Javascript
读jQuery之九 一些瑕疵说明
Jun 21 Javascript
javascript简单事件处理和with用法介绍
Sep 16 Javascript
jquery实现页面百叶窗走马灯式翻滚显示效果的方法
Mar 12 Javascript
AngularJS通过ng-route实现基本的路由功能实例详解
Dec 13 Javascript
移动端点击态处理的三种实现方式
Jan 12 Javascript
vue-cli项目如何使用vue-resource获取本地的json数据(模拟服务端返回数据)
Aug 04 Javascript
微信小程序实现列表页的点赞和取消点赞功能
Nov 02 Javascript
Vue 后台管理类项目兼容IE9+的方法示例
Feb 20 Javascript
vue pages 多入口项目 + chainWebpack 全局引用缩写说明
Sep 21 Javascript
vue+element ui实现锚点定位
Jun 29 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
一个简单的自动发送邮件系统(三)
2006/10/09 PHP
PHP simple_html_dom.php+正则 采集文章代码
2009/12/24 PHP
浅析PHP7新功能及语法变化总结
2016/06/17 PHP
PHP命名空间定义与用法实例分析
2019/08/14 PHP
javascript学习笔记(二) js一些基本概念
2012/06/18 Javascript
javascript验证身份证完全方法具体实现
2013/11/18 Javascript
jQuery 鼠标经过(hover)事件的延时处理示例
2014/04/14 Javascript
jquery左边浮动到一定位置时显示返回顶部按钮
2014/06/05 Javascript
jQuery增加与删除table列的方法
2016/03/01 Javascript
Bootstrap实现水平排列的表单
2016/07/04 Javascript
nodejs利用http模块实现银行卡所属银行查询和骚扰电话验证示例
2016/12/30 NodeJs
详解JS中的attribute属性
2017/04/25 Javascript
JavaScript中BOM对象原理与用法分析
2019/07/09 Javascript
小程序登录之支付宝授权的实现示例
2019/12/13 Javascript
[02:35]DOTA2英雄基础教程 末日使者
2013/12/04 DOTA
Python+Django在windows下的开发环境配置图解
2009/11/11 Python
python文件读写并使用mysql批量插入示例分享(python操作mysql)
2014/02/17 Python
windows下python安装小白入门教程
2018/09/18 Python
python实现剪切功能
2019/01/23 Python
深入理解Django-Signals信号量
2019/02/19 Python
详解Python sys.argv使用方法
2019/05/10 Python
win10安装tesserocr配置 Python使用tesserocr识别字母数字验证码
2020/01/16 Python
解决python DataFrame 打印结果不换行问题
2020/04/09 Python
基于python实现获取网页图片过程解析
2020/05/11 Python
python中添加模块导入路径的方法
2021/02/03 Python
AmazeUI 网格的实现示例
2020/08/13 HTML / CSS
美国大尺码女装零售商:TORRID
2016/10/01 全球购物
Debenhams爱尔兰:英国知名的百货公司
2017/01/02 全球购物
美国按摩椅批发网站:Titan Chair
2018/12/27 全球购物
通信工程专业个人找工作求职信范文
2013/09/21 职场文书
师范大学应届生求职信
2013/11/21 职场文书
询价采购方案
2014/06/09 职场文书
建筑工地文明标语
2014/10/09 职场文书
2014年后备干部工作总结
2014/12/08 职场文书
2015年统计员个人工作总结
2015/07/23 职场文书
MySQL系列之十一 日志记录
2021/07/02 MySQL