27
头图
Welcome to the front-end detective of WeChat public account
Reminder: Before reading this article, you can review this article: "Selection" and "Cursor" in the Web , there are many native APIs that you may not know much about, the following is the practical application of this content

In writing editing, there are many punctuation marks that need to appear in pairs, such as quotation marks, parentheses, book titles, etc., as follows:

image-20220719173514938

In order to facilitate input, some input methods come with an automatic punctuation pairing function. What does that mean? For example, enter a front bracket, automatically complete the back bracket, and then the cursor is in the middle. The following is a demonstration of the Xiaomi mobile phone's own input method:

Kapture 2022-07-19 at 16.40.40.gif

Not only the input method, most editors also implement similar functions, such as vscode

Kapture 2022-07-19 at 18.34.08.gif

So, with such a useful feature, how can the input box in the web also support it?

First, the realization principle

The principle is actually very simple and can be divided into the following steps

  1. Detect the input content, if it is the above punctuation mark, go to the next step
  2. According to the input punctuation, automatically complete the corresponding second half
  3. Move the cursor between two punctuation marks

Is it very easy to understand? However, the details are far more than these, involving a lot of relatively uncommon native methods, let's take a look at how to achieve it

2. Detect the input content

What is detected here is that when the keyboard is pressed, it is necessary to know what character is currently pressed, so at first I thought of using the keydown method

 editor.addEventListener("keydown", (ev) => {
    console.log(ev.key, ev.code)
})

In the keydown method, the attributes related to the key value are ev.key and ev.code , as follows

image-20220719191656032

It seems that there is no problem, you can use ev.key to distinguish what character is input. In fact, there are still many problems, such as the inability to distinguish between Chinese and English punctuation input .

For example: enter square brackets in Chinese and English respectively

image-20220720112520980

As you can see, both ev.key and ev.code are exactly the same!

There are even more outrageous, under the Chinese input method, some punctuation appears in sequence , such as Chinese single and double quotation marks, press once is the upper quotation mark , press again is the lower quotation mark , there are also semi-brackets, press once is , press again is and so on, such input is even more impossible to judge

image-20220722194904406

Why is this so? Because these punctuations are all on a key, keydown events reflect keyboard-related properties, as follows

4 punctuation marks are densely packed on one button

image-20220722194218651

So, we need to use another way to detect the input content.

Here, you can use input event to monitor, ev.data represents the current input character .

 editor.addEventListener("input", (ev) => {
    console.log(ev.data)
})

Note that here are the characters, that is, the text that is actually entered into the page, as follows

image-20220722200436085

It should be noted that under the windows Chinese input method, the input will be triggered twice, as follows

image-20220722201555426

This is because under the windows Chinese input method, the punctuation input is the same as the ordinary pinyin input, and there is a process of candidate words, like this

image-20220722201936188

So solving this problem is also very simple, just use the compositionend event, which means that after the candidate is over

 editor.addEventListener("compositionend", (ev) => {
    console.log(ev.data)
})

image-20220722202634674

Therefore, the complete writing for compatibility windows and Mac OS should be like this

 const input = function(ev){
  if (ev.inputType === "insertText" || ev.type === 'compositionend') {
    console.log(ev)
  }
}

editor.addEventListener('compositionend', input)
editor.addEventListener('input', input)

Since we only detect punctuation, there is no need to worry about repeated triggering.

Three, two input boxes

The next step is to implement the specific matching. Before that, let's figure out the two types of input boxes.

One is the native default form input box input and textarea

 <input type="text">
<textarea></textarea>

Another is to manually add attributes to elements contenteditable="true" , or CSS attributes -webkit-user-modify

 <div contenteditable="true">yux阅文前端</div>

or

 div{
    -webkit-user-modify: read-write;
}

Why separate the two? Because the two types of cursors are handled completely differently

For more details, please refer to this previous article: "Selection" and "Cursor" in the Web

Fourth, the form input box

First look at the form input box, here is textarea as an example

 <textarea></textarea>

First, we need to list the punctuation marks that need to be matched, including Chinese and English

 const quotes = {
  "'": "'",
  '"': '"',
  "(": ")",
  "(": ")",
  "【": "】",
  "[": "]",
  "《": "》",
  "「": "」",
  "『": "』",
  "{": "}",
  "“": "”",
  "‘": "’",
};

Next, according to the method of detecting the input content mentioned above to automatically complete the punctuation, in the native input box, you can use the setRangeText method to manually insert the content

HTMLInputElement.setRangeText() - Web APIs | MDN (mozilla.org)
 const input = function(ev){
  const quote = quotes[ev.data];
  if (quote && (ev.inputType === "insertText" || ev.type === 'compositionend')) {
    this.setRangeText(quote)
  }
}

The effect is as follows

Kapture 2022-07-24 at 15.04.16

Is it very easy? However, there are still some problems, such as Chinese quotation marks, which are a bit strange

Kapture 2022-07-24 at 15.07.20

Why is this so? The reason is that the upper quotation marks and lower quotation marks in Chinese appear in turn , that is to say, the first time you press it is the upper quotation mark, and the second time you press it is the lower quotation mark, which is completely determined by the system input method and cannot be modified (it does not exist in English). The problem, because upper and lower quotes are the same)

中文的上引号和下引号依次出现

So, how to solve this problem? The way I think of is this, the upper and lower quotes are handled separately. If it is an upper quotation mark, process it according to the previous idea; if it is a lower quotation mark, move the cursor forward one place, and then complete the upper quotation mark, as shown below

image-20220724151234018

The specific implementation is to add quotation marks to the listed punctuation marks, and add a logo. Identifying these symbols requires special treatment

 const quotes = {
  // 添加中文下引号映射
  "”": "“",
  "’": "‘",
};
const quotes_reverse = ["”", "’"];

Then if it is a lower quotation mark, you need to move the cursor one place to the left, you can use the setSelectionRange method, this method can manually set the position of the selection, the current cursor position can be passed through two attributes selectionStartselectionEnd to get

HTMLInputElement.setSelectionRange() - Web APIs | MDN (mozilla.org)

After completing the punctuation, you need to move the cursor between the two. The specific implementation is as follows

 const input = function(ev){
  const quote = quotes[ev.data];
  if (quote && (ev.inputType === "insertText" || ev.type === 'compositionend')) {
    const reverse = quotes_reverse.includes(ev.data);
    if (reverse) {
      this.setSelectionRange(this.selectionStart - 1, this.selectionEnd - 1)
    }
    this.setRangeText(quote)
    if (reverse) {
      this.setSelectionRange(this.selectionStart + 1, this.selectionEnd + 1)
    }
  }
}

This perfectly supports Chinese punctuation.

Kapture 2022-07-24 at 15.26.36

The full code can be accessed at: textarea-auto-quotes(runjs.work)

Five, rich text input box

Let's look at a more general input box, the rich text editor

 <div id="editor" contenteditable="true">yux阅文前端</div>

The idea is actually the same as the previous plain text, but the cursor is handled differently.

First, to add content to the cursor, it needs to be processed under the range object, and a method of insertNode is used. Note that this method needs to pass in a node node, and pure characters need to createTextNode create

Range.insertNode() - Web APIs | MDN (mozilla.org)

The specific implementation is as follows

 const selection = document.getSelection();
const input = function(ev){
  const quote = quotes[ev.data];
  if (quote && (ev.inputType === "insertText" || ev.type === 'compositionend')) {
    const newQuote = document.createTextNode(quote);
    const range = selection.getRangeAt(0);
    range.insertNode(newQuote);
  }
}

The effect is as follows

Kapture 2022-07-24 at 15.48.09

As you can see, the inserted punctuation is automatically selected, which is the default behavior. So, how to make the cursor positioned between the two? Here you can use the setEndBefore method to set the position of the end point of the selection

Range.setEndBefore() - Web APIs | MDN (mozilla.org)
 const selection = document.getSelection();
const input = function(ev){
  const quote = quotes[ev.data];
  if (quote && (ev.inputType === "insertText" || ev.type === 'compositionend')) {
    const newQuote = document.createTextNode(quote);
    const range = selection.getRangeAt(0);
    range.insertNode(newQuote);
    range.setEndBefore(newQuote); // 将光标移动到newQuote之前
  }
}

Before means "before", so the end point of the selection is before the newly generated character, and the cursor naturally moves between the two

image-20220724160114075

Then to deal with the problem of Chinese quotation marks, it also needs special processing. Move the cursor one position to the left, and you can use the setStart and setEnd methods to indicate the starting point of the selection.

Range.setStart() - Web APIs | MDN (mozilla.org)

Range.setEnd() - Web APIs | MDN (mozilla.org)

The specific implementation is as follows

 const input = function(ev){
  const quote = quotes[ev.data];
  if (quote && ev.inputType === "insertText") {
    const newQuote = document.createTextNode(quote);
    const range = selection.getRangeAt(0);
    const reverse = quotes_reverse.includes(ev.data);
    if (reverse) {
      const { startContainer, startOffset, endContainer, endOffset } = range;
      range.setStart(startContainer, startOffset - 1);
      range.setEnd(endContainer, endOffset - 1);
    }
    range.insertNode(newQuote);
    if (reverse) {
      range.setStartAfter(newQuote);
    } else {
      range.setEndBefore(newQuote);
    }
  }
}

In this way, rich text also supports automatic matching of Chinese and English punctuation.

Kapture 2022-07-24 at 16.09.28

There is also a small detail that can be optimized. You can see in the developer tools that the newly added punctuation is independent one by one #text , resulting in dividing the entire text into many small fragments, as follows

image-20220724161136838

print child nodes

image-20220724161259521

It's all plain text here, is there a way to merge it? Of course there are, the method used is normalize , which can "normalize" the child nodes

Node.normalize() - Web APIs | MDN (mozilla.org)
 const input = function(ev){
  const quote = quotes[ev.data];
  if (quote && ev.inputType === "insertText") {
    // 规范化子节点
    range.commonAncestorContainer.normalize();
  }
}

Now look at the effect (note the characters in the console)

Kapture 2022-07-24 at 16.17.57

There is only one print child node

image-20220724161949004

The complete code can be viewed: contenteditable-auto-quotes(runjs.work)

RunJS , front-end code creation and sharing online.

6. Integration into public methods

The above case is implemented for a specific element. If there are multiple input boxes, it may be a bit troublesome, so it is necessary to integrate it and implement a more general method.

First of all, we can put the event listener on document instead of a specific input box

 document.addEventListener('compositionend', commonInput)
document.addEventListener('input',  commonInput)

Here one is used commonInput to handle the case of form input boxes and rich text

 function commonInput(ev) {
  const tagName = ev.target.tagName;
  if (tagName === 'TEXTAREA' || tagName === 'INPUT') {
    inputTextArea.call(ev.target, ev)
  } else {
    input.call(ev.target, ev)
  }
}
Note that here this points to the problem, use call to point to the currently edited input box ev.target

Then inputTextArea and input represent the specific processing of the previous form input and rich text, respectively

The following is the complete code, you can paste it directly into any console to try it out, it is equivalent to a polyfill

 (function(){
  /*
        * @desc: 自动匹配标点符号
        * @email: yanwenbin1991@live.com
        * @author: XboxYan
        */
  const quotes = {
    "'": "'",
    '"': '"',
    "(": ")",
    "(": ")",
    "【": "】",
    "[": "]",
    "《": "》",
    "「": "」",
    "『": "』",
    "{": "}",
    "“": "”",
    "‘": "’",
    "”": "“",
    "’": "‘",
  };

  const quotes_reverse = ["”", "’"];
  const selection = document.getSelection();
  function commonInput(ev) {
    const tagName = ev.target.tagName;
    if (tagName === 'TEXTAREA' || tagName === 'INPUT') {
      inputTextArea.call(ev.target, ev)
    } else {
      input.call(ev.target, ev)
    }
  }
  document.addEventListener('compositionend', commonInput)
  document.addEventListener('input',  commonInput)

  function inputTextArea(ev){
    const quote = quotes[ev.data];
    if (quote && (ev.inputType === "insertText" || ev.type === 'compositionend')) {
      const reverse = quotes_reverse.includes(ev.data);
      if (reverse) {
        this.setSelectionRange(this.selectionStart - 1, this.selectionEnd - 1)
      }
      this.setRangeText(quote)
      if (reverse) {
        this.setSelectionRange(this.selectionStart + 1, this.selectionEnd + 1)
      }
    }
  }
  function input(ev){
    const quote = quotes[ev.data];
    if (quote && ev.inputType === "insertText") {
      const newQuote = document.createTextNode(quote);
      const range = selection.getRangeAt(0);
      const reverse = quotes_reverse.includes(ev.data);
      if (reverse) {
        const { startContainer, startOffset, endContainer, endOffset } = range;
        range.setStart(startContainer, startOffset - 1);
        range.setEnd(endContainer, endOffset - 1);
      }
      range.insertNode(newQuote);
      if (reverse) {
        range.setStartAfter(newQuote);
      } else {
        range.setEndBefore(newQuote);
      }
      range.commonAncestorContainer.normalize();
    }
  }
})()

In practice, the following is a comment input box of a website. After injecting the above code into the console, it can also perfectly support automatic matching of punctuation.

Kapture 2022-07-26 at 19.53.42

7. Summary and Explanation

Unexpectedly, a small function actually contains so many uncommon API , let's summarize

  1. Automatically pairing punctuation marks can greatly improve the typing experience
  2. keydown The event cannot distinguish between Chinese and English input methods, and cannot distinguish multiple punctuation marks corresponding to the same key
  3. input events can be detected by ev.data the currently input character
  4. Entering Chinese punctuation under the windows operating system will trigger twice input , the reason is that, like ordinary Chinese, the candidate box is triggered
  5. Under the windows operating system, it can be realized through the compositionend event, which effectively avoids the situation of two triggers.
  6. The native form input box and contenteditable editable elements have completely different cursor processing methods and need to be processed separately
  7. The Chinese punctuation is a bit special. The upper and lower quotation marks in Chinese are on the same button. When inputting, they appear in sequence and cannot be modified.
  8. If it is an upper quotation mark, insert a lower quotation mark at the cursor; if it is a lower quotation mark, move the cursor forward one place, and then complete the upper quotation mark
  9. In the native input box, you can use the setRangeText method to manually insert content
  10. In the rich text input box, you can use the insertNode method to manually insert the content, the text needs to be created with createTextNode
  11. In the native input box, you can use the setSelectionRange method to manually set the position of the selection
  12. In the rich text input box, you can use the setStart and setEnd methods to manually set the position of the selection

The overall implementation is not much in terms of the amount of code, mainly DOM related to API , which seems a little strange. Why does it feel unfamiliar? Of course, I haven't used it at ordinary times. This is closely related to the current environment. vue and react Although these frameworks provide developers with a lot of convenience, they also make the , farther and farther away from DOM , which leads to many native API never seen at all, is this not a loss?

Finally, if you think it's good and helpful to you, please like, bookmark, and forward ❤❤❤

Welcome to the front-end detective of WeChat public account

XboxYan
18.1k 声望14.1k 粉丝