I want to create a one-time event triggered by clicking anywhere. This event is created by clicking a button. I do not want the event to trigger upon clicking the button, only any subsequent clicks anywhere (including the button).
So say I've got some html like the following:
<body>
<div id="someparent">
<div id="btn"></div>
</div>
</body>
And the following javascript (jquery):
$('#btn').click( function() {
$(document).one('click', function() {
console.log('triggered');
});
});
$('#someparent').click(function() {
// this must always be triggered
});
I want to avoid stopping event propagation, but in the above example, the event is bound to document, the event then bubbles up, and the event is triggered.
One way to fix this seems to be to wrap the event creation in a timeout:
$('#btn').click( function() {
setTimeout(function() {
$(document).one('click', function() {
console.log('triggered');
});
}, 1);
});
$('#someparent').click(function() {
// this must always be triggered
});
Now, this works fine, but I'm wondering whether this is safe. Does some notion of order of execution guarantee that this will always work, or is it just working by chance? I know there are other solutions (another nested .one() event for instance), but I'm specifically looking for an answer to how setTimeout and event propagation interoperates.
The following fiddle shows two divs. The first one has the wrong behaviour (the document event is triggered immediately). Clicking the second div, and then anywhere on document (white area) illustrates the wanted behaviour:
https://jsfiddle.net/Lwocuuf8/7/
Event bubbling means that, after an event triggers on the deepest possible element, it then triggers on parents in nesting order.
From: https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#Adding_messages
Adding messages
In web browsers, messages are added any time an event occurs and there
is an event listener attached to it. If there is no listener, the
event is lost. So a click on an element with a click event handler
will add a message--likewise with any other event.
Calling setTimeout will add a message to the queue after the time
passed as second argument. If there is no other message in the queue,
the message is processed right away; however, if there are messages,
the setTimeout message will have to wait for other messages to be
processed. For that reason the second argument indicates a minimum
time and not a guaranteed time.
So, the behaviour you have, is not by chance, you are guaranteed that the messages in the queue will be processed before processing the message you add after a timeout.
Here is a workaround by using a flag instead of a second event :
var documentWaitingClick = false;
$(function() {
$(document).on('click', function(e) {
if (documentWaitingClick) {
//simulate the "one"
documentWaitingClick = false;
console.log('document click');
} else if (e.target.tagName === 'BUTTON') {
documentWaitingClick = true;
console.log('button click')
}
});
});
My understanding of what you want: after clicking an "activation" button, the next click anywhere on the page (including on the "activation" button) should trigger a special event. This is easily handled with a flag:
var activationFlag = false;
$('#btn').click(function(event){
event.preventDefault();
if(!activationFlag) {
event.stopPropagation();
console.log('Global event activated');
}
activationFlag = true;
});
$('#someparent').click(function(event){
if(activationFlag) {
console.log('Time for a special event');
} else {
event.preventDefault();
event.stopPropagation();
}
});
Related
My code -
hits.on('rowSelected', function (evt) {
setTimeout(function() { alert('hi'); }, 5000);
});
so I have a hits table. When a row is selected within that table, this event gets fired. The issue is I want to not be able to select a row again for 5 seconds. IMO, this isn't working because the event on the table gets fired every time a row is selected without giving the setTimeout time to delay.
Is there a better way?
The most elegant solution I could come up with is below, using a button to simulate the behavior.
It works as follows:
Bind a named event handler (handler) to the element, and use jQuery's one() to only execute it once.
In the event handler, handle the event.
Use setTimeout() to wait 5 seconds before re-binding the event handler for the next execution.
let button = $('#button');
button.one('click', function handler() {
console.log('clicked'); // handle event here
setTimeout(() => $(this).one('click', handler), 5000);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="button">Click me!</button>
This approach has the following benefits:
Does not require external variables to hold the state of the event handler (i.e. whether it's active or not).
Does not require unbinding of the event handler, as every time it's only bound for only a single execution.
Does not pollute the outer scope with a named event handler.
What I would recommend is making use of a variable to store whether the element has been clicked or not.
Run your setTimeout() inside an if conditional that checks against this variable. Once the element is clicked, update the variable so that it cannot be clicked again. Then, once the timeout has resolved, enable the click again.
This can be seen in the following:
var hits = $("p");
var clickable = true; // It's clickable by default
hits.on('click', function(evt) {
if (clickable) { // Check whether it can be clicked
clickable = false; // Once clicked, don't allow further clicks
setTimeout(function() {
alert('hi');
clickable = true; // Make it clickable again
}, 5000);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>Click this paragraph.</p>
Hope this helps! :)
Problem
I have a .keypress() event inside of a .click() event. The first time the user clicks on the element, everything works fine, but subsequent clicks trigger the .keypress() event again without "closing" the first one. I've tried adding event.cancelBubble = true; and an empty return statement to break out of the function, but it hasn't worked because predictably, the rest of the code in that execution doesn't get executed but the event is still active and a key press could still trigger it. Is there a way to close the .keypress() event when foo gets clicked?
Code
$(foo).click(function(){
//Do stuff
$(foo).keypress(function(event) {
//Do stuff
});
});
do you mean keypress called more than once?
in your code, every time you click foo, a new anonymous function will be added to keypress event.
unbind the previous keypress event handler before binding new handler
$(foo).click(function(){
//Do stuff
$(foo).off('keypress').on('keypress', function(event) {
//Do stuff
});
});
I'm not entirely sure what you're asking but I think you want to turn off the keypress after the first click. This should toggle it though you may want to make the pressed var global; maybe.
$(foo).click(function(){
//Do stuff
var pressed = false;
$(foo).keypress(function(event) {
if(!pressed){
//Do stuff
pressed = true;
} else {
event.off();
pressed = false;
}
});
});
The key here is event.off() which will remove the listener.
I have an input element with 2 events attached: focus and click. They both fire off the same helper function.
When I tab to the input, the focus event fires and my helper is run once. No problems there.
When the element already has focus, and I click on it again, the click event fires and my helper runs once. No problems there either.
But when the element does not have focus, and I click on it, BOTH events fire, and my helper is run TWICE. How can I keep this helper only running once?
I saw a couple similar questions on here, but didn't really follow their answers. I also discovered the .live jQuery handler, which seems like it could work if I had it watch a status class. But seems like there should be a simpler way. The .one handler would work, except I need this to work more than once.
Thanks for any help!
The best answer here would be to come up with a design that isn't trying to trigger the same action on two different events that can both occur on the same user action, but since you haven't really explained the overall problem you're coding, we can't really help you with that approach.
One approach is to keep a single event from triggering the same thing twice is to "debounce" the function call and only call the function from a given element if it hasn't been called very recently (e.g. probably from the same user event). You can do this by recording the time of the last firing for this element and only call the function if the time has been longer than some value.
Here's one way you could do that:
function debounceMyFunction() {
var now = new Date().getTime();
var prevTime = $(this).data("prevActionTime");
$(this).data("prevActionTime", now);
// only call my function if we haven't just called it (within the last second)
if (!prevTime || now - prevTime > 1000) {
callMyFunction();
}
}
$(elem).focus(debounceMyFunction).click(debounceMyFunction);
This worked for me:
http://jsfiddle.net/cjmemay/zN8Ns/1/
$('.button').on('mousedown', function(){
$(this).data("mouseDown", true);
});
$('.button').on('mouseup', function(){
$(this).removeData("mouseDown");
});
$('.button').on('focus', function(){
if (!$(this).data("mouseDown"))
$(this).trigger('click.click');
});
$(".button").on('click.click',evHandler);
Which I stole directly from this:
https://stackoverflow.com/a/9440580/264498
You could use a timeout which get's cleared and set. This would introduce a slight delay but ensures only the last event is triggered.
$(function() {
$('#field').on('click focus', function() {
debounce(function() {
// Your code goes here.
console.log('event');
});
});
});
var debounceTimeout;
function debounce(callback) {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(callback, 500);
}
Here's the fiddle http://jsfiddle.net/APEdu/
UPDATE
To address a comment elsewhere about use of a global, you could make the doubleBounceTimeout a collection of timeouts with a key passed in the event handler. Or you could pass the same timeout to any methods handling the same event. This way you could use the same method to handle this for any number of inputs.
Live demo (click).
I'm just simply setting a flag to gate off the click when the element is clicked the first time (focus given). Then, if the element gets focus from tabbing, the flag is also removed so that the first click will work.
var $foo = $('#foo');
var flag = 0;
$foo.click(function() {
if (flag) {
flag = 0;
return false;
}
console.log('clicked');
});
$foo.focus(function() {
flag = 1;
console.log('focused');
});
$(document).keyup(function(e) {
if (e.which === 9) {
var $focused = $('input:focus');
if ($focused.is($foo)) {
flag = 0;
}
}
});
It seems to me that you don't actually need the click handler. It sounds like this event is attached to an element which when clicked gains focus and fires the focus handler. So clicking it is always going to fire your focus handler, so you only need the focus handler.
If this is not the case then unfortunately no, there is no easy way to achieve what you are asking. Adding/removing a class on focus and only firing the click when the class isn't present is about the only way I can think of.
I have it - 2 options
1 - bind the click handler to the element in the focus callback
2 - bind the focus and the click handler to a different class, and use the focus callback to add the click class and use blur to remove the click class
Thanks for the great discussion everybody. Seems like the debouncing solution from #jfriend00, and the mousedown solution from Chris Meyers, are both decent ways to handle it.
I thought some more, and also came up with this solution:
// add focus event
$myInput.focus(function() {
myHelper();
// while focus is active, add click event
setTimeout(function() {
$myInput.click(function() {
myHelper();
});
}, 500); // slight delay seems to be required
});
// when we lose focus, unbind click event
$myInput.blur(function() {
$myInput.off('click');
});
But seems like those others are slightly more elegant. I especially like Chris' because it doesn't involve dealing with the timing.
Thanks again!!
Improving on #Christopher Meyers solution.
Some intro: Before the click event fires, 2 events are preceding it, mousedown & mouseup, if the mousedown is fired, we know that probably the mouseup will fire.
Therefore we probably wouldn't like that the focus event handler would execute its action. One scenario in which the mouseup wouldn't fire is if the user starts clicking the button then drags the cursor away, for that we use the blur event.
let mousedown = false;
const onMousedown = () => {
mousedown = true;
};
const onMouseup = () => {
mousedown = false;
// perform action
};
const onFocus = () => {
if (mousedown) return;
// perform action
};
const onBlur = () => {
mousedown = false;
// perform action if wanted
};
The following events would be attached:
const events = [
{ type: 'focus', handler: onFocus },
{ type: 'blur', handler: onBlur },
{ type: 'mousedown', handler: onMousedown },
{ type: 'mouseup', handler: onMouseup }
];
I am having an issue where an prevent double events
so to start with i have a piece of code that triggers
jQuery(window).trigger('swipeForward');
so this listens for this trigger
jQuery(window).on('swipeForward', swipeHandlerNext );
the idea of the swipe handlers is so that a user cant swipe twice and create a double event
this would then execute the swipeHandlerNext function
function swipeHandlerNext(event) {
// If event isn't already marked as handled, handle it
if(event.handled !== true) {
// Kill event handler, preventing any more clicks
jQuery(".pageSize").off("swipeForward");
// Do your stuff here
pageSize = jQuery(".mainHeader").width();
slide("forward", pageSize);
console.log(" swipe complete page forward via swipe");
// Mark event as handled
event.handled = true;
}
return false;
}
this obviously executes the slide function. this is the one that has .animate command
function slide(data, pageSize) {
if (!pageSize) {
pageSize = jQuery(".mainHeader").width();
}
var promise = calcLeft(data, pageSize);
jQuery.when(promise).then(function(result) {
console.log(result);
jQuery('#pageHolder').delay(500).animate({
left: result
}, 400, function() {
console.log("animation started");
calcNav(pageSize);
calcPage(pageSize);
jQuery(".pageSize").on("swipeForward", swipeHandlerNext);
console.log("animation complete");
});
});
}
It is not however preventing the double slide.
Thanks for any help
Why Off() doesn't work in your example
jQuery's off() method expects the selector to match the one originally passed to .on() when attaching event handlers.
In your initial event binding you are attaching the event to the window element with jQuery(window).on(...). But In the handler functions, you are removing and then reattaching the event to the .pageSize element with jQuery('.pageSize').off(...) and jQuery('.pageSize').on(...).
In other words, at not point are you actually removing the event handler bound to the window element and so the user can keep on swiping.
Why event.handled doesn't work in your example
Every time the swipe event takes place, a separate event object is created and passed to the handlers. So the event object is not a global variable which you can modify and check for its status in subsequent swipes.
Possible solutions following your example
Match the selectors passed to the on() and off() methods.
Set and unset a global variable as an indication that a swipe is underway.
Im having some problems with .on() and how to use it instead of .bind() in this situation.
What im trying to do here is i click a link and that is supose to bind another click event, but instead it triggers that event right away. I looked in the documentation/jquery.js file and this is how im suppose to do it.
Demo: http://jsfiddle.net/bNaFV/
$('#click_me').on('click', function(){
$('#show_me').addClass('remain');
//this is only suppose to bind that next time i click anywhere on the document it should hide
// not right away
$(document).on('click', 'html', function(){
$('#show_me').hide();
});
});
$("#click_me").hover(
function () {
$('#show_me').show();
},
function () {
if ($('#show_me').hasClass('remain')){
return;
} else {
$('#show_me').hide();
}
}
);
click me<br /><br />
<div id="show_me"></div>
You need to stop the propagation of the event:
$('#click_me').on('click', function(e){
e.stopPropagation(); //Stop the event from bubbling further
$('#show_me').addClass('remain');
$(document).on('click', 'html', function(){
$('#show_me').hide();
});
});
This is because the event has been captured at the #click_me element. You then bind an event handler for that same event type somewhere higher up the DOM tree. The event then continues bubbling up the tree and reaches the document, where it triggers the new event handler.
Here's a working example.
Update (see comments)
As noted by #zerkms in the comments, I think you probably only want to bind the event handler to document once. You could use the one method to do so, which unbinds the event handler after it's been executed once:
$(document).one('click', 'html', function(){
$('#show_me').hide();
});