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


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 相关文章推荐
javascript将url中的参数加密解密代码
Nov 17 Javascript
非jQuery实现照片散落桌子上,单击放大的LightBox效果
Nov 28 Javascript
js窗口关闭提示信息(兼容IE和firefox)
Oct 23 Javascript
JavaScript数据推送Comet技术详解
Apr 07 Javascript
Jquery鼠标放上去显示全名的实现方法
Feb 06 Javascript
vue组件 $children,$refs,$parent的使用详解
Jul 31 Javascript
轻松理解vue的双向数据绑定问题
Oct 30 Javascript
详解使用Nuxt.js快速搭建服务端渲染(SSR)应用
Mar 13 Javascript
Three.js中矩阵和向量的使用教程
Mar 19 Javascript
Js逆向实现滑动验证码图片还原的示例代码
Mar 10 Javascript
JS的时间格式化和时间戳转换函数示例详解
Jul 27 Javascript
关于vue中如何监听数组变化
Apr 28 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 缓存函数代码
2008/08/27 PHP
PHP的简易冒泡法代码分享
2012/08/28 PHP
php利用新浪接口查询ip获取地理位置示例
2014/01/20 PHP
PHP实现显示照片exif信息的方法
2014/07/11 PHP
php 批量查询搜狗sogou代码分享
2015/05/17 PHP
WordPress开发中自定义菜单的相关PHP函数使用简介
2016/01/05 PHP
asp.net+jquery滚动滚动条加载数据的下拉控件
2010/06/25 Javascript
jQuery 版元素拖拽原型代码
2011/04/25 Javascript
javascript实用小函数使用介绍
2013/11/11 Javascript
JavaScript简单表格编辑功能实现方法
2015/04/16 Javascript
javascript实现根据时间段显示问候语的方法
2015/06/18 Javascript
JavaScript、jQuery与Ajax的关系
2016/01/24 Javascript
jQuery flip插件实现的翻牌效果示例【附demo源码下载】
2016/09/20 Javascript
jQuery视差滚动效果网页实现方法经验总结
2016/09/29 Javascript
bootstrap 设置checkbox部分选中效果
2017/04/20 Javascript
详解vue axios用post提交的数据格式
2018/08/07 Javascript
微信小程序实现复选框效果
2018/12/28 Javascript
解决Layui当中的导航条动态添加后渲染失败的问题
2019/09/25 Javascript
JS替换字符串中指定位置的字符(多种方法)
2020/05/28 Javascript
JS继承实现方法及优缺点详解
2020/09/02 Javascript
Python中规范定义命名空间的一些建议
2016/06/04 Python
浅谈numpy数组中冒号和负号的含义
2018/04/18 Python
Python3字符串encode与decode的讲解
2019/04/02 Python
python3.4+pycharm 环境安装及使用方法
2019/06/13 Python
Django接收post前端返回的json格式数据代码实现
2019/07/31 Python
python提取照片坐标信息的实例代码
2019/08/14 Python
opencv-python 读取图像并转换颜色空间实例
2019/12/09 Python
python3注册全局热键的实现
2020/03/22 Python
jupyter notebook 参数传递给shell命令行实例
2020/04/10 Python
python+opencv实现车道线检测
2021/02/19 Python
CSS3 滤镜 webkit-filter详细介绍及使用方法
2012/12/27 HTML / CSS
十八大报告观后感
2014/01/28 职场文书
焦裕禄纪念馆观后感
2015/06/09 职场文书
张丽莉事迹观后感
2015/06/16 职场文书
婚礼嘉宾致辞
2015/07/28 职场文书
html5+实现plus.io进行拍照和图片等获取
2022/06/01 HTML / CSS