I'm doing my first set of javascript unit tests against a plugin I'm writing.
It's based on a jquery ui widget and looks like this
$.widget("my.carousel", {
// Set up the widget
_create: function () {
var self = this;
_items = $(this.element).children();
_totalItems = _items.length;
_items.each(function(index) {
$(this).addClass('my-carousel-item');
if (index > 0) {
$(this).effect("scale", { percent: 50 });
}
});
}
So when it gets applied to an element it gets all of it's children and scales them to 50% apart from the first.
In my qunit tests I have
$(document).ready(function () {
test('my-carousel-items are scaled to a given percentage if they are not the current item', function () {
//setup
var list = $('<div id="testDiv" />');
for (var i = 0; i < 5; i++) {
$('<div style="height:40px; background-color:red; margin-bottom:5px;">').appendTo(list);
}
$('#testArea').append(list);
var carousel = list.carousel();
expect(5);
carousel.children().each(function(index) {
if (index == 0)
equals($(this).css('height'), 40);
else
equals($(this).css('height'), 20);
});
});
});
Altough this does actually display correctly (the height value of the elements is 20px) the results of the test fail. I assume it's because the scale effect is applied after the test has run. If anyone has any advice to give i'd welcome it.
You have to stop the animations before running the test.
$("*").stop(false, true);
Or you can wait them to finish (described here)
var wait = setInterval(function() {
if(!$("#testDiv div").is(":animated") ) {
clearInterval(wait);
// run tests
}
}, 100);
Related
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/
What I need to achieve is if we click on submit button, there is particular div should show up.
Here is my code:
http://jsfiddle.net/7tn5d/
But if I click on submit button multiple times, the function calls sort of queue up and run one after other.
Is there a way to invalidate other onclicks when current animation is running?
Code:
animating = 0;
doneanim = 0;
$(function () {
$("#submit_tab").click(function (e) {
if (animating == 1) return;
animating = 1;
$("#submit_cont").show("blind", {}, 1000);
animating = 0;
});
});
To prevent it from performing the action multiple times, simple cease the previous animation. So:
$('#submit_cont').stop().show("blind",{},1000);
However, I have noticed that you have attempted to prevent the animation from running, if an animation is already running. Although it takes 1 second or 1000 milliseconds to show the div, the execution of the condition does not pause until the animation is complete. You must define a function to run after the animation is complete, like so:
animating = 0;
doneanim = 0;
$(function () {
$("#submit_tab").click(function (e) {
if (animating == 1) return;
animating = 1;
$("#submit_cont").show("blind", 1000, function() { animation = 0; });
});
});
Hope that helped...
You almost got it right with the semaphore! It's just that, in jQuery's show(), you would have to put the semaphore reset as an argument. Here's the fixed version - http://jsfiddle.net/snikrs/xe5A3/
animating = 0;
doneanim = 0;
$(function () {
$("#submit_tab").click(function (e) {
if (animating == 1) return;
animating = 1;
$("#submit_cont").show("blind", 1000, function() {
animating = 0;
});
});
});
You can use the :animated selector to check:
$(function () {
$("#submit_tab").click(function (e) {
var $cont = $("#submit_cont");
if (!$cont.is(':animated')) {
$cont.show("blind", {}, 1000);
}
});
});
Now if you stick with the external semaphore idea then its better to stick that on the elemnt with .data() instead of using a global variable:
$(function () {
$("#submit_tab").click(function (e) {
var $cont = $('#submit_cont'),
animating = $cont.data('isAnimating');
if (animating) {
return;
} else {
$cont.data('isAnimating', 1);
$("#submit_cont").show("blind", 1000, function() { $cont.data('isAnimating', 0); });
}
});
});
Something like this (see documentation) :)
$("#submit_cont").show("blind", function(){
animating = 0;
});
You can add a $("#submit_cont").clearQueue(); after the animation finished :
$("#submit_tab").click(function (e) {
$("#submit_cont").show("blind", 1000, function() {
$("#submit_cont").clearQueue();
});
});
Updated JSFiddle
I found a different solution for this, which in my opinion looks cleaner:
var tab = $("submit_tag");
tab.on("click", function(){
var cont = $("submit_cont");
var animating = tab.queue("fx").length;
if(animating === 0){
cont.show("blind", {}, 1000);
}
});
I wrote a slideshow plugin, but for some reason maybe because I've been working on it all day, I can't figure out exactly how to get it to go back to state one, once it's reached the very last state when it's on auto mode.
I'm thinking it's an architectual issue at this point, because basically I'm attaching the amount to scroll left to (negatively) for each panel (a panel contains 4 images which is what is currently shown to the user). The first tab should get: 0, the second 680, the third, 1360, etc. This is just done by calculating the width of the 4 images plus the padding.
I have it on a setTimeout(function(){}) currently to automatically move it which works pretty well (unless you also click tabs, but that's another issue). I just want to make it so when it's at the last state (numTabs - 1), to animate and move its state back to the first one.
Code:
(function($) {
var methods = {
init: function(options) {
var settings = $.extend({
'speed': '1000',
'interval': '1000',
'auto': 'on'
}, options);
return this.each(function() {
var $wrapper = $(this);
var $sliderContainer = $wrapper.find('.js-slider-container');
$sliderContainer.hide().fadeIn();
var $tabs = $wrapper.find('.js-slider-tabs li a');
var numTabs = $tabs.size();
var innerWidth = $wrapper.find('.js-slider-container').width();
var $elements = $wrapper.find('.js-slider-container a');
var $firstElement = $elements.first();
var containerHeight = $firstElement.height();
$sliderContainer.height(containerHeight);
// Loop through each list element in `.js-slider-tabs` and add the
// distance to move for each "panel". A panel in this example is 4 images
$tabs.each(function(i) {
// Set amount to scroll for each tab
if (i === 1) {
$(this).attr('data-to-move', innerWidth + 20); // 20 is the padding between elements
} else {
$(this).attr('data-to-move', innerWidth * (i) + (i * 20));
}
});
// If they hovered on the panel, add paused to the data attribute
$('.js-slider-container').hover(function() {
$sliderContainer.attr('data-paused', true);
}, function() {
$sliderContainer.attr('data-paused', false);
});
// Start the auto slide
if (settings.auto === 'on') {
methods.auto($tabs, settings, $sliderContainer);
}
$tabs.click(function() {
var $tab = $(this);
var $panelNum = $(this).attr('data-slider-panel');
var $amountToMove = $(this).attr('data-to-move');
// Remove the active class of the `li` if it contains it
$tabs.each(function() {
var $tab = $(this);
if ($tab.parent().hasClass('active')) {
$tab.parent().removeClass('active');
}
});
// Add active state to current tab
$tab.parent().addClass('active');
// Animate to panel position
methods.animate($amountToMove, settings);
return false;
});
});
},
auto: function($tabs, settings, $sliderContainer) {
$tabs.each(function(i) {
var $amountToMove = $(this).attr('data-to-move');
setTimeout(function() {
methods.animate($amountToMove, settings, i, $sliderContainer);
}, i * settings.interval);
});
},
animate: function($amountToMove, settings, i, $sliderContainer) {
// Animate
$('.js-slider-tabs li').eq(i - 1).removeClass('active');
$('.js-slider-tabs li').eq(i).addClass('active');
$('#js-to-move').animate({
'left': -$amountToMove
}, settings.speed, 'linear', function() {});
}
};
$.fn.slider = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
return false;
}
};
})(jQuery);
$(window).ready(function() {
$('.js-slider').slider({
'speed': '10000',
'interval': '10000',
'auto': 'on'
});
});
The auto and animate methods are where the magic happens. The parameters speed is how fast it's animated and interval is how often, currently set at 10 seconds.
Can anyone help me figure out how to get this to "infinitely loop", if you will?
Here is a JSFiddle
It would probably be better to let go of the .each() and setTimeout() combo and use just setInterval() instead. Using .each() naturally limits your loop to the length of your collection, so it's better to use a looping mechanism that's not, and that you can break at any point you choose.
Besides, you can readily identify the current visible element by just checking for .active, from what I can see.
You'd probably need something like this:
setInterval(function () {
// do this check here.
// it saves you a function call and having to pass in $sliderContainer
if ($sliderContainer.attr('data-paused') === 'true') { return; }
// you really need to just pass in the settings object.
// the current element you can identify (as mentioned),
// and $amountToMove is derivable from that.
methods.animate(settings);
}, i * settings.interval);
// ...
// cache your slider tabs outside of the function
// and just form a closure on that to speed up your manips
var slidertabs = $('.js-slider-tabs');
animate : function (settings) {
// identify the current tab
var current = slidertabs.find('li.active'),
// and then do some magic to determine the next element in the loop
next = current.next().length >= 0 ?
current.next() :
slidertabs.find('li:eq(0)')
;
current.removeClass('active');
next.addClass('active');
// do your stuff
};
The code is not optimized, but I hope you see where I'm getting at here.
I'm fairly new to jQuery and I'm using the script below. Basically it uses two unordered lists to create tab functionality (one for tabs, one for content). Right now when you click through the tabs, the output is switched from "display:list-item;" to "display:none;". I'm trying to change this to "position:absolute;left:-10000px;" and "position:relative;left:0;" so that all the content gets rendered but just moves off the page rather than be hidden.
I'm having the issue you see at the bottom of the page here http://jqueryui.com/demos/tabs/ except it's not being controlled in the CSS. It's being controlled in the script below somehow that I'm unfamiliar with. Any help would be appreciated.
//INITIALIZATION
$.featureList(
$(".tabs li a"),
$(".output > li"), {
start_item : 0
}
);
//SCRIPT
(function($) {
$.fn.featureList = function(options) {
var tabs = $(this);
var output = $(options.output);
new jQuery.featureList(tabs, output, options);
return this;
};
$.featureList = function(tabs, output, options) {
function slide(nr) {
if (typeof nr == "undefined") {
nr = visible_item + 1;
nr = nr >= total_items ? 0 : nr;
}
tabs.removeClass('current').filter(":eq(" + nr + ")").addClass('current');
output.stop(true, true).filter(":visible").fadeOut();
output.filter(":eq(" + nr + ")").fadeIn(function() {
visible_item = nr;
});
}
var options = options || {};
var total_items = tabs.length;
var visible_item = options.start_item || 0;
options.pause_on_hover = options.pause_on_hover || true;
options.transition_interval = options.transition_interval || 0;
output.hide().eq( visible_item ).show();
tabs.eq( visible_item ).addClass('current');
tabs.click(function() {
if ($(this).hasClass('current')) {
return false;
}
slide( tabs.index( this) );
});
if (options.transition_interval > 0) {
var timer = setInterval(function () {
slide();
}, options.transition_interval);
if (options.pause_on_hover) {
tabs.mouseenter(function() {
clearInterval( timer );
}).mouseleave(function() {
clearInterval( timer );
timer = setInterval(function () {
slide();
}, options.transition_interval);
});
}
}
};
})(jQuery);
The action in that script is happening with .FadeIn and .Fadeout, which animate opacity. Fadeout applies display:none at the end of the opacity animation. Correspondingly, FadeIn only works on elements that are set to display:none. Fadein just won't work on visibility: hidden or opacity:0. Check out the jquery documentation, it's mostly pretty good.
So you want to substitute a css position change for those two lines of code. There are a bunch of different ways to do this, depending mostly on whether or not you want the elements to animate off the page of just leap there.
Also FYI The easiest way to share this sort of stuff for troubleshooting is to make a jsfiddle with a reduced subset of your code, just the relevant stuff, and then everybody can poke away at it until it works. :)
Can anybody help me on this one...I have a button which when is hovered, triggers an action. But I'd like it to repeat it for as long as the button is hovered.
I'd appreciate any solution, be it in jquery or pure javascript - here is how my code looks at this moment (in jquery):
var scrollingposition = 0;
$('#button').hover(function(){
++scrollingposition;
$('#object').css("right", scrollingposition);
});
Now how can i put this into some kind of while loop, so that #object is moving px by px for as #button is hovered, not just when the mouse enters it?
OK... another stab at the answer:
$('myselector').each(function () {
var hovered = false;
var loop = window.setInterval(function () {
if (hovered) {
// ...
}
}, 250);
$(this).hover(
function () {
hovered = true;
},
function () {
hovered = false;
}
);
});
The 250 means the task repeats every quarter of a second. You can decrease this number to make it faster or increase it to make it slower.
Nathan's answer is a good start, but you should also use window.clearInterval when the mouse leaves the element (mouseleave event) to cancel the repeated action which was set up using setInterval(), because this way the "loop" is running only when the mouse pointer enters the element (mouseover event).
Here is a sample code:
function doSomethingRepeatedly(){
// do this repeatedly when hovering the element
}
var intervalId;
$(document).ready(function () {
$('#myelement').hover(function () {
var intervalDelay = 10;
// call doSomethingRepeatedly() function repeatedly with 10ms delay between the function calls
intervalId = setInterval(doSomethingRepeatedly, intervalDelay);
}, function () {
// cancel calling doSomethingRepeatedly() function repeatedly
clearInterval(intervalId);
});
});
I created a sample code on jsFiddle which demonstrates how to scroll the background-image of an element left-to-right and then backwards on hover with the code shown above:
http://jsfiddle.net/Sk8erPeter/HLT3J/15/
If its an animation you can "stop" an animation half way through. So it looks like you're moving something to the left so you could do:
var maxScroll = 9999;
$('#button').hover(
function(){ $('#object').animate({ "right":maxScroll+"px" }, 10000); },
function(){ $('#object').stop(); } );
var buttonHovered = false;
$('#button').hover(function () {
buttonHovered = true;
while (buttonHovered) {
...
}
},
function () {
buttonHovered = false;
});
If you want to do this for multiple objects, it might be better to make it a bit more object oriented than a global variable though.
Edit:
Think the best way of dealing with multiple objects is to put it in an .each() block:
$('myselector').each(function () {
var hovered = false;
$(this).hover(function () {
hovered = true;
while (hovered) {
...
}
},
function () {
hovered = false;
});
});
Edit2:
Or you could do it by adding a class:
$('selector').hover(function () {
$(this).addClass('hovered');
while ($(this).hasClass('hovered')) {
...
}
}, function () {
$(this).removeClass('hovered');
});
var scrollingposition = 0;
$('#button').hover(function(){
var $this = $(this);
var $obj = $("#object");
while ( $this.is(":hover") ) {
scrollingposition += 1;
$obj.css("right", scrollingposition);
}
});