XoopsModules25x/smallworld

View on GitHub
assets/js/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 (xoops_smallworld) {
    // 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
    xoops_smallworld.galleriffic = {
        version: '2.0.1',

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

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

            hash = xoops_smallworld.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 = xoops_smallworld.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 = xoops_smallworld.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, xoops_smallworldli) { ... }
        onImageRemoved: undefined  // accepts a delegate like such: function(imageData, xoops_smallworldli) { ... }
    };

    // Primary Galleriffic initialization function that should be called on the thumbnail container.
    xoops_smallworld.fn.galleriffic = function (settings) {
        //  Extend Gallery Object
        xoops_smallworld.extend(this, {
            // Returns the version of the script
            version: xoops_smallworld.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 = xoops_smallworld.galleriffic.normalizeHash(xoops_smallworld(link).attr('href'));
                    xoops_smallworld.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 xoops_smallworldli = (typeof listItem === "string") ? xoops_smallworld(listItem) : listItem;
                var xoops_smallworldaThumb = xoops_smallworldli.find('a.thumb');
                var slideUrl = xoops_smallworldaThumb.attr('href');
                var title = xoops_smallworldaThumb.attr('title');
                var xoops_smallworldcaption = xoops_smallworldli.find('.caption').remove();
                var hash = xoops_smallworldaThumb.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: xoops_smallworldcaption,
                    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 xoops_smallworldthumbsUl = gallery.find('ul.thumbs');
                        if (insert)
                            xoops_smallworldthumbsUl.children(':eq(' + position + ')').before(xoops_smallworldli);
                        else
                            xoops_smallworldthumbsUl.append(xoops_smallworldli);

                        if (gallery.onImageAdded)
                            gallery.onImageAdded(imageData, xoops_smallworldli);
                    });
                }

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

                // Setup attributes and click handler
                xoops_smallworldaThumb.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 xoops_smallworld.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 xoops_smallworldli = gallery.find('ul.thumbs')
                        .children(':eq(' + index + ')')
                        .remove();

                    if (gallery.onImageRemoved)
                        gallery.onImageRemoved(imageData, xoops_smallworldli);
                });

                // 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(xoops_smallworld(this), true, false);
                });

                return this;
            },

            isPreloadComplete: false,

            // Initalizes the image preloader
            preloadInit: function () {
                // edit. Cgheck to see if defined
                if (!this.currentImage) {
                    return this;
                }
                // end edit
                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.xoops_smallworldcontrolsContainer) {
                    this.xoops_smallworldcontrolsContainer
                        .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.xoops_smallworldcontrolsContainer) {
                    this.xoops_smallworldcontrolsContainer
                        .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)
                    xoops_smallworld.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) {
                // edit. check to see if defined
                if (!imageData) {
                    return this;
                }
                // end edit
                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.xoops_smallworldcontrolsContainer) {
                    this.xoops_smallworldcontrolsContainer
                        .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.xoops_smallworldimageContainer.find('span.current').addClass('previous').removeClass('current');
                var previousCaption = 0;

                if (this.xoops_smallworldcaptionContainer) {
                    previousCaption = this.xoops_smallworldcaptionContainer.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.xoops_smallworldloadingContainer) {
                                gallery.xoops_smallworldloadingContainer.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.xoops_smallworldimageContainer
                    .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.xoops_smallworldcaptionContainer) {
                    // Construct new hidden caption for the image
                    newCaption = this.xoops_smallworldcaptionContainer
                        .append('<span class="image-caption current"></span>')
                        .find('span.current').css('opacity', '0')
                        .append(imageData.caption);
                }

                // Hide the loading conatiner
                if (this.xoops_smallworldloadingContainer) {
                    this.xoops_smallworldloadingContainer.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 xoops_smallworldthumbs = this.find('ul.thumbs').children();
                xoops_smallworldthumbs.filter('.selected').removeClass('selected');
                xoops_smallworldthumbs.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 xoops_smallworldtopPager = this.find('div.top');
                    if (xoops_smallworldtopPager.length == 0)
                        xoops_smallworldtopPager = this.prepend('<div class="top pagination"></div>').find('div.top');
                    else
                        xoops_smallworldtopPager.empty();

                    if (needsPagination)
                        this.buildPager(xoops_smallworldtopPager);
                }

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

                    if (needsPagination)
                        this.buildPager(xoops_smallworldbottomPager);
                }

                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 xoops_smallworldthumbsUl = this.find('ul.thumbs');
                xoops_smallworldthumbsUl.find('li').each(function (i) {
                    var xoops_smallworldli = xoops_smallworld(this);
                    if (i >= startIndex && i <= stopIndex) {
                        xoops_smallworldli.show();
                    } else {
                        xoops_smallworldli.hide();
                    }
                });

                this.displayedPage = page;

                // Remove the noscript class from the thumbs container ul
                xoops_smallworldthumbsUl.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
        xoops_smallworld.extend(this, defaults, settings);

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

        // Select containers
        if (this.imageContainerSel) this.xoops_smallworldimageContainer = xoops_smallworld(this.imageContainerSel);
        if (this.captionContainerSel) this.xoops_smallworldcaptionContainer = xoops_smallworld(this.captionContainerSel);
        if (this.loadingContainerSel) this.xoops_smallworldloadingContainer = xoops_smallworld(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.xoops_smallworldloadingContainer)
            this.xoops_smallworldloadingContainer.hide();

        // Setup controls
        if (this.controlsContainerSel) {
            this.xoops_smallworldcontrolsContainer = xoops_smallworld(this.controlsContainerSel).empty();

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

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

            if (this.renderNavControls) {
                this.xoops_smallworldcontrolsContainer
                    .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 = xoops_smallworld.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) {
            xoops_smallworld(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);