Cooking without rice: small procedures to achieve a “@ function at function” input box


What is at function

The so-called at function is to allow users to input “@” character after inputting person’s name and other information in the chat box, and then call up a person selection control to facilitate users to input person’s name quickly.

For example: microblog input box, QQ space input box. We can enter the “@” character in an input box, and then call up a floating layer or full screen human selection control (usually a floating layer on the desktop and a full screen control on the mobile).

This article is about the process of realizing a @ function in a small program. Another oneImplementation of @ person selection function based on contenteditable Technology
The implementation of web version is described in.

Demand difference of at function

When we receive the demand of at function, we need to first determine a question: is there a phenomenon of duplicate names in the names of people we at. That is: when the two names “@ ABC” and “@ ABC” appear in the input box at the same time, whether the two people necessarily represent the same person.

For example, in the scenario of sina Weibo, the Weibo account generated by Sina Weibo must be unique, so the technical implementation can be simplified as follows:You just need to render the person selected by the user from the person selection control to the input box

In demand scenarios such as QQ space, a user’s nickname that we select can actually be renamed. At this time, our technical solution must consider:How to map a person's name in an input box with his corresponding account information one by one. Only in this way can we clearly restore who the two “@ ABC” are when we save the user’s input message to the background.

Here is the input box of QQ space. I can input two people with the same name. They can send at messages to two different friends of mine

Cooking without rice: small procedures to achieve a

In this paper, I’m talking about QQ space, which can be renamed. Therefore, our technical solution needs to consider how to map the at person’s name to the account information record.

Small program: it’s hard to make a meal without rice

On the web side, we usually use div with contentable, and then with the cursor control API of range and selection to achieve the at function similar to chat box.

Among them:

  • The contenteditable API allows us to insert HTML tags into the editor, so that we can “plug” the account information into the tags, thus restoring the account information from the tags when submitting in the background
  • The range API provides the ability to control the cursor selection and set the selection content, which allows us to delete the at character after the user selects the name of the control, and render the newly selected name label to the input box.

howeverThe input box and textarea of the applet are not as powerful as the web API (such as range and selection), and can’t use contenteditable to realize rich text in the applet. The applet has only one bindingput event:

Cooking without rice: small procedures to achieve a

The event returns three parameters:

  • Value: the latest value in the current input box.
  • Cursor: current cursor position
  • Keycode: the keyboard key of the current input event


We’re going to be in a place like thissimpleUsing the onlyvalue\\cursor\\keyCodeThree parameters realize “at detection”, “name rendering”, “deletion detection”, “duplicate name support (i.e. account information restoration)”. The biggest difficulty is how to record account information to support duplicate names.

There are three possible solutions

  1. Every time the at character is entered and a person’s name is selected, we record the account information in another data structure. For example, maintaining apersons: []Array. But we need to change the content of the input box before the userAdd, delete, change and searchAt the same time, it is very difficult to add, delete, modify and query our people structure synchronously.
  2. We wonder if we can put the account information into the input box when the at name is filled in the input box. It’s like contenteditable. Therefore, we can try to use some invisible characters to express the identification of an account information. But this kind of scheme is very complicated. For example, when we delete a person’s name, it’s hard to say whether we can delete invisible characters synchronously and whether there will be cursor problems.
  3. A virtual layer idea is adopted. When the user enters any character, we intercept the user’s input. After intercepting the input, we first update our internal database according to the requirementsVirtual layerIn the virtual layer, we save the user account information data according to a certain structure, then render it into text and fill it in the input box.

Finally, I choose the third scheme, as shown in the figure
Cooking without rice: small procedures to achieve a

Call method

The specific computing logic in the virtual layer is encapsulated in the richmessage class. In the applet component, first bind the input event to the input box

< input placeholder = "please input" bindinginput = "eventinput" / >

In the component script:

    data: {
        inputContent: '',
    attached() {
       this.myCommentRichMessage = new RichMessage();
    methods: {
        eventInput(e) {
            const res =  this.myCommentRichMessage.doInput(event.detail);
            //After input, re render the input content
            res.then(str => {
                if (typeof str === 'string') {
                        inputContent: str

When you want to submit data to the background, you can call the toproto method to convert the message into a specific data structure

const pbdata = myCommentRichMessage.transformToProto()


Richmessage class

According to the input cursor and value, it calculates where the character is added or deleted. Msgbox is responsible for maintaining the message box of virtual layer.

  • When the user adds new characters, modify or add the message data structure of the specific position in the message box.
  • When the user delete the character, the character data at the corresponding position in the message box will be deleted
const MessageBox = require('./MessageBox');

class RichMessage {
    constructor(options) {
        options = options || {};
        this._msgBox = new MessageBox();

    doInput(inputInfo) {
        const { keyCode } = inputInfo;
        //Make a judgment to prevent the input event triggered when the mouse or mobile phone keyboard moves away (keycode is undefined)
        if (isNaN(keyCode)) return Promise.resolve(inputInfo);
        if (keyCode == 8) {
            return this.removeOneCharactor(inputInfo);
        else {
            return this.typeOneCharactor(inputInfo);

module.exports = RichMessage;

Message box implementation

Message box is responsible for two types of message management: plain text message and at message. It must implement the following three APIs:

  • Addcharactor method. Add a new character in the POS position and reconstruct the current virtual layer data structure
  • Delete charactor method. Delete a character in the POS position and reconstruct the current virtual layer data structure
  • Print method. All messages in the virtual layer are rendered to get a complete text

The internal implementation will be a little complicated, please check GitHub for more code. For example:

  • When you add a character, it involvesDetermine whether the POS location adds or modifies an existing messageInsert at message in POS position. Do you want to cut a text message into two partsAnd so on.
  • When deleting characters, if the at message is deleted, the whole at message body should be deleted
  • After each change of message, it is necessary to merge the messages reasonably, just as it is necessary to defragment the memory (for example, two adjacent text messages should be merged)
const TextMessage = require('./TextMessage');
const AtMessage = require('./AtMessage');

class MessageBox {
    constructor() {
        this._msgs = [];

    //Add plain text character to POS position
    addCharactor(pos, char) {
        //Prepare the message to add
        const getNewMsg = this._getNewMsg(char);
        return getNewMsg.then(newMsg => {
            //Find the location you want to place
            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))) {
                    //The location to operate is exactly in the message structure
                    findedMsg = msg;
                    findedMsgIndex = i;
                countPos += msgRenderLen;

            if (findedMsg) {
                //When the message MSG is found, the new MSG is inserted into the MSG structure
                this._mergeMsg(findedMsgIndex, newMsg, pos - countPos);
            else {
                //If the message block is not found, it will be added at the end of the box
            //Message defragmentation -- that is, merging messages of the same type (for example, two adjacent textmessages can be represented by one)
            return this.print();

    //Delete characters from start to end (including end itself)
    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) {
                //Find the intersection coordinates in this msg
                const msgPosStart = Math.max(countPosStart, start);
                const msgPosEnd = Math.min(countPosEnd, end);
                    startPos: msgPosStart - countPosStart,
                    endPos: msgPosEnd - countPosStart
            countPosStart += msgRenderLen;
        //Delete the found MSG in turn (if it is at information, the whole MSG will be deleted; If it is an ordinary character, only the corresponding coordinate character will be deleted; If the entire MSG becomes empty after deletion, it will be removed during defragmentation.)
        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);

    //Output all the current MSG structure into a visual string after the complete string
    print() {
        let str = '';
        str = this._msgs.reduce((last, cur) => {
            return last += cur.render();
        }, '');
        return str;


module.exports = MessageBox;

Complete code

Take a look at GitHub for complete code…

Recommended Today

What is “hybrid cloud”?

In this paper, we define the concept of “hybrid cloud”, explain four different cloud deployment models of hybrid cloud, and deeply analyze the industrial trend of hybrid cloud through a series of data and charts. 01 introduction Hybrid cloud is a computing environment that integrates multiple platforms and data centers. Generally speaking, hybrid cloud is […]