4
\$\begingroup\$

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.

jsFiddle

GitHub

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");
 }
});
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Aug 20, 2013 at 7:52
\$\endgroup\$
2
  • 1
    \$\begingroup\$ We cannot address the error you're receiving (that's for Stack Overflow), but we can address the request for best practices. \$\endgroup\$ Commented Aug 20, 2013 at 7:59
  • \$\begingroup\$ Yes of course. Hopefully the error will be addressed by implementing better best practices anyway. \$\endgroup\$ Commented Aug 20, 2013 at 8:17

1 Answer 1

5
\$\begingroup\$

From a once over;

  • Very well commented
  • Assigning element to element is overkill here and does nothing:

    var $element = $(element), // Reference to the jQuery version of DOM element
     element = element; // Reference to the actual DOM element
    
  • From a naming perspective, I would refrain from using the tag type in the variable name:

    // Declare method's variables
    var fullslideLi,
     ulH,
     loaded,
     fontSize;
    

    You are tying yourself to an implementation detail with the name, you could go for element or el in those variable names

  • Consider Math.min here:

     // 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;
     }
    

    could be

     slider.settings.moveQty = Math.min( origSlideQty, slider.settings.moveQty , slider.settings.displayQty );
    

    furthermore from a DRY perspective I would probably have moved slider.settings to a local var settings:

     settings.moveQty = Math.min( origSlideQty, settings.moveQty , settings.displayQty );
    
  • window.setTimeout returns a timeoutID, you should store this timeoutID and clear it after the timeout. And then only call window.setTimeout if the timeoutID is cleared out. I hope that makes sense. It should prevent your problems
  • You can set more than 1 property with .css:

     // Apply the height to the UL
     $element.css({
     height : ulH + "px"
     });
     // Show the slider if hidden
     $element.css({
     opacity : 1
     });
    

    could be

     // Apply the height to the UL and show the slider
     $element.css({
     height : ulH + "px", 
     opacity : 1
     });
    
answered Oct 15, 2014 at 2:05
\$\endgroup\$
1
  • 2
    \$\begingroup\$ Thanks for the thorough comments. I asked this question over a year ago and can see many things I would do different now. But nevertheless you've still showed me a couple of ideas I wouldn't have thought about 👍 \$\endgroup\$ Commented Oct 15, 2014 at 4:21

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.