神秘鼠标事件关闭 jQuery UI 对话框
Mysterious mouse event closes jQuery UI dialog
这明显是一个SSCCE.
所以我们的任务是编写导弹发射控制系统的前端。我们选择 Spartan 布局,因为这是非常严重的:只有一个文本输入框和一个用于输入代码的按钮:
为了安全起见,单击 "OK" 按钮后,我们将显示一个对话框,要求用户确认:
作为可用性的画龙点睛之笔,我们为 Enter 按钮添加了一个关键侦听器,该侦听器也会导致单击 "OK" 按钮(使用 $.trigger()
) .
遗憾的是,确认对话框仅在用户单击 "OK" 按钮时显示,而在单击 Enter 时不会显示。当我们点击 Enter 时,对话框根本不会出现。
最糟糕的是,在添加一些调试消息后,对话框似乎确实显示了几分之一毫秒,然后由于某种原因单击了 "Yeap" 按钮。所以当按下 Enter 时,导弹发射立即被确认!
Fiddle here.
代码如下:
function inputKeyListener(evt) {
console.log('key listener - triggered key code is: ' + evt.keyCode);
if (evt.keyCode === $.ui.keyCode.ENTER) {
evt.stopPropagation();
$('#missile-launch-button').click(); // Directly calling confirm() doesn't work either
}
}
function missileLaunchButtonClickHandler(e) {
e.stopPropagation();
confirm();
}
function confirm() {
var launchCode = $('#missile-launch-code-input').val();
const dialog = $('#missile-launch-confirmation-modal');
dialog.dialog({
closeOnEscape: false,
dialogClass: 'no-close',
open: function(event, ui) {
console.log('confirm :: open is called');
},
close: function() {
console.log('confirm :: close is called');
},
resizable: false,
height: "auto",
width: 400,
modal: true,
buttons: {
"Yeap": function() {
console.log('Confirmation button was clicked');
$(this).dialog("close");
console.log('missile launch with code [' + launchCode + '] was confirmed!');
},
"Maybe not just yet": function(ev) {
console.log('Abort button was clicked');
$(this).dialog("close");
console.log('Armageddon was averted');
}
}
});
dialog.dialog('open');
console.log('by this time the dialog should be displayed');
}
$('#missile-launch-confirmation-modal').dialog({
autoOpen: false
});
$('#missile-launch-button').click(missileLaunchButtonClickHandler);
$(document).on('keydown', inputKeyListener);
<link rel='stylesheet' href='https://code.jquery.com/ui/1.11.4/themes/vader/jquery-ui.css'>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<div id='missile-launch-confirmation-modal' title='Confirm missile launch' </div>
<span class="ui-icon ui-icon-alert" style="float:left; margin:12px 12px 20px 0;"></span> Are you sure you want to unleash nuclear Armageddon?
</div>
</div>
<div>
<div>
<div>Enter missile launch code:</div>
<div>
<input id='missile-launch-code-input' type='text' autofocus/>
</div>
<div>
<button id='missile-launch-button' type='button'>OK</button>
</div>
</div>
</div>
更新
在上面的代码中,inputKeyListener
绑定到文档上的 keydown
。将它更窄地绑定到文本输入上的 keydown
,如:
$('#missile-launch-code-input').on('keydown', inputKeyListener);
…导致完全相同的行为。
更新二
表明 stopPropagation
在这里无效,因为“事件冒泡在这里并没有真正发挥作用”并解释说 preventDefault
应该用于“[停止]键事件到达其他页面元素(即那个按钮)”。我对这两个陈述放在一起感到有点困惑。我认为 stopPropagation
恰好 用来阻止“ 键事件到达其他页面元素 ”。此外还有两点混淆。
第一个混淆点是确认对话框 div
不是文本输入 div
的父 DOM 元素,因此不清楚键盘事件是如何在文本输入 div
被 sibling(不是 parent)DOM 元素拦截。我认为这实际上是 stopPropagation
无效的原因,但我仍然不清楚为什么(不管 stopPropagation
)事件到达兄弟 div
中的确认对话框按钮.
第二个混淆点是,如果我们记录我们在 "Yeap" 按钮函数处理程序中捕获的事件,例如像这样:
buttons: {
"Yeap": function(ev) {
console.log(ev);
... 我们在控制台中看到的实际上是:
…所以是鼠标事件,不是确认对话框的键盘事件。鉴于(在一个简单的点击 Enter 的场景中)我们正在生成的唯一 mouse 事件是在 inputKeyListener
:
$('#missile-launch-button').click();
… 这意味着是这个事件导致对话框的确认,而不是我们通过点击 Enter
得到的键盘事件
这似乎是 jQuery UI 的一个例子,它对自己的好处有点太有用了:当 dialog
打开时,它会把里面的第一个按钮放在焦点上,正好赶上 "enter" 键事件触发按钮(这是当用户点击 "enter" 时浏览器的默认行为,同时按钮处于焦点状态。)
在 inputKeyListener
中使用 preventDefault
可阻止按键事件到达其他页面元素(即该按钮)。 stopPropagation
是无害的,但在 inputKeyListener
或 missileLaunchButtonClickHandler
中没有任何有用的效果,因为事件冒泡在这里并没有真正发挥作用。
这是一个没有 preventDefault 或 stopPropagation 的演示,包含一个用于无害地捕捉自动对焦的虚拟按钮,只是为了确认这是正在发生的事情:
function inputKeyListener(evt) {
console.log('key listener - triggered key code is: ' + evt.keyCode);
if (evt.keyCode === $.ui.keyCode.ENTER) {
// $('#missile-launch-button').click(); // Directly calling confirm() doesn't work either
confirm(); // Does too!
}
}
function missileLaunchButtonClickHandler(e) {
confirm();
}
function confirm() {
var launchCode = $('#missile-launch-code-input').val();
const dialog = $('#missile-launch-confirmation-modal');
dialog.dialog({
closeOnEscape: false,
dialogClass: 'no-close',
open: function(event, ui) {
console.log('confirm :: open is called');
},
close: function() {
console.log('confirm :: close is called');
},
resizable: false,
height: "auto",
width: 400,
modal: true,
buttons: {
"Hmmmm": function() {
console.log('First button inside the dialog was clicked.');
},
"Yeap": function() {
console.log('Confirmation button was clicked');
$(this).dialog("close");
console.log('missile launch with code [' + launchCode + '] was confirmed!');
},
"Maybe not just yet": function(ev) {
console.log('Abort button was clicked');
$(this).dialog("close");
console.log('Armageddon was averted');
}
}
});
dialog.dialog('open');
console.log('by this time the dialog should be displayed');
}
$('#missile-launch-confirmation-modal').dialog({
autoOpen: false
});
$('#missile-launch-button').click(missileLaunchButtonClickHandler);
$(document).on('keydown', inputKeyListener);
<link rel='stylesheet' href='https://code.jquery.com/ui/1.11.4/themes/vader/jquery-ui.css'>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<div id='missile-launch-confirmation-modal' title='Confirm missile launch' </div>
<span class="ui-icon ui-icon-alert" style="float:left; margin:12px 12px 20px 0;"></span> Are you sure you want to unleash nuclear Armageddon?
</div>
</div>
<div>
<div>
<div>Enter missile launch code:</div>
<div>
<input id='missile-launch-code-input' type='text' autofocus/>
</div>
<div>
<button id='missile-launch-button' type='button'>OK</button>
</div>
</div>
</div>
关于 event.preventDefault 与 event.stopPropagation
为了扩展这一点,根据 "Update II":stopPropagation
防止事件冒泡到 parent DOM 个节点。通常,例如,单击事件从直接单击的节点向上冒泡通过每个 parent 节点。
这里 stopPropagation
不相关的原因是因为 dialog
不是输入元素的 parent:事件冒泡不会达到 dialog
。所以没有理由用 stopPropagation
停止事件冒泡,因为无论如何它都不会触发任何有意义的事情。
相比之下,event.preventDefault
停止的事件与 DOM 结构无关——这些事件不关心 parent、兄弟、孙子或三代堂兄两次被除名; event.preventDefault
仅仅意味着 "whatever the default behavior of the browser would have been in this situation, don't do that." 因此,例如,在表单提交上 event.preventDefault
会停止提交表单。
在此问题中描述的情况下,如果用户在按钮处于焦点状态时按下 "enter" 键,则浏览器的默认行为是触发点击事件(是的,鼠标事件)在那个按钮上,不管按钮在 DOM 中的什么位置。所以在这里使用 event.preventDefault
可以防止默认行为,这正是你想要的。
首先,您需要在 inputKeyListener 函数中调用 missileLaunchButtonClickHandler。
在您需要将 "preventDefault" 添加到 missileLaunchButtonClickHandler 函数之后,因为当您按下 ENTER 时对话框会自动关闭。 preventDefault避免对话框自动关闭。
将您的 missileLaunchButtonClickHandler 函数更改为:
function missileLaunchButtonClickHandler(e) {
//e.stopPropagation();
e.preventDefault();
confirm();
}
并将您的 inputKeyListener 修改为:
function inputKeyListener (evt) {
console.log('key listener - triggered key code is: '+evt.keyCode);
if (evt.keyCode === $.ui.keyCode.ENTER) {
evt.stopPropagation();
missileLaunchButtonClickHandler(evt);
$('#missile-launch-button').click(); // directly calling confirm() doesn't work either
}
}
这明显是一个SSCCE.
所以我们的任务是编写导弹发射控制系统的前端。我们选择 Spartan 布局,因为这是非常严重的:只有一个文本输入框和一个用于输入代码的按钮:
为了安全起见,单击 "OK" 按钮后,我们将显示一个对话框,要求用户确认:
作为可用性的画龙点睛之笔,我们为 Enter 按钮添加了一个关键侦听器,该侦听器也会导致单击 "OK" 按钮(使用 $.trigger()
) .
遗憾的是,确认对话框仅在用户单击 "OK" 按钮时显示,而在单击 Enter 时不会显示。当我们点击 Enter 时,对话框根本不会出现。
最糟糕的是,在添加一些调试消息后,对话框似乎确实显示了几分之一毫秒,然后由于某种原因单击了 "Yeap" 按钮。所以当按下 Enter 时,导弹发射立即被确认!
Fiddle here.
代码如下:
function inputKeyListener(evt) {
console.log('key listener - triggered key code is: ' + evt.keyCode);
if (evt.keyCode === $.ui.keyCode.ENTER) {
evt.stopPropagation();
$('#missile-launch-button').click(); // Directly calling confirm() doesn't work either
}
}
function missileLaunchButtonClickHandler(e) {
e.stopPropagation();
confirm();
}
function confirm() {
var launchCode = $('#missile-launch-code-input').val();
const dialog = $('#missile-launch-confirmation-modal');
dialog.dialog({
closeOnEscape: false,
dialogClass: 'no-close',
open: function(event, ui) {
console.log('confirm :: open is called');
},
close: function() {
console.log('confirm :: close is called');
},
resizable: false,
height: "auto",
width: 400,
modal: true,
buttons: {
"Yeap": function() {
console.log('Confirmation button was clicked');
$(this).dialog("close");
console.log('missile launch with code [' + launchCode + '] was confirmed!');
},
"Maybe not just yet": function(ev) {
console.log('Abort button was clicked');
$(this).dialog("close");
console.log('Armageddon was averted');
}
}
});
dialog.dialog('open');
console.log('by this time the dialog should be displayed');
}
$('#missile-launch-confirmation-modal').dialog({
autoOpen: false
});
$('#missile-launch-button').click(missileLaunchButtonClickHandler);
$(document).on('keydown', inputKeyListener);
<link rel='stylesheet' href='https://code.jquery.com/ui/1.11.4/themes/vader/jquery-ui.css'>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<div id='missile-launch-confirmation-modal' title='Confirm missile launch' </div>
<span class="ui-icon ui-icon-alert" style="float:left; margin:12px 12px 20px 0;"></span> Are you sure you want to unleash nuclear Armageddon?
</div>
</div>
<div>
<div>
<div>Enter missile launch code:</div>
<div>
<input id='missile-launch-code-input' type='text' autofocus/>
</div>
<div>
<button id='missile-launch-button' type='button'>OK</button>
</div>
</div>
</div>
更新
在上面的代码中,inputKeyListener
绑定到文档上的 keydown
。将它更窄地绑定到文本输入上的 keydown
,如:
$('#missile-launch-code-input').on('keydown', inputKeyListener);
…导致完全相同的行为。
更新二
stopPropagation
在这里无效,因为“事件冒泡在这里并没有真正发挥作用”并解释说 preventDefault
应该用于“[停止]键事件到达其他页面元素(即那个按钮)”。我对这两个陈述放在一起感到有点困惑。我认为 stopPropagation
恰好 用来阻止“ 键事件到达其他页面元素 ”。此外还有两点混淆。
第一个混淆点是确认对话框 div
不是文本输入 div
的父 DOM 元素,因此不清楚键盘事件是如何在文本输入 div
被 sibling(不是 parent)DOM 元素拦截。我认为这实际上是 stopPropagation
无效的原因,但我仍然不清楚为什么(不管 stopPropagation
)事件到达兄弟 div
中的确认对话框按钮.
第二个混淆点是,如果我们记录我们在 "Yeap" 按钮函数处理程序中捕获的事件,例如像这样:
buttons: {
"Yeap": function(ev) {
console.log(ev);
... 我们在控制台中看到的实际上是:
…所以是鼠标事件,不是确认对话框的键盘事件。鉴于(在一个简单的点击 Enter 的场景中)我们正在生成的唯一 mouse 事件是在 inputKeyListener
:
$('#missile-launch-button').click();
… 这意味着是这个事件导致对话框的确认,而不是我们通过点击 Enter
得到的键盘事件这似乎是 jQuery UI 的一个例子,它对自己的好处有点太有用了:当 dialog
打开时,它会把里面的第一个按钮放在焦点上,正好赶上 "enter" 键事件触发按钮(这是当用户点击 "enter" 时浏览器的默认行为,同时按钮处于焦点状态。)
在 inputKeyListener
中使用 preventDefault
可阻止按键事件到达其他页面元素(即该按钮)。 stopPropagation
是无害的,但在 inputKeyListener
或 missileLaunchButtonClickHandler
中没有任何有用的效果,因为事件冒泡在这里并没有真正发挥作用。
这是一个没有 preventDefault 或 stopPropagation 的演示,包含一个用于无害地捕捉自动对焦的虚拟按钮,只是为了确认这是正在发生的事情:
function inputKeyListener(evt) {
console.log('key listener - triggered key code is: ' + evt.keyCode);
if (evt.keyCode === $.ui.keyCode.ENTER) {
// $('#missile-launch-button').click(); // Directly calling confirm() doesn't work either
confirm(); // Does too!
}
}
function missileLaunchButtonClickHandler(e) {
confirm();
}
function confirm() {
var launchCode = $('#missile-launch-code-input').val();
const dialog = $('#missile-launch-confirmation-modal');
dialog.dialog({
closeOnEscape: false,
dialogClass: 'no-close',
open: function(event, ui) {
console.log('confirm :: open is called');
},
close: function() {
console.log('confirm :: close is called');
},
resizable: false,
height: "auto",
width: 400,
modal: true,
buttons: {
"Hmmmm": function() {
console.log('First button inside the dialog was clicked.');
},
"Yeap": function() {
console.log('Confirmation button was clicked');
$(this).dialog("close");
console.log('missile launch with code [' + launchCode + '] was confirmed!');
},
"Maybe not just yet": function(ev) {
console.log('Abort button was clicked');
$(this).dialog("close");
console.log('Armageddon was averted');
}
}
});
dialog.dialog('open');
console.log('by this time the dialog should be displayed');
}
$('#missile-launch-confirmation-modal').dialog({
autoOpen: false
});
$('#missile-launch-button').click(missileLaunchButtonClickHandler);
$(document).on('keydown', inputKeyListener);
<link rel='stylesheet' href='https://code.jquery.com/ui/1.11.4/themes/vader/jquery-ui.css'>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js"></script>
<div id='missile-launch-confirmation-modal' title='Confirm missile launch' </div>
<span class="ui-icon ui-icon-alert" style="float:left; margin:12px 12px 20px 0;"></span> Are you sure you want to unleash nuclear Armageddon?
</div>
</div>
<div>
<div>
<div>Enter missile launch code:</div>
<div>
<input id='missile-launch-code-input' type='text' autofocus/>
</div>
<div>
<button id='missile-launch-button' type='button'>OK</button>
</div>
</div>
</div>
关于 event.preventDefault 与 event.stopPropagation
为了扩展这一点,根据 "Update II":stopPropagation
防止事件冒泡到 parent DOM 个节点。通常,例如,单击事件从直接单击的节点向上冒泡通过每个 parent 节点。
这里 stopPropagation
不相关的原因是因为 dialog
不是输入元素的 parent:事件冒泡不会达到 dialog
。所以没有理由用 stopPropagation
停止事件冒泡,因为无论如何它都不会触发任何有意义的事情。
相比之下,event.preventDefault
停止的事件与 DOM 结构无关——这些事件不关心 parent、兄弟、孙子或三代堂兄两次被除名; event.preventDefault
仅仅意味着 "whatever the default behavior of the browser would have been in this situation, don't do that." 因此,例如,在表单提交上 event.preventDefault
会停止提交表单。
在此问题中描述的情况下,如果用户在按钮处于焦点状态时按下 "enter" 键,则浏览器的默认行为是触发点击事件(是的,鼠标事件)在那个按钮上,不管按钮在 DOM 中的什么位置。所以在这里使用 event.preventDefault
可以防止默认行为,这正是你想要的。
首先,您需要在 inputKeyListener 函数中调用 missileLaunchButtonClickHandler。
在您需要将 "preventDefault" 添加到 missileLaunchButtonClickHandler 函数之后,因为当您按下 ENTER 时对话框会自动关闭。 preventDefault避免对话框自动关闭。
将您的 missileLaunchButtonClickHandler 函数更改为:
function missileLaunchButtonClickHandler(e) {
//e.stopPropagation();
e.preventDefault();
confirm();
}
并将您的 inputKeyListener 修改为:
function inputKeyListener (evt) {
console.log('key listener - triggered key code is: '+evt.keyCode);
if (evt.keyCode === $.ui.keyCode.ENTER) {
evt.stopPropagation();
missileLaunchButtonClickHandler(evt);
$('#missile-launch-button').click(); // directly calling confirm() doesn't work either
}
}