I have recently written my first jQuery plugin. It is a responsive jQuery carousel plugin. It seems to be functioning well apart from on window re-size. Sometimes if the window is re-sized too fast I get the following console error RangeError: Maximum call stack size exceeded
, despite a timer function being used to prevent continuous re-rendering.
I'm looking for any best practices on things I've missed and should be following.
Commented plugin
(function($) {
$.fullslide = function(element, options) {
// Declare plugins variables
var autoInt; // variable to store the timing function
// Default options
var defaults = {
displayQty : 1,
moveQty : 1,
moveDuration : 1000,
moveEasing : "swing",
slideMargin : 20,
minWidth : "",
maxWidth : "",
autoSlide : true,
autoDirection : "left",
slideDelay : 3000,
pauseOnHover : true,
onReady : function() {},
onBeforeSlide : function() {},
onAfterSlide : function() {}
}
// Use "slider" to reference the current instance of the object
var slider = this;
// This will hold the merged default, and user-provided options
slider.settings = {}
// Set vars to be used from outside of plugin, specifically for event bindings
slider.vars = {}
var $element = $(element), // Reference to the jQuery version of DOM element
element = element; // Reference to the actual DOM element
slider.init = function() {
/**
* The "constructor" method - set the slider up at runtime,
* including creating DOM elements and setting widths.
*/
// Declare method's variables
var fullslideLi,
ulH,
loaded,
fontSize;
// The plugin's final properties are the merged default and
// user-provided options (if any)
slider.settings = $.extend({}, defaults, options);
// Set hoverPrevent to 0 - used to prevent accidental auto start when manually stopped
slider.vars.hoverPrevent = 0;
// Create DOM elements
$element.wrap('<div class="fullslide-wrap" style="opacity:0;" />');
$element.closest(".fullslide-wrap").append("<div class='fullslide-controls'><a href='#' class='fullslide-left'>left</a> <a href='#' class='fullslide-right'>right</a></div>");
// Count how many slides are orginally and update the variable
fullslideLi = $element.children("li");
origSlideQty = $(fullslideLi).length;
// Check the font size of the LI to help us know when the contents
// have been loaded
fontSize = parseInt( $(fullslideLi).css("fontSize") );
// Ensure we're not trying to display more slides than we have
if( slider.settings.displayQty > origSlideQty ) {
slider.settings.displayQty = origSlideQty;
}
// Ensure we cannot move more slides than we have
if( slider.settings.moveQty > origSlideQty ) {
slider.settings.moveQty = origSlideQty;
}
// Ensure we don't try to move more than we are displaying
if( slider.settings.moveQty > slider.settings.displayQty ) {
slider.settings.moveQty = slider.settings.displayQty;
}
// Triplicate the slides so there's always enough in view when sliding
$(fullslideLi).clone().prependTo($element);
$(fullslideLi).clone().appendTo($element);
// Set the slides' width
setWidths();
// Wait for the content to be loaded before firing the callback
loaded = 0;
var waitForLoad = function() {
// Contents should be loaded when the last LI is bigger than
// the LIs default font size
if( loaded !== 1 ) {
if( $(fullslideLi).last().outerHeight() > fontSize ) {
loaded = 1;
waitForLoad();
} else {
window.setTimeout( waitForLoad, 500 );
}
} else {
// Wait a bit longer to ensure it was fully loaded
window.setTimeout( function() {
// Everything loaded
ulH = $(fullslideLi).first().height();
// Apply the height to the UL
$element.css({
height : ulH + "px"
});
// Show the slider if hidden
$element.css({
opacity : 1
});
$element.closest(".fullslide-wrap").css({
opacity : 1
});
// All done, if an 'onReady' function has been set, call it now
if( slider.settings.onReady && typeof(slider.settings.onReady) === "function" ) {
slider.settings.onReady();
}
// If set to slide automatically, start now
if( slider.settings.autoSlide === true ) {
slider.startAuto();
}
}, 1000 );
}
}
waitForLoad();
}
// ---------------------------------------------------------------------------------------------------------- //
// ------------------------------------------- PUBLIC METHODS ----------------------------------------------- //
// ---------------------------------------------------------------------------------------------------------- //
slider.slide = function(dir, qty, duration, easing) {
/**
* Invoke the sliding action, either left or right
*/
// Declare method's variables
var slideWpx,
moveDistance,
relativeMovement,
fullslideLi;
// If parameters haven't been sent with the function, set to defaults
dir = (typeof dir === "undefined") ? "left" : dir;
qty = (typeof qty === "undefined") ? slider.settings.moveQty : qty;
duration = (typeof duration === "undefined") ? slider.settings.moveDuration : duration;
easing = (typeof easing === "undefined") ? slider.settings.moveEasing : easing;
// Do not do anything if the ul is currently animating
if( !$element.filter(':animated').length ) {
// Before sliding happens pass in the 'onBeforeSlide' function, if set
if( slider.settings.onBeforeSlide && typeof(slider.settings.onBeforeSlide) === "function" ) {
slider.settings.onBeforeSlide();
}
// Get the current state of the slides as the variable fullslideLi
fullslideLi = $element.children("li");
// Get width in px of slide so we know how far to move it
slideWpx = $(fullslideLi).first().outerWidth();
// Calculate the move distance
moveDistance = (slideWpx * qty) + (slider.settings.slideMargin * qty);
// Set the relative movement as a variable
if( dir === "left" ) {
relativeMovement = "-=" + moveDistance;
} else {
relativeMovement = "+=" + moveDistance;
}
// Call the animation function - if moving left move the li's that have slid out of view to the end,
// else if moving right move the li's that have slid out of view to the start
$element.animate({
left : relativeMovement
}, duration, easing, function() {
// Once animation is complete -
// Get the current state of the li elements
fullslideLi = $element.children("li");
if( dir === "left" ) {
// If moving left move the amount of slides moved by to the end
$(fullslideLi).slice(0, slider.settings.moveQty).appendTo($element);
} else {
// Else if moving right, move the amount of slides moved to the start
$(fullslideLi).slice(-slider.settings.moveQty).prependTo($element);
}
// Reset the position to take into account the removed slides
offsetFirstSlide();
// After sliding happens, if set pass in the 'onAfterSlide' function as a callback function
// to setUlHeight()
if( slider.settings.onAfterSlide && typeof(slider.settings.onAfterSlide) === "function" ) {
setUlHeight( slider.settings.onAfterSlide );
} else {
// Otherwise just call setUlHeight
setUlHeight();
}
});
}
};
slider.startAuto = function(allowHoverStop) {
/**
* Begin the automatic scrolling
*/
if( allowHoverStop && allowHoverStop === true ) {
slider.vars.hoverPrevent = 0;
}
// Start the interval timer
autoInt = setInterval( function() { slider.slide( slider.settings.autoDirection ); }, slider.settings.slideDelay );
};
slider.stopAuto = function(preventHoverStart) {
/**
* Stop the automatic scrolling
*/
// If preventHoverStart has been passed then don't allow hover events to trigger auto slider
if( preventHoverStart && preventHoverStart === true ) {
slider.vars.hoverPrevent = 1;
}
clearInterval(autoInt);
};
// ---------------------------------------------------------------------------------------------------------- //
// ------------------------------------------- PRIVATE METHODS ---------------------------------------------- //
// ---------------------------------------------------------------------------------------------------------- //
var offsetFirstSlide = function() {
/**
* Cater for the fact that there are always slides to the left of the
* currenly viewed slides. This offsets the ul so that we don't see
* the slides to the left.
*/
// Declare method's variables
var fullslideLi,
slideWpx,
offset;
// Get the current state of the slides as the variable fullslideLi
fullslideLi = $element.children("li");
// Get the width in pixels of the slide so we know how much to offset it by
slideWpx = $(fullslideLi).first().outerWidth();
// Calculate the offset - we have duplicated all the original slides infront of the viewable slide, so that's how much we offset by, plus margin
offset = origSlideQty * (slideWpx + slider.settings.slideMargin);
// Apply the offset
$element.css("left", "-" + offset + "px");
};
var setWidths = function() {
/**
* Calculate and set the widths of the slides and the ul container.
*/
// Declare method's variables
var fullslideLi,
slideQty,
ulW,
ulWpx,
slideWpx,
totalM,
slideW;
// Set slide widths
// Assign fullslideLi var
fullslideLi = $element.children("li");
// Get the current number of slides to work out the width of ul
slideQty = $(fullslideLi).length;
// To get the ul width, calculate the percentage from the amount will be shown then
ulW = (100 / slider.settings.displayQty) * slideQty;
// Apply the width and the height to the ul
$element.css("width", ulW + "%");
// Set the cell width in %
slideW = 100 / slideQty;
// Calculate the width of the ul and slide in px
ulWpx = $element.css("width");
slideWpx = parseInt(ulWpx) / slideQty;
// Include the margins in with the calculation, if any
if( slider.settings.slideMargin > 0 ) {
// Get the total margin for the whole slideshow
totalM = slider.settings.slideMargin * (slider.settings.displayQty - 1);
// Calculate width of the slides
slideWpx = slideWpx - (totalM / slider.settings.displayQty);
}
// Calculate if the min width is exceeded
if( slider.settings.minWidth ) {
if( slideWpx < slider.settings.minWidth ) {
// Don't decrement if the current display quantity is 1
if( slider.settings.displayQty > 1 ) {
--slider.settings.displayQty;
// Recall this function with the new display quantity setting
setWidths();
// Prevent subsquent rendering of the CSS before the sizes have been recalculated
return;
}
// Slide width is bigger than min width
// Calculate if another slide could fit in, without making all slides less than min width
// AND that we don't display more than one of each
} else if( (Math.ceil(slideWpx * slider.settings.displayQty / (slider.settings.displayQty + 1)) > slider.settings.minWidth) && (slider.settings.displayQty < origSlideQty) ) {
++slider.settings.displayQty;
// Recall this function with the new display quantity setting
setWidths();
// Prevent subsquent rendering of the CSS before the sizes have been recalculated
return;
}
// Calculate if the max width is exceeded
} else if( slider.settings.maxWidth ) {
if( slideWpx > slider.settings.maxWidth ) {
// Don't increment if the display quantity is the same amount as original slide quantity
if( slider.settings.displayQty < origSlideQty ) {
++slider.settings.displayQty;
// Recall this function with the new display quantity setting
setWidths();
// Prevent subsquent rendering of the CSS before the sizes have been recalculated
return;
}
// Slide width is smaller than max width
// Calculate if a slide could be removed, without making all slides greater than max width
// AND that we don't display any less than one
} else if( Math.floor(slideWpx * (slider.settings.displayQty) / (slider.settings.displayQty - 1) <= slider.settings.maxWidth) && (slider.settings.displayQty > 1) ) {
--slider.settings.displayQty;
// Recall this function with the new display quantity setting
setWidths();
// Prevent subsquent rendering of the CSS before the sizes have been recalculated
return;
}
}
// Apply the slide margins, if any
if( slider.settings.slideMargin > 0 ) {
$(fullslideLi).css("marginRight", slider.settings.slideMargin + "px");
}
//Apply the width
$(fullslideLi).css("width", slideWpx + "px");
// Once all the sizes are set, we need to offset the ul to hide the slides to the left of the viewable slides
offsetFirstSlide();
};
var setUlHeight = function(callback) {
/**
* Set the height of the ul to be called after every slide moves
*/
// Declare method's variables
var fullslideLi,
ulH;
// Assign fullslideLi var
fullslideLi = $element.children("li");
// Find the height of the LI so we can set the height of the UL to prevent wrapping
ulH = $(fullslideLi).first().height();
// Apply the height to the ul and animate it
$element.animate({
height : ulH + "px"
}, 100, function() {
// If a callback has been include call it now
if( callback && typeof(callback) === "function" ) {
callback();
}
});
};
var waitOnEvent = (function() {
/**
* Function to set a timeout for the window resize event
*/
var timers = {};
return function (callback, ms, uniqueId) {
if (!uniqueId) {
uniqueId = "1";
}
if (timers[uniqueId]) {
clearTimeout (timers[uniqueId]);
}
timers[uniqueId] = setTimeout(callback, ms);
};
})();
// Resize the slides when window size changes
$(window).resize(function() {
waitOnEvent(function() {
setWidths();
setUlHeight();
}, 500, "reset1");
});
// ---------------------------------------------------------------------------------------------------------- //
// ------------------------------------------------ RUNTIME ------------------------------------------------- //
// ---------------------------------------------------------------------------------------------------------- //
// Call the "constructor" method
slider.init();
}
// ---------------------------------------------------------------------------------------------------------- //
// ------------------------------------------- PLUGIN DEFINITION -------------------------------------------- //
// ---------------------------------------------------------------------------------------------------------- //
// Add the plugin to the jQuery.fn object
$.fn.fullslide = function(options) {
// Iterate through the DOM elements we are attaching the plugin to
return this.each(function() {
// If plugin has not already been attached to the element
if( undefined == $(this).data('fullslide') ) {
// Create a new instance of the plugin
// Pass the DOM element and the user-provided options as arguments
var slider = new $.fullslide(this, options);
// Store a reference to the plugin object
$(this).data('fullslide', slider);
}
});
}
// ---------------------------------------------------------------------------------------------------------- //
// --------------------------------------------- EVENT BINDINGS --------------------------------------------- //
// ---------------------------------------------------------------------------------------------------------- //
// Slide right on press of the left control
$('body').on('click', '.fullslide-left', function(event) {
var selfEl = $(this).parent(".fullslide-controls").prev("ul");
// Do not do anything if the ul is currently animating
var pref = {};
pref.moveQty = selfEl.data('fullslide').settings.moveQty,
pref.moveDuration = selfEl.data('fullslide').settings.moveDuration,
pref.easing = selfEl.data('fullslide').settings.easing;
selfEl.data('fullslide').slide("right", pref.moveQty, pref.moveDuration, pref.easing);
event.preventDefault();
});
// Slide left on press of the right control
$('body').on('click', '.fullslide-right', function(event) {
var selfEl = $(this).parent(".fullslide-controls").prev("ul");
var pref = {};
pref.moveQty = selfEl.data('fullslide').settings.moveQty,
pref.moveDuration = selfEl.data('fullslide').settings.moveDuration,
pref.easing = selfEl.data('fullslide').settings.easing;
selfEl.data("fullslide").slide("left", pref.moveQty, pref.moveDuration, pref.easing);
event.preventDefault();
});
// Pause on hover
$('body').on(
{
mouseenter: function() {
var selfEl = $(this).children("ul");
if( selfEl.data('fullslide').settings.autoSlide === true && selfEl.data('fullslide').settings.pauseOnHover === true && selfEl.data('fullslide').vars.hoverPrevent === 0 ) {
selfEl.data("fullslide").stopAuto();
}
},
mouseleave: function() {
var selfEl = $(this).children("ul");
if( selfEl.data('fullslide').settings.autoSlide === true && selfEl.data('fullslide').settings.pauseOnHover === true && selfEl.data('fullslide').vars.hoverPrevent === 0 ) {
selfEl.data("fullslide").startAuto();
}
}
}
, '.fullslide-wrap');
})(jQuery);
An example call
var slider = $(".slide123").fullslide({
displayQty : 2,
slideMargin : 0,
moveDuration : 2000,
onAfterSlide : function() {
console.log("slid");
}
});