简体   繁体   中英

Mysterious mouse event closes jQuery UI dialog

This is obviously a SSCCE .

So we are tasked with writing the front-end of a missile launch control system. We opt for a Spartan layout given that this is deadly serious: just a text input box and a button to enter the code:

在此输入图像描述

For safety purposes, upon clicking on the "OK" button we will display a dialog asking the user to confirm:

在此输入图像描述

As a usability finishing touch we add a key listener for the Enter button that will also result in clicking the "OK" button (using $.trigger() ).

Unfortunately, the confirmation dialog is only displayed when the user clicks on the "OK" button but not when hitting Enter . When we hit Enter the dialog does not appear at all.

Worst still, after adding some debugging messages it appears that the dialog is indeed displayed for a fraction of a millisecond and then for some reason the "Yeap" button is clicked. So when Enter is hit, missile launch is immediately confirmed!

Fiddle here .

Code below:

 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> 

Update

In the code above the inputKeyListener is bound to keydown on document. Binding it more narrowly to the keydown on the text input, as in:

$('#missile-launch-code-input').on('keydown', inputKeyListener);

… results in the exact same behavior.

Update II

This answer suggests that stopPropagation is ineffective here because " event bubbling isn't really in play here " and explains that preventDefault should be used to " [stop] the key event from reaching other page elements (ie that button) ". I am a bit confused with those two statements taken together. I thought that stopPropagation is precisely what one uses to stop a " key event from reaching other page elements ". Moreover there are two further points of confusion.

The first point of confusion is that the confirmation dialog div is not a parent DOM element of the text input div , so it is unclear how a keyboard event at the text input div is intercepted by a sibling (not parent ) DOM element. I think this is in fact the reason that stopPropagation is ineffective but still it's not clear to me why (regardless of stopPropagation ) the event reaches the confirmation dialog button which lies in a sibling div .

The second point of confusion is that if we log the event we capture in the "Yeap" button function handler, eg like this:

buttons: {
       "Yeap": function(ev) {
       console.log(ev); 

… what we in fact see in the console is:

在此输入图像描述

… so it is a mouse event, not a keyboard event that confirms the dialog. Given that (in the scenario where one simple hits Enter ) the only mouse event we are generating is in the inputKeyListener :

$('#missile-launch-button').click();

… this means that it is this event that results in the confirmation of the dialog, not the keyboard event we get by hitting Enter

This appears to be a case of jQuery UI being slightly too helpful for its own good: when a dialog opens, it puts the first button inside it into focus, just in time for the "enter" key event to trigger the button (which is the browser's default behavior when the user hits "enter" while a button is in focus.)

Using preventDefault in your inputKeyListener stops the key event from reaching other page elements (ie that button). stopPropagation is harmless but will have no useful effect in either inputKeyListener or missileLaunchButtonClickHandler because event bubbling isn't really in play here.

Here's a demo with no preventDefault or stopPropagation, and a dummy button included to harmlessly catch the autofocus, just to confirm that this is what's happening:

 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> 

on event.preventDefault vs event.stopPropagation

To expand on this, per "Update II": stopPropagation prevents events bubbling up to parent DOM nodes. Normally, for example, click events bubble upwards from the node directly clicked on through every parent node.

The reason stopPropagation is irrelevant here is because dialog is not a parent of the input element: event bubbling wouldn't have reached dialog . So there's no reason to stop the event bubbling with stopPropagation , because it wouldn't have triggered anything meaningful anyway.

The events stopped by event.preventDefault , by contrast, have nothing to do with the DOM structure -- these events don't care if parent, sibling, grandchild, or third cousin twice removed; event.preventDefault simply means "whatever the default behavior of the browser would have been in this situation, don't do that." So event.preventDefault on a form submit stops the form being submitted, for example.

In the case described in this question, the default browser behavior if the user hits the "enter" key while a button is in focus is to trigger a click event (which is, yes, a mouse event) on that button, regardless of where the button is in the DOM. So using event.preventDefault here prevents that default behavior, which is what you want.

At first, you need to call the missileLaunchButtonClickHandler inside your inputKeyListener function.

After you need to add a "preventDefault" to your missileLaunchButtonClickHandler function because the dialog is closing automatically when you hit ENTER. The preventDefault avoid the dialog to close automatically.

Change your missileLaunchButtonClickHandler function to this:

function missileLaunchButtonClickHandler(e) {
   //e.stopPropagation();
   e.preventDefault();
   confirm();
 }

and modify your inputKeyListener to this:

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
       }
     }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM