I have a web page with several sections.
To switch from one section to another I don't use scroll but everything is done by clicking (menu, pagination, arrows, etc...)
As soon as the user goes on a section, the background changes color, each section to its color.
I have no problem creating this kind of function but I have a performance question.
Or perhaps it's a question of logic I dunno
It would be better if I bind my event on the scroll and ask for the color change as soon as the section is in the right position
window.addEventListener('scroll', requestAnimationFrame(function(){
if ( sectionPosition === 0 ){
// Do something...
}
}))
or it would be better if I bind my events on the different clickable elements with event delegation
window.addEventListener('click', function(event){
let selector = event.target.getAttribute("href");
if( selector === "#section-one"){
//....
}
if(selector === "#section-two"){
//....
}
})
I think the second option is fine, adding listener on scroll will cause calling the callback with each px you scroll so make the action explicit is better than implicit with many unneeded calls
For the best you need to add click event listener on each element not on the whole window to avoid firing the click callback with any click.
To add click event on targeted item
const sectionOneEl = document.querySelector('#section-one');
sectionOneEl.addEventListener('click', ev => {
// section one click handler
});
Related
I have a special scenario where I need to capture key events on a higher level, and redirect them to a lower level. I'm trying to write code like this, but actually it seems like jQuery is dumbly using the exact same event object, so target is not changed, so Maximum callstack size exceeded...
$("#outer").on("keydown", function(evt) {
if (evt.target !== $("#inner")[0] && !$.contains($("#inner")[0], evt.target)) {
$("#inner").trigger(evt);
}
});
With a sample markup
<div id="#outer">
<div id="#inner">
</div>
</div>
What is the proper way to redirect an event to another element? I could of course manually "hack" the target, but is it safe?
I'm working on a plugin which dynamically shows a menu as a popup or as a modal based on screen size. If I have a modal, I need this key redirection. The consumer should not know about wether it's a popup or a modal. I'm using bootstrap's modal plugin, which captures key events because of a "focus-lock thing", so the only way I can make it transparent is that I redirect these events to my real menu.
You could attach the event handler onto the outer and specify a selector to use to match the event:
$('#outer').on('keydown', '#inner', event => {
/* this is executed when #inner is clicked */
});
If you really want to use trigger, you could put it in a setTimeout:
$('#outer').on('keydown', event => {
if (yourCondition) {
setTimeout(() => $('#inner').trigger(event), 0);
}
});
The event will bubble up and you're right, it's the same event. This isn't a jQuery thing but actually just how javascript works. You may want to set a listener on the child element, and use preventDefault();
$("#outer").on("keydown", doSomeThing);
$("#inner").on("keydown", doSomeThingElse);
function doSomeThing(e) {
}
function doSomeThingElse(e) {
e.preventDefault();
}
this will allow you to separate your listeners into distinct functions.
Thank you all for the answers and comments. A few of you suggested preventing propagation of the event at the #inner div. I need to avoid this, I need a way where any external consumer will see this event just as it was triggered by the #inner element for real.
In the meantime I digged into jQuery source and found this line in trigger.
if ( !event.target ) {
event.target = elem;
}
So when jQuery initializes the event to trigger, it only assignes the element to the target if the target is not yet specified. sure they have some good reasons for this behavior, which I cannot see at this moment.
So the best thing I could come up with is a function like this.
function redirectEvent(currentTarget, evt, newTarget) {
if (currentTarget !== newTarget && !$.contains(currentTarget, newTarget)) {
evt = $.Event(evt.type, evt);
delete evt.target;
$(newTarget).trigger(evt);
}
}
As far as first tests go, I can't see any side-effect or drawback.
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 want to trigger a function if a user clicks anywhere on the page, even clicking on no element or link. Is it possible?
The extension runs only on youtube.com so I can't add every element on the page to the trigger and I assume that every page has different element's ids.
Emmanouil Chountasis is correct, you can use the code at "Detect left mouse button press" to detect a left mouse click crossbrowser.
To the heart of your question, I think what you're looking for is Event Delegation. In jQuery,
// Select a wrapper for the events
$('body')
// Whenever any element in the <body> is clicked
.on('click', '*', function (evt) {
// Emmanouil Chountasis's suggestion would be called right here
if (isLeftClick(evt)) {
// ... do stuff
}
});
See http://learn.jquery.com/events/event-delegation/
Reed's answer works fine, but it triggers the action multiple times. I found this solution that only works on left mouse triggers and executes once per click.
$("body").unbind().click(function() {
//Do Stuff
});
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;
});
So I have a button inside a list row that is used to delete the row from the page (calls ajax stuff to delete the object represented by the row, but that's not important for my question). The whole row is bound to a click event which would redirect to another page.
In other words, the containing row is click bound and the inner button is click bound, which is causing me problems since clicking the inner button also triggers the containing row click event (as it should).
I've tried binding a hover event for all delete buttons that unbinds the row click on mouseover, and rebinds it on mouseout, like this pseudocode below:
$('.delete-button').hover(
function() {
$('.list-row').unbind();
$('.delete-button').bind('click', function() { /* delete action */ });
},
function() {
$('.delete-button').unbind();
$('.list-row').bind('click', function() { /* list row action */ });
}
);
This isn't working very well, and I'm convinced there is a better way to approach it. Should I take the button out of the containing list-row? It's way easier to have it in there since my list row contains custom attributes that have data I need for the ajax calls and I can just var rid = $('.delete-button).parent().attr('row-id'); to get the data, but I'm not opposed to change :)
Thanks!
In your click event handler for the button, you need to call e.stopPropagation(). This will prevent the event from bubbling up the DOM tree. More info here: http://api.jquery.com/event.stopPropagation/
edit: you already accepted (thanks!), but maybe this code snippet would help explain some of the concepts better:
$('.list-row').click(function() {
/* list row action */
});
$('.delete-button').click(function(e) {
// die, bubbles, die
e.stopPropagation();
// if you also need to prevent the default behavior for the button itself,
// uncomment the following line:
// e.preventDefault();
// note that if you are doing both e.stopPropagation() AND e.preventDefault()
// you should just `return false;` at the end of the handler (which is jQuery-
// sugar for doing both of these at once)
/* delete action */
})
There's a few ways of approaching this. As #jmar777 has already said you may attach an altered event to the click handler on the button, stopping propagation.
If you want to do this with the same function as you're applying to the div then you can approach it as such:
if($(event.target).is("input")) {
event.stopPropagation();
}
Another approach is to actually not bind the click event to the button, for any time the browser supports clicks on the containing element. As you will always trigger that, then you don't actually need the button to handle it too! This does require you to handle IE6 etc a little differently from everything else though...
Let your handler function return false