原文链接:http://javascript.info/focus-blur,translate with ❤️ by zhangbao.
当用户使用鼠标点击或 Tab 键切换时,会让元素获得焦点。HTML 还提供了 autofocus 特性,在页面加载完毕或者通过其他什么方式,自动让元素获得焦点。
获得焦点,通常表示:“这里准备接收数据了”,这就到了我们可以执行代码,来初始化或加载某些东西的时刻。
失去焦点(“blur”)甚至更重要。当用户点击别处或者按下 Tab 键切换到下一个表单控件时,发生该事件。
失去焦点表示:“数据已经输入了”,因此,我们到了可以执行代码来检查的阶段,甚至可以进行将数据保存到服务器上等操作。
在处理焦点事件时,有一些重要的特性。我们会尽力在本章阐述。
focus/blur 事件
focus 事件称为聚焦,blur 称之为失焦。
下面,我们用这些事件来验证表单框的输入内容。
下例中:
blur处理器检查输入框里是否输入了 Email 地址,如果没有,就显示错误信息。focus处理器隐藏错误信息(在blur的时候还会重新检查):
<style>.invalid { border-color: red; }#error { color: red }</style>您的 Email 地址: <input type="email" id="input"><div id="error"></div><script>input.onblur = function () {if (!input.value.includes('@')) { // 不是邮箱input.classList.add('invalid');error.innerHTML = '请输入正确的 Email 地址';}};input.onfocus = function () {if (this.classList.contains('invalid')) {// 删除“错误”指示,因为用户此时要重新输入内容this.classList.remove('invalid');error.innerHTML = '';}};</script>
现代 HTML 提供一些表单特进行内容验证:比如 required、pattern 等,有时正是我们需要的。当我们需要更大的灵活性时,可以借助 JavaScript。如果输入值正确,我们可以自动地将修改后的值发送到服务器。
focus/blur 方法
elem.focus() 和 elem.blur() 方法用于设置/取消元素的聚焦。
我们可以写个例子,如果输入值无效,就不让用户离开输入框:
<style>.error {background: red;}</style>您的 Email 地址: <input type="email" id="input"><input type="text" style="width:220px" placeholder="确保输入的 Email 地址是有效的,再 focus 这里"><script>input.onblur = function () {if (this.value.includes('@')) {// 显示错误this.classList.add('error');// 然后重新 focusinput.focus();} else {this.classList.remove('error');}};</script>
这种方式适用于除火狐之外的所有浏览器(bug)。
如果我们往表单里输入了内容,使用 Tab 或点击离开 <input>,就会触发 onblur,从而导致重新 focus input 的情况发生。
JavaScript 触发失焦
失焦的发生有许多场景。
一个场景就是用户点击了别处。但也可以通过 JavaScript 去触发它。例如:
弹出
alert框时,焦点会自动聚焦在它身上,触发元素的失焦(blur事件),当alert框消失后,焦点又重新回到元素身上(focus事件)。如果一个元素从 DOM 中删除,也会引发失焦。如果后来又重新插入进来,就不会触发聚焦了。
这些特性有时会导致
focus/blur处理程序失误——在意料之外的情况下触发。最好的方法,是在使用这些事件时要小心。如果我们想要跟踪用户发起的焦点消失,我们应该避免自己无意触发这些事件。
tabindex:让所有元素都支持 focus
默认许多元素都不支持聚焦。
不同浏览器之间的支持列表是不同的,但有一些元素是肯定支持 focus/blur 事件的:像 <button>、<input>、<select> 和 <a> 等。
另一方面,为格式化内容而存在的元素,像 <div>、<span> 和 <table> 默认是不支持 focus 的。也就是说,elem.focus 方法在这些元素上没有效果,这些元素不会触发 focus/blur 事件。
但在添加 tabindex 特性后,情况就不一样了。
这个特性的目的,是在使用 Tab 键切换元素时,指定它们被 focus 的顺序。
如果我们有两个元素。第一个写了 tabindex="1",第二个写了 tabindex="2",当按下 Tab 键的时候,会首先聚焦 tabindex 值是 1 的元素,然后再聚焦 tabindex 值是 2 的元素。
有两个特殊值:
tabindex="0":使元素最后被聚焦。tabindex="-1":使用Tab键切换时,会忽略该元素。
如果一个元素有 tabindex 特性,就表示支持聚焦。
下面例子里有一个列表,点击第一项然后按下 Tab 键:
点击第一项然后按下 Tab 键,跟踪元素聚焦顺序。<ul><li tabindex="1">一</li><li tabindex="0">零</li><li tabindex="2">二</li><li tabindex="-1">负一</li></ul><style>li { cursor: pointer; }:focus { outline: 1px dashed green; }</style>
聚焦的顺序是这样的:1 -> 2 -> 0(零总是最后一个)。正常情况下,<li> 不支持聚焦,但是在添加 tabindex 之后,就支持聚焦了,我们用 :focus 去添加元素聚焦时的样式。
使用 **elem.tabIndex`` 同样有效**
我们也可以使用 JavaScript 添加
tabindex属性,也就是elem.tabIndex的方式,让元素支持聚焦。
委托:focusin/focusout
focus、blur 事件不冒泡。
例如,我们在 <form> 上使用 onfocus,让表单在聚焦时高亮:
<!-- 在 <form> 聚焦时,添加类名 focused --><form onfocus="this.className = 'focused'"><input type="text" name="name" value="姓"><input type="text" name="surname" value="名"></form><style> .focused { outline: 1px solid red; } </style>
我们会发现上面的代码不起作用,因为用户是在 <input> 上聚焦,focus 事件也只发生在这个输入框上,不会冒泡。因此,不会触发 form.onfocus。
有两种解决方案:
首先,有一个有趣的历史特性:focus/blur 不冒泡,但是在捕获阶段是向下传播的。
岂不是将事件绑定在捕获阶段不就行了!
<form id="form"><input type="text" name="name" value="姓"><input type="text" name="surname" value="名"></form><style> .focused { outline: 1px solid red; } </style><script>// 将事件处理程序放在捕获阶段(最后一个参数值是 true)form.addEventListener('focus', () => form.classList.add('focused'), true);form.addEventListener('focus', () => form.classList.remove('focused'), true);</script>
第二个方案是使用 focusin 和 focusout 事件,它们等同于 focus/blur,唯一不同的是,这两个事件冒泡。
需要注意的是,必须使用 elem.addEventListener 的方式绑定才有效,使用 on<event> 的方式是行不通的。
这是另一种变体代码:
<form id="form"><input type="text" name="name" value="姓"><input type="text" name="surname" value="名"></form><style> .focused { outline: 1px solid red; } </style><script>// 我们使用冒泡的 focusin 和 focusout 事件form.addEventListener("focusin", () => form.classList.add('focused'));form.addEventListener("focusout", () => form.classList.remove('focused'));</script>
总结
focus 和 blur 事件是在元素获取焦点/失去焦点时触发的。
它们的特性如下:
它们不冒泡。这个缺点可以使用捕获阶段的事件处理程序或
focusin/focusout事件解决。默认许多元素都不支持聚焦。使用
tabIndex可以让元素具备聚焦的能力。
使用 document.activeElement 属性可以获取页面当前的聚焦元素。
练习题
问题
一、可编辑的 div
创建一个 <div>,在点击的时候变成 <textarea>。
文本框让在 <div> 中编辑 HTML 成为可能。
当用户按下 Enter 或者文本框失去焦点时,<textarea> 变为 <div>,然后文本框的内容变为 <div> 中的 HTML 内容了。
二、在点击时编辑 TD
让表格单元格变成点击后可编辑的。
当点击时——单元格变成“可编辑的”(内部出现文本框),我们可以修改里面的 HTML 结构。单元格不可以调整大小,一直保持原始尺寸大小。
单元格下面出现的 OK 和 CANCEL 按钮点击后表示完成/取消编辑。
一次只能编辑一个单元格内容。当一个
<td>处于“编辑模式”时,点击其他单元格的操作都会被忽略。表格可能包含许多单元格。需要使用事件委托。
三、键盘驱动的老鼠
聚焦老鼠,使用方向按键移动位置:
这是 demo。
P.S. 只在 #mouse 上绑定事件处理程序。P.P.S 无需修改任何 HTML/CSS,让这个方法也适用于其他元素。
答案
一、可编辑的 div
<!DOCTYPE HTML><html><head><link type="text/css" rel="stylesheet" href="my.css"><meta charset="utf-8"></head><body><ul><li>点击 div 进入编辑模式</li><li>Enter 或失去焦时会保存</li></ul>允许写入 HTML。<div id="view" class="view">Text</div><script>let area = null;let view = document.getElementById('view');view.onclick = function() {editStart();};function editStart() {area = document.createElement('textarea');area.className = 'edit';area.value = view.innerHTML;area.onkeydown = function(event) {if (event.key == 'Enter') {this.blur();}};area.onblur = function() {editEnd();};view.replaceWith(area);area.focus();}function editEnd() {view.innerHTML = area.value;area.replaceWith(view);}</script></body></html>
二、在点击时编辑 TD
解决思路:
点击的时候,使用
<textarea>接受单元格里的innerHTML内容,而且文本框具有与单元格一样的尺寸,且没有边框。可以借助 JavaScript 和 CSS 来完成正确尺寸的设置。将
textarea.value的值设置成td.innerHTML。聚焦文本框。
在单元格下面,显示 OK/CANCEL 按钮,处理发生在按钮上的点击事件。
核心 JavaScript 代码如下:
let table = document.getElementById('bagua-table');let editingTd;table.onclick = function(event) {// 3 possible targetslet target = event.target.closest('.edit-cancel,.edit-ok,td');if (!table.contains(target)) return;if (target.className == 'edit-cancel') {finishTdEdit(editingTd.elem, false);} else if (target.className == 'edit-ok') {finishTdEdit(editingTd.elem, true);} else if (target.nodeName == 'TD') {if (editingTd) return; // already editingmakeTdEditable(target);}};function makeTdEditable(td) {editingTd = {elem: td,data: td.innerHTML};td.classList.add('edit-td'); // td is in edit state, CSS also styles the area insidelet textArea = document.createElement('textarea');textArea.style.width = td.clientWidth + 'px';textArea.style.height = td.clientHeight + 'px';textArea.className = 'edit-area';textArea.value = td.innerHTML;td.innerHTML = '';td.appendChild(textArea);textArea.focus();td.insertAdjacentHTML("beforeEnd",'<div class="edit-controls"><button class="edit-ok">OK</button><button class="edit-cancel">CANCEL</button></div>');}function finishTdEdit(td, isOk) {if (isOk) {td.innerHTML = td.firstChild.value;} else {td.innerHTML = editingTd.data;}td.classList.remove('edit-td');editingTd = null;}
完整代码查看这里。
三、键盘驱动的老鼠
我可以使用 mouse.onclick 处理点击,使用 position: fixed 让老鼠“可移动”,然后使用 mouse.onkeydown 处理方向按键的操作。
唯一需要注意的地方是,keydown 事件只在支持聚焦的元素上触发,因此我们需要给元素添加 tabindex 特性以支持此事件。因为我们不能修改 HTML,所以我们要使用 mouse.tabIndex 来实现。
P.S. 我们也可以将 mouse.onclick 替换成 mouse.onfocus。
<!DOCTYPE HTML><html><head><meta charset="utf-8"><style>#mouse {display: inline-block;cursor: pointer;margin: 0;}#mouse:focus {outline: 1px dashed black;}</style></head><body><p>点击老鼠,使用方向按键就可以移动它!</p><pre id="mouse">_ _(q\_/p)/. .\=\_t_/= __/ \ ((( )) )/\) (/\ /\ Y /-'nn^nn</pre><script>mouse.tabIndex = 0;mouse.onclick = function() {this.style.left = this.getBoundingClientRect().left + 'px';this.style.top = this.getBoundingClientRect().top + 'px';this.style.position = 'fixed';};mouse.onkeydown = function(e) {switch (e.key) {case 'ArrowLeft':this.style.left = parseInt(this.style.left) - this.offsetWidth + 'px';return false;case 'ArrowUp':this.style.top = parseInt(this.style.top) - this.offsetHeight + 'px';return false;case 'ArrowRight':this.style.left = parseInt(this.style.left) + this.offsetWidth + 'px';return false;case 'ArrowDown':this.style.top = parseInt(this.style.top) + this.offsetHeight + 'px';return false;}};</script></body></html>
在线例子。
(完)
JavaScript 触发失焦
使用 **elem.tabIndex`` 同样有效**