当点击不仅仅是点击

Avatar of Travis Almand
Travis Almand

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

click 事件非常简单易用;您可以监听该事件,并在事件触发时运行代码。它几乎适用于所有 HTML 元素,是 DOM API 的核心功能。

与 DOM 和 JavaScript 经常出现的情况一样,需要考虑一些细微差别。click 事件的一些细微差别通常不太令人担忧。它们是次要的,并且在大多数用例中,大多数人可能根本不会注意到它们。

例如,click 事件监听交互元素的祖先,即<button> 元素。与按钮点击相关的细微差别,以及这些细微差别,例如鼠标指针的“点击”和键盘的“点击”之间的区别。从这个角度来看,点击并不总是像通常定义的那样是“点击”。我确实遇到过一些情况(虽然不多),在这些情况下,区分这两种类型的点击非常有用。

我们如何区分不同类型的点击?这就是我们要深入探讨的内容!

首先

<button> 元素,如 MDN 所述,很简单

HTML 元素表示一个可点击的按钮,用于提交表单或文档中的任何位置,以实现可访问的标准按钮功能。默认情况下,HTML 按钮以类似于用户代理运行的平台的样式呈现,但您可以使用 CSS 更改按钮的外观。

我们将在其中介绍的部分显然是该描述中“文档中的任何位置,以实现可访问的标准按钮功能”部分。您可能知道,按钮元素可以在表单中具有本机功能,例如,在某些情况下它可以提交表单。我们实际上只关心元素的基本点击功能。因此,请考虑仅将一个简单的按钮放置在页面上,以便在有人与之交互时执行特定功能。

请注意,我说的是“与之交互”而不是仅仅点击它。出于历史和可用性原因,可以通过使用 Tab 键将焦点放在按钮上,然后使用键盘上的空格Enter键来“点击”按钮。这与键盘导航和辅助功能略有重叠;此本机功能早在辅助功能成为关注点之前就已存在。然而,遗留功能确实在很大程度上帮助了辅助功能,原因显而易见。

在上面的示例中,您可以点击按钮,其文本标签将发生更改。片刻之后,原始文本将重置。您也可以点击笔中的其他位置,使用 Tab 键将焦点放在按钮上,然后使用空格Enter键“点击”它。相同的文本也会出现并重置。没有 JavaScript 来处理键盘功能;这是浏览器的本机功能。从根本上讲,在此示例中,按钮只知道 click 事件,但不知道它是如何发生的。

需要考虑的一个有趣的区别是按钮在不同浏览器中的行为,尤其是它的样式方式。这些示例中的按钮设置为在其活动状态下切换颜色;因此,您点击它,它就会变成紫色。请考虑这张显示与键盘交互时状态的图片。

键盘交互状态

第一个是静态状态,第二个是当按钮从键盘 Tab 键切换到它时获得焦点时,第三个是键盘交互,第四个是交互的结果。使用 Firefox,您只会看到第一、二和最后的状态;当使用EnterSpace键“点击”它时,您不会看到第三个状态。它在交互期间保持第二种或“聚焦”状态,然后切换到最后一种状态。文本按预期更改,但颜色没有更改。Chrome 给我们更多一点,因为您会看到与 Firefox 相同的前两种状态。如果您使用空格键“点击”按钮,您将看到第三个状态的颜色变化,然后是最后一个状态。有趣的是,使用 Chrome,如果您使用Enter键与按钮交互,您将不会看到颜色变化的第三个状态,就像 Firefox 一样。如果您好奇,Safari 的行为与 Chrome 相同。

事件侦听器的代码非常简单

const button = document.querySelector('#button');

button.addEventListener('click', () => {
  button.innerText = 'Button Clicked!';
  
  window.setTimeout(() => {
    button.innerText = '"click" me';
  }, 2000);
});

现在,让我们考虑一下这段代码。如果您发现自己处于需要知道是什么导致“点击”发生的情况时该怎么办?click 事件通常与指针设备(通常是鼠标)相关联,但此处空格Enter键触发的是相同的事件。其他表单元素根据上下文具有类似的功能,但任何默认情况下不可交互的元素都需要额外的键盘事件才能工作。按钮元素不需要此额外的事件侦听器。

我不会过多地介绍想要了解触发 click 事件的原因。我可以说,我偶尔会遇到需要知道这些原因的情况。有时出于样式原因,有时出于辅助功能原因,有时出于特定功能原因。通常,不同的上下文或情况会提供不同的原因。

请考虑以下内容,不要将其视为 The Way™,而更多地将其视为对我们正在讨论的这些细微差别的探索。我们将探索处理与按钮元素交互的各种方式、生成的事件以及利用这些事件的特定功能。希望以下示例可以从事件中提供一些有用的信息;或者根据需要扩展到其他 HTML 元素。

哪个是哪个?

知道键盘与鼠标点击事件的一种简单方法是利用keyupmouseup事件,将 click 事件排除在等式之外。

现在,当您使用鼠标或键盘时,更改后的文本会反映哪个事件是哪个。键盘版本甚至会告知您使用了空格还是Enter键。

这是新代码

const button = document.querySelector('#button');

function reset () {
  window.setTimeout(() => {
    button.innerText = '"click" me';
  }, 2000);
}

button.addEventListener('mouseup', (e) => {
  if (e.button === 0) {
    button.innerText = 'MouseUp Event!';
    reset();
  }
});

button.addEventListener('keyup', (e) => {
  if (e.code === 'Space' || e.code === 'Enter') {
    button.innerText = `KeyUp Event: ${e.code}`;
    reset();
  }
});

确实有点冗长,但我们稍后会进行一些重构。此示例说明了需要处理的细微差别。mouseupkeyup事件在这种情况下有其自身的功能需要考虑。

对于mouseup事件,鼠标上的几乎每个按钮都可以触发此事件。例如,我们通常不希望鼠标右键触发按钮上的“click”事件。因此,我们查找值为 0 的e.button来识别主鼠标按钮。这样,它的工作方式与 click 事件相同,但我们确切地知道它是鼠标。

对于keyup事件,也会发生类似的事情,键盘上的几乎每个键都会触发此事件。因此,我们查看事件的code属性以等待按下空格Enter键。因此,现在它的工作方式与 click 事件相同,但我们知道使用了键盘。我们甚至知道我们期望在按钮上使用的两个键中的哪一个。

另一种确定哪个是哪个的方法

虽然前面的示例有效,但对于这样一个简单的概念来说,代码似乎有点太多了。我们实际上只想了解“点击”是否来自鼠标或键盘。在大多数情况下,我们可能并不关心点击源是空格键还是Enter键。但是,如果我们确实关心,我们可以利用keyup事件的属性来记录哪个是哪个。

在关于click事件的各种规范(这将我们引向UI 事件规范)中,某些属性被分配给事件。有些浏览器有更多属性,但我目前想关注detail属性。此属性直接与触发事件本身的鼠标输入相关联。因此,如果使用了鼠标按钮,则该属性应返回 1 作为值。它还可以潜在地报告一个较高的数字,表示多次点击,这通常与设备操作系统确定的双击阈值相关联。作为奖励,此属性报告 0 表示 click 事件是由鼠标输入以外的其他事件(例如键盘)引起的。

我将花点时间感谢评论中的 Jimmy。我最初使用了一种不同的方法来确定键盘与鼠标点击,但它在所有浏览器中都不一致,因为 Safari 报告的值略有不同。Jimmy 建议使用detail属性,因为它更一致;因此,我相应地更新了我的示例。感谢 Jimmy 的建议!

这是我们的新代码

const button = document.querySelector('#button');

button.addEventListener('click', (e) => {
  button.innerText = e.detail === 0 ? 'Keyboard Click Event!' : 'Mouse Click Event!';
  
  window.setTimeout(() => {
    button.innerText = '"click" me';
  }, 2000);
});

回到仅使用click事件,但这次我们查找属性值以确定这是否是键盘或鼠标“点击”。尽管请注意,我们不再有办法确定键盘上使用了哪个键,但在这种情况下,这并不是什么大问题。

许多中的哪一个?

现在是时候谈谈指针事件了。正如 MDN 所描述的

当今的大多数网页内容都假设用户的指向设备是鼠标。但是,由于许多设备支持其他类型的指向输入设备,例如笔/触控笔和触摸屏,因此需要扩展现有的指向设备事件模型。指针事件解决了这个需求。

所以现在让我们考虑一下需要知道哪种类型的指针参与了点击该按钮。仅仅依靠 click 事件并不能提供此信息。Chrome 在 click 事件中有一个有趣的属性,sourceCapabilities。此属性又有一个名为 firesTouchEvents 的属性,它是一个布尔值。此信息并不总是可用,因为 Firefox 和 Safari 尚未支持此功能。然而,指针事件几乎随处可用,甚至包括所有浏览器中的 IE11。

此事件可以提供有关触摸或笔事件的有趣数据。例如压力、接触尺寸、倾斜度等等。对于我们这里的示例,我们只关注 pointerType,它告诉我们导致事件的设备类型。

关于上面提到的 click 事件中的 detail 属性,还需要说明一点。指针事件也具有 detail 属性,但目前规范规定该属性的值应始终为零。这显然与之前的想法相冲突,即值为零表示键盘,值为零以上表示鼠标输入。由于我们不能依赖指针事件中的该属性,因此很难在同一情况下同时包含 click 和 pointer 事件。公平地说,您可能也不想这样做。

点击按钮现在会告诉您使用了哪个指针。这段代码非常简单

const button = document.querySelector('#button');

button.addEventListener('pointerup', (e) => {
  button.innerText = `Pointer Event: ${e.pointerType}`;
  
  window.setTimeout(() => {
    button.innerText = '"click" me';
  }, 2000);
});

实际上,与之前的示例并没有太大区别。我们在按钮上监听 pointerup 事件并输出事件的 pointerType。现在的区别在于没有为 click 事件添加事件监听器。因此,通过 Tab 键切换到按钮并使用空格键或 Enter 键不会有任何反应。click 事件仍然触发,但我们没有监听它。此时,我们只有与按钮绑定的代码,该代码仅响应指针事件。

这显然会在功能上留下一个空白,即键盘交互性,因此我们仍然需要包含 click 事件。由于我们已经使用指针事件来处理更传统的鼠标点击(以及其他指针事件),因此我们必须锁定 click 事件。我们需要仅允许键盘本身触发 click 事件。

此代码与上面的“哪个是哪个”示例类似。不同之处在于我们使用 pointerup 而不是 mouseup

const button = document.querySelector('#button');

function reset () {
  window.setTimeout(() => {
    button.innerText = '"click" me';
  }, 2000);
}

button.addEventListener('pointerup', (e) => {
  button.innerText = `Pointer Event: ${e.pointerType}`;
  reset();
});

button.addEventListener('click', (e) => {
  if (e.detail === 0) {
    button.innerText = 'Keyboard  ||Click Event!';
    reset();
  }
});

这里我们再次使用 detail 属性来确定 click 是否是由键盘引起的。这样,鼠标点击将由指针事件处理。如果想了解使用的键是空格键还是 Enter 键,则可以使用上面提到的 keyup 示例。即使那样,也可以根据您的处理方式,使用 keyup 事件代替 click 事件。

另一种确定许多中的哪一个的方法

为了始终如一地需要重构以获得更简洁的代码,我们可以尝试以不同的方式编写此代码。

是的,与之前的工作方式相同。现在代码是

const button = document.querySelector('#button');

function btn_handler (e) {
  if (e.type === 'click' && e.detail > 0) {
    return false;
  } else if (e.pointerType) {
    button.innerText = `Pointer Event: ${e.pointerType}`;
  } else if (e.detail === 0) {
    button.innerText = 'Keyboard Click Event!';
  } else {
    button.innerText = 'Something clicked this?';
  }
  
  window.setTimeout(() => {
    button.innerText = '"click" me';
  }, 2000);
}

button.addEventListener('pointerup', btn_handler);
button.addEventListener('click', btn_handler);

另一个需要考虑的缩减版本:这次我们将代码减少到一个单一的处理程序方法,pointerupclick 事件都调用该方法。首先,我们检测鼠标“点击”是否导致了事件,因为 detail 属性的值大于零;如果是,我们希望忽略它,转而使用指针事件。

然后,该方法检查指针事件,并在找到该事件后,报告发生的指针类型。否则,该方法检查键盘交互,如果 detail 等于零,则相应地报告。如果两者都不是罪魁祸首,它只会报告某些内容导致了此代码的运行。

因此,这里我们有大量关于如何在处理按钮交互的同时报告这些交互源的示例。然而,这只是我们在项目中习惯使用的少数几种表单元素之一。类似的代码如何在其他元素中工作?

选中复选框

确实,类似的代码在复选框中也以非常相同的方式工作。

正如您现在可能预期的那样,还有一些细微差别。<input type="checkbox"> 的正常用法是相关的标签元素,该元素通过 for 属性绑定到输入。此组合的一个主要功能是,点击标签元素将选中相关的复选框。

现在,如果我们要在两个元素上都附加 click 事件的事件监听器,我们会得到应该很明显的结果,即使它们有点奇怪。例如,点击复选框时,我们会触发一个 click 事件。如果我们点击标签,我们会触发两个 click 事件。如果我们要 console.log 这些事件的目标,我们将在双事件中看到一个事件是针对标签的(这是有道理的,因为我们点击了它),但复选框还有第二个事件。即使我知道这些应该是预期的结果,但它有点奇怪,因为我们期望的是来自用户交互的结果。然而,结果包括由浏览器引起的交互。

因此,下一步是查看如果我们在相同场景中监听 pointerup(就像前面的一些示例一样)会发生什么。在这种情况下,点击标签元素时,我们不会触发两个事件。这也有道理,因为我们不再监听从复选框触发 click 事件,而 click 事件是在点击标签时触发的。

还有一个场景需要考虑。请记住,我们可以选择将复选框放在标签元素内部,这在出于样式目的自定义构建的复选框中很常见。

<label for="newsletter">
  <input type="checkbox" />
  Subscribe to my newsletter
</label>

在这种情况下,我们实际上只需要在标签上放置一个事件监听器,而不需要在复选框本身上放置。这减少了涉及的事件监听器的数量,但我们得到了相同的结果。如果点击标签,则点击事件会作为一个事件触发,如果点击复选框,则会触发两个事件。pointerup 事件也与之前一样,如果点击任一元素,则会触发单个事件。

在尝试模仿按钮元素之前示例的行为时,所有这些都是需要考虑的事情。值得庆幸的是,这并没有太多内容。这是一个查看表单元素复选框使用了哪种类型的交互的示例

此示例包含上面提到的两种类型的复选框场景;顶行是带有 for 属性的复选框/标签组合,底行是标签内的复选框。点击任意一个都会在它们下方输出一条消息,说明发生了哪种类型的交互。因此,用鼠标点击一个或使用键盘导航到它们然后与 Space 进行交互;就像按钮示例一样,它应该会告诉您哪种交互类型导致了它。

为了简化我需要的事件监听器的数量,我用一个实际响应复选框交互的容器 div 将复选框包装起来。您不一定非要这样操作,但这对我来说是一种方便的操作方式。

const checkbox_container = document.querySelector('#checkbox_container');
const checkbox_msg = document.querySelector('#checkbox_msg');

function chk_handler (e) {
  if (e.target.tagName === 'LABEL' || e.target.tagName === 'INPUT') {
    if (e.pointerType) {
      checkbox_msg.innerText = `Pointer Event: ${e.pointerType}`;
    } else if (e.code === 'Space') {
      checkbox_msg.innerText = `Keyboard Event: ${e.code}`;
    }
    
    window.setTimeout(() => {
      checkbox_msg.innerText = 'waiting...';
    }, 2000);
  }
}

checkbox_container.addEventListener('pointerup', chk_handler);
checkbox_container.addEventListener('keyup', chk_handler);

因此,由于我们在容器 div 上监听这些事件,我想将目标锁定到标签和输入。从技术上讲,在某些情况下,可以“点击”容器 div 本身;我们不希望发生这种情况。然后我们检查指针事件并更新消息。之后,我们尝试识别可能来自 keyup 事件的 Space 键代码。您可能还记得,上面的按钮示例同时使用了 Enter Space 键。事实证明,复选框通常不会在浏览器中响应 Enter 键。另一个需要记住的有趣的细微差别。

无线电广播您的单选按钮

值得庆幸的是,对于单选按钮输入,我们仍然可以使用相同的代码和类似的 HTML 结构。这主要是因为复选框和单选按钮的创建方式基本相同——只是单选按钮倾向于组合在一起,而复选框即使在组合中也是独立的。正如您将在以下示例中看到的,它的工作方式相同

同样,相同的代码附加到类似的容器 div 上,以避免为每个相关元素添加大量事件监听器。

当细微差别成为机会时

我选择“细微差别”(nuance)这个词是因为我们这里讨论的内容并不是编程圈里通常意义上的带有负面含义的“问题”(issues)。我总是尝试将这些事情视为学习经验或机会。我今天所知晓的东西如何能让我更进一步,或者也许是时候向外探索新事物来解决我面临的问题。希望以上示例能根据项目的不同需求,提供一种略微不同的看待事物的方式。

尽管本文由于表单元素与键盘交互的点击细微差别而更多地关注表单元素,但其中一些或所有内容都可以扩展到其他元素。这完全取决于具体情况。例如,我记得很多次需要根据上下文在同一个元素上执行多个事件;通常是为了可访问性和键盘导航的原因。您是否构建过自定义的<select>元素来获得比标准元素更好的设计,并且还能响应键盘导航?当您遇到这种情况时,您就会明白我的意思。

请记住:“点击”(click)今天并不总是我们认为它一直以来所代表的意思。