3
\$\begingroup\$

This is my first jQuery plugin (used the jQuery ui widget factory). All code improvements are welcome.

GitHub

The plugin creates a gallery with one big picture and a given amount of clickable thumbnails. When the last thumbnail of set has been clicked it fades into the next thumbnail set.

;(function ($, window, document, undefined) {

$.widget("company.imageSlider", {

    options: {
        debug: false,
        templateId: "",
        thumbAmount: 4,
        photos: [],
        imagePath: "./img/landscape/",
        thumbPath: "./img/thumbs/"
    },

    thumbSetPosition: {
        val: 0
    },

    thumbSetZindex: {
        val: 0
    },

    _create: function () {
        this.options.debug == true && console.log("Create has been called");

        this.thumbSetPosition = 1;
        this.thumbSetZindex = 999;
        this.element.addClass("plugin-is-active");
    },

    _getPhotosByRange: function (from, to) {
        return this.options.photos.slice(from, to)
    },

    _getPhotoCount: function() {
        return this.options.photos.length;
    },

    _buildMainFrame: function () {
        this.options.debug == true && console.log("Building main image frame");

        var widget = this;
        var container = $(".company-image-slider__main-image");

        // Add mouse over events
        container.on("mouseenter", function () {
            $(this).find(".company-image-slider__nav").fadeIn(500);
        }).on("mouseleave", function () {
            $(this).find(".company-image-slider__nav").fadeOut(500);
        });

        // Add on click event
        container.find(".company-image-slider__nav").on("click", function () {
            if ($(this).data("direction") == "next") {
                widget._nextImage();
            } else {
                widget._prevImage();
            }
        });
    },

    _nextImage: function () {
        this.options.debug == true && console.log("Selecting next image");

        var activeImage = $(".company-image-slider__thumb-container").find(".is-active");
        var nextImage = activeImage.next();

        activeImage.removeClass("is-active");
        nextImage.addClass("is-active");

        if (nextImage.hasClass("is-last")) {
            nextImage.trigger("click", {widget: this, direction: "next"}, this._scrollToThumbSet);
            return false;
        }

        this._refreshMainFrame(activeImage.next().find("img"));
    },

    _prevImage: function () {
        this.options.debug == true && console.log("Selecting previous image");

        var activeImage = $(".company-image-slider__thumb-container").find(".is-active");
        var prevImage = activeImage.prev();

        activeImage.removeClass("is-active");
        prevImage.addClass("is-active");

        // when the first image was auto-selected,
        // we need to go back as well.
        if (activeImage.hasClass("is-first")) {
            activeImage.trigger("click", {widget: this, direction: "prev"}, this._scrollToThumbSet);
            return false;
        }

        if (prevImage.hasClass("is-first")) {
            prevImage.trigger("click", {widget: this, direction: "prev"}, this._scrollToThumbSet);
            return false;
        }

        this._refreshMainFrame(activeImage.prev().find("img"));
    },

    _refreshMainFrame: function (image) {
        var currentImage = $('.company-image-slider__main-image').find("img");
        currentImage.attr("src", this.options.imagePath + image.data("origin"));
    },

    _buildThumbs: function (photos) {
        var widget = this;
        var options = this.options;
        options.debug == true && console.log("Building thumbnails");

        var template = $(options.templateId);
        template.addClass("is-loaded");

        var container = $(".company-image-slider__thumb-container");

        $.each(photos, function (i, photo) {

            var thumb;
            if (i == 0) {
                // clone the thumb-set container once
                thumb = template.clone();
            } else {
                thumb = template.find(".company-image-slider__thumb-container__thumb-set").clone();
            }

            var img = thumb.find( "img" );
            img.attr({src: options.thumbPath + photo.src, alt: photo.alt});
            img.attr("data-origin", photo.src);
            img.attr("data-index", i);

            // Add first and last classes to the thumbnail html
            i == 0 && thumb.find( ".company-image-slider__thumb--small" ).addClass( "is-active" ).addClass( "is-first" );
            i == options.thumbAmount - 1 && thumb.find( ".company-image-slider__thumb--small" ).addClass( "is-last" );

            if (i == 0) {
                // only apply the thumb-set container once
                thumb.find(".company-image-slider__thumb-container__thumb-set").css("z-index", widget.thumbSetZindex);
                widget.thumbSetZindex--;
                container.append($.trim(thumb.html()));
            } else {
                // place the thumbnail html inside the thumb-set container
                // and remove all the white spaces
                $( ".company-image-slider__thumb-container > .company-image-slider__thumb-container__thumb-set:last-child" ).append(
                    $.trim(thumb.html())).contents().filter(function () {
                        return this.nodeType === 3;
                    }).remove();
            }
        });

        // Add click events
        $( ".company-image-slider__thumb--small" ).on(
            "click", { widget: this }, this._handleThumbClick
        );

        $( ".company-image-slider__thumb--small.is-first" ).on(
            "click", { widget: this, direction: "prev" }, function (e) {
                widget._scrollToThumbSet(e);
                widget._clearThumbs();

            }
        );

        $( ".company-image-slider__thumb--small.is-last" ).on(
            "click", { widget: this, direction: "next" }, function (e) {
                widget._scrollToThumbSet(e);
                widget._clearThumbs();
            }
        );

        // Load remaining images message container
        widget._showRemainingImages($( ".company-image-slider__thumb--small.is-last" ));
    },

    _showRemainingImages: function(element) {
        var html =  $( "<div></div>" );
        html.addClass( "company-image-slider__message--block" );

        var i = $(element).find( "img" ).data( "index" ) + 1;
        var nextInLine = this._getPhotoCount() - i * this.thumbSetPosition;

        if(nextInLine > 0) {
            html.text( nextInLine + " more " );
            $(element).prepend( html );
            html.fadeIn( 500 );
        }
    },

    _scrollToThumbSet: function (e) {
        var widget = e.data.widget;
        widget.options.debug == true && console.log("Scrolling to next slides");

        if (e.data.direction == "next") {
            if (widget.thumbSetPosition < widget.options.photos.length / widget.options.thumbAmount) {
                widget.thumbSetPosition++;
            }
        } else {
            if (widget.thumbSetPosition > 1) {
                widget.thumbSetPosition--;
            }
        }

        var nextSetEnd = widget.thumbSetPosition * widget.options.thumbAmount;
        var nextSetStart = nextSetEnd - widget.options.thumbAmount;
        var photos = widget._getPhotosByRange(nextSetStart, nextSetEnd);

        widget._buildThumbs(photos);
    },

    _handleThumbClick: function (event) {
        event.data.widget.options.debug == true && console.log("Handling thumbnail click");

        $(this).siblings().removeClass("is-active");
        $(this).addClass("is-active");

        event.data.widget._refreshMainFrame($(this).find("img"));
    },

    _clearThumbs: function () {
        this.options.debug == true && console.log("Clearing old thumbs nails");

        var oldContainer = $( ".company-image-slider__thumb-container > div:nth-child(3)" );
        oldContainer.fadeOut(1000, function () {
            $(this).remove();
        });
    },

    _destroy: function () {
        this.options.debug == true && console.log("Destroy has been called");

        if (this.element.hasClass("plugin-is-active")) {
            this.element.removeClass("plugin-is-active");
        }
    },

    start: function () {
        this.options.debug == true && console.log("Plugin Company Image Slider running");

        this._buildMainFrame();

        var photos = this._getPhotosByRange(0, 4);
        var image = $( ".company-image-slider__main-image img" ).attr("src", photos[0].src).attr("alt", photos[0].alt).attr("data-origin", photos[0].src);

        this._refreshMainFrame(image);
        this._buildThumbs(photos);
        this._trigger("complete", null, {value: image});
    }
});

})(jQuery, window, document);
\$\endgroup\$
3
\$\begingroup\$

Overall, I would say the code is well written. I can't speak about the jQueryUI parts because I am not as familiar with it. However, there are a couple of things you can do to improve it though. Most of these would be what I would call micro-optimizations.

First, you need to DRY your code as much as possible. For instance, you repeat this line a lot.

this.options.debug == true && console.log("Selecting previous image");

If you find you are repeating a line of code then it really should be a function. Also, there is no need to check for a value of true.

_log : function ( txt ) {
  this.options.debug && console.log( txt );
}

Another line that is used repeatedly is the one similar to below:

prevImage.trigger("click", {widget: this, direction: "prev"}, this._scrollToThumbSet);

Again, you can make a function out of this:

_triggerClick :  function ( el, dir, fn ) {
  el.trigger("click", {widget: this, direction: dir }, fn);
}

this._triggerClick( prevImage, "prev", this._scrollToThumbSet );
this._triggerClick( activeImage, "prev, this._scrollToThumbSet );

The other thing is you have some very long functions. These should probably be broken up into smaller sub functions. For instance, your _buldThumbs is huge. just the $.each is over 20 lines long.

Also, you have (i == 0) checks three separate times in that function. You should use === instead. But there should be a way to simplify that code.

var isFirst = !!(!i); // !i is same as i===0 !! forces a boolean value

var thumb = ( isFirst ) ? template.clone() : template.find(".company-image-slider__thumb-container__thumb-set").clone();

You should also cache all of your selections that occur more once. You do this in most cases, but there are a few places you didn't. In the _handleThumbClick function, you have $(this) twice. Go ahead and set a variable:

var $t = $(this);
$t.siblings().removeClass("is-active");
$t.addClass("is-active");

Or just chain it together:

$(this).addClass("is-active").siblings().removeClass("is-active");

If all of your slideshow is inside of a single div container, you can do a selection on that container, cache it, and use it and .find to do all of your other selections. This would improve jQuery's performance.

var $ss = $('.company-image-slider');  //or whatever
var thumbContainer = $ss.find('.company-image-slider__thumb-container');
//etc etc

Lastly, in your destroy function you check to see if the element has the class before you remove it. You can call removeClass either way. If the class is there, it is removed; otherwise it does nothing.

I hope that helps. Feel free to leave a comment if you have questions.

\$\endgroup\$
  • \$\begingroup\$ Thank you Gary. It's appreciated. I will use your comments to improve the code when I find time to do so. \$\endgroup\$ – Floris Jun 8 '15 at 9:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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