神秘的鼠标事件关闭 jQuery UI 对话框

Posted

技术标签:

【中文标题】神秘的鼠标事件关闭 jQuery UI 对话框【英文标题】:Mysterious mouse event closes jQuery UI dialog 【发布时间】:2017-10-16 02:00:02 【问题描述】:

这显然是SSCCE。

因此,我们的任务是编写导弹发射控制系统的前端。我们选择 Spartan 布局,因为这非常严重:只有一个文本输入框和一个用于输入代码的按钮:

为了安全起见,点击“确定”按钮后,我们将显示一个对话框,要求用户确认:

作为可用性的最后润色,我们为 Enter 按钮添加了一个按键侦听器,该侦听器也将导致单击“确定”按钮(使用 $.trigger())。

不幸的是,确认对话框仅在用户单击“确定”按钮时显示,而在按 Enter 时不显示。当我们点击 Enter 时,对话框根本不会出现。

更糟糕的是,在添加了一些调试消息后,似乎对话框确实显示了几分之一毫秒,然后由于某种原因单击了“是”按钮。所以当Enter被击中时,导弹发射立即被确认!

小提琴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);

…导致完全相同的行为。

更新二

This answer 建议 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

得到的键盘事件

【问题讨论】:

我刚刚将evt.stopPropagation(); 替换为evt.preventDefault();,它按预期工作。 @GauravBhor 是的,但为什么呢? preventDefault() 用于防止“浏览器默认行为”。在这种情况下,“浏览器的默认行为”如何将鼠标事件发送到不相关的 div 元素? 【参考方案1】:

这似乎是 jQuery UI 对其自身的好处有点过于有用的情况:当 dialog 打开时,它会将其中的第一个按钮置于焦点,正好赶上“enter”键事件触发按钮(这是当用户在按钮处于焦点时点击“enter”时浏览器的默认行为。)

在您的inputKeyListener 中使用preventDefault 会阻止键事件到达其他页面元素(即该按钮)。 stopPropagation 是无害的,但在 inputKeyListenermissileLaunchButtonClickHandler 中都没有有用的效果,因为事件冒泡在这里并没有真正发挥作用。

这是一个没有 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

为了对此进行扩展,根据“更新 II”:stopPropagation 防止事件冒泡到父 DOM 节点。通常,例如,单击事件从直接单击的节点向上冒泡,通过每个父节点。

stopPropagation 在这里无关紧要的原因是因为dialog 不是输入元素的父元素:事件冒泡不会到达dialog。所以没有理由用stopPropagation 阻止事件冒泡,因为它不会触发任何有意义的事情。

相比之下,event.preventDefault 停止的事件与 DOM 结构无关——这些事件不关心父母、兄弟姐妹、孙子或第三代堂兄是否被两次删除; event.preventDefault 只是意味着“无论浏览器在这种情况下的默认行为是什么,都不要那样做。”例如,表单提交上的event.preventDefault 会停止提交表单。

在这个问题中描述的情况下,如果用户在按钮处于焦点时点击“输入”键,则默认浏览器行为是在该按钮上触发点击事件(是的,鼠标事件),无论按钮在 DOM 中的哪个位置。所以在这里使用event.preventDefault 可以防止这种默认行为,这正是你想要的。

【讨论】:

【参考方案2】:

首先,您需要在 inputKeyListener 函数中调用 missileLaunchButtonClickHandler

在您需要向 missileLaunchButtonClickHandler 函数添加“preventDefault”之后,因为当您按下 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
       
     

【讨论】:

以上是关于神秘的鼠标事件关闭 jQuery UI 对话框的主要内容,如果未能解决你的问题,请参考以下文章

拖动元素jquery ui时连续触发鼠标悬停事件?

jquery-ui-dialog - 如何挂钩对话框关闭事件

Jquery UI 对话框关闭事件所有可能的方式合二为一

jquery-ui-dialog - 如何挂钩到对话框关闭事件

如何避免jQuery UI可拖动也触发点击事件[重复]

如何避免jQuery UI可拖动也触发点击事件[重复]