背景

最近在做产品优化,产品让给表单减少一个性能,就是回车后主动进入下一个表单元素,这样就不必频繁应用鼠标进行切换了,能够大大晋升表单输出的流畅性,让用户一路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

倡议大家能够给表单元素加上该指令,表单的输出体验几乎棒极了~~