74

Say I have something like the following to trap the click event of a button:

$("#button_id").click(function() {
  //disable click event
  //do something
  //re-enable click event
}

How do I temporarily disable the click event of the button until the end of the processing of the original click occurs? I basically have the div disappear after the button is clicked, but if the user clicks on the button fast several times, it processes all those clicks before the div gets a chance to disappear. I want to "debounce" the button so that only the first click gets registered before the div disappears.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • similar: http://stackoverflow.com/questions/4674991/intercept-click-event-on-a-button-ask-for-confirmation-then-proceed/4675010#4675010 – cregox Mar 05 '11 at 03:04
  • 3 good ways to do it http://stackoverflow.com/a/1922012/781695 – user Mar 23 '14 at 07:24

12 Answers12

74

I noticed this post was old but it appears top on google and this kind of solution was never offered so I decided to post it anyway.

You can just disable cursor-events and enable them again later via css. It is supported on all major browsers and may prove useful in some situations.

$("#button_id").click(function() {

   $("#button_id").css("pointer-events", "none");
   //do something
   $("#button_id").css("pointer-events", "auto");
}
Defain
  • 1,335
  • 1
  • 10
  • 14
  • This is a good solution in theory but it is not that universally supported: IE support started from version 11 only. This means that there currently remain many browsers for which you have to find another solution. – C.Champagne Dec 18 '14 at 13:07
  • There are easy ways to fix that for older IE versions if you still need to have support for them (you can use the code above in wp8 HTML5 mobile applications but not in 7). In older versions I would recommend using this method to fix it http://www.vinylfox.com/forwarding-mouse-events-through-layers/ – Defain Dec 19 '14 at 06:46
  • -1; like several other answers here, this doesn't work, because you're *synchronously* disabling pointer events, executing the rest of the handler, and re-enabling pointer events, before considering whether to execute the handler for the next click. At least in Chrome (but probably in literally all browsers ever) this will behave completely identically to the same code without the `.css()` calls. See http://jsfiddle.net/at75H/16/ for a demo; open your console, click the button a bunch of times, and observe that the handler runs many times in succession despite you disabling pointer events. – Mark Amery May 05 '16 at 18:21
  • 1
    This will not work for buttons that are clicked programmatically. – Cybernetic Dec 10 '18 at 18:43
  • This is perfect for the temporary use case because to re-enable the click you don't need to know what was on the click in the first place. Just set it back to auto and you're done. – Vincent Jul 22 '20 at 22:24
37

This is a more idiomatic alternative to the artificial state variable solutions:

$("#button_id").one('click', DoSomething);

function DoSomething() {
  // do something.

  $("#button_id").one('click', DoSomething);
}

One will only execute once (until attached again). More info here: http://docs.jquery.com/Events/one

Dave Ward
  • 59,815
  • 13
  • 117
  • 134
  • 1
    -1; this doesn't work. For example, try rapidly clicking the button many times in this fiddle: http://jsfiddle.net/at75H/1/ – Mark Amery Mar 22 '14 at 17:23
  • @MarkAmery: Isn't that because you're reattaching the handler inside the callback? If you don't do that, it works as expected: http://jsfiddle.net/at75H/2/ – Dave Ward Mar 22 '14 at 21:06
  • 1
    You reattach the handler inside the callback in your answer, which I based my fiddle upon. You need to reattach it at *some* point, or else you're permanently disabling the handler instead of temporarily doing so. I think the clean solution here is to defer the reattaching of the handler using a 0ms `setTimeout` call - I'll write an answer about this when I have the chance. – Mark Amery Mar 23 '14 at 00:14
11
$("#button_id").click(function() {
  if($(this).data('dont')==1) return;
  $(this).data('dont',1);
  //do something
  $(this).data('dont',0);
}

Remeber that $.data() would work only for items with ID.

Thinker
  • 14,234
  • 9
  • 40
  • 55
  • I like this solution better than the .one option. I've been working with elements that may be observing than one event. By using .one, or by calling .off and .on again, I would change the order of the callbacks. By adding a data attribute that is observed by the callback functions, you're able to keep the callback order intact – mikeweber Apr 17 '12 at 15:59
  • 3
    Your early return here will *absolutely never* take effect. JavaScript is single threaded, so if there are several calls to this function queued, you can be guaranteed that by the time the second one starts, the first will have finished and set your `dont` attribute back to 0. – Mark Amery Mar 22 '14 at 17:36
8

You can unbind your handler with .off, but there's a caveat; if you're doing this just prevent the handler from being triggered again while it's already running, you need to defer rebinding the handler.

For example, consider this code, which uses a 5-second hot sleep to simulate something synchronous and computationally expensive being done from within the handler (like heavy DOM manipulation, say):

<button id="foo">Click Me!</div>
<script>
    function waitForFiveSeconds() {
        var startTime = new Date();
        while (new Date() - startTime < 5000) {}
    }
    $('#foo').click(function handler() {
        // BAD CODE, DON'T COPY AND PASTE ME!
        $('#foo').off('click');
        console.log('Hello, World!');
        waitForFiveSeconds();
        $('#foo').click(handler);
    });
</script>

This won't work. As you can see if you try it out in this JSFiddle, if you click the button while the handler is already executing, the handler will execute a second time once the first execution finishes. What's more, at least in Chrome and Firefox, this would be true even if you didn't use jQuery and used addEventListener and removeEventListener to add and remove the handler instead. The browser executes the handler after the first click, unbinding and rebinding the handler, and then handles the second click and checks whether there's a click handler to execute.

To get around this, you need to defer rebinding of the handler using setTimeout, so that clicks that happen while the first handler is executing will be processed before you reattach the handler.

<button id="foo">Click Me!</div>
<script>
    function waitForFiveSeconds() {
        var startTime = new Date();
        while (new Date() - startTime < 5000) {}
    }
    $('#foo').click(function handler() {
        $('#foo').off('click');
        console.log('Hello, World!');
        waitForFiveSeconds();

        // Defer rebinding the handler, so that any clicks that happened while
        // it was unbound get processed first.
        setTimeout(function () {
            $('#foo').click(handler);
        }, 0);
    });
</script>

You can see this in action at this modified JSFiddle.

Naturally, this is unnecessary if what you're doing in your handler is already asynchronous, since then you're already yielding control to the browser and letting it flush all the click events before you rebind your handler. For instance, code like this will work fine without a setTimeout call:

<button id="foo">Save Stuff</div>
<script>
    $('#foo').click(function handler() {
        $('#foo').off('click');
        $.post( "/some_api/save_stuff", function() {
            $('#foo').click(handler);
        });
    });
</script>
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • Thank you for this. I ran into this exact issue the past couple of days where I needed to temporarily unbind a focus event we are using. – Donald Matheson May 05 '16 at 15:43
  • Thank you! I had a slightly different use case, but using setTimeout allows the function to exit before the click handler gets reattached. This saved me after many hours of troubleshooting. – mags Dec 13 '19 at 16:10
6

You can do it like the other people before me told you using a look:

A.) Use .data of the button element to share a look variable (or a just global variable)

if ($('#buttonId').data('locked') == 1)
    return
$('#buttonId').data('locked') = 1;
// Do your thing
$('#buttonId').data('locked') = 0;

B.) Disable mouse signals

$("#buttonId").css("pointer-events", "none");
// Do your thing
$("#buttonId").css("pointer-events", "auto");

C.) If it is a HTML button you can disable it (input [type=submit] or button)

$("#buttonId").attr("disabled", "true");
// Do your thing
$("#buttonId").attr("disabled", "false");

But watch out for other threads! I failed many times because my animation (fading in or out) took one second. E.g. fadeIn/fadeOut supports a callback function as second parameter. If there is no other way just do it using setTimeout(callback, delay).

Greets, Thomas

Chaoste
  • 544
  • 6
  • 17
  • 1
    JavaScript executed in the browser is single-threaded; I don't know what you mean by "watch out for other threads", but I'm pretty sure you're misunderstanding what's going on. – Mark Amery Jul 10 '15 at 17:14
  • 1
    pointer-events is the way to go for links, but compatibility only goes to IE 11. – Alexandre Martini Aug 22 '16 at 09:02
3

If #button_id implies a standard HTML button (like a submit button) you can use the 'disabled' attribute to make the button inactive to the browser.

$("#button_id").click(function() {
    $('#button_id').attr('disabled', 'true');

    //do something

     $('#button_id').removeAttr('disabled');
});   

What you may need to be careful with, however, is the order in which these things may happen. If you are using the jquery hide command, you may want to include the "$('#button_id').removeAttr('disabled');" as part of a call back, so that it does not happen until the hide is complete.

[edit] example of function using a callback:

$("#button_id").click(function() {
    $('#button_id').attr('disabled', 'true');
    $('#myDiv').hide(function() { $('#button_id').removeAttr('disabled'); });
});   
FerrousOxide
  • 644
  • 1
  • 5
  • 11
  • Like most answers here, this answer (or at least the first code example) is flawed: your first example *synchronously* disables the button, executes the handler, and re-enables the button, all *before* the browser considers how to process the next click. See http://jsfiddle.net/at75H/18/ for a demo of this failing to work; if you open your console and click the button many times, you will see the handler fire many times, despite adding the `disabled` attribute. You need to defer the removal of the attribute (e.g. with `setTimeout`) for this to work. – Mark Amery May 05 '16 at 18:46
0

Try utilizing .one()

var button = $("#button"),
  result = $("#result"),
  buttonHandler = function buttonHandler(e) {
    result.html("processing...");
    $(this).fadeOut(1000, function() {
      // do stuff
      setTimeout(function() {
        // reset `click` event at `button`
        button.fadeIn({
          duration: 500,
          start: function() {
            result.html("done at " + $.now());
          }
        }).one("click", buttonHandler);

      }, 5000)
    })
  };

button.one("click", buttonHandler);
#button {
  width: 50px;
  height: 50px;
  background: olive;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>
<div id="result"></div>
<div id="button">click</div>
guest271314
  • 1
  • 15
  • 104
  • 177
  • Almost threw a -1 at this, but perhaps I'm being too harsh. Unlike most answers, this *works* (since you defer readding the handler with a `setTimeout`), but there's no explanation of what's going on; I don't feel like this would be useful to me if I didn't already understand the need to defer readding the handler. – Mark Amery May 05 '16 at 18:53
  • @MarkAmery _Almost threw a -1 at this, but perhaps I'm being too harsh. Unlike most answers, this works (since you defer readding the handler with a `setTimeout`), but there's no explanation of what's going on_ The actual approach is to utilize `.one()`. `setTimeout` , `.fadeOut()` , `.fadeIn()` were for demonstration purpose of using `.one()`. That is, event is called at most once; re-attach event using `.one()` when all synchronous or asynchronous processes complete. If click at `button` occurs before tasks completed, no event would be attached; event is attached, re-attached one at a time – guest271314 May 05 '16 at 19:04
0

it is better that use current event and dont save handler in global handler. i get current element event then unbind then bind again. for a handler.

var element =  $("#elemid")[0];
var tempHandler = jQuery._data(element)["events"]["click"][0].handler;
$("#elemid").unbind("click");

// do the job that click not suppose to listen;
$("#elemid").bind("click" , tempHandler );

for all handler

var element =  $("#elemid")[0];
var clickHandlerList = jQuery._data(element)["events"]["click"];
var handlerList = [];
for(var i  = 0 ; i <  clickHandlerList .length ; i++) {
    handlerList .push(clickHandlerList [i].handler);
}
$("#elemid").unbind("click");
// do the job that click not suppose to listen;
for(var i  = 0 ; i <  handlerList.length ; i++) {
    // return back all handler to element.
    $("#elemid").bind("click" , handlerList[i]);
}
mehdi
  • 645
  • 1
  • 9
  • 9
0

I just barely ran into this problem when trying to display a loading spinner while I waited for a function to complete. Because I was appending the spinner into the HTML, the spinner would be duplicated each time the button was clicked, if you're not against defining a variable on the global scale, then this worked well for me.

    var hasCardButtonBeenClicked = '';
    $(".js-mela-card-button").on("click", function(){  
        if(!hasCardButtonBeenClicked){
            hasCardButtonBeenClicked = true;
            $(this).append('<i class="fa fa-circle-o-notch fa-spin" style="margin-left: 3px; font-size: 15px;" aria-hidden="true"></i>');
        }    
    });

Notice, all I'm doing is declaring a variable, and as long as its value is null, the actions following the click will occur and then subsequently set the variables value to "true" (it could be any value, as long as it's not empty), further disabling the button until the browser is refreshed or the variable is set to null.

Looking back it probably would have made more sense to just set the hasCardButtonBeenClicked variable to "false" to begin with, and then alternate between "true" and "false" as needed.

Tyler Edwards
  • 184
  • 2
  • 9
-1

This example work.


HTML code:

  <div class="wrapper">
     <div class="mask">Something</div> 
  </div>

jQuery:

    var fade = function(){
        $(".mask").fadeToggle(500,function(){
            $(this).parent().on("click",function(){
                $(this).off("click");
                fade();
            });
        });
    };

    $(".wrapper").on("click",function(){
        $(this).off("click");
        fade();     
    });
Adnan Mulalic
  • 129
  • 2
  • 3
-2
$("#button_id").click(function() {
    $('#button_id').attr('disabled', 'true');
    $('#myDiv').hide(function() { $('#button_id').removeAttr('disabled'); });
}); 

Don't use .attr() to do the disabled, use .prop(), it's better.

SeanWM
  • 16,789
  • 7
  • 51
  • 83
-2

This code will display loading on the button label, and set button to disable state, then after processing, re-enable and return back the original button text:

$(function () {

        $(".btn-Loading").each(function (idx, elm) {
            $(elm).click(function () {
                //do processing
                if ($(".input-validation-error").length > 0)
                    return;
                $(this).attr("label", $(this).text()).text("loading ....");
                $(this).delay(1000).animate({ disabled: true }, 1000, function () {
                    //original event call
                    $.when($(elm).delay(1000).one("click")).done(function () {
                        $(this).animate({ disabled: false }, 1000, function () {
                            $(this).text($(this).attr("label"));
                        })
                    });
                    //processing finalized
                });
            });
        });
        // and fire it after definition
    }
   );
sgress454
  • 24,870
  • 4
  • 74
  • 92
Mohamed.Abdo
  • 2,054
  • 1
  • 19
  • 12