新聞中心
在寫作編輯中,有很多需要成對出現(xiàn)的標(biāo)點(diǎn)符號,比如引號、括號、書名號等,如下所示:

在遵義等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都做網(wǎng)站、成都網(wǎng)站設(shè)計(jì) 網(wǎng)站設(shè)計(jì)制作按需策劃設(shè)計(jì),公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),成都品牌網(wǎng)站建設(shè),營銷型網(wǎng)站建設(shè),外貿(mào)網(wǎng)站制作,遵義網(wǎng)站建設(shè)費(fèi)用合理。
為了方便輸入,某些輸入法自帶了標(biāo)點(diǎn)自動(dòng)配對功能。什么意思呢?比如輸入一個(gè)前括號,自動(dòng)補(bǔ)全后括號,然后光標(biāo)位于中間。下面是小米手機(jī)自帶輸入法的演示:
標(biāo)點(diǎn)自動(dòng)配對
不僅僅是輸入法,大部分編輯器也實(shí)現(xiàn)了類似的功能,比如 vscode:
vscode標(biāo)點(diǎn)自動(dòng)配對
那么,這么好用的特性,如何讓 web 中的輸入框也能支持呢?
一、實(shí)現(xiàn)原理
原理其實(shí)非常簡單,可以分為以下幾個(gè)步驟:
- 檢測輸入的內(nèi)容,如果是以上標(biāo)點(diǎn)符號就下一步
- 根據(jù)輸入的標(biāo)點(diǎn),自動(dòng)補(bǔ)全與之對應(yīng)的后半部分
- 將光標(biāo)移到兩個(gè)標(biāo)點(diǎn)之間
是不是非常好理解呢?但是,里面的細(xì)節(jié)遠(yuǎn)不止這些,涉及到非常多的比較生僻的原生方法,一起看看如何實(shí)現(xiàn)的吧
二、檢測輸入的內(nèi)容
這里檢測的是在鍵盤按下的時(shí)候,需要知道當(dāng)前按下的是什么字符,所以一開始我想到了用keydown方法
editor.addEventListener("keydown", (ev) => {
console.log(ev.key, ev.code)
})在keydown方法中,與鍵值相關(guān)的屬性有ev.key和ev.code,如下:
看似好像沒啥問題,可以通過ev.key區(qū)分具體輸入的是什么字符。其實(shí)還有很多問題,比如無法區(qū)分中英文標(biāo)點(diǎn)輸入。
舉個(gè)例子:在中英文下分別輸入方括號。
可以看到,兩者的ev.key和ev.code是完全一樣的!
還有更離譜的,在中文輸入法下,某些標(biāo)點(diǎn)是依次出現(xiàn)的,比如中文的單雙引號,按一次是上引號“,再按一次是下引號”,還有半括號,按一次是「,再按一次是『等等,像這類輸入就更加沒法判斷了。
為啥會(huì)這樣呢?因?yàn)檫@些標(biāo)點(diǎn)都在一個(gè)按鍵上,keydown事件反應(yīng)的是和鍵盤相關(guān)的屬性,如下:
- 一個(gè)按鍵上密密麻麻的塞下了4個(gè)標(biāo)點(diǎn)符號。
所以,我們需要用別的方式來檢測輸入的內(nèi)容。
在這里,可以用input事件來監(jiān)聽,ev.data表示當(dāng)前輸入的字符。
editor.addEventListener("input", (ev) => {
console.log(ev.data)
})注意,這里是字符,也就是真正輸入到頁面的文字,如下:
需要注意的是,在windows中文輸入法下,input 會(huì)觸發(fā)兩次,如下:
這是由于在 windows 中文輸入法下,標(biāo)點(diǎn)輸入也和普通拼音輸入一樣,有候選詞的過程,就像這樣:
所以解決這個(gè)問題也很簡單,用compositionend事件就可以了,表示候選結(jié)束之后
editor.addEventListener("compositionend", (ev) => {
console.log(ev.data)
})因此,兼容 windows 和 Mac OS的完整寫法應(yīng)該是這樣
const input = function(ev){
if (ev.inputType === "insertText" || ev.type === 'compositionend') {
console.log(ev)
}
}
editor.addEventListener('compositionend', input)
editor.addEventListener('input', input)因?yàn)槲覀冎粰z測標(biāo)點(diǎn)符號,所以也無需擔(dān)心重復(fù)觸發(fā)的問題。
三、兩種輸入框
接下來就是具體的匹配實(shí)現(xiàn)了,在此之前先搞清楚兩種類型的輸入框。
一種是原生默認(rèn)的表單輸入框input和textarea。
還有一種是手動(dòng)給元素添加屬性contenteditable="true",或者 CSS 屬性 -webkit-user-modify。
yux閱文前端
或者
div{
-webkit-user-modify: read-write;
}為啥要分這兩種呢?因?yàn)檫@兩種類型的光標(biāo)處理方式完全不一樣。
四、表單輸入框
先來看表單輸入框,這里以textarea為例:
首先我們需要羅列一下需要匹配的標(biāo)點(diǎn)符號,包含中英文。
const quotes = {
"'": "'",
'"': '"',
"(": ")",
"(": ")",
"【": "】",
"[": "]",
"《": "》",
"「": "」",
"『": "』",
"{": "}",
"“": "”",
"‘": "’",
};接下來,根據(jù)前面提到的檢測輸入內(nèi)容的方法來自動(dòng)補(bǔ)全標(biāo)點(diǎn),在原生輸入框中,可以用setRangeText方法來手動(dòng)插入內(nèi)容。
- HTMLInputElement.setRangeText() - Web APIs | MDN (mozilla.org)[3]
const input = function(ev){
const quote = quotes[ev.data];
if (quote && (ev.inputType === "insertText" || ev.type === 'compositionend')) {
this.setRangeText(quote)
}
}效果如下:
是不是非常容易呢?不過還有些問題,比如中文的引號,就有些奇怪。
為啥會(huì)這樣呢?原因在于,中文的上引號和下引號是依次出現(xiàn)的,也就是說第一次按是上引號,第二次按就是下引號了,完全是由系統(tǒng)輸入法決定的,無法修改(英文不存在這個(gè)問題,因?yàn)樯弦柡拖乱柺窍嗤模?/p>
中文的上引號和下引號依次出現(xiàn)。
那么,如何解決這個(gè)問題呢?我想到的方式是這樣的,對上引號和下引號分別進(jìn)行處理。如果是上引號,就按照前面的思路進(jìn)行處理;如果是下引號,就將光標(biāo)往前移動(dòng)一位,然后補(bǔ)全上引號,示意如下
具體實(shí)現(xiàn)就是,在羅列的標(biāo)點(diǎn)符號添加下引號,并且添加標(biāo)識(shí),標(biāo)識(shí)這些符號需要特殊處理。
const quotes = {
// 添加中文下引號映射
"”": "“",
"’": "‘",
};
const quotes_reverse = ["”", "’"];然后如果是下引號,需要將光標(biāo)往左移動(dòng)一位,可以用到setSelectionRange方法,這個(gè)方法可以手動(dòng)設(shè)置選區(qū)的位置,當(dāng)前光標(biāo)的位置可以通過兩個(gè)屬性selectionStart、selectionEnd來獲取。
- HTMLInputElement.setSelectionRange() - Web APIs | MDN (mozilla.org)[4]
補(bǔ)全標(biāo)點(diǎn)之后還需要將光標(biāo)移動(dòng)到兩者之間,具體實(shí)現(xiàn)如下:
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)
}
}
}這樣就完美支持中文標(biāo)點(diǎn)符號了。
完整代碼可以訪問:textarea-auto-quotes(codepen.io)[5]或者textarea-auto-quotes (juejin.cn)[6]
五、富文本輸入框
下面來看一種更普遍的輸入框,富文本編輯器。
yux閱文前端
思路其實(shí)和前面純文本一致,只是光標(biāo)的處理方式不同。
首先,向光標(biāo)處加入內(nèi)容,需要在range對象下處理,用到一個(gè)insertNode的方法,注意,這個(gè)方法需要傳入一個(gè) node 節(jié)點(diǎn),純字符需要用createTextNode創(chuàng)建。
- Range.insertNode() - Web APIs | MDN (mozilla.org)[7]
具體實(shí)現(xiàn)如下:
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);
}
}
效果如下:
可以看到,插入的標(biāo)點(diǎn)符號被自動(dòng)選中了,這是默認(rèn)行為。那么,如何讓光標(biāo)定位到兩者之間呢?這里可以用到setEndBefore方法,可以設(shè)置選區(qū)的結(jié)束點(diǎn)位置。
- Range.setEndBefore() - Web APIs | MDN (mozilla.org)[8]
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); // 將光標(biāo)移動(dòng)到newQuote之前
}
}
Before表示“之前”,所以選區(qū)的結(jié)束點(diǎn)在新生成的字符之前,光標(biāo)自然就移到兩者之間了。
然后來處理中文引號的問題,同樣是需要特殊處理,將光標(biāo)往左移動(dòng)一位,可以用到setStart和setEnd方法,表示設(shè)置選區(qū)的起始點(diǎn)。
- Range.setStart() - Web APIs | MDN (mozilla.org)[9]
- Range.setEnd() - Web APIs | MDN (mozilla.org)[10]
具體實(shí)現(xiàn)如下
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);
}
}
}這樣富文本也支持中英文標(biāo)點(diǎn)自動(dòng)配對了。
還有一點(diǎn)小細(xì)節(jié)可以優(yōu)化,在開發(fā)者工具中可以看到,新添加的標(biāo)點(diǎn)都是一個(gè)個(gè)獨(dú)立的#text,導(dǎo)致把整個(gè)文本分割成立很多的小片段,如下:
打印一下子節(jié)點(diǎn)。
這里都是純文本,有辦法合并一下嗎?當(dāng)然也有,用到的方法是normalize,可以將子節(jié)點(diǎn)“規(guī)范化”。
- Node.normalize() - Web APIs | MDN (mozilla.org)[11]
const input = function(ev){
const quote = quotes[ev.data];
if (quote && ev.inputType === "insertText") {
// 規(guī)范化子節(jié)點(diǎn)
range.commonAncestorContainer.normalize();
}
}現(xiàn)在看下效果(注意觀察控制臺(tái)的字符)。
打印子節(jié)點(diǎn)也只有一個(gè)了。
完整代碼可以查看:contenteditable-auto-quotes(codepen.io)[12] 或者 contenteditable-auto-quotes(juejin.cn)[13]。
六、整合成公共方法
以上案例是針對具體某一個(gè)元素實(shí)現(xiàn),如果有多個(gè)輸入框,可能會(huì)有點(diǎn)麻煩,所以有必要整合一下,實(shí)現(xiàn)一個(gè)更為通用的方法。
首先,我們可以把事件監(jiān)聽放在document上,而不是具體的某個(gè)輸入框。
document.addEventListener('compositionend', commonInput)
document.addEventListener('input', commonInput)這里用了一個(gè)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)
}
}注意,這里的this指向問題,使用call 指向了當(dāng)前編輯的輸入框ev.target。
然后inputTextArea和input分別表示前面表單輸入和富文本的具體處理。
下面是完整代碼,你可以直接粘貼到任意控制臺(tái)進(jìn)行試用,相當(dāng)于一個(gè)polyfill。
(function(){
/*
* @desc: 自動(dòng)匹配標(biāo)點(diǎn)符號
* @email: [email protected]
* @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();
}
}
})()實(shí)戰(zhàn)一下,以下是某網(wǎng)站的一個(gè)評論輸入框,在控制臺(tái)注入以上代碼后,也能夠完美支持自動(dòng)匹配標(biāo)點(diǎn)。
七、總結(jié)和說明
想不到一個(gè)小小的功能居然包含了這么多不常見的API,下面總結(jié)一下:
- 自動(dòng)配對標(biāo)點(diǎn)符號可以很好的提升輸入體驗(yàn);
- keydown事件無法區(qū)分中英文輸入法,也無法區(qū)分同一按鍵對應(yīng)的多個(gè)標(biāo)點(diǎn)符號;
- input事件可以通過ev.data檢測當(dāng)前輸入的字符;
- windows 操作系統(tǒng)下輸入中文標(biāo)點(diǎn)符號會(huì)觸發(fā)兩次input,原因是和普通中文一樣,觸發(fā)了候選框;
- windows 操作系統(tǒng)下可以通過compositionend事件來實(shí)現(xiàn),有效避免了兩次觸發(fā)的情況;
- 原生表單輸入框和contenteditable可編輯元素的光標(biāo)處理方式完全不一樣,需要分開處理;
- 中文標(biāo)點(diǎn)有點(diǎn)特殊,中文的上引號和下引號在同一按鍵上,輸入的時(shí)候是依次出現(xiàn)的,無法修改;
- 如果是上引號,就在光標(biāo)處插入下引號;如果是下引號,就將光標(biāo)往前移動(dòng)一位,然后補(bǔ)全上引號;
- 在原生輸入框中,可以用setRangeText方法來手動(dòng)插入內(nèi)容;
- 在富文本輸入框中,可以用insertNode方法來手動(dòng)插入內(nèi)容,文本需要用createTextNode創(chuàng)建;
- 在原生輸入框中,可以用到setSelectionRange方法手動(dòng)設(shè)置選區(qū)的位置;
- 在富文本輸入框中,可以用setStart和setEnd方法手動(dòng)設(shè)置選區(qū)的位置;
整體實(shí)現(xiàn)從代碼量看,其實(shí)并不多,主要是一些和 DOM相關(guān)的API,看著好像有些陌生。為啥會(huì)覺得陌生呢?當(dāng)然是平時(shí)沒有用到過,這和現(xiàn)在的大環(huán)境是密接相關(guān)的,vue和react這些框架雖然給開發(fā)者提供了很多便利,不過也使得離原生,離DOM越來越遠(yuǎn),這樣就導(dǎo)致很多原生API壓根就沒見過,這何嘗不是一種損失呢?
參考資料
[1]Web 中的“選區(qū)”和“光標(biāo)”: https://juejin.cn/post/7068232010304585741
[2]Web 中的“選區(qū)”和“光標(biāo)”: https://juejin.cn/post/7068232010304585741#heading-1
[3]HTMLInputElement.setRangeText() - Web APIs | MDN (mozilla.org): https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setRangeText
[4]HTMLInputElement.setSelectionRange() - Web APIs | MDN (mozilla.org): https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange
[5]textarea-auto-quotes(codepen.io): https://codepen.io/xboxyan/pen/zYWzVGB
[6]textarea-auto-quotes (juejin.cn): https://code.juejin.cn/pen/7123852466059378702
[7]Range.insertNode() - Web APIs | MDN (mozilla.org): https://developer.mozilla.org/en-US/docs/Web/API/Range/insertNode
[8]Range.setEndBefore() - Web APIs | MDN (mozilla.org): https://developer.mozilla.org/en-US/docs/Web/API/Range/setEndBefore
[9]Range.setStart() - Web APIs | MDN (mozilla.org): https://developer.mozilla.org/en-US/docs/Web/API/Range/setStart
[10]Range.setEnd() - Web APIs | MDN (mozilla.org): https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
[11]Node.normalize() - Web APIs | MDN (mozilla.org): https://developer.mozilla.org/en-US/docs/Web/API/Node/normalize
[12]contenteditable-auto-quotes(codepen.io): https://codepen.io/xboxyan/pen/QWmgXML
[13]contenteditable-auto-quotes(juejin.cn): https://code.juejin.cn/pen/7123851982644707336
當(dāng)前題目:提升Web輸入體驗(yàn)!JS如何自動(dòng)配對標(biāo)點(diǎn)符號?
文章URL:http://fisionsoft.com.cn/article/djdjodd.html


咨詢
建站咨詢
