当前位置: 首页 > Web前端 > HTML

提升网页输入体验!JS如何自动配对标点符号?

时间:2023-04-02 15:52:29 HTML

欢迎关注微信公众号前端大侦探温馨提示:阅读本文之前,您可以先回顾一下这篇文章:Web中的“选择”和“光标”,有很多你可能不知道的原生API,以下内容是该内容在写作和编辑中的实际应用。需要成对出现的标点符号有很多,比如引号、括号、书名号等,具体如下:为了方便输入,有些输入法自带标点符号自动配对功能。这意味着什么?比如输入一个前括号,自动补全后括号,然后光标在中间。下面是小米手机内置输入法的演示:不仅仅是输入法,大多数编辑器也实现了类似的功能,比如vscode。那么,Web中的输入框如何支持这样一个有用的功能呢?一、实现原理原理其实很简单。检测输入内容可以分为以下几个步骤。如果是上面的标点符号,下一步就是根据输入的标点符号自动补全相应的后半部分。把光标移到两个标点是不是很容易看懂?不过里面的细节远不止这些,涉及到很多比较少见的native方法。让我们来看看如何实现它。字符是什么,所以一开始我想到了在keydown方法中使用keydown方法editor.addEventListener("keydown",(ev)=>{console.log(ev.key,ev.code)}),相关keyvalueev.key和ev.code的属性如下。好像没什么问题。您可以使用ev.key来区分具体的输入字符。其实还是有很多问题,比如不能区分中英文标点符号输入。例如:分别输入中文和英文的方括号,可以看到两者的ev.key和ev.code是完全一样的!更离谱的是,在中文输入法下,某些标点符号是依次出现的,比如中文的单引号和双引号,上引号按一次,下引号再按一次,半括号,按一次对于”,再按一次就是“等等,这样的输入更无法判断。为什么会这样?因为这些标点符号都在一个按钮上,而keydown事件反映的是与键盘相关的属性。在next按钮上密密麻麻的有4个标点符号,所以我们需要用其他的方法来检测输入的内容。这里可以使用input事件进行监听,ev.data表示当前输入的字符。editor.addEventListener("input",(ev)=>{console.log(ev.data)})注意这里是字符,即实际输入到页面的文本。下面需要注意的是,在windows中文输入法下,会触发两次输入,如下。这是因为在windows中文输入法下,标点输入和普通拼音输入是一样的。有一个候选词的过程,就这样,所以解决这个问题很简单,只需要用compositionend事件就可以了,意思是候选词结束后editor.addEventListener("compositionend",(ev)=>{console.log(ev.data)})所以兼容windows和MacOS的完整写法应该是constinput=function(ev){if(ev.inputType==="insertText"||ev.type==='compositionend'){console.log(ev)}}editor.addEventListener('compositionend',input)editor.addEventListener('input',input)因为我们只检测标点符号,所以不用担心重复触发。3.两种类型的输入框接下来就是实现具体的匹配了。在此之前,我们需要弄清楚输入框的两种类型。一种是原生默认的表单输入框input和textarea另一种是手动给元素添加属性contenteditable="true",或者CSS属性-webkit-user-修改yux阅读前端

或div{-webkit-user-modify:read-write;}为什么要分这两种?因为这两种游标的处理方式完全不同。更详细的可以参考上一篇文章:Web中的“选择”和“游标”。textarea>首先我们需要列出需要匹配的标点符号,包括中英文constquotes={"'":"'",'"':'"',"(":")","(":")","【":"】","[":"]","《": "》","「":""","『":"』","{":"}",""":""","'":"'",};接下来按照上面说的检测输入内容自动补全标点的方法,在原生输入框,您可以使用setRangeText方法手动插入内容HTMLInputElement.setRangeText()-WebAPIs|MDN(mozilla.org)constinput=function(ev){constquote=quotes[EV.数据];if(quote&&(ev.inputType==="insertText"||ev.type==='compositionend')){this.setRangeText(quote)}}如下效果是不是很简单?但是还是有一些问题,比如中文的引号。有点奇怪为什么会这样?原因是中文中的上引号和下引号是按顺序出现的,也就是说第一次按是上引号,按第二次是下引号。完全由系统输入法决定,不能修改(英文不存在这个问题,因为上下引号相同)那么,这个问题怎么解决呢?我的思路是这样的,引号和引号分开处理如果是上引号,按照前面的思路进行;如果是下引号,则将光标向前移动一位,然后补全上引号,说明下面的具体实现是在列出的标点符号中添加下引号,并添加标识,标识这些符号需要特殊处理constquotes={//添加中文小引号映射""":""","'":"'",};constquotes_reverse=[""","'"];那么如果是引号,需要将光标向左移动一位,可以使用setSelectionRange方法,该方法可以手动设置选择的位置当前光标位置可以通过selectionStart和selectionEnd这两个属性获取HTMLInputElement.setSelectionRange()-WebAPIs|MDN(mozilla.org)完成标点后,需要在两者之间移动光标,具体实现是如下constinput=function(ev){constquote=quotes[ev.data];if(quote&&(ev.inputType==="insertText"||ev.type==='compositionend')){constreverse=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(runjs.work)五、富文本输入框我们来看一个比较常见的输入框,富文本编辑yux阅读前端
其实和之前的纯文本是一样的,只是光标的处理方式不同而已。首先,给游标添加内容需要在range对象下进行处理,使用insertNode的一个方法,注意这个方法需要传入一个node节点,纯字符需要使用createTextNode来创建Range.insertNode()-WebAPIs|MDN(mozilla.org)具体实现如下constselection=document.getSelection();constinput=function(ev){constquote=quotes[ev.data];if(quote&&(ev.inputType==="insertText"||ev.type==='compositionend')){constnewQuote=document.createTextNode(quote);constrange=selection.getRangeAt(0);range.insertNode(newQuote);}}效果如下,可以看到,插入的标点符号被自动选中,这是默认行为。那么,如何将光标定位在两者之间呢?此处可以使用setEndBefore方法来设置选择范围的终点。setEndBefore()-WebAPIs|MDN(mozilla.org)constselection=document.getSelection();constinput=function(ev){constquote=quotes[ev.data];if(quote&&(ev.inputType==="insertText"||ev.type==='compositionend')){constnewQuote=document.createTextNode(quote);constrange=selection.getRangeAt(0);range.insertNode(newQuote);范围.setEndBefore(newQuote);//将光标移动到newQuote之前}}Before的意思是“之前”,所以选择区域的终点在新生成的字符之前,光标自然会移动到两者之间的时候,我们需要处理这个问题中文引号。它还需要特殊处理。将光标向左移动一位。您可以使用setStart和setEnd方法来指示选择区域的起点。Range.setStart()-WebAPI|MDN(mozilla.org)Range.setEnd()-WebAPI|MDN(mozilla.org)的实现如下constinput=function(ev){constquote=quotes[ev.data];if(quote&&ev.inputType==="insertText"){constnewQuote=document.createTextNode(quote);constrange=selection.getRangeAt(0);constreverse=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);}}}这样的富文本还支持中英文标点符号自动配对,还有一个小细节可以优化,可以在开发者工具中看到,新增的标点符号都是独立的#text,导致整个文本分成很多小片段,打印这里的子节点都是纯文本,有没有办法合并?当然有,使用的方法是normalize,可以对子节点进行“规范化”Node.normalize()-WebAPIs|MDN(mozilla.org)constinput=function(ev){constquote=quotes[ev.data];if(quote&&ev.inputType==="insertText"){//标准化子节点range.commonAncestorContainer.normalize();}}现在看效果(注意console中的字符),只有一个打印子节点是完整的代码可以查看:contenteditable-auto-quotes(runjs.work)RunJS,前端在线创建分享-结束代码6.集成到公共方法中以上案例是针对特定元素实现的。如果有多个输入框,可能会有点麻烦。所以有必要集成和实现一个更通用的方法。首先,我们可以把事件监听器放在文档上,而不是具体的输入框document.addEventListener('compositionend',commonInput)document.addEventListener('input',commonInput)这里使用了一个commonInput来处理表单输入框和如果是富文本函数commonInput(ev){consttagName=ev.target.tagName;if(tagName==='TEXTAREA'||tagName==='INPUT'){inputTextArea.call(ev.target,ev)}else{input.call(ev.target,ev)}}注意这里要点问题,使用call指向当前编辑的输入框ev.target然后inputTextArea和input分别代表之前的表单输入和富文本的具体处理下面是完整代码,可以直接粘贴到任意控制台试用的话,相当于一个polyfill(function(){/**@desc:自动匹配标点符号*@email:yanwenbin1991@live.com*@author:XboxYan*/constquotes={"'":"'",'"':'"',"(":")","(":")","【":"】","[":"]","《": "》","「":""","『":"』,"{":"}",""":""","'":"'",""":""","'":"'",};constquotes_reverse=[""","'"];constselection=document.getSelection();函数commonInput(ev){consttagName=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)functioninputTextArea(ev){constquote=quotes[ev.data];if(quote&&(ev.inputType==="insertText"||ev.type==='compositionend')){constreverse=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)}}}函数输入(ev){constquote=quotes[ev.data];if(quote&&ev.inputType==="insertText"){constnewQuote=document.createTextNode(quote);constrange=selection.getRangeAt(0);constreverse=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();}}})()我们来实战看看。下面是某网站的一个评论输入框。将以上代码注入控制台后,也可以完美支持标点自动匹配。很多不常用的API,总结一下标点符号自动配对,提升输入体验。keydown事件不能区分中英文输入法,也不能区分同一个键对应的多个标点符号。input事件可以通过ev.data检测当前输入在windows操作系统下输入中文标点符号会触发两次输入,原因是和普通中文一样,在windows操作系统下触发候选框可以通过compositionend事件来实现,有效避免了触发两次的情况上面,输入时顺序出现,不可修改。如果是上引号,则在光标处插入一个下引号;如果是下引号,则将光标向前移动一位,然后在原输入框中补全上引号。您可以使用setRangeText方法在富文本输入框中手动插入内容。您可以使用insertNode方法手动插入内容。需要使用createTextNode在本机输入框中创建文本。您可以使用setSelectionRange方法手动设置选择在富文本输入框中的位置。其中,可以使用setStart和setEnd方法手动设置选择区域的位置。从整体实现来看,代码并不多,主要是DOM相关的一些API。好像有点奇怪,为什么会觉得奇怪呢?当然,我之前没有用过,这跟现在的环境有很大关系。Vue和React的框架虽然为开发者提供了很多便利,但也让他们离原生和DOM越来越远,导致很多原生API根本就没见过。这不是损失吗?最后,如果觉得对你有好处和帮助,欢迎点赞、收藏、转发???欢迎关注微信公众号前侦探