How do I handle a click anywhere in the page, even when a certain element stops the propagation?

JavascriptJqueryEventsJquery Events

Javascript Problem Overview


We are working on a JavaScript tool that has older code in it, so we cannot re-write the whole tool.

Now, a menu was added position fixed to the bottom and the client would very much like it to have a toggle button to open and close the menu, except closing needs to happen automatically when a user starts doing things out side of the menu, for example, when a user goes back into the page, and selects something or clicks on a form field.

This could technically work with a click event on the body, triggering on any click, however there are numerous items in the older code, where a click event was handled on an internal link, and return false was added to the click function, in order for the site not to continue to the link's href tag.

So clearly, a general function like this does work, but not when clicked on an internal link where the return false stops the propagation.

$('body').click(function(){
  console.log('clicked');
});

Is there a way I can force the body click event anyway, or is there another way I can let the menu dissappear, using some global click event or anything similar?

Without having to rewrite all other clicks in the application that were created years ago. That would be a monster task, especially since I have no clue how I would rewrite them, without the return false, but still don't let them go to their href.

Javascript Solutions


Solution 1 - Javascript

Events in modern DOM implementations have two phases, capturing and bubbling. The capturing phase is the first phase, flowing from the defaultView of the document to the event target, followed by the bubbling phase, flowing from the event target back to the defaultView. For more information, see http://www.w3.org/TR/DOM-Level-3-Events/#event-flow.

To handle the capturing phase of an event, you need to set the third argument for addEventListener to true:

document.body.addEventListener('click', fn, true); 

Sadly, as Wesley mentioned, the capturing phase of an event cannot be handled reliably, or at all, in older browsers.

One possible solution is to handle the mouseup event instead, since event order for clicks is:

  1. mousedown
  2. mouseup
  3. click

If you can be sure you have no handlers cancelling the mouseup event, then this is one way (and, arguably, a better way) to go. Another thing to note is that many, if not most (if not all), UI menus disappear on mouse down.

Solution 2 - Javascript

In cooperation with Andy E, this is the dark side of the force:

var _old = jQuery.Event.prototype.stopPropagation;

jQuery.Event.prototype.stopPropagation = function() {
    this.target.nodeName !== 'SPAN' && _old.apply( this, arguments );
};     

Example: http://jsfiddle.net/M4teA/2/

Remember, if all the events were bound via jQuery, you can handle those cases just here. In this example, we just call the original .stopPropagation() if we are not dealing with a <span>.


You cannot prevent the prevent, no.

What you could do is, to rewrite those event handlers manually in-code. This is tricky business, but if you know how to access the stored handler methods, you could work around it. I played around with it a little, and this is my result:

$( document.body ).click(function() {
    alert('Hi I am bound to the body!');
});

$( '#bar' ).click(function(e) {
    alert('I am the span and I do prevent propagation');
    e.stopPropagation();
});
                   
$( '#yay' ).click(function() {
    $('span').each(function(i, elem) {
        var events        = jQuery._data(elem).events,
            oldHandler    = [ ],
            $elem         = $( elem );
    
        if( 'click' in events ) {                        
            [].forEach.call( events.click, function( click ) {
                oldHandler.push( click.handler );
            });
        
            $elem.off( 'click' );
        }
    
        if( oldHandler.length ) {
            oldHandler.forEach(function( handler ) {
                $elem.bind( 'click', (function( h ) {
                    return function() {
                        h.apply( this, [{stopPropagation: $.noop}] );
                    };
                }( handler )));
            });
        }
    });

    this.disabled = 1;
    return false;
});

Example: http://jsfiddle.net/M4teA/

Notice, the above code will only work with jQuery 1.7. If those click events were bound with an earlier jQuery version or "inline", you still can use the code but you would need to access the "old handler" differently.

I know I'm assuming a lot of "perfect world" scenario things here, for instance, that those handles explicitly call .stopPropagation() instead of returning false. So it still might be a useless academic example, but I felt to come out with it :-)

edit: hey, return false; will work just fine, the event objects is accessed in the same way.

Solution 3 - Javascript

If you make sure that this is the first event handler work, something like this might do the trick:

$('*').click(function(event) {
    if (this === event.target) { // only fire this handler on the original element
        alert('clicked');
    }
});

Note that, if you have lots of elements in your page, this will be Really Very Slow, and it won't work for anything added dynamically.

Solution 4 - Javascript

this is the key (vs evt.target). See example.

document.body.addEventListener("click", function (evt) {
    console.dir(this);
    //note evt.target can be a nested element, not the body element, resulting in misfires
    console.log(evt.target);
    alert("body clicked");
});

<h4>This is a heading.</h4>
<p>this is a paragraph.</p>

Solution 5 - Javascript

What you really want to do is bind the event handler for the capture phase of the event. However, that isn't supported in IE as far as I know, so that might not be all that useful.

http://www.quirksmode.org/js/events_order.html

Related questions:

Solution 6 - Javascript

I think this is what you need:

$("body").trigger("click");

This will allow you to trigger the body click event from anywhere.

Solution 7 - Javascript

You could use jQuery to add an event listener on the document DOM.

    $(document).on("click", function () {
        console.log('clicked');
    });

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Solution 8 - Javascript

document.body.addEventListener("keyup", function(event) {
  if (event.keyCode === 13) {
  event.preventDefault();
  console.log('clicked ;)');
}
});

DEMO

https://jsfiddle.net/muratkezli/51rnc9ug/6/

Solution 9 - Javascript

I know this is an old question, but to add to @lonesomeday's answer, you can do the same in vanilla JavaScript with:

document.querySelectorAll('*')
  .forEach(element => element.addEventListener('click', e => {
    console.log('clicked: ', e.target)
  }))

This will add the listener to each element instead of to the body, and from experience this will let you execute the click event even if the page is navigating away or if there's already an onclick with stopPropagation in it.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSanderView Question on Stackoverflow
Solution 1 - JavascriptAndy EView Answer on Stackoverflow
Solution 2 - JavascriptjAndyView Answer on Stackoverflow
Solution 3 - JavascriptlonesomedayView Answer on Stackoverflow
Solution 4 - JavascriptRonnie RoystonView Answer on Stackoverflow
Solution 5 - JavascriptWesley PetrowskiView Answer on Stackoverflow
Solution 6 - JavascriptJames JohnsonView Answer on Stackoverflow
Solution 7 - JavascriptwattryView Answer on Stackoverflow
Solution 8 - JavascriptMurat KezliView Answer on Stackoverflow
Solution 9 - JavascriptDavid KamerView Answer on Stackoverflow