Deutschland United States United Kingdom
ContentLion - Open Source CMS

jquery.galleriffic.js

Blame | Last modification | View Log

/**
 * 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,
                onSlideChange:             undefined, // accepts a delegate like such: function(prevIndex, 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;

                                if (this.onSlideChange)
                                        this.onSlideChange(this.currentImage.index, index);
                               
                                this.currentImage = imageData;
                                this.preloadRelocate(index);
                               
                                this.refresh();
                               
                                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);