I have following simple JS code (https://stackblitz.com/edit/web-platform-ueq5aq?file=script.js):
const baton = document.querySelector('button');
baton.addEventListener('mousedown', (e) => {
console.log('baton');
baton.addEventListener('click', (e) => {
console.log('baton click');
});
});
When I click a button, I get 'baton' and 'baton click' logged to console. Now my question is what exactly happens here? As I understand it, the moment script is executed, handler mousedown is added to even queue. When I actually click button, this handler is run, so it's taken from event queue, added to call stack and it is executed. When it is executed, handler "click" is added to event queue.
How actually event onClick is triggered after onMouseDown? How is that related to event queue? Why onMouseDown handler is run before click event happens? I'm asking because I have a lot more complex code where result is different in different scenarios.
When user navigates to page in SPA which contains similiar script, and then clicks button 'baton' order is:
mousedown event -> handler mousedown -> handler click -> click event
And when user reloads page, so SPA is loaded right on that page, and clicks button 'baton' order is:
mousedown event -> click event -> handler mousedown
I am seeking answer and truth. Any help will be greatly appreciated.
Ps. Unfortunately I'm not able to reproduce this error in example repository - it happens in quite complex web app which production code I can't share here for obvious reasons.
Ps2. Just to clarify, because probably it isn't stated clearly enough: I'm not asking "why mousedown event is triggered before click event", but "why mousedown HANDLER is run before click event". This is NOT obvious, because handlers are not run immediately. In order of handler to be run, it first have to wait to call stack to be empty, so event queue can be processed by JS engine.
The browser tracks the element you clicked the moused down on. Then it tracks the element you lifted the mouse button on. If the element you lifted the mouse button on is the same element or a child element of the target element. Then a click event is dispatched to the last element you lifted the mouse on. The event then propagates up the element chain to every parent element unless the event is told to stop propagating.
If you click down on element A and mouse up on element B. Then A gets mouse down event, and B gets mouse up event, but neither get a click event. Same thing if you navigate the browser to another page in between the mouse down and mouse up.
From MDN Web Docs
An element receives a click event when a pointing device button (such as a mouse's primary mouse button) is both pressed and released while the pointer is located inside the element.
So there is a mouseup event and then the click event.
EDIT after question edit:
"why mousedown HANDLER is run before click event?"
Your already executing mousedown handler registers the click handler so how should the click handler run before it?
All click handlers registered in all previous mousedown handlers will run after the mousedown and mouseup events too.
Perhaps we should start by clarifying a few things.
Events in the browser, are modeled more like a "nesting hierarchy", then a queue -- How it works is referred to as Event Bubbling -- [Wikipedia][1]
But, essentially what you are doing, when adding an EventListener, is hooking into one or more points of the DOM, and saying hey, when X Event passes through here, use function Y to handle it, before passing it along up the stack.
Once an EventListener has been "added" it remains active waiting to be given an event. What exactly it does is defined in its handler function.
let myYFunction = function( e ) { ... }
let myXListener = baton.addEventListern('X event', myYFunction );
// at this point, anytime 'X event' happens to baton, myYFunction will
// be called to handle it...
Now let's take a look at your examples, lets break things down a little,
const baton = document.querySelector('button');
This first line, is simply querying the DOM, to find the first element of type 'button' in the page. Right... This is "where" we want to insert our event handler. We could add them to any element, anywhere in the DOM, we could even hook into the 'body' element if we wanted to.
Ok, then you have this bit,
baton.addEventListener('mousedown', (e) => {
console.log('baton');
baton.addEventListener('click', (e) => {
console.log('baton click');
});
});
Which is "nesting" the creation of the 'click' Event Listener, but only after a 'mousedown' event has been "handled". There is no real reason the 'click' event had to be registered within the function body of the mousedown handler.
If we re-write it a bit, it may be clearer what is actually going on.
baton.addEventListener('mousedown', (e) => {
console.log('baton mousedown');
}
baton.addEventListener('click', (e) => {
console.log('baton click');
});
Additionally I would also point out, that how it is being done currently "works" -- but it is actually hiding a tiny bit of sloppy coding... you see every time the 'mousedown' event is triggered a new 'click' eventListener is being registered... so eventually you may end up with many, many, many click handlers responding to a single 'click' event... Check out MDN to learn more about [this][2]
I hope this answers your initial questions as to what is going on.
To your question "When I click a button, I get 'baton' and 'baton click' logged to console. Now my question is what exactly happens here?" -- To me, it would look something like this:
a 'mousedown' eventListener is added, however nothing "executes"
a 'mousedown' event takes place, now your 'mousedown' listener executes its function, which in turn logs out to the console, and registers a new 'click' handler -- but again, does not execute.
Moving forward, steps 1 and 2 are repeated for every 'mousedown' seen by baton. Additionally, for any 'click' event passed through baton --- which happens after every 'mousedown' on baton:
A 'click' event occurs, your 'click' handler is then executed and logs out to the console.
SPA event handling strategies
When working with SPAs, where multiple "pages" are displayed, in the same page load... it can get messy, all these event listeners hanging around piling up on one another. If you are going to employ eventListeners between "Pages" of your SPA, you might want to look into how to "remove" them too. - [MDN][3]
That way, you only have eventListeners active for the current "Page" of your SPA.
Also, consider "generalizing" your handlers, and attaching them higher up in the DOM... This would allow you to have only a few event listeners which "route" events to their "logical" handlers.
Random/Different Behaviors
With the steps outlines above, 1, 2 and 3 and how they don't all happen at the same time. You will see what appears to be random output to the console... try and run something like this, to get a proper sense of things:
let cCount = 0;
let mCount = 0;
let tCount = 0;
const baton = document.querySelector('button');
baton.addEventListener('mousedown', (e) => {
console.log('mousedown # ' + (mCount++) + ' order:' + tCount++);
baton.addEventListener('click', (e) => {
console.log('click # ' + (cCount++) + ' order:' + tCount++);
});
});
[1]: https://en.wikipedia.org/wiki/Event_bubbling#:~:text=Event%20bubbling%20is%20a%20type,Provided%20the%20handler%20is%20initialized).
[2]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
[3]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
Related
I am working with a very large application with a lot of JavaScript. I am trying to determine how I can find the location where a click event is being removed from a specific element.
There is a simple event listener on a specific field added via jQuery (this is an example).
$(document).ready(function() {
$('#id-name').on('click', function(e) {
window.alert('hello');
});
});
If break execution right after this and examine the element in Chrome Inspector, I see the event attached to the element. However, once I continue execution and the page finishes loading, the element no longer has the event. Something is removing it, but I can't find out where this is happening.
Is there a way to listen for "event removal" and trigger code then, so that I can identify where and how this is getting removed? Any other suggestions in locating where the click event is being removed?
Maybe you could override the removeEventListener method and trace it back to see what's calling it.
window.removeEventListener = (type, listener, useCapture)=>console.trace(type, listener, useCapture)
If an element is clicked twice, the 'dblclick' event is fired. If the element continues to be clicked, the 'dblclick' event does not continue to be fired. The 'dblclick' event will only be fired once until a "cooloff" period is complete, eg. there is a time to wait before another 'dblclick' event can be fired no matter how many times the element is clicked.
I cannot find any documentation that specifies how long must elapse before another 'dblclick' can occur. Does anyone know what the 'dblclick' "cooloff" period is?
I have tried to test this by scripting the clicking of an element, but for some reason javascript-invoked clicks do not trigger 'dblclick' events. So I have tested manually and I can't get a double-click to occurred any sooner than ~400ms after another double-click has occurred. You can try yourself: https://jsfiddle.net/5v4pcx2k/8/
code
If you're wondering, this is basically what it seems like the browser is doing https://jsfiddle.net/b0y5ej2y/3/
There are quite a few bugs and inefficiencies in your jsfiddle.
h1 element has no end tag (you think your ending it with /hi)
you don't need jquery
the general advice around the webs, seem to be don't mix click and dblclick event listeners on the same element
the event already has a property on it that tells you when it fired, you don't need to ask for the current time again with Date.now()
Having said all that, I still couldn't manually click any faster than about ~700ms. I think what's happening is it's the window manager / OS / mouse driver is artificially holding back double clicks to some speed limit.
Here's my version of a jsfiddle test for dblclick speed, with a working programatic dblclick simulation which can dblclick as fast as every 4ms on my computer.
https://jsfiddle.net/stephencarmody/v0b3dpwc/
var lastOne;
foobar.addEventListener('dblclick', function (event) {
log.innerText = 'elapsed: ' + (event.timeStamp - lastOne);
lastOne = event.timeStamp;
});
function simulateClick () {
var event = new MouseEvent('dblclick', {
'view': window,
'bubbles': true,
'cancelable': true
});
foobar.dispatchEvent(event);
}
setInterval(simulateClick, 0);
Comment out the setInterval line to test manual dblclick events.
Is there a way to be notified or perform some callback function once an event has finished propagating in JavaScript?
Equivalently, and more specifically: is there a way to 'prioritize' an event and make sure that it is called after every other event listener has been fired (similarly almost to the !important value in CSS).
For instance, if I have 3 event listeners - 2 attached to the window and 1 to some button element. Can I force a certain one of those events to be called LAST, regardless of where it lies in the DOM? I understand that there are event phases and the ability to attach a listener to the capture or bubbling phase but this still means there's a preset order.
edit: the specific problem:
I'm attempting to build components (in React JS) which are aware of a click being registered outside of themselves (i.e. anywhere on the window/document except themselves) - often as a way of closing/hiding the component. Each of these components will register a listener on the window object which fires a function belonging to that component.
The trouble is, when another component [B] (inherently lower down in the DOM than the window) is clicked to let's say toggle the display of [A], [B]'s event fires first and toggles the state 'showA', the event bubbles up and [A]'s window event listener kicks in and re-toggles the state 'showA' - so, [A] remains hidden after changing state twice. I can't use stopPropagation as other window events need to fire. I've tried to unbind listeners but this doesn't happen in time.
An example of what currently happens all in one go is:
'show thing' button clicked
add listener to window for closing 'thing'
'window but not thing' was clicked
remove listener to close 'thing'
If only I could wait until the click event had finished bubbling before adding the new listener, I'd have no issue
I did leave an answer to your original question but I see you've updated it. I wouldn't say this is React specific but a common implementation for components that need to close/de-activate when the document is clicked.
For instance, the following snippet is an implementation for a speed dial spin out button;
(function () {
var VISIBLE_CLASS = 'is-showing-options',
btn = document.getElementById('.btn'),
ctn = document.getElementById('.ctn'),
showOpts = function(e) {
var processClick = function (evt) {
if (e !== evt) {
ctn.classList.remove(VISIBLE_CLASS);
ctn.IS_SHOWING = false;
document.removeEventListener('click', processClick);
}
};
if (!ctn.IS_SHOWING) {
ctn.IS_SHOWING = true;
ctn.classList.add(VISIBLE_CLASS);
document.addEventListener('click', processClick);
}
};
btn.addEventListener('click', showOpts);
}.call(this));
When the user clicks the button, the container is shown for the speed dial options and an event listener is bound to the document. However, you need to make sure that the initial event that is fired is not the one that triggers the takedown straight away (this is sometime a gotcha). This check is made with if (e !== evt) .... For further clicks the event check is made and the relevant action taken ending in removal of the event listener from the document.
Of course in your particular case if you want to only close when the element isn't clicked then you could make relevant checks on the evt.target and evt.currentTarget in the callback (in the snippet case, processClick).
Hopefully, this can help you out with registering close down callbacks for your individual components.
I am working with an existing web app, in the app there are a variety of submit buttons on different forms, some using regular http post, some defining an onClick function, and some binding a js event handler to the button using a class on the element.
What I want to do, is bind another event handler to these buttons by just adding a class to the buttons, but what I want to determine is will the new event handler be guaranteed to be executed, or could one of the form submit actions happen before it does meaning my new function isn't hit.
The example scenario is I want to add a class to these buttons that bimds them all to a common js function that simply logs usage to some api. Is there a risk that the logging function isn't called because the form submit has navigated away from the page?
I've not done loads of js development, and I could test this 100 times over and just get lucky with it firing.
Below is some code I have tested with for one of the examples - again, I'm not asking how to bind multiple events, the question is to about my understanding of the spec and whether execution of all handlers is guaranteed.
$(document).ready(function(){
$('.testingBtn').click(function() {
window.location.replace("http://stackoverflow.com");
});
$( ".testingBtn" ).click(function(){
alert('submitting!');
});
});
<input class="testingBtn" type="submit" id="submitform" value="Complete Signup" />
As seen above, I can bind the multiple events, and in this example, just directed to another url, but this could be a form.submit() etc. In my testing the alert has always fired first, but am I just getting lucky with the race conditions?
In JS, you don't really have control over what order the event handlers are called, but with careful delegation and well-placed listeners, it is possible.
Delegation is one of the most powerful features of the event model. As you may or may not know: in JS, an event is handed to the top of the dom, from where it propagates down to the element onto which the event should be applied. It stands to reason, therefore, that an event listener attached to the global object will call its handler prior to a listener that has been attached to the element itself.
window.addEventListener('click',function(e)
{
e = e || window.event;
var target = e.target || e.srcElement;
console.log('window noticed you clicked something');
console.log(target);//<-- this is the element that was clicked
}, false);//<-- we'll get to the false in a minute
It's important to note we actually have access to the event object in the handlers. In this case, we left the event object untouched, so it'll just continue to propagate down to the target, on its way down, it might meet with something like this:
document.getElementById('container').addEventListener('click', function(e)
{
e = e || window.event;
var target = e.target || e.srcElement;
if (target.tagName.toLowerCase() !== 'a' || target.className.match(/\bclickable\b/))
{
return e;//<return the event, unharmed
}
e.returnValue = false;
if (e.preventDefault)
{
e.preventDefault();
}
}, false);
Now, this handler will be called after the listener at the window level calls its helper. This time, the event is changed if the clicked element didn't have the clickable class, or the element is a link. The event is canceled, but it lives on, still. The event is still free to propagate further down the dom, so we might encounter something like:
document.getElmentById('form3').addEventListener('click',function(e)
{
e = e || window.event;
if (e.returnValue === false || e.isDefaultPrevented)
{//this event has been changed already
//do stuff, like validation or something, then you could:
e.cancelBubble = true;
if (e.stopPropagation)
{
e.stopPropagation();
}
}
}, false);
Here, by calling stopPropagation, the event is killed off. It can't propagate further down the dom to its target unless the event was already altered. If not, the event object travels further down the DOM, as if nothing happened.
Once it reaches its target node, the event enters its second phase: the bubble phase. Instead of propagating down into the deeps of the DOM, it climbs back up, to the top level (all the way to the global object, where it was dispatched... from whence it came and all that).
In the bubble phase, all the same rules apply as in the propagation phase, only the other way around. The event object will encounter the elements that are closest to the target element first, and the global object last.
There's a lot of handy, and clear diagrams for this here. I can't put it any better than good 'ol quirksmode, so I suggest you read what they have to say there.
Bottom line: when dealing with 2 event listeners, attach them both on a different level to sort-of queue them the way you like.
If you want to guarantee both are called, only stop the event from propagating in that handler that will be called last.
When you've got two listeners, attached to the same element/object for the same event, I've never come across a situation where the listener that was attached first, wasn't also called first.
That's it, I'm off to bed, hoping I made sense
jQuery makes this easy.
$(document).on('click', '.someclass', function() {
doStuff();
});
$(document).on('click', '.someclass', function() {
doMoreStuff();
});
Handlers then both will fire on click. jQuery keeps a queue of handers for you. And handles document clicks that match a selector of your choice so that they can be triggered no matter when your buttons are created.
I am/was having a similar issue as this. However I can not affect the order of/delegate the pre-existing 'click' events (added by Wicket framework).
But I still need to execute a new custom event before any of the 'click' or 'change' events handled by the framework.
Luckily there are several events that are actually executed in order. The 'mousedown' and the 'mouseup' happens to happen before the 'click'.
http://en.wikipedia.org/wiki/DOM_events
$(document).on('mousedown', function (event) {
event = event || window.event
var target = event.target || event.srcElement;
console.log(target + ' before default event'); // Hold mouse button down to see this message in console before any action is executed
});
OR
$(document).on('mouseup', function (event) {
event = event || window.event
var target = event.target || event.srcElement;
alert(target + ' before default event'); // You may not notice this event fires when the page changes unless this is an alert
});
This will allow the logging to be done (e.g. via ajax) before the actual event is executed e.g. a page change via (ajax) link.
Of course you may need to have more sophisticated means to detect for what the additional event handling should be done, but you can use for example the 'target' information for this. => This script monitors everything on the page, as this is how I need this to be done.
Is there any way to know, in a jQuery onmouseup handler, if the event is going to be followed by a click event for the same element?
I have an event handler for a menu hyperlink which unbinds itself when the user either clicks on an element or "drops" (as in drag-n-drop) on an element. I want to avoid prematurely unbinding the handler on mouseup if a click is coming next.
I realize I can track mousedown and mouseup events myself or otherwise hack up a solution (e.g. wait 50 msecs to see if a click comes soon), but I was hoping to avoid rolling my own implementation if there's something built-in for this purpose.
There is nothing built-in because it's really specific to your needs. Thus, there would kilometers of code and documentation to maintain if jQuery would handle any combination of clicks, long clicks, moves, etc.
It's also hard to give you a snippet that satisfies your needs, but a setTimeout is usually the first step to take, with something like that :
obj.mouseup = function (){
obj.click = action; // do action
setTimeout ( function() {
obj.click = functionOrigin // after 500 ms, disable the click interception
}, 500);
};
you can use $(selector).data('events') for that
$('div').mouseup(function(){
if($(this).data('events').click){
console.log('Has a click event handler')
}
});