原文链接: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');
// 然后重新 focus
input.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 targets
let 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 editing
makeTdEditable(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 inside
let 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>
在线例子。
(完)