你能掌控自己的内心,而非外在事件。认识到这一点,你便会找到力量。
——马可·奥勒留,《沉思录》
(插图展示了一台鲁布·戈德堡机械,包含球体、跷跷板、剪刀和锤子,它们通过连锁反应相互作用,最终点亮灯泡。)
有些程序需要处理用户的直接输入,比如鼠标和键盘操作。这类输入无法提前以规整的数据结构准备好——它们是实时逐段产生的,程序必须在事件发生时做出响应。
事件处理器
想象这样一种界面:要知道键盘上某个键是否被按下,只能读取该键的当前状态。为了能对按键做出反应,你必须不断读取键的状态,才能在它被释放前捕捉到按下的动作。这种情况下,执行任何耗时的计算都很危险,因为可能会错过按键事件。
一些原始设备就是这样处理输入的。更先进的方式是让硬件或操作系统监测到按键事件,并将其放入队列。程序可以定期检查队列中的新事件并做出响应。
当然,程序必须记得去查看队列,而且要频繁查看——因为从按键按下到程序检测到事件之间的任何延迟,都会让软件显得反应迟钝。这种方式称为“轮询”,大多数程序员都倾向于避免使用。
更好的机制是让系统在事件发生时主动通知代码。浏览器通过允许我们将函数注册为特定事件的处理器来实现这一点。
<p>点击文档激活处理器。</p>
<script>
window.addEventListener("click", () => {
console.log("有人敲门?");
});
</script>
window 是浏览器提供的一个内置对象,代表包含文档的浏览器窗口。调用它的 addEventListener 方法,会注册第二个参数(函数),使其在第一个参数描述的事件发生时被调用。
事件与 DOM 节点
每个浏览器事件处理器都注册在特定的上下文中。在前面的例子中,我们在 window 对象上调用 addEventListener,为整个窗口注册了处理器。DOM 元素和其他一些类型的对象也有这个方法。事件监听器只会在其注册的对象所关联的上下文中发生事件时被调用。
<button>点击我</button>
<p>这里没有处理器。</p>
<script>
let button = document.querySelector("button");
button.addEventListener("click", () => {
console.log("按钮被点击了。");
});
</script>
这个例子将处理器附加到了按钮节点上。点击按钮会触发该处理器,但点击文档的其他部分则不会。
给节点设置 onclick 属性也能达到类似效果。对于大多数类型的事件,都可以通过“on + 事件名”的属性来附加处理器。
但一个节点只能有一个 onclick 属性,因此这种方式每个节点只能注册一个处理器。而 addEventListener 方法允许添加任意数量的处理器——即使元素上已经有其他处理器,再添加新的也没问题。
removeEventListener 方法的参数与 addEventListener 类似,用于移除已注册的处理器。
<button>一次性按钮</button>
<script>
let button = document.querySelector("button");
function once() {
console.log("完成。");
button.removeEventListener("click", once);
}
button.addEventListener("click", once);
</script>
传给 removeEventListener 的函数必须和传给 addEventListener 的是同一个函数。当需要注销处理器时,最好给处理器函数命名(如例子中的 once),以便能将同一个函数传给这两个方法。
事件对象
虽然我们之前忽略了,但事件处理器函数会接收一个参数:事件对象。这个对象包含了关于事件的额外信息。例如,如果想知道按下的是鼠标哪个按钮,可以查看事件对象的 button 属性。
<button>用任何方式点击我</button>
<script>
let button = document.querySelector("button");
button.addEventListener("mousedown", event => {
if (event.button == 0) {
console.log("左键");
} else if (event.button == 1) {
console.log("中键");
} else if (event.button == 2) {
console.log("右键");
}
});
</script>
事件对象中存储的信息因事件类型而异(本章后面会讨论不同类型的事件)。对象的 type 属性始终包含一个标识事件的字符串(如 "click" 或 "mousedown")。
事件冒泡
对于大多数事件类型,注册在包含子节点的父节点上的处理器,也会接收到发生在子节点上的事件。如果段落中有一个按钮被点击,段落上的事件处理器也会收到点击事件。
但如果段落和按钮都有处理器,更具体的处理器——即按钮上的那个——会先执行。事件会从发生的节点“冒泡”到其父节点,再到文档的根节点。最后,在特定节点上的所有处理器都执行完毕后,注册在整个窗口上的处理器才有机会响应事件。
在任何时候,事件处理器都可以调用事件对象的 stopPropagation 方法,阻止事件继续向上传播。例如,当按钮位于另一个可点击元素内部时,若不希望按钮的点击触发外部元素的点击行为,这就很有用。
下面的例子在按钮和包含它的段落上都注册了 "mousedown" 处理器。当右键点击按钮时,按钮的处理器会调用 stopPropagation,阻止段落的处理器执行。当用其他鼠标按钮点击时,两个处理器都会运行。
<p>包含 <button>按钮</button> 的段落。</p>
<script>
let para = document.querySelector("p");
let button = document.querySelector("button");
para.addEventListener("mousedown", () => {
console.log("段落的处理器。");
});
button.addEventListener("mousedown", event => {
console.log("按钮的处理器。");
if (event.button == 2) event.stopPropagation();
});
</script>
大多数事件对象都有一个 target 属性,指向事件发生的原始节点。可以用这个属性来确保不会意外处理从不想处理的节点冒泡上来的事件。
也可以利用 target 属性来广泛捕捉特定类型的事件。例如,如果有一个包含大量按钮的节点,在外部节点上注册一个点击处理器,通过 target 属性判断是否点击了按钮,可能比在所有按钮上分别注册处理器更方便。
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", event => {
if (event.target.nodeName == "BUTTON") {
console.log("点击了", event.target.textContent);
}
});
</script>
默认行为
许多事件都有默认行为。点击链接会跳转到链接的目标地址;按下向下箭头,浏览器会向下滚动页面;右键点击会显示上下文菜单,等等。
对于大多数类型的事件,JavaScript 事件处理器会在默认行为发生前被调用。如果处理器不希望执行默认行为(通常是因为它已经处理了事件),可以调用事件对象的 preventDefault 方法。
这可以用来实现自定义键盘快捷键或上下文菜单。但也可能被用来恶意干扰用户预期的行为。例如,下面是一个无法跳转的链接:
<a href="https://developer.mozilla.org/">MDN</a>
<script>
let link = document.querySelector("a");
link.addEventListener("click", event => {
console.log("不行哦。");
event.preventDefault();
});
</script>
除非有非常充分的理由,否则不要这样做。破坏预期的行为会让使用页面的用户感到不适。
不同浏览器对某些事件的拦截能力不同。例如,在 Chrome 中,关闭当前标签页的键盘快捷键(ctrl-W 或 command-W)无法被 JavaScript 处理。
键盘事件
按下键盘上的键时,浏览器会触发 "keydown" 事件;释放键时,会触发 "keyup" 事件。
<p>按住 V 键时,页面会变成紫色。</p>
<script>
window.addEventListener("keydown", event => {
if (event.key == "v") {
document.body.style.background = "violet";
}
});
window.addEventListener("keyup", event => {
if (event.key == "v") {
document.body.style.background = "";
}
});
</script>
尽管名字是 "keydown",但它不仅在键被物理按下时触发。当按键被按住时,事件会在每次按键重复时再次触发。有时需要注意这一点。例如,如果在按键按下时添加一个按钮,释放时移除,那么按住键不放可能会意外添加成百上千个按钮。
上面的例子通过事件对象的 key 属性来判断事件对应的键。这个属性的值是一个字符串,对于大多数键,对应按下该键会输入的字符。对于特殊键(如回车),它的值是键的名称(这里是 "Enter")。如果按住 shift 键的同时按下某个键,可能会影响键的名称——"v" 会变成 "V","1" 可能会变成 "!"(如果在你的键盘上,shift+1 会输入 "!" 的话)。
shift、ctrl、alt 和 meta(Mac 上的 command 键)等修饰键,和普通键一样会产生键盘事件。在处理组合键时,可以通过键盘和鼠标事件的 shiftKey、ctrlKey、altKey 和 metaKey 属性,判断这些键是否被按住。
<p>按 Control+空格 继续。</p>
<script>
window.addEventListener("keydown", event => {
if (event.key == " " && event.ctrlKey) {
console.log("继续!");
}
});
</script>
键盘事件的起源节点取决于按键按下时获得焦点的元素。大多数节点只有设置了 tabindex 属性才能获得焦点,但链接、按钮和表单字段等可以。第 18 章会讨论表单字段。当没有特定元素获得焦点时,document.body 会作为键盘事件的目标节点。
当用户输入文本时,用键盘事件来判断输入的内容是有问题的。有些平台,尤其是安卓手机上的虚拟键盘,不会触发键盘事件。即使使用传统键盘,某些类型的文本输入也不会直接对应按键,例如输入法编辑器(IME)软件——供那些文字无法直接通过键盘输入的用户使用,多个按键会组合成一个字符。
要监测用户输入的内容,<input> 和 <textarea> 等可输入元素会在用户修改内容时触发 "input" 事件。要获取实际输入的内容,最好直接从获得焦点的字段中读取,这在第 18 章会讨论。
指针事件
目前有两种广泛使用的屏幕指向方式:鼠标(包括触摸板、轨迹球等类似设备)和触摸屏。它们会产生不同类型的事件。
鼠标点击
按下鼠标按钮会触发一系列事件。"mousedown" 和 "mouseup" 事件类似于 "keydown" 和 "keyup",在按钮按下和释放时触发。这些事件发生在鼠标指针正下方的 DOM 节点上。
"mouseup" 事件之后,会在同时包含按下和释放按钮动作的最具体节点上触发 "click" 事件。例如,如果在一个段落上按下鼠标按钮,然后将指针移到另一个段落上释放,"click" 事件会发生在包含这两个段落的元素上。
如果两次点击间隔很近,会在第二次 "click" 事件之后触发 "dblclick"(双击)事件。
要获取鼠标事件发生的精确位置,可以查看事件的 clientX 和 clientY 属性,它们包含事件相对于窗口左上角的坐标(以像素为单位);或者 pageX 和 pageY,它们相对于整个文档的左上角(当窗口滚动时,这可能与前者不同)。
下面的程序实现了一个简单的绘图应用。每次点击文档,都会在鼠标指针下方添加一个点。
<style>
body {
height: 200px;
background: beige;
}
.dot {
height: 8px; width: 8px;
border-radius: 4px; /* 圆角 */
background: teal;
position: absolute;
}
</style>
<script>
window.addEventListener("click", event => {
let dot = document.createElement("div");
dot.className = "dot";
dot.style.left = (event.pageX - 4) + "px";
dot.style.top = (event.pageY - 4) + "px";
document.body.appendChild(dot);
});
</script>
第 19 章会创建一个更完善的绘图应用。
鼠标移动
每次鼠标指针移动时,都会触发 "mousemove" 事件。这个事件可用于跟踪鼠标位置,在实现鼠标拖动功能时特别有用。
例如,下面的程序显示一个条,并设置了事件处理器,使得在条上左右拖动可以改变其宽度:
<p>拖动条改变其宽度:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
let lastX; // 跟踪上一次观察到的鼠标 X 坐标
let bar = document.querySelector("div");
bar.addEventListener("mousedown", event => {
if (event.button == 0) {
lastX = event.clientX;
window.addEventListener("mousemove", moved);
event.preventDefault(); // 阻止选中
}
});
function moved(event) {
if (event.buttons == 0) {
window.removeEventListener("mousemove", moved);
} else {
let dist = event.clientX - lastX;
let newWidth = Math.max(10, bar.offsetWidth + dist);
bar.style.width = newWidth + "px";
lastX = event.clientX;
}
}
</script>
注意,"mousemove" 处理器注册在整个窗口上。即使调整大小时鼠标移出了条,只要按钮还按着,我们仍然希望更新它的大小。
当鼠标按钮释放时,必须停止调整条的大小。可以通过 buttons 属性(注意复数形式)来判断当前按下的按钮——当它为 0 时,没有按钮按下。当有按钮按下时,buttons 属性的值是这些按钮代码的总和——左键是 1,右键是 2,中键是 4。例如,同时按住左键和右键时,buttons 的值是 3。
注意,这些代码的顺序与 button 属性不同,在 button 中,中键在右键之前。如前所述,浏览器的编程接口并不总是保持一致性。
触摸事件
我们使用的图形浏览器最初是为鼠标界面设计的,当时触摸屏还很少见。为了让网页在早期触摸屏手机上“能用”,这些设备的浏览器在一定程度上会把触摸事件模拟成鼠标事件。点击屏幕会触发 "mousedown"、"mouseup" 和 "click" 事件。
但这种模拟并不完善。触摸屏的工作方式与鼠标不同:它没有多个按钮,无法在手指离开屏幕时跟踪位置(以模拟 "mousemove"),而且允许多个手指同时触摸屏幕。
鼠标事件只能应对简单的触摸交互——如果给按钮添加 "click" 处理器,触摸用户仍然可以使用它。但像前面例子中可调整大小的条,在触摸屏上就无法工作。
触摸交互会触发特定的事件类型。当手指开始触摸屏幕时,会触发 "touchstart" 事件;触摸时移动手指,会触发 "touchmove" 事件;最后,当手指离开屏幕时,会触发 "touchend" 事件。
由于许多触摸屏可以同时检测多个手指,这些事件不会只关联一组坐标。相反,它们的事件对象有一个 touches 属性,包含类数组的点对象,每个点都有自己的 clientX、clientY、pageX 和 pageY 属性。
可以这样做:在每个触摸点周围显示红色圆圈:
<style>
dot { position: absolute; display: block;
border: 2px solid red; border-radius: 50px;
height: 100px; width: 100px; }
</style>
<p>触摸此页面</p>
<script>
function update(event) {
// 移除现有所有 dot 元素
for (let dot; dot = document.querySelector("dot");) {
dot.remove();
}
// 为每个触摸点创建新 dot
for (let i = 0; i < event.touches.length; i++) {
let {pageX, pageY} = event.touches[i];
let dot = document.createElement("dot");
dot.style.left = (pageX - 50) + "px";
dot.style.top = (pageY - 50) + "px";
document.body.appendChild(dot);
}
}
window.addEventListener("touchstart", update);
window.addEventListener("touchmove", update);
window.addEventListener("touchend", update);
</script>
在触摸事件处理器中,通常需要调用 preventDefault 来覆盖浏览器的默认行为(可能包括滑动页面),并防止触发可能已注册的鼠标事件。
滚动事件
元素被滚动时,会在该元素上触发 "scroll" 事件。这有多种用途,例如知道用户当前正在查看的内容(用于禁用屏幕外动画,或向“邪恶总部”发送监控报告),或显示进度指示(如高亮目录的一部分或显示页码)
下面的例子在文档上方绘制了一个进度条,并在向下滚动时更新其填充程度:
<style>
#progress {
border-bottom: 2px solid blue;
width: 0;
position: fixed;
top: 0; left: 0;
}
</style>
<div id="progress"></div>
<script>
// 创建一些内容
document.body.appendChild(document.createTextNode(
"supercalifragilisticexpialidocious ".repeat(1000)));
let bar = document.querySelector("#progress");
window.addEventListener("scroll", () => {
let max = document.body.scrollHeight - innerHeight;
bar.style.width = `${(pageYOffset / max) * 100}%`;
});
</script>
给元素设置 position: fixed(固定定位)的效果类似于绝对定位,但它会阻止元素随文档其余部分一起滚动。这样我们的进度条就会保持在顶部。通过改变其宽度来指示当前进度。设置宽度时使用 % 而不是 px 作为单位,使元素大小相对于页面宽度。
全局的 innerHeight 变量提供窗口的高度,我们必须从总可滚动高度中减去这个值——当滚动到文档底部时,就无法再继续滚动了。还有 innerWidth 表示窗口宽度。用当前滚动位置 pageYOffset 除以最大滚动位置,再乘以 100,就得到了进度条的百分比。
在滚动事件上调用 preventDefault 无法阻止滚动发生。实际上,事件处理器只有在滚动发生后才会被调用。
焦点事件
当元素获得焦点时,浏览器会在该元素上触发 "focus" 事件;当元素失去焦点时,会触发 "blur" 事件。
与前面讨论的事件不同,这两个事件不会冒泡。子元素获得或失去焦点时,父元素上的处理器不会收到通知。
下面的例子会为当前获得焦点的文本字段显示帮助文本:
<p>姓名:<input type="text" data-help="您的全名"></p>
<p>年龄:<input type="text" data-help="您的年龄(岁)"></p>
<p id="help"></p>
<script>
let help = document.querySelector("#help");
let fields = document.querySelectorAll("input");
for (let field of Array.from(fields)) {
field.addEventListener("focus", event => {
let text = event.target.getAttribute("data-help");
help.textContent = text;
});
field.addEventListener("blur", event => {
help.textContent = "";
});
}
</script>
当用户切换到或离开显示该文档的浏览器标签页或窗口时,window 对象会收到 "focus" 和 "blur" 事件。
加载事件
页面加载完成后,window 和 document.body 对象会触发 "load" 事件。这通常用于安排需要整个文档构建完成后才能执行的初始化操作。记住,<script> 标签的内容在遇到该标签时会立即运行。这可能太早了,例如当脚本需要操作出现在 <script> 标签之后的文档部分时。
像图片和加载外部文件的脚本标签这类元素,也有 "load" 事件,用于指示它们引用的文件已加载完成。与焦点相关事件一样,加载事件不会冒泡。
当关闭页面或导航离开(例如点击链接)时,会触发 "beforeunload" 事件。这个事件的主要用途是防止用户意外关闭文档而丢失工作内容。如果在该事件上阻止默认行为,并将事件对象的 returnValue 属性设置为一个字符串,浏览器会显示一个对话框,询问用户是否真的要离开页面。对话框可能会包含你设置的字符串,但由于一些恶意网站试图利用这些对话框误导用户停留在页面上查看可疑的减肥广告,大多数浏览器已不再显示这些自定义文本。
事件与事件循环
正如第 11 章所讨论的,在事件循环的语境中,浏览器事件处理器的行为类似于其他异步通知。它们在事件发生时被调度,但必须等待其他正在运行的脚本完成后才有机会执行。
事件只能在没有其他脚本运行时才能被处理,这意味着如果事件循环被其他工作占用,任何与页面的交互(通过事件发生)都会被延迟,直到有时间处理它们。因此,如果安排了太多工作(无论是长时间运行的事件处理器,还是大量短时间运行的处理器),页面会变得缓慢且难以使用。
对于确实需要在后台执行耗时操作而又不冻结页面的情况,浏览器提供了一种称为 Web Worker 的机制。Worker 是一个与主脚本并行运行的 JavaScript 进程,拥有自己的时间线。
假设计算一个数的平方是一项繁重、耗时的计算,我们希望在单独的线程中执行。可以编写一个名为 code/squareworker.js 的文件,它通过计算平方并发送消息来响应消息:
addEventListener("message", event => {
postMessage(event.data * event.data);
});
为了避免多个线程操作相同数据带来的问题,Worker 不会与主脚本环境共享其全局作用域或任何其他数据。相反,必须通过来回发送消息进行通信。
下面的代码会创建一个运行该脚本的 Worker,向它发送一些消息,并输出响应:
let squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", event => {
console.log("Worker 响应:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);
postMessage 函数发送消息,这会导致接收方触发 "message" 事件。创建 Worker 的脚本通过 Worker 对象发送和接收消息,而 Worker 则通过在其全局作用域上直接发送和监听来与创建它的脚本通信。只有能表示为 JSON 的值才能作为消息发送——接收方会收到它们的副本,而不是值本身。
定时器
第 11 章中介绍的 setTimeout 函数用于安排另一个函数在指定毫秒数后被调用。有时需要取消已安排的函数,可以通过保存 setTimeout 返回的值并在其上调用 clearTimeout 来实现。
let bombTimer = setTimeout(() => {
console.log("BOOM!");
}, 500);
if (Math.random() < 0.5) { // 50% 的概率
console.log("已拆除。");
clearTimeout(bombTimer);
}
cancelAnimationFrame 函数的工作方式与 clearTimeout 类似。在 requestAnimationFrame 返回的值上调用它会取消该帧(假设尚未调用)。
另一组类似的函数 setInterval 和 clearInterval 用于设置每隔 X 毫秒重复执行的定时器。
let ticks = 0;
let clock = setInterval(() => {
console.log("滴答", ticks++);
if (ticks == 10) {
clearInterval(clock);
console.log("停止。");
}
}, 200);
防抖
有些类型的事件可能会连续快速触发多次,例如 "mousemove" 和 "scroll" 事件。处理这类事件时,必须注意不要执行太耗时的操作,否则处理器会占用太多时间,导致与文档的交互变得缓慢。
如果确实需要在这类处理器中执行一些重要操作,可以使用 setTimeout 来确保不会执行得太频繁。这通常称为事件防抖,有几种略有不同的实现方式。
例如,假设我们想在用户输入内容时做出反应,但不希望对每个输入事件都立即响应。当用户快速输入时,我们只想等待输入暂停后再处理。这时不要在事件处理器中立即执行操作,而是设置一个定时器。同时清除之前的定时器(如果有),这样当事件密集发生时(间隔小于定时器延迟),前一个事件的定时器会被取消。
<textarea>在这里输入内容...</textarea>
<script>
let textarea = document.querySelector("textarea");
let timeout;
textarea.addEventListener("input", () => {
clearTimeout(timeout);
timeout = setTimeout(() => console.log("已输入!"), 500);
});
</script>
向 clearTimeout 传递 undefined 值,或对已触发的定时器调用它,都不会产生任何效果。因此,不必担心调用时机,可以简单地对每个事件都调用它。
如果希望响应间隔至少保持一定时间,且在一系列事件发生期间也要触发(而不仅仅是在事件之后),可以使用稍微不同的模式。例如,我们可能希望响应 "mousemove" 事件以显示鼠标的当前坐标,但每 250 毫秒才更新一次。
<script>
let scheduled = null;
window.addEventListener("mousemove", event => {
if (!scheduled) {
setTimeout(() => {
document.body.textContent =
`鼠标位置:${scheduled.pageX}, ${scheduled.pageY}`;
scheduled = null;
}, 250);
}
scheduled = event;
});
</script>
总结
事件处理器使我们能够检测并响应网页中发生的事件。addEventListener 方法用于注册这类处理器。
每个事件都有一个类型(如 "keydown"、"focus" 等)来标识它。大多数事件在特定的 DOM 元素上触发,然后冒泡到该元素的祖先,允许与这些元素关联的处理器处理它们。
调用事件处理器时,会向其传递一个事件对象,包含关于事件的额外信息。该对象还有一些方法,允许我们停止进一步的传播(stopPropagation)和阻止浏览器对事件的默认处理(preventDefault)。
按下键会触发 "keydown" 和 "keyup" 事件;按下鼠标按钮会触发 "mousedown"、"mouseup" 和 "click" 事件;移动鼠标会触发 "mousemove" 事件;触摸屏交互会产生 "touchstart"、"touchmove" 和 "touchend" 事件。
滚动可以通过 "scroll" 事件检测,焦点变化可以通过 "focus" 和 "blur" 事件检测。文档加载完成后,window 会触发 "load" 事件。
练习
气球
编写一个页面,显示一个气球(使用气球表情 🎈)。按下上箭头时,气球应膨胀(变大)10%;按下下箭头时,应收缩(变小)10%。
可以通过设置父元素的 font-size CSS 属性(style.fontSize)来控制文本(表情也是文本)的大小。记住在值中包含单位,例如像素(10px)。
箭头键的键名是 "ArrowUp" 和 "ArrowDown"。确保按键只改变气球,而不会导致页面滚动。
实现后,再添加一个功能:如果气球膨胀超过一定大小,就会“爆炸”。这里的爆炸是指气球被替换为 💥 表情,并且移除事件处理器(这样就不能再对爆炸效果进行膨胀或收缩操作了)。
<p>🎈</p>
<script>
// 你的代码在这里
</script>
(显示提示...)
鼠标轨迹
在 JavaScript 的早期,那时花哨的主页上满是动画图片,人们想出了一些非常有创意的用法。其中之一就是鼠标轨迹——一系列元素会随着鼠标指针在页面上移动而跟随。
在这个练习中,需要实现一个鼠标轨迹。使用绝对定位的 <div> 元素,设置固定大小和背景颜色(参考“鼠标点击”部分的代码示例)。创建多个这样的元素,当鼠标移动时,在鼠标指针经过的路径上显示它们。
有多种实现方法,可以根据需要使轨迹简单或复杂。一个简单的解决方案是:保留固定数量的轨迹元素,循环使用它们,每次 "mousemove" 事件发生时,将下一个元素移动到鼠标当前位置。
<style>
.trail { /* 轨迹元素的类名 */
position: absolute;
height: 6px; width: 6px;
border-radius: 3px;
background: teal;
}
body {
height: 300px;
}
</style>
<script>
// 你的代码在这里
</script>
(显示提示...)
标签页
标签式面板在用户界面中很常见。它们允许通过从“突出”在元素上方的多个标签中选择,来切换显示的界面面板。
实现一个简单的标签式界面。编写一个函数 asTabs,它接收一个 DOM 节点,并创建一个标签式界面来显示该节点的子元素。函数应在节点顶部插入一组 <button> 元素,每个子元素对应一个按钮,按钮文本从子元素的 data-tabname 属性中获取。除一个子元素外,所有原始子元素都应隐藏(设置 display: none 样式)。点击按钮可以选择当前显示的节点。
实现后,进一步扩展功能:为当前选中标签的按钮设置不同样式,以便清楚地显示哪个标签是选中状态。
<tab-panel>
<div data-tabname="one">标签一</div>
<div data-tabname="two">标签二</div>
<div data-tabname="three">标签三</div>
</tab-panel>
<script>
function asTabs(node) {
// 你的代码在这里
}
asTabs(document.querySelector("tab-panel"));
</script>
(显示提示...)