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");
    }
});
\$\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\$
    – Jamal
    Aug 20, 2013 at 7:59
  • \$\begingroup\$ Yes of course. Hopefully the error will be addressed by implementing better best practices anyway. \$\endgroup\$
    – tommyf
    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
                    });
    
\$\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\$
    – tommyf
    Oct 15, 2014 at 4:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.