神秘鼠标事件关闭 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 元素,因此不清楚键盘事件是如何在文本输入 divsibling(不是 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 是无害的,但在 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

为了扩展这一点,根据 "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
       }
     }