onclick() 和 onblur() 排序问题

176

我有一个文本输入框,其中弹出一个自定义下拉菜单。我希望它能够实现以下功能:

  • 当用户点击输入框外部的任何地方时,菜单应该被移除。
  • 更具体地说,如果用户点击菜单中的某个div,则菜单应该被删除,并且应基于所点击的div执行特殊处理。

以下是我的实现:

输入字段具有onblur()事件,当用户在输入字段外点击时通过将其父元素的innerHTML设置为空字符串来删除菜单。菜单中的div还具有onclick()事件,用于执行特殊处理。

问题在于,当菜单被单击时,onclick()事件永远不会触发,因为先触发了输入字段的onblur()并删除了菜单,包括onclick()

我通过将菜单div的onclick()事件拆分为onmousedown()onmouseup()事件,并在鼠标按下时设置全局标志,在鼠标松开时清除标志解决了这个问题,类似于这个答案中建议的方式。因为onmousedown()onblur()之前触发,如果单击了菜单div之一,则标志将在onblur()中设置,但如果在屏幕上的其他地方单击,则不会设置。如果单击了菜单,则我立即从onblur()返回而不删除菜单,然后等待onclick()触发,在此时可以安全地删除菜单。

是否有更优雅的解决方案?

代码大致如下:

<div class="menu" onmousedown="setFlag()" onmouseup="doProcessing()">...</div>
<input id="input" onblur="removeMenu()" ... />

var mouseflag;

function setFlag() {
    mouseflag = true;
}

function removeMenu() {
    if (!mouseflag) {
        document.getElementById('menu').innerHTML = '';
    }
}

function doProcessing(id, name) {
    mouseflag = false;
    ...
}
7个回答

237

我遇到了和你一模一样的问题,我的用户界面也和你描述的一样。我通过将菜单项的 onClick 替换为 onMouseDown 来解决了这个问题。我没有做其他任何事情;没有使用 onMouseUp, 也没有设置标志。这样做会让浏览器根据这些事件处理程序的优先级自动重新排序,而无需我进行额外的工作。

你知道有什么原因导致这种方法对你不起作用吗?


5
这个真的有效。我在火狐浏览器中测试过,效果非常好。 - Supreme Dolphin
8
它有效。看起来onmousedownonblur之前执行。在Firefox、Chrome和Internet Explorer中测试过。 - Tonatio
28
值得注意的是,这会自然地改变一些行为 - 单击交互将在鼠标按下时处理而不是松开时处理。对于大多数人来说可能没问题(我自己也包括在内),但有一些缺点。最值得注意的是,如果我误点了按钮并将其拖出按钮区域,将阻止 onclick 被调用 - 如果按钮执行非 Pure 函数(删除、发布等),则可能希望保留此行为并采用标志方法。 - Brizee
5
如果您在设置mouseDown时而不是onClick,会对所有<button>元素的可访问性造成影响。:/ - ncubica
6
下面由@ian-macfarlane列出的答案是处理此问题的正确方法。简而言之... 使用带有 event.preventDefault()onMouseDown ,以及使用您想要执行的操作的 onClick - Conal Da Costa
显示剩余4条评论

168

onClick 不应该被替换为 onMouseDown

虽然这种方法“有点可行”,但两者在本质上是不同的事件,用户对它们的期望也不同。在这种情况下使用 onMouseDown 而不是 onClick 会破坏您软件的可预测性。因此,这两个事件是不可互换的。

举个例子:当意外点击一个按钮时,用户希望能够按住鼠标单击,将光标拖到元素外部并释放鼠标按钮,最终不会触发任何操作。 onClick 可以做到这一点。而 onMouseDown 则不允许用户按住鼠标,而是立即触发一个动作,没有任何用户救济措施。 onClick 是我们期望在计算机上触发操作的标准。

在这种情况下,需要在 onMouseDown 事件上调用 event.preventDefault()onMouseDown 默认会触发一个模糊(blur)事件,并且当调用 preventDefault 时不会触发模糊事件。然后,onClick 有机会被调用。模糊事件仍然会发生,只是在 onClick 之后。
毕竟,onClick 事件是 onMouseDownonMouseUp 的组合,当且仅当它们都发生在同一个元素内时才会触发。
一个例子 在这里

19
这对我来说是完美的解决方案,评论中的观点完全正确——替换 onMouseDown 会影响用户体验。已为此点赞。 - Paul Bartlett
9
这应该是正确答案。谢谢您发布这个。 - user1189352
2
值得注意的是,这也会带来一些小的副作用,例如无法通过在元素内单击来选择文本。我想不出太多情况会出现问题,只是感觉有点奇怪。 - Rei Miyasaka
8
еҰӮжһңжҲ‘еңЁonMouseDownдәӢ件дёҠдҪҝз”Ёevent.preventDefault()пјҢйӮЈд№ҲжҲ‘зҡ„onFocusдәӢ件дёҚдјҡиў«и°ғз”ЁгҖӮ - Batman
1
焦点位置在许多应用程序中都很重要。标记的解决方案不允许通过回调将焦点恢复到初始输入框。onBlur 将会发生并移除焦点。这个实现使用 event.preventDefault() 允许 onClick 处理程序将焦点设置到文本框。 - Michel
显示剩余4条评论

2

onmousedown替换为onfocus。因此,当焦点在文本框内时,将触发此事件。

onmouseup替换为onblur。一旦您将焦点移出文本框,onblur将执行。

我想这就是您所需的内容。

更新:

当您执行onfocus函数时,删除您将在onblur中应用的类,并添加您要在onfocus中执行的类。

并且

当您执行onblur函数时,删除您将在onfocus中应用的类,并添加您要在onblur中执行的类。

我不认为需要标志变量。

更新2:

您可以使用onmouseout和onmouseover事件

onmouseover-检测光标是否悬停在其上。

onmouseout-检测光标是否离开。


我已经编辑了我的问题以使其更加清晰。这个解决方案如何避免设置标志的需要?此外,我在菜单上使用 onmousedown()onmouseup(),而不是文本框/输入字段。 - 1''
我还不能接受这个答案,因为我不确定它是否正确。您能否回复我在上面评论中提出的问题? - 1''
当然,我可以看出你可能会像我目前所做的那样使用onmouseover和onmouseout来设置一个标志,以便在鼠标悬停在菜单上时进行设置。然而,我仍然认为你需要在onmouseover中设置一个标志,在onmouseout中清除该标志,这样你就会知道当你点击时是否在菜单上方。你能想到不需要设置标志的方法吗? - 1''
我能看到你的代码吗?把它放在一个fiddle里面。只需要放相关的部分,如果我不看结果也完全没问题,只要看到代码就好了。 - HIRA THAKUR
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/34255/discussion-between-messiah-and-1 - HIRA THAKUR

1

onFocus / onBlur是不冒泡的事件。但是,有些焦点事件会冒泡,这些事件是focusinfocusout

现在来看解决方案:我们将输入框和下拉列表包装到一个div元素中,并将该div的tabindex设置为-1(以便它可以接收焦点,但不出现在Tab顺序中)。现在,我们为该div添加focusinfocusout事件监听器。由于这些事件会冒泡,因此单击输入元素将触发我们div的focusin事件(从而打开下拉列表)。

现在的好处是,单击下拉列表也会触发我们div的focusin事件(因此我们基本上保持焦点,这意味着:focusout/blur从未触发,我们的下拉列表保持打开状态)

您可以使用下面的代码片段尝试此操作(仅在失去焦点时下拉列表才会关闭 - 但是如果您希望在单击下拉列表时也关闭它,请取消JS的一行注释)

const container = document.getElementById("container")
const dropDown = document.getElementById("drop-down")

container.addEventListener("focusin", (event) => {
  dropDown.classList.toggle("hidden", false)
})
container.addEventListener("focusout", (event) => {
  dropDown.classList.toggle("hidden", true)
})
dropDown.addEventListener("click", (event) => {
  console.log("I - the drop down - have been clicked");
  //dropDown.classList.toggle("hidden", true);
});
.container {
  width: fit-content;
}

.drop-down {
  width: 100%;
  height: 200px;
  border: solid 1px black
}

.hidden {
  display: none
}
<div class="container" id="container" tabindex="-1">
  <input id="input" />
  <div class="drop-down hidden" id="drop-down" > Hi I'm a drop down </div>
</div>

然而,如果您想将下拉菜单添加到制表符顺序中,或者在下拉菜单中有按钮或一般元素可以接收焦点,则会出现一个问题。因为这样一来,单击将首先使下拉菜单中的元素获得焦点。这会触发我们的容器div失去焦点,从而关闭下拉菜单,因此焦点事件无法进一步冒泡,也就无法触发我们容器上的focusin事件。

我们可以通过扩展focusout事件监听器来解决此问题。

新的事件监听器如下:

container.addEventListener("focusout", (event) => {
  dropDown.classList.toggle("hidden", !container.matches(":hover"))
})

我们基本上是这样说的:“如果有人悬停在下拉菜单上,请不要关闭它”(此解决方案仅考虑鼠标使用;但在这种情况下,这是可以接受的,因为此解决方案旨在解决使用鼠标时出现的问题,当通过Tab键进入/通过下拉菜单时,一切都从一开始就正常工作)

0

将 onclick 改为 onfocus

即使 onblur 和 onclick 不太兼容,但显然 onfocus 和 onblur 是可以一起使用的。因为即使菜单关闭后,对于在内部点击的元素,onfocus 仍然有效。

我尝试了一下,它确实可行。


-1
我发现适合我的理想解决方案是在我的onBlur函数中添加一个超时。我使用了250毫秒,这为我的模糊事件提供了平滑的行为,并允许我的onClick在onBlur之前触发。我使用这个例子作为参考 https://erikmartinjordan.com/onblur-prevents-onclick-react

1
让你的代码/应用程序依赖于时间来正常运行是相当糟糕的。特别是在更大的项目中,维护依赖于时间的代码非常困难,甚至是不可能的。如果您开始通过设置超时来“修复”问题(因此使您的代码依赖于正确的时间),那么您肯定会再次这样做。您越多这样做,代码库越大,这些超时之间的干扰就越有可能发生,如果发生了:祝您调试好运。 - Lord-JulianXLII
1
是的,你说得对,我不太喜欢我的代码过于依赖时间,因为它不总是能产生一致的结果。我已经实现了一个更有效的解决方案来满足我的需求,时间只是我偶然发现的一个快速解决方案。 - James Mtendamema

-10
你可以在你的 onBlur 处理程序中使用 setInterval 函数,像这样:
<input id="input" onblur="removeMenu()" ... />

function removeMenu() {
    setInterval(function(){
        if (!mouseflag) {
            document.getElementById('menu').innerHTML = '';
        }
    }, 0);
}

setInterval 函数会将你的 onBlur 函数从调用栈中移除,因为你将时间设置为 0,所以这个函数会在其他事件处理程序完成后立即被调用。


7
setInterval 不是一个好的选择,因为你没有取消它的计时器,所以它将不断运行你的函数,最终可能导致你的网站使用最大的CPU资源。如果你要使用这种技术(我不建议),请改用 setTimeout。让你的应用程序依赖于某些事件的定时是有风险的。 - casey
9
危险,威尔·罗宾逊!危险!前方可能存在竞态条件!请注意! - GoreDefex
我最初想出了这个解决方案 (使用 setTimeout 而不是 setInterval),但在正确的顺序中进行定时之前,我不得不将其提高到 250ms。这个 bug 在较慢的设备上可能仍然存在,而且它会让延迟变得明显。我尝试了 onMouseDown,它运行良好。我想知道它是否也需要触摸事件。 - Brennan Cheung
如果你真的想使用onClick,那么类似于间隔的东西并不错。但是你会想要一个带有条件的递归超时,而不是一个间隔,这样它就不会无限运行。这是Selenium条件等待使用的相同模式。 - Will

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接