Ordering events in jquery - javascript

I have a construct like this.......
<div id="right_top_block" class="drop_head">
<div id="right_top_block_head" class="dropper block_head rounded-corner">
Friends
</div>
<div class="box_container">
<div id="box_list" class="drop_list">
<ul id="right_top_ul">
</ul>
</div>
</div>
</div>
I removed the inner contents as it is too lengthy... and i have a jquery like this...
$(document).ready(function() {
$('.drop_head').each(function(i, e) {
$('.dropper', e).click(function() {
$('.drop_list', e).slideToggle(1500);
});
});
$('.dropper').click(function(e) {
$(e.target).parent().removeClass('leaf_class');
if ($(e.target).parent().height() < 300) $(e.target).parent().addClass('leaf_class');
});
});
Now what happens is, each time the dropper is clicked, a leaf class is added to the right_top_block and then a slideDown(as part of slideToggle) on the correspoding drop_list is done( I have many such).... Now if it is clicked again the leaf class is removed from the right_top_block and then a slideUP(as part of slideToggle) on the corresponding drop_list is performed. But what i really want is, when a slideUp is performed, i want the slideToggle to complete and then i want the leaf_class to be removed. How do i change the order of the event execution? I guess due to the delay 1500 i am specifying in the toggle is the cause for this. While don't want to loose the slow transition that it gives, i want that to be completed and then the leaf class to be removed. How do i do this?

First, two remarks about the code in your question:
The code that runs on click is split into two event handlers: one responsible for the slide animation and the other for the class change. That's quite confusing and actually makes the problem more complex. It would be easier if these two handlers were merged into one, or if one handler was performing the whole slideDown() operation sequence and the other the whole slideUp() sequence.
You're using e.target with an element that doesn't seem to contain any descendant from where a click event would bubble up. That's perfectly valid in itself, but using this instead would be shorter and make your intent clearer.
Now, let's look at your requirements. It looks like you want leaf_class to be added before the slideDown() animation starts, but only removed after the slideUp() animation completes.
You can achieve that in a simpler way with a pair of handlers bound to the toggle event, and explicit calls to slideDown() and slideUp() instead of slideToggle().
$('.dropper').toggle(function() {
var $dropHead = $(this).parent();
$dropHead.addClass("leaf_class");
$(".drop_list", $dropHead).slideDown(1500);
}, function() {
var $dropHead = $(this).parent();
$(".drop_list", $dropHead).slideUp(1500, function() {
$dropHead.removeClass("leaf_class");
});
});

If I've understood what you're asking, then you're looking for a callback function to your slideToggle function:
$('.drop_list', e).slideToggle(1500, function() {
//Do stuff after slide is complete
});
The callback function is executed upon completion of the slide animation. See the jQuery docs for more information.

Related

How to trigger a class change event when the code that changes the class is out of reach

I need to trigger an event on a class when that class changes
The only known change noticed in the DOM is that the class obtains a second class (say the class is "selectable", it becomes "selectable selected")
https://jsfiddle.net/zn1xj7wb/1/
In this fiddle, the blue squares may be selected and the css change happens when the class changes (adds "selected")
The goal is to be able to do something in another part of my code like that:
$("[class*='selectable']").on('classChange', function() {
//do stuff like change the background color or add text
//alert("this selectable div has been selected");
});
I am unsure how to proceed as jquery has no event for a class change, and I cannot add "classChange" the trigger to the hidden part of the code that adds and removes the "selected" class for it to be picked up by my code.
EDIT: the reason I need the trigger to be the class change is that it is a graph that uses up the first click to change the class (select the node of the graph) and so a first click on the div of that class does not register, only the second time, and I cannot have to click twice to //do stuff.
I'm not sure I understand your problem, but what I would do is atach the event to the document, like this:
$(document).on("click",".selectable", function() {
//do your stuff here
});
Now, as I've read you need to do something right after you add the class "selected" to "selectable", so you could do it in the function by checking wether it has the class or not and then do your stuff after you add the class "selected".
$(document).on("click",".selectable", function() {
if($(this).hasClass("selected")){
$(this).removeClass("selected")
//do your stuff
}else{
$(this).addClass("selected")
//do some different stuff
}
});
EDIT: Okay, so that won't work (see comments). However, I was able to come up with another solution. While you could regularly scan the whole DOM for changes using an external library, in this instance, you can make the app more performant by limiting your scope to just the selectable items.
What the following code does (jsfiddle link below) is take an initial sampling of the selected elements on the page. Then, once per event loop, it re-samples those selected elements. For each element that wasn't there before, it triggers a custom event:
$(document).ready(function() {
$('.selectable').on('customSelectEvent', (e) =>{
console.log("hello, world!");
// Do your stuff here
});
// Get the starting list of selectable elements
var selecteds = $('.selected');
// Using setInterval to make sure this runs at the back of the event loop
setInterval(() => {
let loopSelecteds = $('.selected');
$.each(loopSelecteds, function(loopIndex, loopSelected) {
let alreadySelected = false;
$.each(selecteds, function(index, selected) {
if ($(selected).get(0) === $(loopSelected).get(0)) {
alreadySelected = true;
}
});
if (!alreadySelected) {
$(loopSelected).trigger('customSelectEvent');
}
});
selecteds = loopSelecteds;
}, 0);
})
Some things to note here:
setInterval(()=>{...}, 0) is being used to cast this operation to the back of the event loop, so it will evaluate once per turn. Use caution when doing this, because if you do it too much, it can impact performance.
$().get(0) === $().get(0) is testing the DOM elements to see if they are the same element. We don't want to trigger the event if they are. Credit: https://stackoverflow.com/a/19546658/10430668
I'm using $.each() here because it's intelligent enough to handle collections of jQuery objects, which other loops weren't (without some fiddling).
Someone spot check me on this, but you may be able to put the custom event listener elsewhere in the code.
JS Fiddle: https://jsfiddle.net/zn1xj7wb/15/
This is my first answer, which doesn't work in this use case. I'll include it so that users who aren't so stuck can benefit from it:
Is there any reason you can't bind another listener to the click event
and test if it's got the right class? Such as:
$(document).ready(function() {
$(".selectable").click((e) => {
const currentElement = $(e.currentTarget);
// This is a little tricky: don't run the code if it has the class pre-setTimeout()
if (currentElement.hasClass('selected')) {
return;
}
// Using setTimeout to cast the evaluation to the end of the event loop
setTimeout(()=>{
if (currentElement.hasClass('selected')) {
// Do your things here.
alert("selected!");
}
},0);
})
})

Javascript: addEventListener not taking updated DOM into account?

I'm very new to javascript and I'm trying to give some dynamic features to a site I'm working on. In particular, I want to have an unfolding menu item that unfolds and folds back whenever the mouse is on and off of it, respectively.
I got the unfolding part down but the event listener that triggers the folding back does it whenever the mouse if off of the area where the menu item used to be, event though it's just been extended.
The HTML markup looks like this:
<nav id="nav">
<ul>
<li id="elemPlaces"><ul id="drop"><li>Places</li></ul></li>
</ul>
</nav>
The event listeners are declared like this:
<script type="text/javascript">
var extended = false;
var listPlace = (<?php echo json_encode($list_place); ?>);
document.getElementById("elemPlaces").addEventListener("mouseover", extend);
document.getElementById("elemPlaces").addEventListener("mouseout", retract);
</script>
and the extend and retract functions are the following:
function extend()
{
if(!extended)
{
var drop = document.getElementById("drop");
var form = document.createElement("form");
form.setAttribute("action", "place.php");
form.setAttribute("method", "get");
drop.appendChild(form);
for(var i = 0; i < listPlace.length; i++)
{
var li = document.createElement("li");
var input = document.createElement("input");
li.setAttribute("class", "dropOption");
input.setAttribute("type", "submit");
input.setAttribute("name", "location");
input.setAttribute("value", listPlace[i]);
li.appendChild(input);
form.appendChild(li);
}
extended = true;
}
}
function retract()
{
var dropOption = document.getElementsByClassName("dropOption");
while(dropOption[0])
{
dropOption[0].parentNode.removeChild(dropOption[0]);
}
extended = false;
}
I realize it all must look amateurish but like I said, I'm new to this. I'd really appreciate it if someone could help me.
Okay JSFiddle seems to be down, so here is a Plunker
Basically :
Use mouseenter and mouseleave (see this question)
I believe in your case you just want to redirect to a page passing a GET option. You can do it using <a href="yourpage.php?yourOption=yourValue">. It is simpler. You don't need forms for this.
As I said, here it's a bad idea to modify the dom. Or well, it depends on where your list comes from. If your $list_place comes from an AJAX asynchronous request, then obviously you can't do anything else but modify the DOM dynamically. However if you know beforehand what the content of your list will be, best it to just write everything to the HTML, and add classes like class="unexpanded/expanded", and have a CSS .unexpanded{display: none}. Then you just need to toggle/change the class
So basically what you want to so, is make sure the menu stays unfolded a certain amount of time before folding back ? Here are some ideas
Instead of calling retract on mouseout, you could call another function with a timeout, like retract_after
function retract_after(){
setTimeout(function(){ retract() }, 3000);
}
But this could lead to weird situations (imagine the user moves the mouse on the menu just after moving it out, before the timeout expired...). So you might want to lookup how to empty the queue or remove EventListeners.
Or, what you could do, is only attach the "retract" eventListener at the end of the mouseover function (and also eventually with a timeout), and remove the eventlistener while or after it is retracting.
Also the animate() function of jquery library already somewhat produces by default the behavior you're looking (it queues animation, but if a user quickly triggers mouseenter and mouseout event listeners, the menu will keep folding/unfolding till it empties the queue)

How to detect when all animations have stopped?

I'm using this code to stop simultaneous animations on 2 elements:
$('#container').find('*').stop(true, true);
The animation can be stopped by an end user hovering over a button, in which case the animation stops after completion (which is what I want). However, the button hover also initiates another function (removes and reloads the elements), and there's a conflict if that function runs before the animations are complete.
I was thinking that using 'after' or 'complete' with the above code might work, but I can't figure out what the syntax would be.
im not sure what you are trying to achieve, but in order to check whether or not there are running/pending animations on the object using jQuery, you can use .promise().done()
example, somehing of this sort:
var animations_running;
$('#container').promise().done(function() {
animations_running=false;
});
$('#container').on("mouseover",".SomethingInside",function(){
if(animations_running==false){
//...do animations...
animations_running=true;
}
});
you can also add a callback function to your jQuery animations as follows:
$('#container').on("mouseover",".SomethingInside",function(){
if(animations_running==false){
$(this).animate({
left:+=50
},500,function(){
//...this is the callback function...
});
animations_running=true;
}
});

Checking for visibility of a toggled element

I have a button which toggles the visibility of a <div> below it and want to modify the text on the button depending on the visibility of said <div>.
Here is a live demo on jsFiddle
If you click on "Saved Data", the first time it works correctly, but the next time you click the text does not change. This in itself is behaviour that I don't understand.
Now, I could use multiple handlers for slideToggle(), however, elsewhere in the code I also set intervals which load data next to "Cookie data:" and "Server data:". I don't want these intervals to do anything if the <div> is not visible so I use something like this:
this.timer_cookiedata = setInterval(function(){
if (!$savedData.is(':visible'))
{
return null;
}
// ..
});
I'm worried these intervals are not going to work properly because of this is(':visible') business. So the question is, why does this happen (else statement is ignored), and what can I do to mitigate this?
Check out the updated fiddle. When you check for visibility right after you call slideToggle, jQuery may not have updated the visibility of the element yet since the animation takes some time to finish. For this exact reason, slideToggle has a callback you can use to perform operations after the animation has finished:
$(function () {
var $savedData = $('#savedData');
$('#btn-savedData')
.click(function () {
var $button = jQuery(this);
//I'm checking the visibility in the callback. Inside the callback,
//I can be sure that the animation has completed and the visibility
//has been updated.
$savedData.slideToggle('fast', function () {
if ($savedData.is(':visible')) {
$button.html('visible');
} else {
$button.html('not visible');
}
});
});
});​

javascript de-anonymizing a click function

really basic stuff here. I'd like to give a click function a name and assign some parameters to it. The goal is code reusability such that I can write only one generic function for common tasks such as for enabling users to delete various data.
Here's a jsfiddle to show you what I mean.
And here's that code:
the HTML:
<button>delete this</button>
<div data-id="3" class="delete">something bad</div>
<div data-id="4" class="delete">something else bad</div>
and the JS:
// this function would be loaded on my site's template and therefore would be available across my entire site.
function deleteThis(data_id){
$('button').on('click', 'button', function(){
$('div[data-id="'+data_id+'"]').hide();
});
}
var clicked_id=3;
function deleteThis(clicked_id);
// this function would be called on the various pages where users can delete things and this variable, clicked_id, would be assigned=3 by the user's action on that page.
How do I give this button click event a name?
update thanks all! the $('button') should have been $(document.body) or the button's parent element. It works if you make that simple change. You can also do it as Michael Buen suggests below.
Just refactor your code, put the delete functionality on its own function
<button>delete this</button>
<div data-id="3" class="delete">something bad</div>
<div data-id="4" class="delete">something else bad</div>
​
$('button').on('click', function() { deleteImmediately(3) });
function deleteImmediately(id) { -- refactored code
$('div[data-id='+id+']').hide();
}​
Live test: http://jsfiddle.net/e2kuj/2/
In your fiddler, the (function deleteThis(){})() is making it private and you are trying to access it as a global!
I think you're misunderstanding events. deleteThis only makes sense if it's in the handler.
corrected HTML: (don't use custom attributes for referencing HTML - They're slower)
<button>delete this</button>
<div id="del_3" class="delete">something bad</div>
<div id="del_4" class="delete">something else bad</div>
JS: (untested)
var deleteTargetId = 'del_3'; //clicked_id renamed for clarity
function deleteThis(targetId){
$('#'+targetId).remove(); //removes rather than hides the html
}
$('button').click( function(){
deleteThis(deleteTargetId);
} );
Now you could swap deleteTarget and the HTML with that ID would get yoinked.
However, if this is homework, I'm wondering if you understand the assignment. The var named 'clicked_id' suggests the idea is to click the divs to make them disappear and use delegation. That one's easy.
You'll need to understand event delegation and event bubbling to see what's going on here. Basically when something is clicked, the event then fires on the parent element and then that parent element's parent element, all the way up to the HTML tag. This happens with all events and doesn't cause the trouble you might think because containers are rarely assigned listeners for events. Links and buttons are more typically end point nodes or at most contain a span or an image. Usually when bubbling causes a problem it's because somebody's doing something awful with HTML or they should've been using delegation in the first place.
'on' is the new piss-poor name for the once appropriately named and less confusion-prone 'delegate' jquery method. Essentially anything in the body with the class 'delete' triggers the handler. We don't care about the ID since the idea is to kill the div that was clicked and 'this' gives us a reference to it. This is unusual behavior for JQ, since most methods would have 'this' point at the 'body' but it's obviously a more useful thing to point at for event delegation.
$('body').on('click', '.delete', function(e){
$(this).remove(); //this refers to the originally clicked element
} );
//note: not the solution you asked for, but possibly the one you needed.
It was almost right. I updated it -> http://jsfiddle.net/fz5ZT/41/
function deleteThis(id){
$('button').click(function(){
$('div[data-id="'+id+'"]').hide();
});
};
deleteThis(3);​
you can remove the element in a different way:
$('button').on('click', function(){
$(this).next("div").remove();
});
http://jsfiddle.net/fz5ZT/46/
You could just use $('button').on('click', 'button', clickHandler); to reference the clickHandler function.
I am big fan of such things since apart from being reusable it has the following advantages.
I will be able to just send across a patch in case there's a bug in the clickHandler
Someone can augment my method which is not possible with anonymous methods
Readable, and also useful to see the stack trace in case of errors
Hope that helps.
Update:
function clickHandler(ev) {
ev.preventDefault();
// ... handler code
}

Categories