mambax7/extgallery

View on GitHub
assets/js/galleriffic/jquery.galleriffic.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * jQuery Galleriffic plugin
 *
 * Copyright (c) 2008 Trent Foley (http://trentacular.com)
 * Licensed under the MIT License:
 *   http://www.opensource.org/licenses/mit-license.php
 *
 * Much thanks to primary contributer Ponticlaro (http://www.ponticlaro.com)
 */
;(function($) {
    // Globally keep track of all images by their unique hash.  Each item is an image data object.
    var allImages = {};
    var imageCounter = 0;

    // Galleriffic static class
    $.galleriffic = {
        version: '2.0.1',

        // Strips invalid characters and any leading # characters
        normalizeHash: function(hash) {
            return hash.replace(/^.*#/, '').replace(/\?.*$/, '');
        },

        getImage: function(hash) {
            if (!hash)
                return undefined;

            hash = $.galleriffic.normalizeHash(hash);
            return allImages[hash];
        },

        // Global function that looks up an image by its hash and displays the image.
        // Returns false when an image is not found for the specified hash.
        // @param {String} hash This is the unique hash value assigned to an image.
        gotoImage: function(hash) {
            var imageData = $.galleriffic.getImage(hash);
            if (!imageData)
                return false;

            var gallery = imageData.gallery;
            gallery.gotoImage(imageData);

            return true;
        },

        // Removes an image from its respective gallery by its hash.
        // Returns false when an image is not found for the specified hash or the
        // specified owner gallery does match the located images gallery.
        // @param {String} hash This is the unique hash value assigned to an image.
        // @param {Object} ownerGallery (Optional) When supplied, the located images
        // gallery is verified to be the same as the specified owning gallery before
        // performing the remove operation.
        removeImageByHash: function(hash, ownerGallery) {
            var imageData = $.galleriffic.getImage(hash);
            if (!imageData)
                return false;

            var gallery = imageData.gallery;
            if (ownerGallery && ownerGallery != gallery)
                return false;

            return gallery.removeImageByIndex(imageData.index);
        }
    };

    var defaults = {
        delay:                     3000,
        numThumbs:                 20,
        preloadAhead:              40, // Set to -1 to preload all images
        enableTopPager:            false,
        enableBottomPager:         true,
        maxPagesToShow:            7,
        imageContainerSel:         '',
        captionContainerSel:       '',
        controlsContainerSel:      '',
        loadingContainerSel:       '',
        renderSSControls:          true,
        renderNavControls:         true,
        playLinkText:              'Play',
        pauseLinkText:             'Pause',
        prevLinkText:              'Previous',
        nextLinkText:              'Next',
        nextPageLinkText:          'Next ›',
        prevPageLinkText:          '‹ Prev',
        enableHistory:             false,
        enableKeyboardNavigation:  true,
        autoStart:                 false,
        syncTransitions:           false,
        defaultTransitionDuration: 1000,
        onSlideChangeOut:          undefined, // accepts a delegate like such: function(prevIndex) { ... }
        onSlideChangeIn:           undefined, // accepts a delegate like such: function(nextIndex) { ... }
        onTransitionOut:           undefined, // accepts a delegate like such: function(slide, caption, isSync, callback) { ... }
        onTransitionIn:            undefined, // accepts a delegate like such: function(slide, caption, isSync) { ... }
        onPageTransitionOut:       undefined, // accepts a delegate like such: function(callback) { ... }
        onPageTransitionIn:        undefined, // accepts a delegate like such: function() { ... }
        onImageAdded:              undefined, // accepts a delegate like such: function(imageData, $li) { ... }
        onImageRemoved:            undefined  // accepts a delegate like such: function(imageData, $li) { ... }
    };

    // Primary Galleriffic initialization function that should be called on the thumbnail container.
    $.fn.galleriffic = function(settings) {
        //  Extend Gallery Object
        $.extend(this, {
            // Returns the version of the script
            version: $.galleriffic.version,

            // Current state of the slideshow
            isSlideshowRunning: false,
            slideshowTimeout: undefined,

            // This function is attached to the click event of generated hyperlinks within the gallery
            clickHandler: function(e, link) {
                this.pause();

                if (!this.enableHistory) {
                    // The href attribute holds the unique hash for an image
                    var hash = $.galleriffic.normalizeHash($(link).attr('href'));
                    $.galleriffic.gotoImage(hash);
                    e.preventDefault();
                }
            },

            // Appends an image to the end of the set of images.  Argument listItem can be either a jQuery DOM element or arbitrary html.
            // @param listItem Either a jQuery object or a string of html of the list item that is to be added to the gallery.
            appendImage: function(listItem) {
                this.addImage(listItem, false, false);
                return this;
            },

            // Inserts an image into the set of images.  Argument listItem can be either a jQuery DOM element or arbitrary html.
            // @param listItem Either a jQuery object or a string of html of the list item that is to be added to the gallery.
            // @param {Integer} position The index within the gallery where the item shouold be added.
            insertImage: function(listItem, position) {
                this.addImage(listItem, false, true, position);
                return this;
            },

            // Adds an image to the gallery and optionally inserts/appends it to the DOM (thumbExists)
            // @param listItem Either a jQuery object or a string of html of the list item that is to be added to the gallery.
            // @param {Boolean} thumbExists Specifies whether the thumbnail already exists in the DOM or if it needs to be added.
            // @param {Boolean} insert Specifies whether the the image is appended to the end or inserted into the gallery.
            // @param {Integer} position The index within the gallery where the item shouold be added.
            addImage: function(listItem, thumbExists, insert, position) {
                var $li = ( typeof listItem === "string" ) ? $(listItem) : listItem;
                var $aThumb = $li.find('a.thumb');
                var slideUrl = $aThumb.attr('href');
                var title = $aThumb.attr('title');
                var $caption = $li.find('.caption').remove();
                var hash = $aThumb.attr('name');

                // Increment the image counter
                imageCounter++;

                // Autogenerate a hash value if none is present or if it is a duplicate
                if (!hash || allImages[''+hash]) {
                    hash = imageCounter;
                }

                // Set position to end when not specified
                if (!insert)
                    position = this.data.length;

                var imageData = {
                    title:title,
                    slideUrl:slideUrl,
                    caption:$caption,
                    hash:hash,
                    gallery:this,
                    index:position
                };

                // Add the imageData to this gallery's array of images
                if (insert) {
                    this.data.splice(position, 0, imageData);

                    // Reset index value on all imageData objects
                    this.updateIndices(position);
                }
                else {
                    this.data.push(imageData);
                }

                var gallery = this;

                // Add the element to the DOM
                if (!thumbExists) {
                    // Update thumbs passing in addition post transition out handler
                    this.updateThumbs(function() {
                        var $thumbsUl = gallery.find('ul.thumbs');
                        if (insert)
                            $thumbsUl.children(':eq('+position+')').before($li);
                        else
                            $thumbsUl.append($li);

                        if (gallery.onImageAdded)
                            gallery.onImageAdded(imageData, $li);
                    });
                }

                // Register the image globally
                allImages[''+hash] = imageData;

                // Setup attributes and click handler
                $aThumb.attr('rel', 'history')
                    .attr('href', '#'+hash)
                    .removeAttr('name')
                    .click(function(e) {
                        gallery.clickHandler(e, this);
                    });

                return this;
            },

            // Removes an image from the gallery based on its index.
            // Returns false when the index is out of range.
            removeImageByIndex: function(index) {
                if (index < 0 || index >= this.data.length)
                    return false;

                var imageData = this.data[index];
                if (!imageData)
                    return false;

                this.removeImage(imageData);

                return true;
            },

            // Convenience method that simply calls the global removeImageByHash method.
            removeImageByHash: function(hash) {
                return $.galleriffic.removeImageByHash(hash, this);
            },

            // Removes an image from the gallery.
            removeImage: function(imageData) {
                var index = imageData.index;

                // Remove the image from the gallery data array
                this.data.splice(index, 1);

                // Remove the global registration
                delete allImages[''+imageData.hash];

                // Remove the image's list item from the DOM
                this.updateThumbs(function() {
                    var $li = gallery.find('ul.thumbs')
                        .children(':eq('+index+')')
                        .remove();

                    if (gallery.onImageRemoved)
                        gallery.onImageRemoved(imageData, $li);
                });

                // Update each image objects index value
                this.updateIndices(index);

                return this;
            },

            // Updates the index values of the each of the images in the gallery after the specified index
            updateIndices: function(startIndex) {
                for (i = startIndex; i < this.data.length; i++) {
                    this.data[i].index = i;
                }

                return this;
            },

            // Scraped the thumbnail container for thumbs and adds each to the gallery
            initializeThumbs: function() {
                this.data = [];
                var gallery = this;

                this.find('ul.thumbs > li').each(function(i) {
                    gallery.addImage($(this), true, false);
                });

                return this;
            },

            isPreloadComplete: false,

            // Initalizes the image preloader
            preloadInit: function() {
                if (this.preloadAhead == 0) return this;

                this.preloadStartIndex = this.currentImage.index;
                var nextIndex = this.getNextIndex(this.preloadStartIndex);
                return this.preloadRecursive(this.preloadStartIndex, nextIndex);
            },

            // Changes the location in the gallery the preloader should work
            // @param {Integer} index The index of the image where the preloader should restart at.
            preloadRelocate: function(index) {
                // By changing this startIndex, the current preload script will restart
                this.preloadStartIndex = index;
                return this;
            },

            // Recursive function that performs the image preloading
            // @param {Integer} startIndex The index of the first image the current preloader started on.
            // @param {Integer} currentIndex The index of the current image to preload.
            preloadRecursive: function(startIndex, currentIndex) {
                // Check if startIndex has been relocated
                if (startIndex != this.preloadStartIndex) {
                    var nextIndex = this.getNextIndex(this.preloadStartIndex);
                    return this.preloadRecursive(this.preloadStartIndex, nextIndex);
                }

                var gallery = this;

                // Now check for preloadAhead count
                var preloadCount = currentIndex - startIndex;
                if (preloadCount < 0)
                    preloadCount = this.data.length-1-startIndex+currentIndex;
                if (this.preloadAhead >= 0 && preloadCount > this.preloadAhead) {
                    // Do this in order to keep checking for relocated start index
                    setTimeout(function() { gallery.preloadRecursive(startIndex, currentIndex); }, 500);
                    return this;
                }

                var imageData = this.data[currentIndex];
                if (!imageData)
                    return this;

                // If already loaded, continue
                if (imageData.image)
                    return this.preloadNext(startIndex, currentIndex);

                // Preload the image
                var image = new Image();

                image.onload = function() {
                    imageData.image = this;
                    gallery.preloadNext(startIndex, currentIndex);
                };

                image.alt = imageData.title;
                image.src = imageData.slideUrl;

                return this;
            },

            // Called by preloadRecursive in order to preload the next image after the previous has loaded.
            // @param {Integer} startIndex The index of the first image the current preloader started on.
            // @param {Integer} currentIndex The index of the current image to preload.
            preloadNext: function(startIndex, currentIndex) {
                var nextIndex = this.getNextIndex(currentIndex);
                if (nextIndex == startIndex) {
                    this.isPreloadComplete = true;
                } else {
                    // Use setTimeout to free up thread
                    var gallery = this;
                    setTimeout(function() { gallery.preloadRecursive(startIndex, nextIndex); }, 100);
                }

                return this;
            },

            // Safe way to get the next image index relative to the current image.
            // If the current image is the last, returns 0
            getNextIndex: function(index) {
                var nextIndex = index+1;
                if (nextIndex >= this.data.length)
                    nextIndex = 0;
                return nextIndex;
            },

            // Safe way to get the previous image index relative to the current image.
            // If the current image is the first, return the index of the last image in the gallery.
            getPrevIndex: function(index) {
                var prevIndex = index-1;
                if (prevIndex < 0)
                    prevIndex = this.data.length-1;
                return prevIndex;
            },

            // Pauses the slideshow
            pause: function() {
                this.isSlideshowRunning = false;
                if (this.slideshowTimeout) {
                    clearTimeout(this.slideshowTimeout);
                    this.slideshowTimeout = undefined;
                }

                if (this.$controlsContainer) {
                    this.$controlsContainer
                        .find('div.ss-controls a').removeClass().addClass('play')
                        .attr('title', this.playLinkText)
                        .attr('href', '#play')
                        .html(this.playLinkText);
                }

                return this;
            },

            // Plays the slideshow
            play: function() {
                this.isSlideshowRunning = true;

                if (this.$controlsContainer) {
                    this.$controlsContainer
                        .find('div.ss-controls a').removeClass().addClass('pause')
                        .attr('title', this.pauseLinkText)
                        .attr('href', '#pause')
                        .html(this.pauseLinkText);
                }

                if (!this.slideshowTimeout) {
                    var gallery = this;
                    this.slideshowTimeout = setTimeout(function() { gallery.ssAdvance(); }, this.delay);
                }

                return this;
            },

            // Toggles the state of the slideshow (playing/paused)
            toggleSlideshow: function() {
                if (this.isSlideshowRunning)
                    this.pause();
                else
                    this.play();

                return this;
            },

            // Advances the slideshow to the next image and delegates navigation to the
            // history plugin when history is enabled
            // enableHistory is true
            ssAdvance: function() {
                if (this.isSlideshowRunning)
                    this.next(true);

                return this;
            },

            // Advances the gallery to the next image.
            // @param {Boolean} dontPause Specifies whether to pause the slideshow.
            // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
            next: function(dontPause, bypassHistory) {
                this.gotoIndex(this.getNextIndex(this.currentImage.index), dontPause, bypassHistory);
                return this;
            },

            // Navigates to the previous image in the gallery.
            // @param {Boolean} dontPause Specifies whether to pause the slideshow.
            // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
            previous: function(dontPause, bypassHistory) {
                this.gotoIndex(this.getPrevIndex(this.currentImage.index), dontPause, bypassHistory);
                return this;
            },

            // Navigates to the next page in the gallery.
            // @param {Boolean} dontPause Specifies whether to pause the slideshow.
            // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
            nextPage: function(dontPause, bypassHistory) {
                var page = this.getCurrentPage();
                var lastPage = this.getNumPages() - 1;
                if (page < lastPage) {
                    var startIndex = page * this.numThumbs;
                    var nextPage = startIndex + this.numThumbs;
                    this.gotoIndex(nextPage, dontPause, bypassHistory);
                }

                return this;
            },

            // Navigates to the previous page in the gallery.
            // @param {Boolean} dontPause Specifies whether to pause the slideshow.
            // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
            previousPage: function(dontPause, bypassHistory) {
                var page = this.getCurrentPage();
                if (page > 0) {
                    var startIndex = page * this.numThumbs;
                    var prevPage = startIndex - this.numThumbs;
                    this.gotoIndex(prevPage, dontPause, bypassHistory);
                }

                return this;
            },

            // Navigates to the image at the specified index in the gallery
            // @param {Integer} index The index of the image in the gallery to display.
            // @param {Boolean} dontPause Specifies whether to pause the slideshow.
            // @param {Boolean} bypassHistory Specifies whether to delegate navigation to the history plugin when history is enabled.
            gotoIndex: function(index, dontPause, bypassHistory) {
                if (!dontPause)
                    this.pause();

                if (index < 0) index = 0;
                else if (index >= this.data.length) index = this.data.length-1;

                var imageData = this.data[index];

                if (!bypassHistory && this.enableHistory)
                    $.historyLoad(String(imageData.hash));  // At the moment, historyLoad only accepts string arguments
                else
                    this.gotoImage(imageData);

                return this;
            },

            // This function is garaunteed to be called anytime a gallery slide changes.
            // @param {Object} imageData An object holding the image metadata of the image to navigate to.
            gotoImage: function(imageData) {
                var index = imageData.index;

                var tomod = index % this.numThumbs;
                var doslidein = (tomod <= this.numThumbs) && (tomod >= 0);
                doslidein = doslidein && (Math.floor(index / this.numThumbs) == Math.floor(this.currentImage.index / this.numThumbs));

                if (this.onSlideChangeOut)
                    this.onSlideChangeOut(this.currentImage.index);

                this.currentImage = imageData;
                this.preloadRelocate(index);

                this.refresh();

                if (this.onSlideChangeIn)
                    if (doslidein)
                        this.onSlideChangeIn(this.currentImage.index);
                    else
                        this.find('ul.thumbs').children().eq(this.currentImage.index).css({'opacity' : 1});

                return this;
            },

            // Returns the default transition duration value.  The value is halved when not
            // performing a synchronized transition.
            // @param {Boolean} isSync Specifies whether the transitions are synchronized.
            getDefaultTransitionDuration: function(isSync) {
                if (isSync)
                    return this.defaultTransitionDuration;
                return this.defaultTransitionDuration / 2;
            },

            // Rebuilds the slideshow image and controls and performs transitions
            refresh: function() {
                var imageData = this.currentImage;
                if (!imageData)
                    return this;

                var index = imageData.index;

                // Update Controls
                if (this.$controlsContainer) {
                    this.$controlsContainer
                        .find('div.nav-controls a.prev').attr('href', '#'+this.data[this.getPrevIndex(index)].hash).end()
                        .find('div.nav-controls a.next').attr('href', '#'+this.data[this.getNextIndex(index)].hash);
                }

                var previousSlide = this.$imageContainer.find('span.current').addClass('previous').removeClass('current');
                var previousCaption = 0;

                if (this.$captionContainer) {
                    previousCaption = this.$captionContainer.find('span.current').addClass('previous').removeClass('current');
                }

                // Perform transitions simultaneously if syncTransitions is true and the next image is already preloaded
                var isSync = this.syncTransitions && imageData.image;

                // Flag we are transitioning
                var isTransitioning = true;
                var gallery = this;

                var transitionOutCallback = function() {
                    // Flag that the transition has completed
                    isTransitioning = false;

                    // Remove the old slide
                    previousSlide.remove();

                    // Remove old caption
                    if (previousCaption)
                        previousCaption.remove();

                    if (!isSync) {
                        if (imageData.image && imageData.hash == gallery.data[gallery.currentImage.index].hash) {
                            gallery.buildImage(imageData, isSync);
                        } else {
                            // Show loading container
                            if (gallery.$loadingContainer) {
                                gallery.$loadingContainer.show();
                            }
                        }
                    }
                };

                if (previousSlide.length == 0) {
                    // For the first slide, the previous slide will be empty, so we will call the callback immediately
                    transitionOutCallback();
                } else {
                    if (this.onTransitionOut) {
                        this.onTransitionOut(previousSlide, previousCaption, isSync, transitionOutCallback);
                    } else {
                        previousSlide.fadeTo(this.getDefaultTransitionDuration(isSync), 0.0, transitionOutCallback);
                        if (previousCaption)
                            previousCaption.fadeTo(this.getDefaultTransitionDuration(isSync), 0.0);
                    }
                }

                // Go ahead and begin transitioning in of next image
                if (isSync)
                    this.buildImage(imageData, isSync);

                if (!imageData.image) {
                    var image = new Image();

                    // Wire up mainImage onload event
                    image.onload = function() {
                        imageData.image = this;

                        // Only build image if the out transition has completed and we are still on the same image hash
                        if (!isTransitioning && imageData.hash == gallery.data[gallery.currentImage.index].hash) {
                            gallery.buildImage(imageData, isSync);
                        }
                    };

                    // set alt and src
                    image.alt = imageData.title;
                    image.src = imageData.slideUrl;
                }

                // This causes the preloader (if still running) to relocate out from the currentIndex
                this.relocatePreload = true;

                return this.syncThumbs();
            },

            // Called by the refresh method after the previous image has been transitioned out or at the same time
            // as the out transition when performing a synchronous transition.
            // @param {Object} imageData An object holding the image metadata of the image to build.
            // @param {Boolean} isSync Specifies whether the transitions are synchronized.
            buildImage: function(imageData, isSync) {
                var gallery = this;
                var nextIndex = this.getNextIndex(imageData.index);

                // Construct new hidden span for the image
                var newSlide = this.$imageContainer
                    .append('<span class="image-wrapper current"><a class="advance-link" rel="history" href="#'+this.data[nextIndex].hash+'" title="'+imageData.title+'">&nbsp;</a></span>')
                    .find('span.current').css('opacity', '0');

                newSlide.find('a')
                    .append(imageData.image)
                    .click(function(e) {
                        gallery.clickHandler(e, this);
                    });

                var newCaption = 0;
                if (this.$captionContainer) {
                    // Construct new hidden caption for the image
                    newCaption = this.$captionContainer
                        .append('<span class="image-caption current"></span>')
                        .find('span.current').css('opacity', '0')
                        .append(imageData.caption);
                }

                // Hide the loading conatiner
                if (this.$loadingContainer) {
                    this.$loadingContainer.hide();
                }

                // Transition in the new image
                if (this.onTransitionIn) {
                    this.onTransitionIn(newSlide, newCaption, isSync);
                } else {
                    newSlide.fadeTo(this.getDefaultTransitionDuration(isSync), 1.0);
                    if (newCaption)
                        newCaption.fadeTo(this.getDefaultTransitionDuration(isSync), 1.0);
                }

                if (this.isSlideshowRunning) {
                    if (this.slideshowTimeout)
                        clearTimeout(this.slideshowTimeout);

                    this.slideshowTimeout = setTimeout(function() { gallery.ssAdvance(); }, this.delay);
                }

                return this;
            },

            // Returns the current page index that should be shown for the currentImage
            getCurrentPage: function() {
                return Math.floor(this.currentImage.index / this.numThumbs);
            },

            // Applies the selected class to the current image's corresponding thumbnail.
            // Also checks if the current page has changed and updates the displayed page of thumbnails if necessary.
            syncThumbs: function() {
                var page = this.getCurrentPage();
                if (page != this.displayedPage)
                    this.updateThumbs();

                // Remove existing selected class and add selected class to new thumb
                var $thumbs = this.find('ul.thumbs').children();
                $thumbs.filter('.selected').removeClass('selected');
                $thumbs.eq(this.currentImage.index).addClass('selected');

                return this;
            },

            // Performs transitions on the thumbnails container and updates the set of
            // thumbnails that are to be displayed and the navigation controls.
            // @param {Delegate} postTransitionOutHandler An optional delegate that is called after
            // the thumbnails container has transitioned out and before the thumbnails are rebuilt.
            updateThumbs: function(postTransitionOutHandler) {
                var gallery = this;
                var transitionOutCallback = function() {
                    // Call the Post-transition Out Handler
                    if (postTransitionOutHandler)
                        postTransitionOutHandler();

                    gallery.rebuildThumbs();

                    // Transition In the thumbsContainer
                    if (gallery.onPageTransitionIn)
                        gallery.onPageTransitionIn();
                    else
                        gallery.show();
                };

                // Transition Out the thumbsContainer
                if (this.onPageTransitionOut) {
                    this.onPageTransitionOut(transitionOutCallback);
                } else {
                    this.hide();
                    transitionOutCallback();
                }

                return this;
            },

            // Updates the set of thumbnails that are to be displayed and the navigation controls.
            rebuildThumbs: function() {
                var needsPagination = this.data.length > this.numThumbs;

                // Rebuild top pager
                if (this.enableTopPager) {
                    var $topPager = this.find('div.top');
                    if ($topPager.length == 0)
                        $topPager = this.prepend('<div class="top pagination"></div>').find('div.top');
                    else
                        $topPager.empty();

                    if (needsPagination)
                        this.buildPager($topPager);
                }

                // Rebuild bottom pager
                if (this.enableBottomPager) {
                    var $bottomPager = this.find('div.bottom');
                    if ($bottomPager.length == 0)
                        $bottomPager = this.append('<div class="bottom pagination"></div>').find('div.bottom');
                    else
                        $bottomPager.empty();

                    if (needsPagination)
                        this.buildPager($bottomPager);
                }

                var page = this.getCurrentPage();
                var startIndex = page*this.numThumbs;
                var stopIndex = startIndex+this.numThumbs-1;
                if (stopIndex >= this.data.length)
                    stopIndex = this.data.length-1;

                // Show/Hide thumbs
                var $thumbsUl = this.find('ul.thumbs');
                $thumbsUl.find('li').each(function(i) {
                    var $li = $(this);
                    if (i >= startIndex && i <= stopIndex) {
                        $li.show();
                    } else {
                        $li.hide();
                    }
                });

                this.displayedPage = page;

                // Remove the noscript class from the thumbs container ul
                $thumbsUl.removeClass('noscript');

                return this;
            },

            // Returns the total number of pages required to display all the thumbnails.
            getNumPages: function() {
                return Math.ceil(this.data.length/this.numThumbs);
            },

            // Rebuilds the pager control in the specified matched element.
            // @param {jQuery} pager A jQuery element set matching the particular pager to be rebuilt.
            buildPager: function(pager) {
                var gallery = this;
                var numPages = this.getNumPages();
                var page = this.getCurrentPage();
                var startIndex = page * this.numThumbs;
                var pagesRemaining = this.maxPagesToShow - 1;

                var pageNum = page - Math.floor((this.maxPagesToShow - 1) / 2) + 1;
                if (pageNum > 0) {
                    var remainingPageCount = numPages - pageNum;
                    if (remainingPageCount < pagesRemaining) {
                        pageNum = pageNum - (pagesRemaining - remainingPageCount);
                    }
                }

                if (pageNum < 0) {
                    pageNum = 0;
                }

                // Prev Page Link
                if (page > 0) {
                    var prevPage = startIndex - this.numThumbs;
                    pager.append('<a rel="history" href="#'+this.data[prevPage].hash+'" title="'+this.prevPageLinkText+'">'+this.prevPageLinkText+'</a>');
                }

                // Create First Page link if needed
                if (pageNum > 0) {
                    this.buildPageLink(pager, 0, numPages);
                    if (pageNum > 1)
                        pager.append('<span class="ellipsis">&hellip;</span>');

                    pagesRemaining--;
                }

                // Page Index Links
                while (pagesRemaining > 0) {
                    this.buildPageLink(pager, pageNum, numPages);
                    pagesRemaining--;
                    pageNum++;
                }

                // Create Last Page link if needed
                if (pageNum < numPages) {
                    var lastPageNum = numPages - 1;
                    if (pageNum < lastPageNum)
                        pager.append('<span class="ellipsis">&hellip;</span>');

                    this.buildPageLink(pager, lastPageNum, numPages);
                }

                // Next Page Link
                var nextPage = startIndex + this.numThumbs;
                if (nextPage < this.data.length) {
                    pager.append('<a rel="history" href="#'+this.data[nextPage].hash+'" title="'+this.nextPageLinkText+'">'+this.nextPageLinkText+'</a>');
                }

                pager.find('a').click(function(e) {
                    gallery.clickHandler(e, this);
                });

                return this;
            },

            // Builds a single page link within a pager.  This function is called by buildPager
            // @param {jQuery} pager A jQuery element set matching the particular pager to be rebuilt.
            // @param {Integer} pageNum The page number of the page link to build.
            // @param {Integer} numPages The total number of pages required to display all thumbnails.
            buildPageLink: function(pager, pageNum, numPages) {
                var pageLabel = pageNum + 1;
                var currentPage = this.getCurrentPage();
                if (pageNum == currentPage)
                    pager.append('<span class="current">'+pageLabel+'</span>');
                else if (pageNum < numPages) {
                    var imageIndex = pageNum*this.numThumbs;
                    pager.append('<a rel="history" href="#'+this.data[imageIndex].hash+'" title="'+pageLabel+'">'+pageLabel+'</a>');
                }

                return this;
            }
        });

        // Now initialize the gallery
        $.extend(this, defaults, settings);

        // Verify the history plugin is available
        if (this.enableHistory && !$.historyInit)
            this.enableHistory = false;

        // Select containers
        if (this.imageContainerSel) this.$imageContainer = $(this.imageContainerSel);
        if (this.captionContainerSel) this.$captionContainer = $(this.captionContainerSel);
        if (this.loadingContainerSel) this.$loadingContainer = $(this.loadingContainerSel);

        // Initialize the thumbails
        this.initializeThumbs();

        if (this.maxPagesToShow < 3)
            this.maxPagesToShow = 3;

        this.displayedPage = -1;
        this.currentImage = this.data[0];
        var gallery = this;

        // Hide the loadingContainer
        if (this.$loadingContainer)
            this.$loadingContainer.hide();

        // Setup controls
        if (this.controlsContainerSel) {
            this.$controlsContainer = $(this.controlsContainerSel).empty();

            if (this.renderSSControls) {
                if (this.autoStart) {
                    this.$controlsContainer
                        .append('<div class="ss-controls"><a href="#pause" class="pause" title="'+this.pauseLinkText+'">'+this.pauseLinkText+'</a></div>');
                } else {
                    this.$controlsContainer
                        .append('<div class="ss-controls"><a href="#play" class="play" title="'+this.playLinkText+'">'+this.playLinkText+'</a></div>');
                }

                this.$controlsContainer.find('div.ss-controls a')
                    .click(function(e) {
                        gallery.toggleSlideshow();
                        e.preventDefault();
                        return false;
                    });
            }

            if (this.renderNavControls) {
                this.$controlsContainer
                    .append('<div class="nav-controls"><a class="prev" rel="history" title="'+this.prevLinkText+'">'+this.prevLinkText+'</a><a class="next" rel="history" title="'+this.nextLinkText+'">'+this.nextLinkText+'</a></div>')
                    .find('div.nav-controls a')
                    .click(function(e) {
                        gallery.clickHandler(e, this);
                    });
            }
        }

        var initFirstImage = !this.enableHistory || !location.hash;
        if (this.enableHistory && location.hash) {
            var hash = $.galleriffic.normalizeHash(location.hash);
            var imageData = allImages[hash];
            if (!imageData)
                initFirstImage = true;
        }

        // Setup gallery to show the first image
        if (initFirstImage)
            this.gotoIndex(0, false, true);

        // Setup Keyboard Navigation
        if (this.enableKeyboardNavigation) {
            $(document).keydown(function(e) {
                var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;
                switch(key) {
                    case 32: // space
                        gallery.next();
                        e.preventDefault();
                        break;
                    case 33: // Page Up
                        gallery.previousPage();
                        e.preventDefault();
                        break;
                    case 34: // Page Down
                        gallery.nextPage();
                        e.preventDefault();
                        break;
                    case 35: // End
                        gallery.gotoIndex(gallery.data.length-1);
                        e.preventDefault();
                        break;
                    case 36: // Home
                        gallery.gotoIndex(0);
                        e.preventDefault();
                        break;
                    case 37: // left arrow
                        gallery.previous();
                        e.preventDefault();
                        break;
                    case 39: // right arrow
                        gallery.next();
                        e.preventDefault();
                        break;
                }
            });
        }

        // Auto start the slideshow
        if (this.autoStart)
            this.play();

        // Kickoff Image Preloader after 1 second
        setTimeout(function() { gallery.preloadInit(); }, 1000);

        return this;
    };
})(jQuery);