您当前的位置: 首页 > 解决方案

ZZ需求,小程序纯文本实现@功能

  • 作者: admin
  • 发布于 2020-04-10 09:51:17
  • 来源:  
  • 栏目:解决方案

导语: 前言 大家肯定对@功能不陌生,在如今的各大社交软件中它是一种不可或缺的功能。实现@人的功能并不复杂,只需将@人员的id传给后端,后端下发通知即可。主要的复杂点在于一键

 

前言 

大家肯定对@功能不陌生,在如今的各大社交软件中它是一种不可或缺的功能。实现@人的功能并不复杂,只需将@人员的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)     } })复制代码

 

 

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

01.jpg

 

数据组装 

从人员列表返回用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}${item.name}">${item.atName} `             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,但也比较有趣,所以就分享给大家,如果大家有更好的解决方案,评论区有请。 

完整代码请移步语雀

结语

若有什么不对的地方,请轻点指教。 



温馨提示:这篇文章没有解决您的问题?欢迎添加微信:18948083295,有微信小程序专业人员,保证有问必答。转载本站文章请注明转自http://www.okeydown.com/(微信小程序网)。

  • 微信扫描二维码关注官方微信
  • ▲长按图片识别二维码
关注我们

微信小程序官方微信

栏目最新
栏目推荐
返回顶部