I have a mousedown event attached in an anchor element which does many stuffs.
I also have a mousedown event attached to the document, and because of bubbling this event is called whenever the event attached to anchor is triggered. This is not what I want.
Can I bind a event with delay?
I dont want to use stopPropagation.
$('a').mousedown ->
...
openWindow()
$(document).mousedown ->
...
closeWindow()
Edit
I create a hack
$.fn.onBubble = (events, selector, data, handler) ->
setTimeout =>
this.on events, selector, data, handler
, 0
Work but like very ugly
As one of the comments mentions, the only way to stop events from bubbling is with stopPropagation. That said, if there are both conditions where you do want to prevent bubbling and others where you do not, you can put event.stopPropagation() into an if-statement:
$(...).mousedown(function(event) {
if(/* some condition */) { event.stopPropagation(); }
});
Alternatively you can add a conditional to the event handler attached to the document. For example:
$(document).mousedown(function(event) {
if($(event.target).is("a")) {
return; // if the element that originally trigged this event
// (i.e. the target) is an anchor, then immediately return.
}
/** code that runs if event not from an anchor **/
});
This snippet uses $.fn.is to determine if the event was triggered by an anchor. If it is generated by an anchor, the code immediately returns, which in effect ignores the event bubble.
EDIT in response to comment:
If I understand correctly, you want to close the window, if the user clicks on anything that is not in the window. In that case try this:
function whenWindowOpens { // Called when the window is opened
var windowElement; // Holds actual window element (not jQuery object)
$(document).bind("mousedown", function(event) {
if($.contains(windowElement, event.target)) {
return; // Ignore mouse downs in window element
}
if($(event.target).is(windowElement)) {
return; // Ignore mouse downs on window element
}
/** close window **/
$(this).unbind(event); // detaches event handler from document
});
}
This is basically a variation on the second solution suggested above. The first two if-statements ensure the mouse down did not occur in (using $.contains) or on (using $.fn.is again) the windowElement. When both statements are false, we close the window and unbind the current event handler. Note that $.contains only takes raw DOM elements -- not jQuery objects. To get the raw DOM element from a jQuery object use $.fn.get.
Related
I'm having trouble understanding when/how event listeners are added and have tried to illustrate my general question through this little snippet.
If the toggle button has a mousedown event, then the little box toggles between being displayed or not displayed. Also, if displayed then for a mousedown event anywhere in the document the box will change to not displayed.
My question is why won't it work if evt.stopPropagation is commented out?
If both .evt.stopPropagation and the document.addEventListener are commented out then the toggle works as expected, but only for the button, and the box is not removed from display for mousedown on the document.
If a console.log message is placed inside the clear function when the event listener on the document is active and evt.stopPropagation is commented out, it can be seen that the mousedown on the button also triggers the mousedown on the document. So, clicking the toggle button displays and removes from display at the same time, and the box is never seen.
I was expecting the button event to add the event listener on the document after its own event, such that the document couldn't register a mousedown event for the first event on the button because it wasn't yet declared; but it appears that both listeners are set up at the same time.
Could you please explain? Thank you for considering my question.
"use strict";
document.querySelector('button').addEventListener( 'mousedown', display, false );
function display( evt )
{
evt.stopPropagation();
let e = evt.currentTarget.nextElementSibling;
if ( e.style.display === 'block' )
{
e.style.display = 'none';
}
else
{
e.style.display = 'block';
document.addEventListener( 'mousedown', clear, false );
}; // end if
} // close display
function clear()
{
document.removeEventListener( 'mousedown', clear, false );
document.querySelector('button').nextElementSibling.style.display = 'none';
}
div {
display: none;
background-color: rgb(150,150,150);
border: 1px solid black;
height: 50px;
width: 50px;
margin-top: 10px;
}
<button>Toggle</button>
<div></div>
#traktor53's answer correctly identified what happens, but I fear their explanation is not clear enough.
What you are experiencing here is basically caused by two facts:
MouseEvents do bubble to their ancestors.
EventTarget.addEventListener is a synchronous method.
To understand better, I will avoid speaking about the capture phase for now.
So when a browser is to dispatch an Event on a target, it will first check all the handlers it has to call on that target, then execute all these handlers, and finally bubble up the DOM to the window (of course, only in case of bubbling event).
Taking your example, we can schematize this bubbling phase like so:
[<button>] -> list of handlers: [`ƒ display`]
execute `ƒ display` // (add handler on document)
continue with parentNode
[<body>] -> list of handlers: none
continue with parentNode
[<html>] -> list of handlers: none
continue with ownerDocument
[document] -> list of handlers: [`ƒ clear`] // (added in `display`)
execute `ƒ clear`
continue with defaultView
[window] -> list of handlers: none
As you can see, the EventHandler you add on document is added before the algorithm got to check what are the EventHandlers attached to document. So when the Event is to fire on the document EventTarget, the latter will have this EventHandler attached to itself.
To demonstrate it, we can even build this ladder code, which will add an EventListener to all the ancestors of the original target from inside the first EventHandler:
document.querySelector('button').addEventListener('mousedown', handle, {once:true});
function handle(evt) {
console.log('firing from', this.toString());
const up = this.defaultView || // window for document
this.parentNode || // elements until html
this.ownerDocument; // html
if(up) up.addEventListener('mousedown', handle, {once:true});
}
<button>click me</button>
Doing so, your clear function will get called right after display got called, reverting immediately what display had done.
In the bubbling phase, events bubble up the DOM from the target element which generated the event, via the chain of elements' parent nodes, to the document object and from there to window:
Source 3.1 figure
Calling event.stopPropagation in a handler stops the event bubbling any further up the DOM (of course).
Now if the next sibling of the toggle is not on display, the display event handler stops propagation, shows the next element and registers a listener on the document node to hide the element again.
If stopPropagation is not called, the mousedown event continues to bubble up the DOM looking for "mousedown" listeners to call. It finds the one added by display to the document node ( i.e. the 'clear` function), calls it, and handler execution hides the next element after the toggle.
You never get to see the next element in the brief period between calling the mousedown handler for button and that for document, and if processing the event bubbling blocks screen update, you never will.
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'm using event delegation in the pagination for my website. When you click the < or > buttons it moves the page up or down. The problem is that if you don't release the mouse button, in a split-second it will keep repeating the click handler.
How can I make it so that this event only occurs once per-click? Here's my code:
$(document).on('mousedown', '#prev', function(event) {
// Page is the value of the URL parameter 'page'
if (page != 1) {
page--;
var state = {
"thisIsOnPopState": true
};
history.replaceState(state, "a", "?page=" + page + "");
}
// Refresh is a custom function that loads new items on the page
refresh();
});
You should use "click" event instead of "mousedown" unless you have a unavoidable reason.
But "mousedown" or "touchstart" event occurs when a user start pressing the mouse button or screen and it will not be fired until you release the button and press it again.
So I assume you are using a chattering mouse or mouses which has macro software.
change event into "click" and see if it works and in the case "click" event is not gonna solve the issue,try using another mouse.
FYI,underscore methods _.throttle or _.debounce might help to support chattering mouses.
throttle_.throttle(function, wait, [options])
Creates and returns a new, throttled version of the passed function, that, when invoked repeatedly, will only actually call the original function at most once per every wait milliseconds. Useful for rate-limiting events that occur faster than you can keep up with.
debounce_.debounce(function, wait, [immediate])
Creates and returns a new debounced version of the passed function which will postpone its execution until after wait milliseconds have elapsed since the last time it was invoked. Useful for implementing behavior that should only happen after the input has stopped arriving. For example: rendering a preview of a Markdown comment, recalculating a layout after the window has stopped being resized, and so on.
http://underscorejs.org/
If you want to use a "delegated" event handler rather than a "direct" event handler to bubble up the event, try to use a more specific target selector than $(document) like $('.some-class') where some-class is the class name directly above the #prev element.
I would also use either the mouseup or click events instead to avoid the mousedown event firing while the mouse click is held down.
According to the API:
The majority of browser events bubble, or propagate, from the deepest,
innermost element (the event target) in the document where they occur
all the way up to the body and the document element.
Try this:
// delegated "click" listener using a more specific target selector
$('.some-class').on('click', '#prev', function(event) {})
You may want to check your HTML to see if you are using #prev multiple times. Usually, just creating the listener on the target ID element should work fine.
// direct "click" listener on an ID element
$('#prev').on('click', function(event) {})
I haven't found the answer to this question, but I have found a solution that fixes the problem. What I have done is added a conditional that only allows the click event to occur once-per-click:
var i = 0;
$(document).on('click', '#prev', function(event) {
if (page != 1 && i === 0) {
page--;
var state = {
"thisIsOnPopState": true
};
history.replaceState(state, "a", "?page=" + page + "");
i = 1;
refresh();
}
});
// Resets 'i' for the next click
$(document).on('mouseup', function() {
i = 0;
});
I am trying to use Javascript to emulate the CSS :target pseudo-class so as to capture all events that result in an element on page being targeted. I've identified 3 trigger events:
window.location.hash already targets an element of the same ID on initialisation
An anchor targeting the element is clicked
The hashchange event is fired independently of the above (for example via the window.history API)
Scenario 2 is important as a distinct case since I would want to invoke the click event's preventDefault. The simplified code for this scenario follows:
$('body').on('click', 'a[href*=#]', function filterTarget(clickEvent){
$(this.hash).trigger('target', [clickEvent]);
});
The problem comes when trying to implement scenario 3:
$(window).on('hashchange', function filterTarget(hashChangeEvent){
$(this.hash).trigger('target', [hashChangeEvent]);
});
If a target handler doesn't cancel the native behaviour for scenario 2, it will be triggered again when the native behaviour causes the resulting hashchange event. How can I filter out these edge cases?
POST-SOLUTION EDIT:
roasted's answer held the key — handle a namespaced hashchange event, then unbind and rebind the handler based on logic handled inside the click handler and its preventDefault. I wrote up the full plugin here.
If i understand it, you don't want the hashchange event to be fired if an anchor tag is clicked. You could then set your logic using namespaced events:
DEMO
$('body').on('click', 'a[href*=#]', function (clickEvent) {
filterTarget(clickEvent,this);
$(window).off('hashchange.filter').on('hashchange.tmp', function () {
$(this).off('hashchange.tmp').on('hashchange.filter', filterTarget);
});
});
$(window).on('hashchange.filter', filterTarget);
function filterTarget(event,elem) {
$(elem?elem.hash:window.location.hash).trigger('target', [event]);
//you could filter depending event.type
alert(event.type + '::'+ (elem?elem.hash:window.location.hash));
}
if the click is setting the hash with the fragment anyway, just throw away duplicates in the hash change event:
onhashchange=function(e){
if(e.newURL == e.oldURL ){return; }
//do your normal hashchange event stuff below:
};
ref: https://developer.mozilla.org/en-US/docs/Web/API/window.onhashchange
this fixes cascade issues no matter what invoked the change.
Seems like you could use mousedown instead of click, if you're going to be calling preventDefault on it. Then presumably the hashchange would not be triggered.
My goal is to prevent all the click events (hiding/showing of elements in the HTML when clicked) unless a certain condition is met (the user has a certain word in an input element).
So i tried to add that logic to the click handler of the document or "html" but the click handler of the other element fired first because of bubble up.
So i tried attaching that logic to "*", and now that click handler fires first- but propagates it to the the other element too, ignoring stopPropagation, preventDefault and return false.
$(document).ready(function(){
$("*").click(function(event){
if ($("#user").val() !== "admin"){
console.log("1");
event.stopPropagation();
event.preventDefault();
return false;
}
});
$("#user").click(function(event){
console.log("2");
// do something
});
});
Why "2" is written to the console after "1" when there shouldn't be any further propagation because of return false/stopPropagation?
How else can i achieve my goal using jQuery?
Thanks!
stopPropagation() prevents the event propagating any further up the ancestor tree. However, it doesn't prevent the remaining event handlers on the current from being fired.
To do this (prevent further propagation and prevent any further event handlers on the current element from being fired), you need to call stopImmediatePropagation() (instead, not as well).
Attaching an event handler to every element in this manner, and calling stopImmediatePropagation() (as well as preventDefault()) will prevent all clicks from having an effect; providing no event handlers are bound before (as handlers are executed in order; you can't undo a handler which has already fired).
This doesn't make it nice though, as finding, enumerating over, and attaching a handler to every element is pretty costly.
To make it nicer, your options are either:
Attach a click event to document, and simply preventDefault() and sacrifice stopImmediatePropagation().
Check the state of #user in each event handler; you can ease the pain of this by rolling your own wrapper function;
function checkUserState(then) {
return function () {
if ($("#user").val() !== "admin") {
then.apply(this, arguments);
}
};
};
... use like so;
$("#user").click(checkUserState(function(event){
console.log("2");
}));
As noted in the comments, I'm purposefully avoiding the suggestion of using event delegation, as whilst allows attaching only one event handler instead of n, it doesn't allow you to stopPropagation() of events.