几乎在所有浏览器中,都具有 Tab 键切换焦点的功能。
但是任性的用户强烈要求一定要有 Enter 键切换焦点的功能。
为了交付上线拿到钱,我们只好再一次毫无原则性的接受了客户的需求。
在上一代人中,大多都有这种操作习惯。习惯把保存称为编辑,习惯用回车替换 Tab。这是受到微软 excel 荼毒的结果。
起初我以为这个功能很简单,无非就是把 Enter 键的功能转接到 Tab 键上面,分分钟就可以解决掉的问题。
可困难马上就出现了,我发现这条路是走不通的。
我们经常可以主动触发某个事件,比如el.click()
就可以调用点击事件,或者使用dispatchEvent
。但是键盘和鼠标事件却不行。
我查阅了很多资料,也做了很多尝试。最后总结出来一个结论,在浏览器中,JavaScript 无法操作用户的键盘或者鼠标,这是出于安全策略的考虑。仔细想一下,如果可以用一段 JavaScript 脚本控制用户键盘和鼠标的话,那么用户只需要打开一个黑客网站,黑客就可以瞬间得到他想得到的一切。
所以,如果要通过除 Tab 键以外的其他方式来触发焦点切换,focus
几乎是唯一的选择。
项目是基于 vue 和 element-ui 做的,为了把实现思路先讲清楚,暂时把这些抛开,从原生的页面中寻找答案。
以下是一个原生的 html 页面。
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>Demo</title> </head> <body> <form> <input placeholder="姓名" /> <input placeholder="性别" /> <input placeholder="年龄" /> </form> </body> </html> 复制代码
接下来要实现通过回车键切换焦点,我把思路梳理如下:
思路有了,实现起来也非常简单。
在文档中添加 script 标签,写入如下代码。
function enterCallback(e) { if (e.keyCode === 13) { // 按下回车后的逻辑 } } window.addEventListener("keydown", enterCallback); 复制代码
要注意,enterCallback
单独拿出来,用于注销监听事件。
监听按键事件最常用的方法就是使用事件委托,将事件绑定到window
对象上。相比较给每一个元素都绑定一个事件的方式,这样做的最大好处就是节省内存空间,性能更好。
判断按下哪个键的方式有很多,比如判断e.key
、e.code
或者e.keyCode
等方式。但绝大多数的情况下都建议使用e.keyCode
。下面是一张来自网络的keyCode
表。
很容易就可以做到这一步。
常见的有两种方式。第一种是e.target
,第二种是document.activeElement
。这种情况下,个人更推荐使用第二种。
function enterCallback(e) { if (e.keyCode === 13) { let activeEl = document.activeElement; } } 复制代码
这一步也比较容易。使用el.nextElementSibling
API 即可获取。
function enterCallback(e) { if (e.keyCode === 13) { let activeEl = document.activeElement; let nextEl = activeEl.nextElementSibling; } } 复制代码
切换焦点调用focus
即可实现。
function enterCallback(e) { if (e.keyCode === 13) { let activeEl = document.activeElement; let nextEl = activeEl.nextElementSibling; nextEl && nextEl.focus(); } } 复制代码
至此一个最简单的 Demo 已经实现了,接下来看看项目中实际的情况。
因为是使用组件开发,加上样式等因素,dom 节点并不像上面写的原生 Demo 那么简单,实际情况是多层嵌套的。下面是实际生成的代码结构。
<div class="el-form-item el-form-item--small" style="margin-bottom: 0vh; width: 25%; display: inline-block;" > <label for="pactcode" class="el-form-item__label" style="width: 130px;" >协议号</label > <div class="el-form-item__content" style="margin-left: 130px;"> <div class="el-input el-input--small"> <!----> <input type="text" autocomplete="off" id="el-input" placeholder="未填写协议号" class="el-input__inner" /> <!----> </div> </div> </div> 复制代码
可以看到,如果每一个输入框都是这种类型的嵌套结构,上面的方法是无法直接解决的。因为nextElementSibling
API 只能找到下一个兄弟元素,而在这里 input 明显找不到下一个兄弟元素。
思路是,通过回溯的手段朝外层寻找,直到找到一个类名包含el-form-item
和el-form-item--small
的祖级元素,然后再从这个祖级元素的下一个兄弟元素中寻找类名包含el-input__inner
的 input 元素。
所以要再写两个函数,分别是寻找祖级元素的findFormItem
和寻找 input 元素的findInput
。
findFormItem:
function findFormItem(el) { const parent = el.parentElement; if (!parent) return document.body; if ( parent.className.includes("el-form-item") && parent.className.includes("el-form-item--small") ) { return parent; } return findFormItem(parent); } 复制代码
findInput:
function findInput(container) { let nextEl = container.nextElementSibling; if (!nextEl) return; let input = nextEl.querySelector("input"); while (input.id === "el-select") { nextEl = nextEl.nextElementSibling; if (!nextEl) return; input = nextEl.querySelector("input"); } if (input.className.includes("el-input__inner")) return input; } 复制代码
有了这两个函数以后,实现回车切换焦点就非常简单了。只需要执行两行代码。
const container = findFormItem(document.activeElement); findInput(container) && findInput(container).focus(); 复制代码
完整的代码大概是这样的。
在methods
中声明三个方法。
methods: { addEnterListener() { if (window.__completeEnterBind__) return; window.addEventListener("keydown", this.enterCallback); window.__completeEnterBind__ = true; }, removeEnterListener() { window.removeEventListener("keydown", this.enterCallback); window.__completeEnterBind__ = false; }, enterCallback(e) { function findFormItem(el) { const parent = el.parentElement; if (!parent) return document.body; if ( parent.className.includes("el-form-item") && parent.className.includes("el-form-item--small") ) { return parent; } return findFormItem(parent); } function findInput(container) { let nextEl = container.nextElementSibling; if (!nextEl) return; let input = nextEl.querySelector("input"); while (input.id === "el-select") { nextEl = nextEl.nextElementSibling; if (!nextEl) return; input = nextEl.querySelector("input"); } if (input.className.includes("el-input__inner")) return input; } if (e.keyCode === 13) { const container = findFormItem(document.activeElement); findInput(container) && findInput(container).focus(); } } } 复制代码
然后在mounted
中添加回车监听和在destroy
中移除回车键听。
mounted() { this.addEnterListener(); }, destroy() { this.removeEnterListener(); }, 复制代码
需要注意的是,项目是多标签页的形式,表单组件可能会被渲染多次,所以通过在 window 对象上添加一个__completeEnterBind__
字段来确保回车换行事件正确绑定。
这样,又完成了一次用户的特殊需求,距离拿到项目款项又近了一步。
原文地址:www.luzhenqian.top/enter-key-t…