背景
最近在做产品优化,产品让给表单减少一个性能,就是回车后主动进入下一个表单元素,这样就不必频繁应用鼠标进行切换了,能够大大晋升表单输出的流畅性,让用户一路 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
倡议大家能够给表单元素加上该指令,表单的输出体验几乎棒极了~~