社区精选 | 提升web输入体验!JS 如何自动配对标点符号?
今天小编为大家带来的是社区作者 XboxYan 的文章,在这篇文章中他介绍了JS 自动配对标点符号的方法。
搬来小板凳,让我们一起学习吧~



一、实现原理
检测输入的内容,如果是以上标点符号就下一步
根据输入的标点,自动补全与之对应的后半部分
将光标移到两个标点之间
二、检测输入的内容
editor.addEventListener("keydown", (ev) => {
console.log(ev.key, ev.code)
})




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



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

const input = function(ev){
if (ev.inputType === "insertText" || ev.type === 'compositionend') {
console.log(ev)
}
}
editor.addEventListener('compositionend', input)
editor.addEventListener('input', input)
三、两种输入框
<input type="text">
<textarea></textarea>
<div contenteditable="true">yux阅文前端</div>
div{
-webkit-user-modify: read-write;
}
四、表单输入框
<textarea></textarea>
const quotes = {
"'": "'",
'"': '"',
"(": ")",
"(": ")",
"【": "】",
"[": "]",
"《": "》",
"「": "」",
"『": "』",
"{": "}",
"“": "”",
"‘": "’",
};
地址:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setRangeText
const input = function(ev){
const quote = quotes[ev.data];
if (quote && (ev.inputType === "insertText" || ev.type === 'compositionend')) {
this.setRangeText(quote)
}
}




const quotes = {
// 添加中文下引号映射
"”": "“",
"’": "‘",
};
const quotes_reverse = ["”", "’"];
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)
}
}
}

完整代码可以访问:textarea-auto-quotes(codepen.io)
五、富文本输入框
<div id="editor" contenteditable="true">yux阅文前端</div>
地址:
https://developer.mozilla.org/en-US/docs/Web/API/Range/insertNode
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);
}
}

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之前
}
}

地址:
https://developer.mozilla.org/en-US/docs/Web/API/Range/setStart
Range.setEnd() - Web APIs | MDN (mozilla.org)
地址:
https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
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);
}
}
}



地址:
https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
const input = function(ev){
const quote = quotes[ev.data];
if (quote && ev.inputType === "insertText") {
// 规范化子节点
range.commonAncestorContainer.normalize();
}
}


六、整合成公共方法
document.addEventListener('compositionend', commonInput)
document.addEventListener('input', commonInput)
function commonInput(ev) {
const tagName = ev.target.tagName;
if (tagName === 'TEXTAREA' || tagName === 'INPUT') {
inputTextArea.call(ev.target, ev)
} else {
input.call(ev.target, ev)
}
}
(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();
}
}
})()

七、总结和说明
自动配对标点符号可以很好的提升输入体验
keydown事件无法区分中英文输入法,也无法区分同一按键对应的多个标点符号
input事件可以通过ev.data检测当前输入的字符
windows 操作系统下输入中文标点符号会触发两次input,原因是和普通中文一样,触发了候选框
windows 操作系统下可以通过compositionend事件来实现,有效避免了两次触发的情况
原生表单输入框和contenteditable可编辑元素的光标处理方式完全不一样,需要分开处理
中文标点有点特殊,中文的上引号和下引号在同一按键上,输入的时候是依次出现的,无法修改
如果是上引号,就在光标处插入下引号;如果是下引号,就将光标往前移动一位,然后补全上引号
在原生输入框中,可以用setRangeText方法来手动插入内容
在富文本输入框中,可以用insertNode方法来手动插入内容,文本需要用createTextNode创建
在原生输入框中,可以用到setSelectionRange方法手动设置选区的位置
在富文本输入框中,可以用setStart和setEnd方法手动设置选区的位置

关注公众号:拾黑(shiheibook)了解更多
赞助链接:
关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

随时掌握互联网精彩