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:
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:
Not only the input method, most editors also implement similar functions, such as vscode
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
- Detect the input content, if it is the above punctuation mark, go to the next step
- According to the input punctuation, automatically complete the corresponding second half
- 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
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
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
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
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
It should be noted that under the windows
Chinese input method, the input will be triggered twice, as follows
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
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)
})
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
Is it very easy? However, there are still some problems, such as Chinese quotation marks, which are a bit strange
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
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 selectionStart
、 selectionEnd
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.
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
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
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.
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.
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
print child nodes
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)
There is only one print child node
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 herethis
points to the problem, usecall
to point to the currently edited input boxev.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.
7. Summary and Explanation
Unexpectedly, a small function actually contains so many uncommon API
, let's summarize
- Automatically pairing punctuation marks can greatly improve the typing experience
-
keydown
The event cannot distinguish between Chinese and English input methods, and cannot distinguish multiple punctuation marks corresponding to the same key -
input
events can be detected byev.data
the currently input character - Entering Chinese punctuation under the windows operating system will trigger twice
input
, the reason is that, like ordinary Chinese, the candidate box is triggered - Under the windows operating system, it can be realized through the
compositionend
event, which effectively avoids the situation of two triggers. - The native form input box and
contenteditable
editable elements have completely different cursor processing methods and need to be processed separately - 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.
- 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
- In the native input box, you can use the
setRangeText
method to manually insert content - In the rich text input box, you can use the
insertNode
method to manually insert the content, the text needs to be created withcreateTextNode
- In the native input box, you can use the
setSelectionRange
method to manually set the position of the selection - In the rich text input box, you can use the
setStart
andsetEnd
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。