This is my second article on getting started

One, foreword

I recently took over a requirement to implement @at notification in the comment box. This can be said to be my knowledge blind spot, but in fact, many applications have this kind of function, such as: QQ space, Weibo search, enterprise wechat TAPD… But I don’t want to do it at first sight ~ 😭(Product manager PS: Why can others do it you can’t do it?)

Two, clear goals

Ii. Analysis of technical scheme

When we look for our technical solutions, we first have to be clear about what functions we want

You know what you want, where you want to go, and when we break down the requirements, features, and components into small pieces, it saves us time on detours (PS: Don’t ask me how).

The dismantling of current demand

  1. When you hold shift + @, the notification list pops up
  2. The user label at @ is inserted into the current cursor position when selected
  3. The rules for generating @ user tags are: highlight, carry user ID, delete information with one click, and cannot be edited.
  4. The text box should adapt its height to the content
  5. The display on Android, IOS, and Web devices is consistent.
  6. It is expansive and future comments may be inserted into image files etc….

Market popular scheme comparison

Ps: There are many ways to plan, and what suits you and your team is the best practice. There is no perfect solution (PS: just a disobedient product manager 🙂🙂🙂)

  1. Textarea, input (example: Sina Weibo)
    • The process will probably be (listen to keyUp, get the cursor position of the node removed from @…) , but… Believe me if you write by hand, you will not be happy!! So recommend the following library to everyone, as long as a little change can be used ~ ~
    • Tribute.js(recommended, ES6)
    • At.js JQ)
  2. Contenteditable (ex: QQ space, gold)
    • HTML5 new attributes specify whether the element content can be edited, can be used as an editor, due to time reasons have not in-depth experience, interested partners can take a look at the following content
    • contenteditable-MDN
    • Contenteditable implements editor, cursor, and input method processing
    • Achieve @ candidate function based on contenteditable technology
  3. Rich text (example: Enterprise wechat TAPD)
    • Supports text, rich text, images, rich configuration, and powerful apis.
    • Considering the extensibility, the depth of the pit and the richness of the API, WANGEditor rich text was chosen as the final solution.

Since the choice of good direction, then open rush, rush!!

Third, preparation

This function is based on the WangEditor rich text editor, this article wangEditor version 4.3.0

npm i wangeditor --save
Copy the code

Initialize the following project structure

    <div ref="editor"></div>

import E from 'wangeditor'
export default {
    data() {
        return {
            editor: ' '}},mounted() {
        this.initEditor() // Initialize the editor
    methods: {
        initEditor() {
            let editor = new E(this.$refs.editor)
            editor.config.placeholder = 'Write comments ~ manually enter @ to notify others'
            editor.config.menus = [] // Display the menu button
            editor.config.showFullScreen = false // Do not display the full screen button
            editor.config.pasteIgnoreImg = true // If the copied content contains both pictures and text, paste only the text, not the picture.
            editor.config.height = '100'
            editor.config.focus = false  // Cancel automatic focus
            this.editor = editor
            // The destruct editor should be defined in the same place as the destruct editor to increase readability and facilitate later maintenance.
            this.$once('hook:beforeDestroy'.() = > { 
                this.editor = null})}}}</script>
Copy the code

Expand knowledge:

“New E(this.$refs.editor)” uses ref instead of ID.

  • The advantage of using a REF is good reusability and scope. Because the ref only remains in this component, when you operate on this ref, it does not interfere with the other components.
  • If you use id, it has the problem of duplication, which means you can’t reuse an element.
  • Example: If I regenerate a rich text component, the initialization fails because the ID is unique. This is why many people recommend using ids as little as possible. (Don’t ask me why I know this!!) .

2. The configuration of WangEditor only supports fixed height. What if we want to support the minimum height of the text box and the adaptive sliding of the text along with the content to the maximum height xx?

<! Ps: in fact, it is possible to change the style, but lazy to find -->
 editor.config.height = '100'
<style lang="scss" scoped>
::v-deep .w-e-text-container {
    min-height: 100px;
    max-height: 300px;
    height: auto ! important;
    border: 1px solid #dbdbdb ! important;
    border-radius: 4px;
    overflow-y: auto;
Copy the code

Iv. Realization of @ function

When you hold shift + @, a list of notifications pops up

  • $event is used to get the keyCode of the keyboard for listening purposes
  • E.preventdefault blocks the default event for the @ character I typed
  • GetSelection gets the cursor position and gives the inserted label a coordinate.
  • Should be compatible with @ event judgment when Chinese input method is used (for example: When Chinese input method is used to type “hahaha @”, @ event cannot be monitored)
  • The Chinese input method is triggered when the @ is entered separately

How to judge Chinese input?

The compositionStart event is triggered when the user starts typing Chinese using the Chinese input method. The CompositionEnd event is triggered when the text input is complete or cancelled. Using this mechanism we can determine whether the Chinese state

  • Compositionstart Event This event is triggered when the user starts typing Chinese characters using pinyin.
  • Compositionend Event The compositionEnd event is fired when the text input is complete.

export default {
    data() {
        return {
            isChineseInputMethod: false // Whether the Chinese input method is in the state}},methods: {
        / /... code
        // Chinese input triggers
        compositionstart() {
            this.isChineseInputMethod = true

        // Chinese input is closed
        compositionend() {
            this.isChineseInputMethod = false}}}Copy the code

Record our current cursor position

export default {
    data() {
        return {
            position: ' '}},methods: {
        // Initialize the editor
        initEditor() {
            // ... init code
            // Record the cursor while editing the text.
            editor.config.onchange = html= > {
                // Do not record the cursor coordinates at this time
                if (this.isRendering == false) {
                    this.setRecordCoordinates() // Record the coordinates}}},// Click each time to get the update coordinates
        onClickEditor() {
        // KeyDown triggers the event to record the cursor
        onKeyDownInput(e) {
            const isCode = ((e.keyCode === 229 && e.key === The '@') || (e.keyCode === 229 && e.code === 'Digit2') || e.keyCode === 50) && e.shiftKey
            if (!this.isChineseInputMethod && isCode) {
                this.setRecordCoordinates() // Save the coordinates}},// Get the current cursor coordinate
        setRecordCoordinates() {
            try {
                // getSelection() returns a Selection object representing the range of text selected by the user or the current position of the cursor.
                const selection = getSelection()
                this.position = {
                    range: selection.getRangeAt(0),
                    selection: selection
            } catch (error) {
                console.log(error, 'Cursor acquisition failed ~')}}}}</script>
Copy the code

@ function of the listener

Ps: The @ character on the keyboard

  • English code is 50, determine whether hold shift + @ key
  • In The Chinese input method, the punctuation keyCode is the same: 229. It is recommended to use event.code or event.key as the judgment of @.
// Editor KeyDown triggers the event
onKeyDownInput(e) {
    // @ keyboard time judgment
    const isCode = ((e.keyCode === 229 && e.key === The '@') || (e.keyCode === 229 && e.code === 'Digit2') || e.keyCode === 50) && e.shiftKey
    // Check whether the state is not Chinese input method and the @ event is monitored
    if (!this.isChineseInputMethod && isCode) {
        // Record the current text cursor coordinate position
        this.setRecordCoordinates() // Save the coordinates
        // The method to open the popover XXXX is omitted here
        // this.openXXX..}}Copy the code

After listening to @ events, we can talk about how to generate @ tags and how to delete @ tags with one key.

  • The rules for generating @ user tags are: highlight, carry user ID, delete information with one click, and cannot be edited
/ data structures: * * * * the userList: [{name: "bad woman", the uid: 18}, {name: 'good man', uid: 888}] * /

// Popover list-candidate - generate @ content
createSelectElement(name, id, type = 'default') {
    // Get the current text cursor position.
    const { selection, range } = this.position
    // Generate the content to be displayed
    let spanNodeFirst = document.createElement('span') = '#409EFF'
    spanNodeFirst.innerHTML = ` @${name}&nbsp; ` // The text message of @ = id // User ID for subsequent parsing of rich text
    spanNodeFirst.contentEditable = false // When set to false, rich text will treat the successful text as a node.
    // Insert a space before the character; otherwise, the tag cannot be deleted when the newline is continuous with two @ tags
    let spanNode = document.createElement('span');
    spanNode.innerHTML = '  ';

    // Create a new blank document fragment and disassemble the corresponding text content
    let frag = document.createDocumentFragment()

    // If the default deletion is triggered by the keyboard, we did not prevent the generation of at sign, so delete at sign and insert ps: if you are array traversal pass type, otherwise it will delete all the characters in front of you.
    if (type === 'default') {
        const textNode = range.startContainer;
        range.setStart(textNode, range.endOffset - 1);
        range.setEnd(textNode, range.endOffset);
        this.isKeyboard = false // Logic for multiple choices
    // Check whether there is text or coordinates
    if ((this.editor.txt.text() || type === 'default') &&this.position && range) {
    } else {
        // Insert data at the beginning of the special processing if there is no content
        this.editor.txt.append(`<span data-id="${id}" style="color: #409EFF" contentEditable="false">@${name}&nbsp; </span>`)}},Copy the code

Expand knowledge:

  1. GetSelection () represents the range of text selected by the user or the current position of the cursor.
  2. Event.returnValue Compatible IE cancel the default Event


By now our core functionality is complete. The position of the cursor is obtained by getSelection through the @person listener event, through our custom tag insertion. I can do it: at any time @ at any time insert function pull ~

Five, business thinking: Android, IOS, Web display multiple consistent

Using rich text is different on each end, so how do we achieve uniform data uniformity?

  • The current approach is to generate an arraylist of comments by parsing the rich text content.
  • Parsing the array list through each end, generating rich text…
  • Compatible with newline characters…
  • Although it cannot be completely unified, the data can be at least consistent (now I think, Textarea, INPU scheme is better).

// Parse editor rich text to generate an array of text information
fetchGenerateContentsArray() {
    // Get the editor's JSON object
    const data = this.editor.txt.getJSON()
    let contents = []
    // Parse the HTML list JSON to generate a text object.
    const generateArray = nodeList= > {
        if (Array.isArray(nodeList)) {
            nodeList.forEach(item= > {
                // For the newline symbol processing 

tags to ensure the consistency of display

if (item && item.tag) { {tag:p, children: [{tag: 'br'}] const notSpecialLabel = item.tag == 'p' && item.children[0] && item.children[0].tag == 'br' if(! notSpecialLabel && ['p'.'br'].includes(item.tag) && contents.length) { const index = contents.length - 1 // Unpack a newline character in the text contents[index].segment += '\n'}}// If the traversal attribute is data-id has id if (item && item.attrs && item.attrs.find(e= > === 'data-id')) { const id = item.attrs.find(e= > === 'data-id').value const content = item.children && item.children[0] | |' ' contents.push({ segment: content.replaceAll(/(<br>)|(<br\/>)/g.' '), userId: id }) return } // If children are an array, then continue recursively through the next layer if (Array.isArray(item.children)) { generateArray(item.children) return } // If there is no array, it is the contents of the text // Delete the

character from the text
if (item.trim()) { contents.push({segment: item.replaceAll(/(<br>)|(<br\/>)/g.' '), userId: ' '}) } }) } } generateArray(data) return contents } Copy the code

Parse the generated array into rich text

// Generate a newline symbol
createLineBreaks(str, target) {
    let label = []
        str = str.replace(target,' ')
        label.push('<br/>')}return label.join(' ')},// Parse the contents of the array to generate rich text
createCommentHtml(data) {
    // Generate @ tag
    const anchorPoint = (id, value) = > {
        let defaultSpan = `<span>&nbsp; </span><span data-id="${id}" style="color: #409EFF" contentEditable="false">${value}</span>`

if (/(\r\n)|(\n)/g.test(value)) { value.replaceAll(/(\r\n)|(\n)/g.' ') defaultSpan = defaultSpan + this.createLineBreaks(value, '\n')}return defaultSpan } // Put the processed text into an array and generate a string by joining. const createHtml = [] data && data.forEach(item= > { // If there is an ID, use the anchor style if (item.userId) { const json = anchorPoint(item.userId, item.segment) createHtml.push(json) } else { createHtml.push(item.segment.replaceAll(/(\r\n)|(\n)/g.'<br />'))}})// Clear the text data this.resetQuery() // Insert the generated content into edito this.editor.txt.html(`<p>${createHtml.join(' ')}</p>`)},// Clear the text data resetQuery() { this.editor.txt.clear() } Copy the code

Gets the position of the cursor coordinates in the text

Caret-pos gets the caret/cursor position/offset from the textarea, contentedtiable, or iframe body

import { position, offset } from 'caret-pos'
// Get the current cursor position
getPosition () {
  const ele = this.editor.$textElem.elems[0]
  const pos = position(ele)
  const off = offset(ele)
  const parentW = ele.offsetWidth
  // This is the popover list
  const childEle = document.getElementsByClassName("userPopupList")
  const childW = childEle.offsetWidth
  // The frame offset exceeds the parent element's width and height
  if (parentW - pos.left < childW) {
    this.left = off.left - childW
  } else {
    this.left = off.left
  } = + 20
Copy the code
<div class="userPopupList" :style="{left: left + 'px', top: top + 'px'}">. you @ popup list</div>
Copy the code


Don’t give up the quest to find out what the problem is. Don’t look down on those seemingly “useless” knowledge, if this just once in front of you and you did not reject it, how much lower your learning cost at this time?

This feature was just squeezed out during development, and a lot of things were not written well enough or perfect enough, so hopefully this article will help you save a little time in your development. We welcome your feedback and hope we can make progress together