背景
最近在做产品优化,产品让给表单减少一个性能,就是回车后主动进入下一个表单元素,这样就不必频繁应用鼠标进行切换了,能够大大晋升表单输出的流畅性,让用户一路Next。
这个需要很正当,也十分通用,实践上全副表单都应该反对这样的成果,很多大厂的产品,也都是反对这个成果的,当初问题就变成了如何完满设计一个计划,以实现这个成果。
制订指标
在进行技术开发之前,我习惯先给本人定义下技术指标,而不是上来就做。定义好技术指标,就定义好了咱们的需要,定义好了要做什么,做成什么样,而不是上来先思考如何去做,这样更合乎做事的方法论。
针对这个问题,我给本人定了如下几个指标:
配置要简略:
- 因为大量的表单都须要进行这个配置,所以配置肯定要尽可能简略,尽量一行代码搞定
- 不须要配置程序,依据表单中的程序,主动聚焦下一个表单
要反对配置哪些表单元素参加回车聚焦
- 默认应该反对所有含有input和textarea的元素参加回车聚焦
- 然而也要反对自定义,自定义要简略,比方指定含有某个className的元素参加回车聚焦
- 要反对主动聚焦首个表单元素
- 要可能主动滚动到聚焦的表单元素
- 要可能跳过disabled的元素,以及一些不须要聚焦的表单元素,如radio、checkbox、submit等
- 要反对vue2和vue3
计划制订
网上有一些文章,根本都是针对某个表单元素,监听keydown事件,而后非凡解决其逻辑,这样的解决方案没有通用性,而且也非常复杂,每个表单都要大量的有效反复代码。
思考通过指令的形式来解决这个问题,冀望开发一个 v-focus-next指令,只有配置了这个指令,其中的表单元素就主动反对回车聚焦。
冀望的应用形式:
<div v-focus-next> <input/> <input/> <input/> <input/> <input/> <textarea/></div>
组件也同样反对该指令
<el-form v-focus-next > <el-form-item label="名称"> <el-input v-model="form.name" id="name" /> </el-form-item> <el-form-item label="年龄"> <el-input v-model="form.age" id="age" disabled /> </el-form-item> </el-form>
如果咱们只想让className为 focus-next的参加回车聚焦,则这么配置。
<div v-focus-next="'.focus-next'"> <input class=focus-next/> <input/> <input class=focus-next/> <input/> <input class=focus-next/> <textarea/></div>
主动聚焦首个表单,应该只有设置下autoFocus即可
<div v-focus-next.autoFocus></div>
到目前为止,咱们只是在定义要做的事件应该是什么样的,并没有开始编码,无论是编写组件,还是指令,都心愿先定义好对外的接口,而后评估这样的接口设计是否足够易用,最初再去实现。
记住:定义接口比实现更重要。
核心技术点
如何兼容Vue2和Vue3
Vue2和Vue3反对的指令钩子函数并不相同。
Vue3的指令
const myDirective = { // 在绑定元素的 attribute 前 // 或事件监听器利用前调用 created(el, binding, vnode, prevVnode) { // 上面会介绍各个参数的细节 }, // 在元素被插入到 DOM 前调用 beforeMount(el, binding, vnode, prevVnode) {}, // 在绑定元素的父组件 // 及他本人的所有子节点都挂载实现后调用 mounted(el, binding, vnode, prevVnode) {}, // 绑定元素的父组件更新前调用 beforeUpdate(el, binding, vnode, prevVnode) {}, // 在绑定元素的父组件 // 及他本人的所有子节点都更新后调用 updated(el, binding, vnode, prevVnode) {}, // 绑定元素的父组件卸载前调用 beforeUnmount(el, binding, vnode, prevVnode) {}, // 绑定元素的父组件卸载后调用 unmounted(el, binding, vnode, prevVnode) {}}
Vue2的指令
// 注册一个全局自定义指令 `v-focus`Vue.directive('focus', { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el, binding, vNode, prevVnode) { // 聚焦元素 el.focus() }, bind: function(el, binding, vNode, prevVnode){}, update: function(el, binding, vNode, prevVnode){}, componentUpdated: function(el, binding, vNode, prevVnode){}, unbind: function(el, binding, vNode, prevVnode){}, })
能够看到,Vue2和Vue3只是钩子函数周期不同,参数根本还是统一,咱们只有判断以后环境的Vue版本,而后设置不同的钩子函数即可。
咱们晓得,在开发Vue中间件时,install办法能够拿到以后环境的Vue实例,能够通过Vue.version来获取以后环境的Vue版本。
import focusNext3 from './focus-next3.js'; //vue3指令的具体实现import focusNext2 from "./focus-next2.js"; //vue2指令的具体实现export default { install: function (Vue){ let version = Vue.version; //拿到Vue版本 if(version.startsWith('3.')) { Vue.directive('focus-next', focusNext3); }else if(version.startsWith('2.')){ Vue.directive('focus-next', focusNext2); }else{ console.error('v-focus-next只反对vue2/3≈') } }}
指令实现思路
在元素绑定了指令v-focus-next之后,咱们能够监听以后绑定Dom的keydown事件,而后在keyDown事件中判断是否输出了回车符。如果输出了回车符,则获取以后事件event.target后的第一个无效表单元素,并调用该元素的focus办法。
在组件卸载之后记得革除掉监听的keydown事件。
function mounted (el, binding, vNode) { function keyDown(event){ if(event.keyCode !== 13){ return; } let targetNode = event.target; //找到下一个无效的节点 let nextNode = findNextNode(vNode.el, targetNode, binding); if(!nextNode){ return; } setTimeout(()=>{ nextNode.focus(); }); } el.addEventListener('keydown', keyDown); el.__FOCUS_NEXT_KEYDOWN_HANDLER__ = keyDown;}function beforeUnmount (el, binding, vNode) { el.removeEventListener("keydown", el.__FOCUS_NEXT_KEYDOWN_HANDLER__);}export default { mounted, beforeUnmount}
接下来重点展现下如何获取以后event.target的下一个无效元素。
咱们能够先找到所有反对回车聚焦的表单元素,而后查找以后event.target所在位置index,而后返回index+1地位的元素。
这里分2种状况:
event.target自身就属于反对回车聚焦的表单元素:
- 这种状况能够通过在所有反对回车聚焦的表单元素中,找到target所在位置即可
event.target不在反对回车聚焦的表单元素中
- 这种状况可依据dom地位,找到target后的第一个反对回车聚焦的表单元素
- dom1.compareDocumentPosition(dom2),能够判断两个dom的地位
export function findNextNode(rootDom, targetNode, binding){ let selector = binding.value || 'input, textarea'; //先找到该rootDom下所有无效的input、textarea元素 let nodes = findAllInputs(rootDom, selector); let isByCompare = false; let index = nodes.findIndex((item,index) => { //如果回车事件的target和item相等,则阐明找到了 if(item === targetNode || item.contains(targetNode)){ return true } //回车事件的target 不肯定在所有无效的nodes中 //比方咱们设置了只让 className='test'的元素反对聚焦回车 //那么某个没有className='test'的input回车时,nodes就不蕴含该target //此时能够依据地位来判断,target前面的第一个无效元素,就是要主动聚焦的元素 if(targetNode.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_FOLLOWING){ isByCompare = true; return true } return false }); if(isByCompare){ return nodes[index] }else{ if(index === -1 || index == nodes.length - 1){ return null; } return nodes[index + 1]; }}function findAllInputs(rootDom, selector){ //查问selector外部的所有无效input、textarea //selector可能是className,绑定在div上,而非input、textarea上 //必须找到其外部的input、textarea return [...rootDom.querySelectorAll(selector)].reduce(function(nodes, node) { if(['INPUT', 'TEXTAREA'].includes(node.tagName)) { nodes.push(node); return nodes; } let childNodes = node.querySelectorAll('input, textarea'); if(childNodes.length){ let childNode = findFirstAvailableInput(childNodes) if(childNode){ nodes.push(childNode); } return nodes; } return nodes; },[]).filter(item=>{ if(item.tagName ==='INPUT' && !item.disabled && !['submit', 'reset', 'file', 'hidden', 'checkbox', 'radio'].includes(item.type) ){ return true; }else if(item.tagName ==='TEXTAREA' && !item.disabled ){ return true; } return false; })}function findFirstAvailableInput(nodes){ for(let i=0;i<nodes.length;i++){ const input = nodes[i]; if(input.tagName ==='INPUT' && !input.disabled && !['submit', 'reset', 'file', 'hidden', 'checkbox', 'radio'].includes(input.type) ){ return input; }else if(input.tagName ==='TEXTAREA' && !input.disabled ){ return input; } }}
主动聚焦
主动聚焦实现比较简单,能够在指令mounted时,找到第一个无效的反对回车聚焦的元素,调用其focus办法。
function mounted (el, binding, vNode) { if(binding.modifiers.autoFocus){ autoFocus(vNode.el, binding) } //其余代码}export function autoFocus(rootDom, binding){ let selector = binding.value || 'input, textarea'; let nodes = findAllInputs(rootDom, selector); if(nodes.length){ setTimeout(()=>{ nodes[0].focus() }) }}
残缺代码能够查看我的github源码,欢送动动发财的小手,帮忙点个赞。
https://github.com/501351981/v-focus-next
倡议大家能够给表单元素加上该指令,表单的输出体验几乎棒极了~~