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/
Related
I've a task of building a modal prompt, that's been simple so far describing its methods like "show", "hide" when it comes down just to DOM manupulation.
Now comes the hardship for me... Imagine we have a page on which there are several immediate calls to construct and show several modals on one page
//on page load:
$("browser-deprecated-modal").modal();
$("change-your-city-modal").modal();
$("promotion-modal").modal();
By default my Modal (and other libraries i tried) construct all of these modals at once and show them overlapping each other in reverse order -
i.e $(promotion-modal) is on the top, while the
$("browser-deprecated-modal") will be below all of them. that's not what i want, let alone overlapping overlays.
I need each modal to show up only when the previous one (if there'are any) has been closed. So, first we should see "browser-deprecated-modal" (no other modals underneath), upon closing it there must pop up the second one and so on.
I've been trying to work it out with this:
$.fn.modal = function(options) {
return this.each(function() {
if (Modal.running) {
Modal.toInstantiateLater.push({this,options});
} else {
var md = new Modal(this, options);
}
});
}
destroy :function () {
....
if (Modal.toInstantiateLater.length)
new Modal (Modal.toInstantiateLater[0][0],Modal.toInstantiateLater[0][1]);
}
keeping a track of all calls to construct a Modal in a array and in the "destroy" method make a check of this array is not empty.
but it seems awkward and buggy me thinks.
i need a robust and clear solution. I've been thinking about $.Callbacks or $.Deferred,
kinda set up a Callback queue
if (Modal.running) { //if one Modal is already running
var cb = $.Callbacks();
cb.add(function(){
new Modal(this, options);
});
} else { //the road is clear
var md = new Modal(this, options);
}
and to trigger firing cb in the destroy method, but i'm new to this stuff and stuck and cannot progress, whether it's right or not, or other approach is more suitable.
Besides, I read that callbacks fire all the functions at once (if we had more than one extra modal in a queue), which is not right, because I need to fire Modal creation one by one and clear the Callback queue one by one.
Please help me in this mess.
My code jsfiddle
I got rid of the counter variable, as you can use toInstantiateLater to keep track of where you are, and only had to make a few changes. Give this a try...
Javscript
function Modal(el, opts){
this.el = $(el);
this.opts = opts;
this.overlay = $("<div class='overlay' id='overlay"+Modal.counter+"'></div>");
this.wrap = $("<div class='wrap' id='wrap"+Modal.counter+"'></div>");
this.replace = $("<div class='replace' id='replace"+Modal.counter+"'></div>");
this.close = $("<span class='close' id='close"+Modal.counter+"'></span>")
if (Modal.running) {
Modal.toInstantiateLater.push(this);
}
else {
Modal.running = true;
this.show();
}
}
Modal.destroyAll = function() {
Modal.prototype.destroyAll();
};
Modal.prototype = {
show: function() {
var s = this;
s.wrap.append(s.close);
s.el.before(s.replace).appendTo(s.wrap).show();
$('body').append(s.overlay).append(s.wrap);
s.bindEvents();
Modal.currentModal = s;
},
bindEvents: function() {
var s = this;
s.close.on("click.modal",function(e){
s.destroy.call(s,e);
});
},
destroy: function(e) {
var s = this;
s.replace.replaceWith(s.el.hide());
s.wrap.remove();
s.overlay.remove();
if (Modal.toInstantiateLater.length > 0) {
Modal.toInstantiateLater.shift().show();
}
else {
Modal.running = false;
}
},
destroyAll: function(e) {
Modal.toInstantiateLater = [];
Modal.currentModal.destroy();
}
}
Modal.running = false;
Modal.toInstantiateLater = [];
Modal.currentModal = {};
$.fn.modal = function(options) {
return this.each(function() {
var md = new Modal(this, options);
});
}
$("document").ready(function(){
$("#browser-deprecated-modal").modal();
$("#change-your-city-modal").modal();
$("#promotion-modal").modal();
$("#destroy-all").on("click", function() {
Modal.destroyAll();
});
});
jsfiddle example
http://jsfiddle.net/zz9ccbLn/4/
I've had a go at writing my first plugin. It is a simple jQuery plugin that reorders the DOM based on screen width. If the plugin is used on a single selector, like $("#box3").domorder(); it works as expected.
However, if multiple selectors are used to call the function, or the function is called multiple times, the function only works once, on the first selector it comes to.
I have the each(function(){}) iterating over each selector, but I'm obviously missing something.
jsFiddle
(function($) {
"use strict";
$.fn.domorder = function(options) {
var settings = {
breakpoint : 960,
targetContainer : $(this).parent(),
targetPosition : "start"
};
if (options) {
$.extend(settings, options);
}
return this.each(function(i, el) {
/* remember selector's original order position */
var originalLocation = {};
if ($(el).prev().length) {
/* not originally first child */
originalLocation.prev = $(el).prev()[0];
} else {
/* originally a first child */
originalLocation.parent = $(el).parent()[0];
}
var initiatedomorder = function() {
var winW = $(window).width();
if (winW < settings.breakpoint && !$("body").hasClass("domorder-reordered")) {
/* dom the order of the item */
if (settings.targetPosition === "start") {
$(el).prependTo(settings.targetContainer[0]);
} else {
$(el).appendTo(settings.targetContainer[0]);
}
$("body").addClass("domorder-reordered");
} else if (winW >= settings.breakpoint && $("body").hasClass("domorder-reordered")) {
/* return the reordered item back into the orignal flow */
if (originalLocation.parent) {
/* element was a first child */
$(originalLocation.parent).prepend(el);
} else {
/* element was not a first child */
/* add a line break to preserve inline-block spacing */
$(originalLocation.prev).after(el).after("\n");
}
$("body").removeClass("domorder-reordered");
}
};
initiatedomorder();
$(window).resize(function() {
initiatedomorder();
});
});
};
}(jQuery));
$("body").addClass("domorder-reordered"); cause the problem since only the first iteration will do the ordering.
maybe you should change the $("body") to something like $(el.parentNode).
check this jsfiddle
You are adding the class to the body on the first iteration of the loop. so in the second iteration body has the class and wot go inside the ordering code. you have to rethink about the location of addClass and removeClass
and put the following code outside the each
$(window).resize(function() {
initiatedomorder();
});
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 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);
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();
};