【小程序】无米之炊: 小程序内实现一个具有“@功能at功能”的输入框

什么是at功能

所谓的at功能,就是指的在聊天框中输入人的姓名等信息时,允许用户在输入"@"字符之后,可以调起一个选人控件,方便用户快速输入人名。

例如:微博输入框,QQ空间的说说输入框。我们可以在一个输入框内输入 "@" 字符,然后会调起一个选人浮层或全屏选人控件(在桌面端通常是个浮层,在移动端通常是一个全屏控件)。

本文是讲述在小程序中实现一个@功能的过程。另外一篇 基于contenteditable技术实现@选人功能
中讲述了 Web版本的实现。

at功能的需求差异

在接到 at 功能的需求时,我们需要首先确定一个问题:即我们at出来的人名,是否有重名的现象。即:在输入框中同时出现 "@abc" 和 "@abc" 两个人名时,这俩人是否必然代表同一个人。

像新浪微博的场景下,其at出来的微博账户,必然是唯一的,因此其技术实现方案便可简化为:只需将用户从选人控件中选择的人渲染到输入框即可

而像QQ空间等需求场景下,我们at选出来的一个用户昵称,实际上是可以重名的,这时,我们的技术方案必须考虑到:如何将一个输入框中的人名要跟他对应的账户信息一一映射起来。只有这样,当我们把用户输入的消息保存给后台时,才能清晰的还原出两个"@abc"分别是哪个人。

如下是qq空间输入框,我可以输入2个同名的人,他分别可以给我两个不同的好友发送at消息:

【小程序】无米之炊: 小程序内实现一个具有“@功能at功能”的输入框

本文,我所讨论的是QQ空间这种可重名的场景。因此,我们的技术方案需要考虑如何将at人名与账户信息记录映射起来。

小程序: 难为无米之炊

在 web 端,通常我们使用 div 配合 contentediable,另外再配合 Range和Selection的光标控制 api 来实现类似聊天框里的 at 功能。

其中:

  • contenteditable api允许我们将html标签插入到编辑器当中,这样我们便可以将账户信息“塞到”标签里,从而在提交后台时,从标签里把账户信息还原出来
  • range api提供了控制光标选取和设置选取内容的能力,这允许我们在用户选完控件人名后,我们将at字符删除,并把新选择的人名标签渲染到输入框里。

可是 小程序的输入框input和textarea并没有 web 那么多强大的 API (比如 Range和Selection),也无法在小程序内使用 contenteditable实现富文本。小程序仅有一个 bindinput 事件:

【小程序】无米之炊: 小程序内实现一个具有“@功能at功能”的输入框

其事件返回3个参数:

  • value: 当前输入框中最新的值。
  • cursor: 当前光标的位置
  • keyCode:当前输入事件的键盘按键

思路

我们要在一个如此 朴素 的输入框,利用仅有的 value\\cursor\\keyCode 3个参数实现 "at检测"、"人名渲染"、"删除检测"、"重名支持(即账户信息还原)"。 其最大的难点主要还是如何记录账户信息从而支持重名。

能想到的方案有3种:

  1. 每当输入at字符并选择了人名后,我们在另外一个数据结构中记录该账户信息。例如维持一个 persons: [] 数组。 但是我们要在用户对输入框内容进行 增删改查 的同时对我们的persons结构进行同步增删改查的更新,其修改难度会很复杂。
  2. 我们想,能否在at人名填入到输入框的时候,把账户信息塞到输入框里。就像contenteditable一样。因此,我们可以尝试使用一些不可见的字符,用这些字符来表达某个账户信息的标识。 但这种方案,想想就复杂,例如我们删除一个人名时是否能同步把不可见字符也删掉,是否会有光标问题都不好说。
  3. 采用了一种虚拟层的思路。当用户输入任何字符时,我们都对用户输入进行拦截,拦截到输入后,首先按照需求更新我们内部虚拟层的数据结构,在虚拟层中我们按一定的结构保存好用户账户信息数据,然后将其渲染成文本再填到输入框中。

最终,我选择了采用第三种方案实现,方案如图:
【小程序】无米之炊: 小程序内实现一个具有“@功能at功能”的输入框

调用方法

虚拟层内部具体的计算逻辑封装到了 RichMessage 类中。小程序组件中,首先给input输入框绑定 input 事件:

<input placeholder="请输入" bindinput="eventInput" />

组件脚本中:

{

data: {

inputContent: '',

},

attached() {

this.myCommentRichMessage = new RichMessage();

},

methods: {

eventInput(e) {

const res = this.myCommentRichMessage.doInput(event.detail);

// 输入完之后,重新渲染input内容

res.then(str => {

if (typeof str === 'string') {

this.setData({

inputContent: str

});

}

});

}

}

}

当要把数据提交给后台时,可以调用 toProto 方法,将消息转成具体的数据结构:

const pbdata = myCommentRichMessage.transformToProto()

实现

RichMessage类

负责根据输入的光标和value,计算出是在什么位置新增或删除了字符。并负责维护虚拟层的消息盒子---msgbox。

  • 当用户新增字符,则修改或新增消息盒子中具体位置的消息数据结构。
  • 当用户删除字符,则删除消息盒子中对应位置的字符数据

const MessageBox = require('./MessageBox');

class RichMessage {

constructor(options) {

options = options || {};

this._msgBox = new MessageBox();

}

doInput(inputInfo) {

const { keyCode } = inputInfo;

// 做下判断,防止鼠标或手机键盘移开时触发的input事件(keyCode是undefined)

if (isNaN(keyCode)) return Promise.resolve(inputInfo);

if (keyCode == 8) {

return this.removeOneCharactor(inputInfo);

}

else {

return this.typeOneCharactor(inputInfo);

}

}

}

module.exports = RichMessage;

消息盒子实现

消息盒子负责具体实现两种类型消息的管理:纯文本消息和at消息。其必须实现以下3个api:

  • addCharactor方法。在pos位置新增一个字符,并重构当前虚拟层数据结构
  • deleteCharactor方法。在pos位置删除一个字符,并重构当前虚拟层数据结构
  • print方法。将整个虚拟层各个消息全部渲染,得到一份完整的纯文本

内部实现会有些复杂,更多代码请查看github。例如:

  • 添加字符时会涉及到当 确定pos位置是新增还是修改现有消息pos位置插入at消息,是否要将某个文本消息切分成两半 等情况的处理。
  • 删除字符时,若是删除的at消息则要将整个at消息体删除
  • 每次消息改动后,要像整理'内存'碎片一样,对消息进行合理的合并处理(例如两个相邻的文本消息要合并)

const TextMessage = require('./TextMessage');

const AtMessage = require('./AtMessage');

class MessageBox {

constructor() {

this._msgs = [];

}

// 向pos位置增加普通文本字符

addCharactor(pos, char) {

// 准备要add的消息

const getNewMsg = this._getNewMsg(char);

return getNewMsg.then(newMsg => {

// 找到要放置的位置

let countPos = 0;

let findedMsg = null;

let findedMsgIndex = -1;

for (let i = 0, len = this._msgs.length; i < len; i++) {

const msg = this._msgs[i];

const msgRenderLen = msg.render().length;

if ((pos >= countPos) && (pos <= (countPos + msgRenderLen - 1))) {

// 要操作的位置正好处于该消息结构中

findedMsg = msg;

findedMsgIndex = i;

break;

}

countPos += msgRenderLen;

}

if (findedMsg) {

// 找到了消息msg,则把新msg塞到该msg结构里

this._mergeMsg(findedMsgIndex, newMsg, pos - countPos);

}

else {

// 没找到消息块,那就是放到box末尾新增

this._msgs.push(newMsg);

}

// 消息碎片整理--即把同类型消息合并(例如两个挨着的textmessage则用一个表示即可)

this._defragmentation();

return this.print();

});

}

// 删除start到end间的字符(包含end自身)

deleteCharactor(start, end) {

const findedMsgIndex = [];

const findedMsgPos = [];

let countPosStart = 0;

for (let i = 0, len = this._msgs.length; i < len; i++) {

const msg = this._msgs[i];

const msgRenderLen = msg.render().length;

const countPosEnd = (countPosStart + msgRenderLen) - 1;

if (end >= countPosStart && start <= countPosEnd) {

findedMsgIndex.push(i);

// 找出此msg里的交集坐标

const msgPosStart = Math.max(countPosStart, start);

const msgPosEnd = Math.min(countPosEnd, end);

findedMsgPos.push({

startPos: msgPosStart - countPosStart,

endPos: msgPosEnd - countPosStart

});

}

countPosStart += msgRenderLen;

}

// 对找到的msg依次进行删除的操作 (若是at信息,则整个都删掉;若是普通字符,则只删对应坐标的字符;若删除后整个msg变空了,则在碎片整理时移除)

if (findedMsgIndex && findedMsgIndex.length > 0) {

findedMsgIndex.forEach((findedIndex, index) => {

const msg = this._msgs[findedIndex];

if (msg.type === 'text') {

const deletePos = findedMsgPos[index];

msg.removeChars(deletePos.startPos, deletePos.endPos);

}

if (msg.type === 'at') {

this._msgs.splice(findedIndex, 1);

}

});

}

this._defragmentation();

}

// 输出当前所有 msg 结构转为可视字符串后的完整字符串

print() {

let str = '';

str = this._msgs.reduce((last, cur) => {

return last += cur.render();

}, '');

return str;

}

}

module.exports = MessageBox;

完整代码

完整代码看下github吧:

 https://github.com/cuiyongjia...

以上是 【小程序】无米之炊: 小程序内实现一个具有“@功能at功能”的输入框 的全部内容, 来源链接: utcz.com/a/101257.html

回到顶部