jQuery unbind event from whole array - javascript

Hi does anybody know how is jquery unbind function woking?I have a jquery powered photogallery where I am binding some stuff on load event(change image and so on)..But when the user is quickly walking through the gallery I want to unbind "older" events and than bind load event only on the last image. Otherwise the user will see every image he walked through for a tiny moment and I only want to show the last one..(performance, UX,..)I store every loaded(not exactly loaded, too every requested) image in an array, so everytime I call my loadImage function I call jQuery(myArrayWithImages).unbind("load.backgroundImagesLoader")
but it seems it does not work, the load events are still there (its a bit weird, I cant find it in debugger, but I see it:))
Is there any way to unbind the events from whole array, or from whole js instance, except do it through foreach?
Here is some code snippet..It is whole loadImage function..
function loadImage(index , showLoading, callback){
if(backgroundImages==undefined) backgroundImages=Array();
//if(backgroundImages[window.location.hash][index]==undefined)backgroundImages[window.location.hash][index] = Array();
jQuery("#galleryEl .sig_thumb a").each( function( pos , el ) {
var wh_bg_b = getVierwportSize();
if(index == pos){
var bg_img_path_b = jQuery(el).attr('href');
var size = 'max';
if ( bg_img_path_b != undefined ) {
if ( (wh_bg_b[0] < 1281) && (wh_bg_b[1] < 801) ) {
size = 'min';
bg_img_path_b = bg_img_path_b.substring( 0, bg_img_path_b.lastIndexOf("/") ) + "/1280/" + bg_img_path_b.substring( bg_img_path_b.lastIndexOf("/")+1 );
} else if ( (wh_bg_b[0] < 1441) && (wh_bg_b[1] < 901) ) {
size = 'med';
bg_img_path_b = bg_img_path_b.substring( 0, bg_img_path_b.lastIndexOf("/") ) + "/1440/" + bg_img_path_b.substring( bg_img_path_b.lastIndexOf("/")+1 );
}
}
console.log("test");
if(backgroundImages[bg_img_path_b]!=undefined){
if(backgroundImages[bg_img_path_b].loaded=="true"){
//console.log(index+" "+backgroundImages[window.location.hash][index][size]['loaded']);
if(typeof callback=='function'){
callback.call(this, bg_img_path_b);
} }
else if(backgroundImages[bg_img_path_b].loaded=="loading"){
jQuery(backgroundImages).unbind('load.backgroundImages');
jQuery(backgroundImages[bg_img_path_b]).bind('load.backgroundImages',function(){
backgroundImages[bg_img_path_b].loaded="true";
if(typeof callback=='function'){
callback.call(this, bg_img_path_b);
//console.log("loaded "+index);
}
});
}
}
else{
backgroundImages[bg_img_path_b]=new Image();
backgroundImages[bg_img_path_b].src = bg_img_path_b;
backgroundImages[bg_img_path_b].loaded="loading";
jQuery(backgroundImages).unbind('load.backgroundImages');
jQuery(backgroundImages[bg_img_path_b]).bind('load.backgroundImages',function(){
backgroundImages[bg_img_path_b].loaded="true";
if(typeof callback=='function'){
callback.call(this, bg_img_path_b);
//console.log("loaded "+index);
}
});
}
console.log(backgroundImages[bg_img_path_b]);
//console.log(size);
//console.log(backgroundImages);
}
} );
}

You need to unbind not like
jQuery(myArrayWithImages).unbind("load.backgroundImagesLoader")
but
jQuery(myArrayWithImages).unbind(load.backgroundImagesLoader).
Just read manual, you can use namespaces:
$('#foo').bind('click.myEvents', handler);
and
$('#foo').unbind('click.myEvents');

Related

How do I make jQuery statement execute after .load()?

Sorry for the possibly terrible question title. I don't know how to word it properly. Please offer suggestions.
Anyhoo, I have this html:
<ul class="image_reel">
<li class="thin"><img class="gal_thumb" src="8681.jpg"></li>
<li class=""><img class="gal_thumb" src="DSC_7586.jpg"></li>
<li class="thin"><img class="gal_thumb" src="DSC_7601.jpg"></li>
</ul>
I want to assign the li either a 'thick' or 'thin' class based on the height of the child img. So I have this jQuery:
// add 'thin' class to parent li
jQuery('.image_reel li a img').load(
function() {
var t = jQuery(this);
var h = this.naturalHeight;
if( h < 800 ) {
jQuery(t).parent().parent().addClass( 'thin' );
}
});
// now sort lis
var $images = jQuery('.image_reel');
var $imagesli = $images.children('li');
$imagesli.sort(function(a,b){
var aT = jQuery(a).hasClass('thin');
var bT = jQuery(b).hasClass('thin');
if( aT == true && ( bT == false ) )
return 1;
else
return 0;
});
$imagesli.detach().appendTo($images);
The problem seems to be that the first block seems to execute -after- the second block. Or maybe they execute synchronously? Regardless, the code doesn't work. So... how do I make the first block of code execute -before- the 2nd?
The weird thing is that, if I use the Firefox 'Q' debugger, the code actually 'works'. But without the debugger it doesn't. I assume that the debugger forces the code to run in some sort of special order.
Wrap your section block in a function, and then call it after the load function ends, like put it in the return
// add 'thin' class to parent li
jQuery('.image_reel li a img').load(function() {
var t = jQuery(this);
var h = this.naturalHeight;
if( h < 800 ) {
jQuery(t).parent().parent().addClass( 'thin' );
}
return loadBlock();
});
function loadBlock() {
// now sort lis
var $images = jQuery('.image_reel');
var $imagesli = $images.children('li');
$imagesli.sort(function(a,b){
var aT = jQuery(a).hasClass('thin');
var bT = jQuery(b).hasClass('thin');
if( aT == true && ( bT == false ) ) {
return 1;
} else {
return 0;
}
});
$imagesli.detach().appendTo($images);
}
Or use a package like async .waterfall or .series
You can use something like async as I mentioned above to have better flow control over async functions here's an example on how you could avoid calling on every callback
async.each($("img"), function(e, callback) {
$(this).load(function() {
console.log("bdbdbd");
callback(); // Done loading image
});
}, function() {
console.log("Done loading images");
loadBlock();
});
Async package can and will be your best friend if utilized properly.
I'm on mobile so I can't really test but this worked just fine on jsbin just throw your code in there and it should work.
this works because when you call jQuery('.image_reel li a img').load this will run its own loop on every element it finds and attach the event listener to it.
The method I used was I used async.each which takes an array, in this case I provided $('#img') as the array which will be a collection of any element it finds that matches the query, then async.each will run the loops in parallel so we don't have to wait for one to finish before the loop can proceed to the next thing.
Then in the loop body we call .load on .this which is attaching the .load function on only 1 element at a time and not trying to do its own internal loop on all the elements, so this way when the function completes we know that its done cause that function is only running on on element. Then we call callback(); which is required for async.each to let .each know that the function body is done and it can proceed, when all loops trigger their callback the loop ends and then the main function executes (the function that's the third argument to .each). You can see more about async.each here: async.each
The second block executes before the first block because the load() resolves immediately and only calls the first block later when it has finished. To do what you want, call the second block at the end of the first block.
// add 'thin' class to parent li
jQuery('.image_reel li a img').load(
function() {
var t = jQuery(this);
var h = this.naturalHeight;
if( h < 800 ) {
jQuery(t).parent().parent().addClass( 'thin' );
}
doWork();
});
function doWork() {
// now sort lis
var $images = jQuery('.image_reel');
var $imagesli = $images.children('li');
$imagesli.sort(function(a,b){
var aT = jQuery(a).hasClass('thin');
var bT = jQuery(b).hasClass('thin');
if( aT == true && ( bT == false ) )
return 1;
else
return 0;
});
$imagesli.detach().appendTo($images);
}

Crossbrowser Javascript Listener

I have the next code working correctly:
var links = document.getElementsByClassName('register');
for(var index = 0; index < links.length; ++index)
{
links[index].addEventListener('click', function(){
var newMixpanelEvent = 'mixpanel.track("Click On Searched List", {"user is logged": "no"})';
trackEvent(newMixpanelEvent);
});
}
This is just listening for a click event, and then executing a function to create an event to Mixpanel.
Now I need to check the addEventListener function and attachEvent to make it work almost all browsers, so I do:
var links = document.getElementsByClassName('register');
for(var index = 0; index < links.length; ++index)
{
if( links[index].addEventListener ) {
links[index].addEventListener('click', function(){
var newMixpanelEvent = 'mixpanel.track("Click On Searched List", {"user is logged": "no"})';
trackEvent(newMixpanelEvent);
});
} else if( links[index].attachEvent ) {
links[index].attachEvent('onclick', function(){
var newMixpanelEvent = 'mixpanel.track("Click On Searched List", {"user is logged": "no"})';
trackEvent(newMixpanelEvent);
});
}
}
But this is not firing the events. Seems like if( links[index].addEventListener ) is not passing. Any idea of why?
Well, the above code works right. The problem was that I had not cleared cache. This is what I previously had and didn't work:
if( window.addEventListener ) {
...
But
if( links[index].addEventListener ) {
...
is working right.
As a final fallback you can add the event through onclick attribute. You should define sperate function with the logic for checking available ways for attaching event handlers.
function addCrossBrowserEvent(element, eventName, callback){
if(element.addEventListener){
element.addEventListener(eventName, callback);
}
else if(element.attachEvent){
element.attachEvent('on' + eventName, callback);
}
else{
element['on' + eventName] = callback;
}
}
Usage:
addCrossBrowserEvent(myElement, 'click', function() {
alert('clicked');
});
Note: Also you can try to design it as extension of HTMLElement prototype.
HTMLElement.prototype.addCrossBrowserEvent = function(eventName, callback) {
if(this.addEventListener){
this.addEventListener(eventName, callback);
}
else if(this.attachEvent){
this.attachEvent('on' + eventName, callback);
}
else{
this['on' + eventName] = callback;
}
}
// Usage
document.getElementById('elementId').addCrossBrowserEvent('click', function() {
// ...
});

Multiple instances of a JavaScript carousel

So I have the following code I have written to build a carousel in JavaScript using Hammer.js and jQuery:
var hCarousel = {
container: false,
panes: false,
pane_width: 0,
pane_count: 0,
current_pane: 0,
build: function( element ) {
hCarousel.container = $(element).find('.hcarousel-inner-container');
hCarousel.panes = $(hCarousel.container).find('> .section');
hCarousel.pane_width = 0;
hCarousel.pane_count = hCarousel.panes.length;
hCarousel.current_pane = 0;
hCarousel.setPaneDimensions( element );
$(window).on('load resize orientationchange', function() {
hCarousel.setPaneDimensions( element );
});
$(element).hammer({ drag_lock_to_axis: true })
.on('release dragleft dragright swipeleft swiperight', hCarousel.handleHammer);
},
setPaneDimensions: function( element ){
hCarousel.pane_width = $(element).width();
hCarousel.panes.each(function() {
$(this).width(hCarousel.pane_width);
});
hCarousel.container.width(hCarousel.pane_width*hCarousel.pane_count);
},
next: function() {
return hCarousel.showPane(hCarousel.current_pane+1, true);
},
prev: function() {
return hCarousel.showPane(hCarousel.current_pane-1, true);
},
showPane: function( index ) {
// between the bounds
index = Math.max(0, Math.min(index, hCarousel.pane_count-1));
hCarousel.current_pane = index;
var offset = -((100/hCarousel.pane_count)*hCarousel.current_pane);
hCarousel.setContainerOffset(offset, true);
},
setContainerOffset: function( percent, animate ) {
hCarousel.container.removeClass("animate");
if(animate) {
hCarousel.container.addClass("animate");
}
if(Modernizr.csstransforms3d) {
hCarousel.container.css("transform", "translate3d("+ percent +"%,0,0) scale3d(1,1,1)");
}
else if(Modernizr.csstransforms) {
hCarousel.container.css("transform", "translate("+ percent +"%,0)");
}
else {
var px = ((hCarousel.pane_width*hCarousel.pane_count) / 100) * percent;
hCarousel.container.css("left", px+"px");
}
},
handleHammer: function( ev ) {
ev.gesture.preventDefault();
switch(ev.type) {
case 'dragright':
case 'dragleft':
// stick to the finger
var pane_offset = -(100/hCarousel.pane_count)*hCarousel.current_pane;
var drag_offset = ((100/hCarousel.pane_width)*ev.gesture.deltaX) / hCarousel.pane_count;
// slow down at the first and last pane
if((hCarousel.current_pane == 0 && ev.gesture.direction == Hammer.DIRECTION_RIGHT) ||
(hCarousel.current_pane == hCarousel.pane_count-1 && ev.gesture.direction == Hammer.DIRECTION_LEFT)) {
drag_offset *= .4;
}
hCarousel.setContainerOffset(drag_offset + pane_offset);
break;
case 'swipeleft':
hCarousel.next();
ev.gesture.stopDetect();
break;
case 'swiperight':
hCarousel.prev();
ev.gesture.stopDetect();
break;
case 'release':
// more then 50% moved, navigate
if(Math.abs(ev.gesture.deltaX) > hCarousel.pane_width/2) {
if(ev.gesture.direction == 'right') {
hCarousel.prev();
} else {
hCarousel.next();
}
}
else {
hCarousel.showPane(hCarousel.current_pane, true);
}
break;
}
}
}
And I call this like:
var hSections;
$(document).ready(function(){
hSections = hCarousel.build('.hcarousel-container');
});
Which works fine. But I want to make it so that I can have multiple carousels on the page which again works... but the overall width of the container is incorrect because it's combining the width of both carousels.
How can I run multiple instances of something like this, but the code know WHICH instance it's interacting with so things don't become mixed up, etc.
The problem is your design is not really suited to multiple instances, because of the object literal which has properties of the carousel, but also the build method.
If I was starting this from scratch, I would prefer a more OOP design, with a carousel class that can you instantiate, or have it as a jQuery plugin. That said, it's not impossible to adapt your existing code.
function hCarousel(selector){
function hCarouselInstance(element){
var hCarousel = {
// insert whole hCarousel object code
container: false,
panes: false,
build : function( element ){
...
};
this.hCarousel = hCarousel;
hCarousel.build(element);
}
var instances = [];
$(selector).each(function(){
instances.push(new hCarouselInstance(this));
});
return instances;
}
Usage
For example, all elements with the hcarousel-container class will become an independant carousel.
$(document).ready(function(){
var instances = hCarousel('.hcarousel-container');
});
Explanation:
The hCarousel function is called passing the selector, which can match multiple elements. It could also be called multiple times if needed.
The inner hCarouselInstance is to be used like a class, and instantiated using the new keyword. When hCarousel is called, it iterates over the matched elements and creates a new instance of hCarouselInstance.
Now, hCarouselInstance is a self contained function that houses your original hCarousel object, and after creating the object it calls hCarousel.build().
The instances return value is an array containing each instance object. You can access the hCarousel properties and methods from there, such as:
instances[0].hCarousel.panes;
jQuery plugin
Below is a conversion to a jQuery plugin, which will work for multiple carousels.
(function ( $ ) {
$.fn.hCarousel = function( ) {
return this.each(function( ) {
var hCarousel = {
// insert whole hCarousel object code here - same as in the question
};
hCarousel.build(this);
});
};
}( jQuery ));
Plugin usage:
$('.hcarousel-container').hCarousel();
I would try turning it into a function which you can use like a class. Then you can create separate objects for your carousels.
So you would have something like the following:
function HCarousel (element) {
this.element=element;
this.container= false;
this.panes= false;
this.pane_width= 0;
this.pane_count= 0;
this.current_pane= 0;
}
And then add each method on the class like this.
HCarousel.prototype.build = function() {
this.container = $(element).find('.hcarousel-inner-container');
this.panes = $(hCarousel.container).find('> .section');
this.pane_width = 0;
this.pane_count = hCarousel.panes.length;
this.current_pane = 0;
this.setPaneDimensions( element );
$(window).on('load resize orientationchange', function() {
this.setPaneDimensions( element );
});
$(this.element).hammer({ drag_lock_to_axis: true }).on('release dragleft dragright swipeleft swiperight', hCarousel.handleHammer);
};
etc. That should give you the basic idea. Will take a little bit of re-writing, but then you can create a carousel with something like this:
var carousel1 = new HCarousel('.hcarousel-container');
Hope that puts you on the right track.
Classes don't actually exist in JS, but this is a way to simulate one using a function. Here's a good article on using classes in JS http://www.phpied.com/3-ways-to-define-a-javascript-class/

Uncaught TypeError: Cannot read property 'firstElementChild' of undefined when expanding posts in a certain order

Can any of you guys help me out with a Javascript problem? I'm trying to write a post expansion feature for my site, and for some reason whenever I expand the first post, the second will no longer expand, and will instead redirect to the full post on the thread page. If I expand the second post and then the first next, I don't have the problem.
Here's the code that runs when the page is loaded:
function postExpansionPrep(){
if(!document.body.className){
var links = document.getElementsByClassName("abbrev");
for (var i = 0; i < links.length; i++ ){
(function(e) {
links[e].firstElementChild.addEventListener("click", function(a){ expandPost(links[e].firstElementChild); a.preventDefault(); }, true);
console.log(links[e].firstElementChild.href);
})(i);
}
}
}
And here's the code that runs when you click on the link to expand a post.
function expandPost(link){
if(link.hash){
$.get(link, function(data) {
var loadedPost = $(data).find("#reply" + link.hash.replace("#",""));
document.getElementById("reply" + link.hash.replace("#","")).lastElementChild.innerHTML = $(loadedPost).find("blockquote").last().html();
});
}
else{
$.get(link, function(data) {
var loadedPost = $(data).find("#parent" + link.pathname.substring(link.pathname.lastIndexOf("/")+1));
document.getElementById("parent" + link.pathname.substring(link.pathname.lastIndexOf("/")+1)).lastElementChild.innerHTML = $(loadedPost).find("blockquote").last().html();
});
}
}
You can see the issue in action here: http://www.glauchan.org/test/
By the way, the feature is opt-in, so you need to enable Post Expansion in the Board Options menu.
The document.getElementsByClassName method returns a live node list. Therefore, the links will change, and potentially have not item at position e any more when the handler is fired - resulting in an undefined value that throws an error when accessing its firstElementChild property.
You should not need to get the current element from the links list. Get it dynamically via a.target or just this:
function postExpansionPrep() {
if (document.body.className)
return;
var links = document.getElementsByClassName("abbrev");
for (var i = 0; i < links.length; i++ ) {
var child = links[i].firstElementChild;
if (!child) // could be null as well
continue;
child.addEventListener("click", function(e) {
expandPost(this);
e.preventDefault();
}, true);
console.log(child.href);
}
}
Btw, as you have jQuery available you should use it, especially for the event handler attaching:
if (!document.body.className)
$(".abbrev > :first-child").click(function(e) {
expandPost(this);
e.preventDefault();
});

Is there a more elegant solution than an if-statement with no else clause?

In the following code, if Control (the element that trigers Toggle's first OL) is not Visible it should be set Visible and all other Controls (Controls[i]) so be Hidden.
.js
function Toggle(Control){
var Controls=document.getElementsByTagName("ol",document.getElementById("Quote_App"));
var Control=Control.getElementsByTagName("ol")[0];
if(Control.style.visibility!="visible"){
for(var i=0;i<Controls.length;i++){
if(Controls[i]!=Control){
Reveal("hide",20,0.3,Controls[i]);
}else{
Reveal("show",20,0.3,Control);
};
};
}else{
Reveal("hide",20,0.3,Control);
};
};
Although the function [Toggle] works fine, it is actually setting Controls[i] to Hidden even if it is already.
This is easily rectified by adding an If statement as in the code below, surely there is a more elegant solution, maybe a complex If condition?
.js
function Toggle(Control){
var Controls=document.getElementsByTagName("ol",document.getElementById("Quote_App"));
var Control=Control.getElementsByTagName("ol")[0];
if(Control.style.visibility!="visible"){
for(var i=0;i<Controls.length;i++){
if(Controls[i]!=Control){
if(Controls[i].style.visibility=="visible"){
Reveal("hide",20,0.3,Controls[i]);
};
}else{
Reveal("show",20,0.3,Control);
};
};
}else{
Reveal("hide",20,0.3,Control);
};
};
Your help is appreciated always.
In the ugly pure javascript code world, your solution is fine. But only because you said "elegant", my answer is use jQuery.
I'll write it probably closer to what it really would be, using behaviour-based code rather than event-based, so this won't EXACTLY match your code.. but, it would look something like:
$('#Quote_app ol').click(function() {
if ($(this).is(':visible')) {
$(this).fadeOut();
} else {
$(this).fadeIn();
$('ol', $(this).parent()).not(this).fadeOut();
}
});
That attaches a click event to every ol element underneath something with ID=Quote_app, and if it's currently visible, hides it, and otherwise, shows it, and hides all other ol elements.
if(Controls[i]!=Control && Controls[i].style.visibility=="visible") {
Reveal("hide",20,0.3,Controls[i]);
}
Not sure about what means what in your code. Stratagy is to do default action for all items first, and then do specifica action for selected item. Something like this:
for(var i=0;i<Controls.length;i++){
if(Controls[i].style.visibility=="visible"){
Reveal("hide",20,0.3,Controls[i]);
};
}
Reveal("show",20,0.3,Control);
if( Controls[i] != Control ) {
if( Controls[i].style.visibility == "visible" ){
Reveal( "hide", 20, 0.3, Controls[i] );
};
} else {
Reveal( "show", 20, 0.3, Control );
};
could be rewritten as:
if ( Controls[i] == Control ) {
Reveal( "show", 20, 0.3, Control );
} else if ( Controls[i].style.visibility == "visible" ) {
Reveal( "hide", 20, 0.3, Controls[i] );
}
To follow on from the jQuery suggestion -
jQuery often has a toggle function which beomes even more attractive in this situation as it reduces your code to a couple of lines. There currently isnt a toggleFade function but it can be easily added, to quote Karl Swedberg:
You can write a custom animation like this:
jQuery.fn.fadeToggle = function(speed, easing, callback) {
return this.animate({opacity: 'toggle'}, speed, easing, callback);
};
Then, you can do this:
$(".bio-toggler").click(function () {
$("#bio-form").fadeToggle();
})
;
This will work without you having to use getComputedStyle, assuming your Reveal("hide", ...) function sets visibility to hidden.
if(Controls [i] !== Control && Controls[i].style.visibility !== "hidden") {
Reveal("hide", 20, 0.3, Controls[i]);
}
With a little monkey-patching you could make this a lot cleaner without using any external framework. I have also taken the liberty to reshuffle the logic based on the assumption that the ordering of animations (if any) is unimportant.
if Control is hidden
loop through Controls as C
hide if C != Control
show if C = Control
else
hide Control
Another way to interpret this algorithm is - as long as Controls contains at least one element (doesn't matter which), the visibility of Control will be toggled. And all (Controls minus Control) will be hidden. So I'm again taking the liberty to assume that there will always be one control in Controls, and that Control will always be toggled.
Here's the monkey-patch++ code for it (also on jsfiddle). This eliminates all ifs and elses from the function.
The Toggle function now looks like this:
function Toggle(Control) {
var Controls = document.getElementsByTagName("ol" ..
var Control = Control.getElementsByTagName("ol")[0];
Control.toggle();
Controls.filter(function(c) {
return c != Control && c.isVisible();
}).hide();
};
Here is the code-behind. NodeList and Array that apply a property on a list of elements:
​NodeList.prototype.forEach = function(f) {
for(var i = 0; i <​ this.length; i++) {
f.apply(null, [this[i]]);
}
};
Array.prototype.forEach = NodeList.prototype.forEach;
NodeList.prototype.filter = function(f) {
var results = [];
for(var i = 0; i < this.length; i++) {
if(f.apply(null, [this[i]])) {
results.push(this[i]);
}
}
return results;
};
Array.prototype.filter = NodeList.prototype.filter;
NodeList.prototype.hide = function() {
this.forEach(function(e) {
e.hide();
});
};
Array.prototype.hide = NodeList.prototype.hide;
NodeList.prototype.show = function() {
this.forEach(function(e) {
e.show();
});
};
Array.prototype.show = NodeList.prototype.show;
These methods apply a property on an individual element:
Element.prototype.isVisible = function() {
return this.style.visibility == 'visible' || this.style.visibility == '';
};
Element.prototype.show = function() {
this.style.visibility = 'visible';
};
Element.prototype.hide = function() {
this.style.visibility = 'hidden';
};
Element.prototype.toggle = function() {
this.isVisible() ? this.hide() : this.show();
};

Categories