diff src/main/webapp/imageServer/resources/js/diva.js @ 7:764f47286679

(none)
author jurzua
date Wed, 29 Oct 2014 14:28:34 +0000
parents
children 719475ad0923
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/imageServer/resources/js/diva.js	Wed Oct 29 14:28:34 2014 +0000
@@ -0,0 +1,2592 @@
+window.divaPlugins = [];
+
+// this pattern was taken from http://www.virgentech.com/blog/2009/10/building-object-oriented-jquery-plugin.html
+(function ($)
+{
+    var Diva = function (element, options)
+    {
+        // These are elements that can be overridden upon instantiation
+        // See https://github.com/DDMAL/diva.js/wiki/Code-documentation for more details
+        var defaults = {
+            adaptivePadding: 0.05, // The ratio of padding to the page dimension
+            blockMobileMove: true, // Prevent moving or scrolling the page on mobile devices
+            contained: false, // Determines the location of the fullscreen icon
+            objectData: '', // URL to the JSON file that provides the object dimension data - *MANDATORY*
+            enableAutoHeight: false, // Automatically adjust height based on the window size
+            enableAutoTitle: true, // Shows the title within a div of id diva-title
+            enableAutoWidth: true, // Automatically adjust width based on the window size
+            enableCanvas: true, // Used for the canvas plugin
+            enableDownload: true, // Used for the download plugin
+            enableFilename: true, // Uses filenames and not page numbers for links (i=bm_001.tif, not p=1)
+            enableFullscreen: true, // Enable or disable fullscreen icon (mode still available)
+            enableGotoPage: true, // A "go to page" jump box
+            enableGridIcon: true, // A grid view of all the pages
+            enableGridSlider: true, // Slider to control the pages per grid row
+            enableKeyScroll: true, // Scrolling using the page up/down keys
+            enableLinkIcon: true, // Controls the visibility of the link icon
+            enableSpaceScroll: false, // Scrolling down by pressing the space key
+            enableToolbar: true, // Enables the toolbar. Note that disabling this means you have to handle all controls yourself.
+            enableZoomSlider: true, // Enable or disable the zoom slider (for zooming in and out)
+            fixedPadding: 10, // Fallback if adaptive padding is set to 0
+            fixedHeightGrid: true, // So each page in grid view has the same height (only widths differ)
+            goDirectlyTo: 0, // Default initial page to show (0-indexed)
+            iipServerURL: '', // The URL to the IIPImage installation, including the `?FIF=` - *MANDATORY*
+            inFullscreen: false, // Set to true to load fullscreen mode initially
+            inGrid: false, // Set to true to load grid view initially
+            imageDir: '', // Image directory, either absolute path or relative to IIP's FILESYSTEM_PREFIX - *MANDATORY*
+            maxPagesPerRow: 8, // Maximum number of pages per row, grid view
+            maxZoomLevel: -1, // Optional; defaults to the max zoom returned in the JSON response
+            minPagesPerRow: 2, // 2 for the spread view. Recommended to leave it
+            minZoomLevel: 0, // Defaults to 0 (the minimum zoom)
+            onDocumentLoaded: null, // Callback function for when the document is fully loaded
+            onModeToggle: null, // Callback for toggling fullscreen mode
+            onViewToggle: null, // Callback for switching between grid and document view
+            onJump: null, // Callback function for jumping to a specific page (using the gotoPage feature)
+            onPageLoad: null, // Callback function for loading pages
+            onPageLoaded: null, // Callback function for after the page has been loaded
+            onReady: null, // Callback function for initial load
+            onScroll: null, // Callback function for scrolling
+            onScrollDown: null, // Callback function for scrolling down, only
+            onScrollUp: null, // Callback function for scrolling up only
+            onSetCurrentPage: null, // Callback function for when the current page is set
+            onZoom: null, // Callback function for zooming in general
+            onZoomIn: null, // Callback function for zooming in only
+            onZoomOut: null, // Callback function for zooming out only
+            pageLoadTimeout: 200, // Number of milliseconds to wait before loading pages
+            pagesPerRow: 5, // The default number of pages per row in grid view
+            rowLoadTimeout: 50, // Number of milliseconds to wait before loading a row
+            throbberTimeout: 100, // Number of milliseconds to wait before showing throbber
+            tileHeight: 256, // The height of each tile, in pixels; usually 256
+            tileWidth: 256, // The width of each tile, in pixels; usually 256
+            toolbarParentSelector: null, // The toolbar parent selector. If null, it defaults to the primary diva element. Must be a jQuery selector (leading '#')
+            viewerHeightPadding: 15, // Vertical padding when resizing the viewer, if enableAutoHeight is set
+            viewerWidthPadding: 30, // Horizontal padding when resizing the viewer, if enableAutoHeight is set
+            viewportMargin: 200, // Pretend tiles +/- 200px away from viewport are in
+            zoomLevel: 2 // The initial zoom level (used to store the current zoom level)
+        };
+
+        // Apply the defaults, or override them with passed-in options.
+        var settings = $.extend({}, defaults, options);
+
+        // Things that cannot be changed because of the way they are used by the script
+        // Many of these are declared with arbitrary values that are changed later on
+        var globals = {
+            allTilesLoaded: [], // A boolean for each page, indicating if all tiles have been loaded
+            averageHeights: [], // The average page height for each zoom level
+            averageWidths: [], // The average page width for each zoom level
+            currentPageIndex: 0, // The current page in the viewport (center-most page)
+            dimAfterZoom: 0, // Used for storing the item dimensions after zooming
+            firstPageLoaded: -1, // The ID of the first page loaded (value set later)
+            firstRowLoaded: -1, // The index of the first row loaded
+            gridPageWidth: 0, // Holds the max width of each row in grid view. Calculated in loadGrid()
+            hashParamSuffix: '', // Used when there are multiple document viewers on a page
+            heightAbovePages: [], // The height above each page at the current zoom level
+            horizontalOffset: 0, // Used in documentScroll for scrolling more precisely
+            horizontalPadding: 0, // Either the fixed padding or adaptive padding
+            ID: null, // The prefix of the IDs of the elements (usually 1-diva-)
+            innerSelector: '', // settings.selector + 'inner', for selecting the .diva-inner element
+            itemTitle: '', // The title of the document
+            lastPageLoaded: -1, // The ID of the last page loaded (value set later)
+            lastRowLoaded: -1, // The index of the last row loaded
+            leftScrollSoFar: 0, // Current scroll from the left edge of the pane
+            loaded: false, // A flag for when everything is loaded and ready to go.
+            maxWidths: [], // The width of the widest page for each zoom level
+            maxRatio: 0, // The max height/width ratio (for grid view)
+            minHeight: 0, // Minimum height of the .diva-outer element, as defined in the CSS
+            minRatio: 0, // The minimum height/width ratio for a page
+            minWidth: 0, // Minimum width of the .diva-outer element, as defined in the CSS
+            mobileWebkit: false, // Checks if the user is on a touch device (iPad/iPod/iPhone/Android)
+            numPages: 0, // Number of pages in the array
+            numRows: 0, // Number of rows
+            oldPagesPerRow: 0, // Holds the previous number of pages per row after it is changed
+            oldZoomLevel: -1, // Holds the previous zoom level after zooming in or out
+            orientationChange: false, // For handling device orientation changes for touch devices
+            originalHeight: 0, // Stores the original height of the .diva-outer element
+            originalWidth: 0, // Stores the original width of the .diva-outer element
+            outerSelector: '', // settings.selector + 'outer', for selecting the .diva-outer element
+            pages: [], // An array containing the data for all the pages
+            pageLeftOffsets: [], // Offset from the left side of the pane to the edge of the page
+            pageTimeouts: [], // Stack to hold the loadPage timeouts
+            pageTools: '', // The string for page tools
+            panelHeight: 0, // Height of the document viewer pane
+            panelWidth: 0, // Width of the document viewer pane
+            plugins: [], // Filled with the enabled plugins from window.divaPlugins
+            previousTopScroll: 0, // Used to determine vertical scroll direction
+            preZoomOffset: null, // Holds the offset prior to zooming when double-clicking
+            realMaxZoom: -1, // To hold the true max zoom level of the document (needed for calculations)
+            resizeTimer: -1, // Holds the ID of the timeout used when resizing the window (for clearing)
+            rowHeight: 0, // Holds the max height of each row in grid view. Calculated in loadGrid()
+            scaleWait: false, // For preventing double-zoom on touch devices (iPad, etc)
+            selector: '', // Uses the generated ID prefix to easily select elements
+            singleClick: false, // Used for catching ctrl+double-click events in Firefox in Mac OS
+            scrollbarWidth: 0, // Set to the actual scrollbar width in init()
+            throbberTimeoutID: -1, // Holds the ID of the throbber loading timeout
+            toolbar: null, // Holds an object with some toolbar-related functions
+            topScrollSoFar: 0, // Holds the number of pixels of vertical scroll
+            totalHeights: [], // The total height of all pages (stacked together) for each zoom level
+            totalHeight: 0, // The total height for the current zoom level (including padding)
+            verticalOffset: 0, // See horizontalOffset
+            verticalPadding: 0, // Either the fixed padding or adaptive padding
+            viewerXOffset: 0, // Distance between left edge of viewer and document left edge
+            viewerYOffset: 0 // Like viewerXOffset but for the top edges
+        };
+
+        $.extend(settings, globals);
+
+        // Executes a callback function with the diva instance set as the context
+        // Can take an unlimited number to arguments to pass to the callback function
+        var self = this;
+
+        var executeCallback = function (callback)
+        {
+            var args, i, length;
+
+            if (typeof callback === "function")
+            {
+                args = [];
+                for (i = 1, length = arguments.length; i < length; i++)
+                {
+                    args.push(arguments[i]);
+                }
+
+                callback.apply(self, args);
+
+                return true;
+            }
+
+            return false;
+        };
+
+        var getPageData = function (pageIndex, attribute)
+        {
+            return settings.pages[pageIndex].d[settings.zoomLevel][attribute];
+        };
+
+        // Returns the page index associated with the given filename; must called after settings settings.pages
+        var getPageIndex = function (filename)
+        {
+            var i,
+                np = settings.numPages;
+
+            for (i = 0; i < np; i++)
+            {
+                if (settings.pages[i].f === filename)
+                {
+                    return i;
+                }
+            }
+
+            return -1;
+        };
+
+        // Checks if a tile is within the viewport horizontally
+        var isHorizontallyInViewport = function (left, right)
+        {
+            var panelWidth = settings.panelWidth;
+            var leftOfViewport = settings.leftScrollSoFar - settings.viewportMargin;
+            var rightOfViewport = leftOfViewport + panelWidth + settings.viewportMargin * 2;
+
+            var leftVisible = left >= leftOfViewport && left <= rightOfViewport;
+            var rightVisible = right >= leftOfViewport && right <= rightOfViewport;
+            var middleVisible = left <= leftOfViewport && right >= rightOfViewport;
+
+            return (leftVisible || middleVisible || rightVisible);
+        };
+
+        // Checks if a page or tile is within the viewport vertically
+        var isVerticallyInViewport = function (top, bottom)
+        {
+            var panelHeight = settings.panelHeight;
+            var topOfViewport = settings.topScrollSoFar - settings.viewportMargin;
+            var bottomOfViewport = topOfViewport + panelHeight + settings.viewportMargin * 2;
+
+            var topVisible = top >= topOfViewport && top <= bottomOfViewport;
+            var middleVisible = top <= topOfViewport && bottom >= bottomOfViewport;
+            var bottomVisible = bottom >= topOfViewport && bottom <= bottomOfViewport;
+
+            return (topVisible || middleVisible || bottomVisible);
+        };
+
+        // Check if a tile is near the viewport and thus should be loaded
+        var isTileVisible = function (pageIndex, tileRow, tileCol)
+        {
+            var tileTop = settings.heightAbovePages[pageIndex] + (tileRow * settings.tileHeight) + settings.verticalPadding;
+            var tileBottom = tileTop + settings.tileHeight;
+            var tileLeft = settings.pageLeftOffsets[pageIndex] + (tileCol * settings.tileWidth);
+            var tileRight = tileLeft + settings.tileWidth;
+
+            return isVerticallyInViewport(tileTop, tileBottom) && isHorizontallyInViewport(tileLeft, tileRight);
+        };
+
+        // Check if a tile has been appended to the DOM
+        var isTileLoaded = function (pageIndex, tileIndex)
+        {
+            return document.getElementById(settings.ID + 'tile-' + pageIndex + '-' + tileIndex) === false;
+        };
+
+        // Check if a page index is valid
+        var isPageValid = function (pageIndex)
+        {
+            return pageIndex >= 0 && pageIndex < settings.numPages;
+        };
+
+        // Check if a page is in or near the viewport and thus should be loaded
+        var isPageVisible = function (pageIndex)
+        {
+            var topOfPage = settings.heightAbovePages[pageIndex];
+            var bottomOfPage = topOfPage + getPageData(pageIndex, 'h') + settings.verticalPadding;
+
+            return isVerticallyInViewport(topOfPage, bottomOfPage);
+        };
+
+        // Check if a page has been appended to the DOM
+        var isPageLoaded = function (pageIndex)
+        {
+            return $(document.getElementById(settings.ID + 'page-' + pageIndex)).length > 0;
+        };
+
+        // Appends the page directly into the document body, or loads the relevant tiles
+        var loadPage = function (pageIndex)
+        {
+            // If the page and all of its tiles have been loaded, exit
+            if (isPageLoaded(pageIndex) && settings.allTilesLoaded[pageIndex])
+            {
+                return;
+            }
+
+            // Load some data for this page
+            var filename = settings.pages[pageIndex].f;
+            var width = getPageData(pageIndex, 'w');
+            var height = getPageData(pageIndex, 'h');
+            var heightFromTop = settings.heightAbovePages[pageIndex] + settings.verticalPadding;
+            var pageSelector = settings.selector + 'page-' + pageIndex;
+            var plugin;
+
+            // If the page has not been loaded yet, append the div to the DOM
+            if (!isPageLoaded(pageIndex))
+            {
+                $(document.getElementById(settings.ID + "inner")).append('<div id="' + settings.ID + 'page-' + pageIndex + '" style="top: ' + heightFromTop + 'px; width: ' + width + 'px; height: ' + height + 'px;" class="diva-document-page" title="Page ' + (pageIndex + 1) + '" data-index="' + pageIndex + '" data-filename="' + filename + '">' + settings.pageTools + '</div>');
+
+                // Call the callback function
+                executeCallback(settings.onPageLoad, pageIndex, filename, pageSelector);
+                Events.publish("PageHasLoaded", [pageIndex, filename, pageSelector]);
+
+                // @TODO: Replace this with a notification.
+                // Execute the callback functions for any of the enabled plugins
+                for (plugin in settings.plugins) {
+                    executeCallback(settings.plugins[plugin].onPageLoad, pageIndex, filename, pageSelector);
+                }
+            }
+
+            // There are still tiles to load, so try to load those (after a delay)
+            settings.pageTimeouts.push(setTimeout(function ()
+            {
+                // If the page is no longer in the viewport, don't load any tiles
+                if (!isPageVisible(pageIndex))
+                {
+                    return;
+                }
+
+                var imdir = settings.imageDir + "/";
+                // Load some more data and initialise some variables
+                var rows = getPageData(pageIndex, 'r');
+                var cols = getPageData(pageIndex, 'c');
+                var maxZoom = settings.pages[pageIndex].m;
+                var baseURL = settings.iipServerURL + "?FIF=" + imdir + filename + '&JTL=';
+                var content = [];
+                var allTilesLoaded = true;
+                var tileIndex = 0;
+                var i;
+
+                // Calculate the width and height of outer tiles (non-standard dimensions)
+                var lastHeight = height - (rows - 1) * settings.tileHeight;
+                var lastWidth = width - (cols - 1) * settings.tileWidth;
+
+                // Declare variables used within the loops
+                var row, col, tileHeight, tileWidth, top, left, displayStyle, zoomLevel, imageURL;
+
+                // Adjust the zoom level based on the max zoom level of the page
+                zoomLevel = settings.zoomLevel + maxZoom - settings.realMaxZoom;
+                baseImageURL = baseURL + zoomLevel + ',';
+
+                // Loop through all the tiles in this page
+                row = 0;
+                while (row < rows)
+                {
+                    col = 0;
+                    while (col < cols)
+                    {
+                        top = row * settings.tileHeight;
+                        left = col * settings.tileWidth;
+
+                        // If the tile is in the last row or column, its dimensions will be different
+                        tileHeight = (row === rows - 1) ? lastHeight : settings.tileHeight;
+                        tileWidth = (col === cols - 1) ? lastWidth : settings.tileWidth;
+
+                        imageURL = baseImageURL + tileIndex;
+
+                        // this check looks to see if the tile is already loaded, and then if
+                        // it isn't, if it should be visible.
+                        if (!isTileLoaded(pageIndex, tileIndex)) {
+                            if (isTileVisible(pageIndex, row, col)) {
+                                content.push('<div id="' + settings.ID + 'tile-' + pageIndex + '-' + tileIndex + '" style="display:inline; position: absolute; top: ' + top + 'px; left: ' + left + 'px; background-image: url(\'' + imageURL + '\'); height: ' + tileHeight + 'px; width: ' + tileWidth + 'px;"></div>');
+                            } else {
+                                // The tile does not need to be loaded - not all have been loaded
+                                allTilesLoaded = false;
+                            }
+                        }
+                        tileIndex++;
+                        col++;
+                    }
+                    row++;
+                }
+
+                settings.allTilesLoaded[pageIndex] = allTilesLoaded;
+                $(document.getElementById(settings.ID + 'page-' + pageIndex)).append(content.join(''));
+
+                executeCallback(settings.onPageLoaded, pageIndex, filename, pageSelector);
+
+            }, settings.pageLoadTimeout));
+        };
+
+        // Delete a page from the DOM; will occur when a page is scrolled out of the viewport
+        var deletePage = function (pageIndex)
+        {
+            $(document.getElementById(settings.ID + 'page-' + pageIndex)).empty().remove();
+        };
+
+        // Check if the bottom of a page is above the top of a viewport (scrolling down)
+        // For when you want to keep looping but don't want to load a specific page
+        var pageAboveViewport = function (pageIndex)
+        {
+            var bottomOfPage = settings.heightAbovePages[pageIndex] + getPageData(pageIndex, 'h') + settings.verticalPadding;
+            var topOfViewport = settings.topScrollSoFar;
+
+            return bottomOfPage < topOfViewport;
+        };
+
+        // Check if the top of a page is below the bottom of a viewport (scrolling up)
+        var pageBelowViewport = function (pageIndex)
+        {
+            var topOfPage = settings.heightAbovePages[pageIndex];
+            var bottomOfViewport = settings.topScrollSoFar + settings.panelHeight;
+
+            return topOfPage > bottomOfViewport;
+        };
+
+        // Called by adjust pages - determine what pages should be visible, and show them
+        var attemptPageShow = function (pageIndex, direction)
+        {
+            if (direction > 0)
+            {
+                // Direction is positive - we're scrolling down
+                if (isPageValid(pageIndex))
+                {
+                    // If the page should be visible, then yes, add it
+                    if (isPageVisible(pageIndex))
+                    {
+                        loadPage(pageIndex);
+
+                        settings.lastPageLoaded = pageIndex;
+
+                        // Recursively call this function until there's nothing to add
+                        attemptPageShow(settings.lastPageLoaded + 1, direction);
+                    }
+                    else if (pageAboveViewport(pageIndex))
+                    {
+                        // If the page is below the viewport. try to load the next one
+                        attemptPageShow(pageIndex + 1, direction);
+                    }
+                }
+            }
+            else
+            {
+                // Direction is negative - we're scrolling up
+                if (isPageValid(pageIndex))
+                {
+                    // If it's near the viewport, yes, add it
+                    if (isPageVisible(pageIndex))
+                    {
+                        loadPage(pageIndex);
+
+                        // Reset the first page loaded to this one
+                        settings.firstPageLoaded = pageIndex;
+
+                        // Recursively call this function until there's nothing to add
+                        attemptPageShow(settings.firstPageLoaded - 1, direction);
+                    }
+                    else if (pageBelowViewport(pageIndex))
+                    {
+                        // Attempt to call this on the next page, do not increment anything
+                        attemptPageShow(pageIndex - 1, direction);
+                    }
+                }
+            }
+        };
+
+        // Called by adjustPages - see what pages need to be hidden, and hide them
+        var attemptPageHide = function (pageIndex, direction)
+        {
+            if (direction > 0)
+            {
+                // Scrolling down - see if this page needs to be deleted from the DOM
+                if (isPageValid(pageIndex) && pageAboveViewport(pageIndex))
+                {
+                    // Yes, delete it, reset the first page loaded
+                    deletePage(pageIndex);
+                    settings.firstPageLoaded = pageIndex + 1;
+
+                    // Try to call this function recursively until there's nothing to delete
+                    attemptPageHide(settings.firstPageLoaded, direction);
+                }
+            }
+            else
+            {
+                // Direction must be negative (not 0 - see adjustPages), we're scrolling up
+                if (isPageValid(pageIndex) && pageBelowViewport(pageIndex))
+                {
+                    // Yes, delete it, reset the last page loaded
+                    deletePage(pageIndex);
+                    settings.lastPageLoaded = pageIndex - 1;
+
+                    // Try to call this function recursively until there's nothing to delete
+                    attemptPageHide(settings.lastPageLoaded, direction);
+                }
+            }
+        };
+
+        // Handles showing and hiding pages when the user scrolls
+        var adjustPages = function (direction)
+        {
+            var i;
+
+            // Direction is negative, so we're scrolling up
+            if (direction < 0)
+            {
+                attemptPageShow(settings.firstPageLoaded, direction);
+                setCurrentPage(-1);
+                attemptPageHide(settings.lastPageLoaded, direction);
+            }
+            else if (direction > 0)
+            {
+                // Direction is positive so we're scrolling down
+                attemptPageShow(settings.lastPageLoaded, direction);
+                setCurrentPage(1);
+                attemptPageHide(settings.firstPageLoaded, direction);
+            }
+            else
+            {
+                // Horizontal scroll, check if we need to reveal any tiles
+                var lpl = settings.lastPageLoaded;
+                for (i = Math.max(settings.firstPageLoaded, 0); i <= lpl; i++)
+                {
+                    if (isPageVisible(i))
+                    {
+                        loadPage(i);
+                    }
+                }
+            }
+
+            executeCallback(settings.onScroll, settings.topScrollSoFar);
+
+            // If we're scrolling down
+            if (direction > 0)
+            {
+                executeCallback(settings.onScrollDown, settings.topScrollSoFar);
+            }
+            else if (direction < 0)
+            {
+                // We're scrolling up
+                executeCallback(settings.onScrollUp, settings.topScrollSoFar);
+            }
+        };
+
+        // Check if a row index is valid
+        var isRowValid = function (rowIndex)
+        {
+            return rowIndex >= 0 && rowIndex < settings.numRows;
+        };
+
+        // Check if a row should be visible in the viewport
+        var isRowVisible = function (rowIndex)
+        {
+            var topOfRow = settings.rowHeight * rowIndex;
+            var bottomOfRow = topOfRow + settings.rowHeight + settings.fixedPadding;
+
+            return isVerticallyInViewport(topOfRow, bottomOfRow);
+        };
+
+        // Check if a row (in grid view) is present in the DOM
+        var isRowLoaded = function (rowIndex)
+        {
+            return $(settings.selector + 'row-' + rowIndex).length > 0;
+        };
+
+        var loadRow = function (rowIndex)
+        {
+            // If the row has already been loaded, don't attempt to load it again
+            if (isRowLoaded(rowIndex))
+            {
+                return;
+            }
+
+            // Load some data for this and initialise some variables
+            var heightFromTop = (settings.rowHeight * rowIndex) + settings.fixedPadding;
+            var content = [];
+
+            // Create the opening tag for the row div
+            content.push('<div class="diva-row" id="' + settings.ID + 'row-' + rowIndex + '" style="height: ' + settings.rowHeight + '; top: ' + heightFromTop + 'px;">');
+
+            // Declare variables used in the loop
+            var i, pageIndex, filename, realWidth, realHeight, pageWidth, pageHeight, leftOffset, imageURL;
+            var imdir = settings.imageDir + "/";
+
+            // Load each page within that row
+            var ppr = settings.pagesPerRow;
+            for (i = 0; i < ppr; i++)
+            {
+                pageIndex = rowIndex * settings.pagesPerRow + i;
+
+                // If this page is the last row, don't try to load a nonexistent page
+                if (!isPageValid(pageIndex))
+                {
+                    break;
+                }
+
+                // Calculate the width, height and horizontal placement of this page
+                filename = settings.pages[pageIndex].f;
+                realWidth = getPageData(pageIndex, 'w');
+                realHeight = getPageData(pageIndex, 'h');
+                pageWidth = (settings.fixedHeightGrid) ? (settings.rowHeight - settings.fixedPadding) * realWidth / realHeight : settings.gridPageWidth;
+                pageHeight = (settings.fixedHeightGrid) ? settings.rowHeight - settings.fixedPadding : pageWidth / realWidth * realHeight;
+                leftOffset = parseInt(i * (settings.fixedPadding + settings.gridPageWidth) + settings.fixedPadding, 10);
+
+                // Make sure they're all integers for nice, round numbers
+                pageWidth = parseInt(pageWidth, 10);
+                pageHeight = parseInt(pageHeight, 10);
+
+                // Center the page if the height is fixed (otherwise, there is no horizontal padding)
+                leftOffset += (settings.fixedHeightGrid) ? (settings.gridPageWidth - pageWidth) / 2 : 0;
+                imageURL = settings.iipServerURL + "?FIF=" + imdir + filename + '&amp;HEI=' + (pageHeight + 2) + '&amp;CVT=JPEG';
+
+                // Append the HTML for this page to the string builder array
+                content.push('<div id="' + settings.ID + 'page-' + pageIndex + '" class="diva-page" style="width: ' + pageWidth + 'px; height: ' + pageHeight + 'px; left: ' + leftOffset + 'px;" title="Page ' + (pageIndex + 1) + '"></div>');
+
+                // Add each image to a queue so that images aren't loaded unnecessarily
+                addPageToQueue(rowIndex, pageIndex, imageURL, pageWidth, pageHeight);
+            }
+
+            // Append this row to the DOM
+            content.push('</div>');
+            $(document.getElementById(settings.ID + "inner")).append(content.join(''));
+        };
+
+        var deleteRow = function (rowIndex)
+        {
+            $(document.getElementById(settings.ID + 'row-' + rowIndex)).empty().remove();
+        };
+
+        // Check if the bottom of a row is above the top of the viewport (scrolling down)
+        var rowAboveViewport = function (rowIndex)
+        {
+            var bottomOfRow = settings.rowHeight * (rowIndex + 1);
+            var topOfViewport = settings.topScrollSoFar;
+
+            return (bottomOfRow < topOfViewport);
+        };
+
+        // Check if the top of a row is below the bottom of the viewport (scrolling up)
+        var rowBelowViewport = function (rowIndex)
+        {
+            var topOfRow = settings.rowHeight * rowIndex;
+            var bottomOfViewport = settings.topScrollSoFar + settings.panelHeight;
+
+            return (topOfRow > bottomOfViewport);
+        };
+
+        // Same thing as attemptPageShow only with rows
+        var attemptRowShow = function (rowIndex, direction)
+        {
+            if (direction > 0)
+            {
+                if (isRowValid(rowIndex))
+                {
+                    if (isRowVisible(rowIndex))
+                    {
+                        loadRow(rowIndex);
+                        settings.lastRowLoaded = rowIndex;
+
+                        attemptRowShow(settings.lastRowLoaded + 1, direction);
+                    }
+                    else if (rowAboveViewport(rowIndex))
+                    {
+                        attemptRowShow(rowIndex + 1, direction);
+                    }
+                }
+            }
+            else
+            {
+                if (isRowValid(rowIndex))
+                {
+                    if (isRowVisible(rowIndex))
+                    {
+                        loadRow(rowIndex);
+                        settings.firstRowLoaded = rowIndex;
+
+                        attemptRowShow(settings.firstRowLoaded - 1, direction);
+                    }
+                    else if (rowBelowViewport(rowIndex))
+                    {
+                        attemptRowShow(rowIndex - 1, direction);
+                    }
+                }
+            }
+        };
+
+        var attemptRowHide = function (rowIndex, direction)
+        {
+            if (direction > 0)
+            {
+                if (isRowValid(rowIndex) && rowAboveViewport(rowIndex))
+                {
+                    deleteRow(rowIndex);
+                    settings.firstRowLoaded++;
+
+                    attemptRowHide(settings.firstRowLoaded, direction);
+                }
+            }
+            else
+            {
+                if (isRowValid(rowIndex) && rowBelowViewport(rowIndex))
+                {
+                    deleteRow(rowIndex);
+                    settings.lastRowLoaded--;
+
+                    attemptRowHide(settings.lastRowLoaded, direction);
+                }
+            }
+        };
+
+        var adjustRows = function (direction)
+        {
+            if (direction < 0)
+            {
+                attemptRowShow(settings.firstRowLoaded, -1);
+                setCurrentRow(-1);
+                attemptRowHide(settings.lastRowLoaded, -1);
+            }
+            else if (direction > 0)
+            {
+                attemptRowShow(settings.lastRowLoaded, 1);
+                setCurrentRow(1);
+                attemptRowHide(settings.firstRowLoaded, 1);
+            }
+
+            executeCallback(settings.onScroll, settings.topScrollSoFar);
+
+            // If we're scrolling down
+            if (direction > 0)
+            {
+                executeCallback(settings.onScrollDown, settings.topScrollSoFar);
+            }
+            else if (direction < 0)
+            {
+                // We're scrolling up
+                executeCallback(settings.onScrollUp, settings.topScrollSoFar);
+            }
+        };
+
+        // Used to delay loading of page images in grid view to prevent unnecessary loads
+        var addPageToQueue = function (rowIndex, pageIndex, imageURL, pageWidth, pageHeight)
+        {
+            settings.pageTimeouts.push(setTimeout(function ()
+            {
+                if (isRowVisible(rowIndex))
+                {
+                    $(settings.selector + 'page-' + pageIndex).html('<img src="' + imageURL + '" style="width: ' + pageWidth + 'px; height: ' + pageHeight + 'px;" />');
+                }
+            }, settings.rowLoadTimeout));
+        };
+
+        // Determines and sets the "current page" (settings.currentPageIndex); called within adjustPages
+        // The "direction" is either 1 (downward scroll) or -1 (upward scroll)
+        var setCurrentPage = function (direction)
+        {
+            var middleOfViewport = settings.topScrollSoFar + (settings.panelHeight / 2);
+            var currentPage = settings.currentPageIndex;
+            var pageToConsider = settings.currentPageIndex + direction;
+            var changeCurrentPage = false;
+            var pageSelector = settings.selector + 'page-' + pageToConsider;
+
+            // When scrolling up:
+            if (direction < 0)
+            {
+                // If the previous page > middle of viewport
+                if (pageToConsider >= 0 && (settings.heightAbovePages[pageToConsider] + getPageData(pageToConsider, 'h') + (settings.verticalPadding) >= middleOfViewport))
+                {
+                    changeCurrentPage = true;
+                }
+            }
+            else if (direction > 0)
+            {
+                // When scrolling down:
+                // If this page < middle of viewport
+                if (settings.heightAbovePages[currentPage] + getPageData(currentPage, 'h') + settings.verticalPadding < middleOfViewport)
+                {
+                    changeCurrentPage = true;
+                }
+            }
+
+            if (changeCurrentPage)
+            {
+                // Set this to the current page
+                settings.currentPageIndex = pageToConsider;
+                // Now try to change the next page, given that we're not going to a specific page
+                // Calls itself recursively - this way we accurately obtain the current page
+                if (direction !== 0)
+                {
+                    if (!setCurrentPage(direction))
+                    {
+                        var filename = settings.pages[pageToConsider].f;
+                        executeCallback(settings.onSetCurrentPage, pageToConsider, filename);
+                        Events.publish("VisiblePageDidChange", [pageToConsider, filename]);
+                    }
+                }
+                return true;
+            }
+
+            return false;
+        };
+
+        // Sets the current page in grid view
+        var setCurrentRow = function (direction)
+        {
+            var currentRow = Math.floor(settings.currentPageIndex / settings.pagesPerRow);
+            var rowToConsider = currentRow + parseInt(direction, 10);
+            var middleOfViewport = settings.topScrollSoFar + (settings.panelHeight / 2);
+            var changeCurrentRow = false;
+
+            if (direction < 0)
+            {
+                if (rowToConsider >= 0 && (settings.rowHeight * currentRow >= middleOfViewport || settings.rowHeight * rowToConsider >= settings.topScrollSoFar))
+                {
+                    changeCurrentRow = true;
+                }
+            }
+            else if (direction > 0)
+            {
+                if ((settings.rowHeight * (currentRow + 1)) < settings.topScrollSoFar && isRowValid(rowToConsider))
+                {
+                    changeCurrentRow = true;
+                }
+            }
+
+            if (changeCurrentRow)
+            {
+                settings.currentPageIndex = rowToConsider * settings.pagesPerRow;
+
+                if (direction !== 0)
+                {
+                    if (!setCurrentRow(direction))
+                    {
+                        var pageIndex = settings.currentPageIndex;
+                        var filename = settings.pages[pageIndex].f;
+                        Events.publish("VisiblePageDidChange", [pageIndex, filename]);
+                    }
+                }
+
+                return true;
+            }
+
+            return false;
+        };
+
+        // Helper function for going to a particular page
+        // Vertical offset: from the top of the page (including the top padding)
+        // Horizontal offset: from the center of the page; can be negative if to the left
+        var gotoPage = function (pageIndex, verticalOffset, horizontalOffset)
+        {
+            verticalOffset = (typeof verticalOffset !== 'undefined') ? verticalOffset : 0;
+            horizontalOffset = (typeof horizontalOffset !== 'undefined') ? horizontalOffset: 0;
+            var desiredTop = settings.heightAbovePages[pageIndex] + verticalOffset;
+            var desiredLeft = (settings.maxWidths[settings.zoomLevel] - settings.panelWidth) / 2 + settings.horizontalPadding + horizontalOffset;
+
+            $(settings.outerSelector).scrollTop(desiredTop);
+            $(settings.outerSelector).scrollLeft(desiredLeft);
+
+            // Pretend that this is the current page
+            settings.currentPageIndex = pageIndex;
+            //settings.toolbar.updateCurrentPage();
+            var filename = settings.pages[pageIndex].f;
+
+            Events.publish("VisiblePageDidChange", [pageIndex, filename]);
+            executeCallback(settings.onSetCurrentPage, pageIndex, filename);
+
+            // Execute the onJump callback
+            executeCallback(settings.onJump, pageIndex);
+        };
+
+        // Calculates the desired row, then scrolls there
+        var gotoRow = function (pageIndex)
+        {
+            var desiredRow = Math.floor(pageIndex / settings.pagesPerRow);
+            var desiredTop = desiredRow * settings.rowHeight;
+            $(settings.outerSelector).scrollTop(desiredTop);
+
+            // Pretend that this is the current page (it probably isn't)
+            settings.currentPageIndex = pageIndex;
+            var filename = settings.pages[pageIndex].f;
+            Events.publish("VisiblePageDidChange", [pageIndex, filename]);
+        };
+
+        // Helper function called by loadDocument to scroll to the desired place
+        var documentScroll = function ()
+        {
+            // If settings.preZoomOffset is defined, the zoom was trigged by double-clicking
+            // We then zoom in on a specific region
+            if (settings.preZoomOffset)
+            {
+                var clickedPage = settings.preZoomOffset.i;
+                var heightAbovePage = settings.heightAbovePages[clickedPage] + settings.verticalPadding;
+                var pageLeftOffset = settings.pageLeftOffsets[clickedPage];
+                var zoomRatio = Math.pow(2, settings.zoomLevel - settings.oldZoomLevel);
+
+                var distanceFromViewport = {
+                    x: settings.preZoomOffset.originalX - settings.viewerXOffset,
+                    y: settings.preZoomOffset.originalY - settings.viewerYOffset
+                };
+
+                var newDistanceToEdge = {
+                    x: settings.preZoomOffset.x * zoomRatio,
+                    y: settings.preZoomOffset.y * zoomRatio
+                };
+
+                var newScroll = {
+                    x: newDistanceToEdge.x - distanceFromViewport.x + pageLeftOffset,
+                    y: newDistanceToEdge.y - distanceFromViewport.y + heightAbovePage
+                };
+
+                $(settings.outerSelector).scrollTop(newScroll.y).scrollLeft(newScroll.x);
+
+                settings.preZoomOffset = undefined;
+            }
+            else
+            {
+                // Otherwise, we just scroll to the page saved in settings.goDirectlyTo (must be valid)
+                // Make sure the value for settings.goDirectlyTo is valid
+                if (!isPageValid(settings.goDirectlyTo))
+                {
+                    settings.goDirectlyTo = 0;
+                }
+
+                // We use the stored y/x offsets (relative to the top of the page and the center, respectively)
+                gotoPage(settings.goDirectlyTo, settings.verticalOffset, settings.horizontalOffset);
+                settings.horizontalOffset = 0;
+                settings.verticalOffset = 0;
+            }
+        };
+
+        // Don't call this when not in grid mode please
+        // Scrolls to the relevant place when in grid view
+        var gridScroll = function ()
+        {
+            // Figure out and scroll to the row containing the current page
+            gotoRow(settings.goDirectlyTo);
+        };
+
+        // If the given zoom level is valid, returns it; else, returns the min
+        var getValidZoomLevel = function (zoomLevel)
+        {
+            return (zoomLevel >= settings.minZoomLevel && zoomLevel <= settings.maxZoomLevel) ? zoomLevel : settings.minZoomLevel;
+        };
+
+        var getValidPagesPerRow = function (pagesPerRow)
+        {
+            return (pagesPerRow >= settings.minPagesPerRow && pagesPerRow <= settings.maxPagesPerRow) ? pagesPerRow : settings.maxPagesPerRow;
+        };
+
+        // Reset some settings and empty the viewport
+        var clearViewer = function ()
+        {
+            settings.allTilesLoaded = [];
+            $(settings.outerSelector).scrollTop(0);
+            settings.topScrollSoFar = 0;
+            $(settings.innerSelector).empty();
+            settings.firstPageLoaded = 0;
+            settings.firstRowLoaded = -1;
+            settings.previousTopScroll = 0;
+
+            // Clear all the timeouts to prevent undesired pages from loading
+            clearTimeout(settings.resizeTimer);
+
+            while (settings.pageTimeouts.length)
+            {
+                clearTimeout(settings.pageTimeouts.pop());
+            }
+        };
+
+        // Called when we don't necessarily know which view to go into
+        var loadViewer = function ()
+        {
+            if (settings.inGrid)
+            {
+                loadGrid();
+            }
+            else
+            {
+                loadDocument();
+            }
+        };
+
+        // Called every time we need to load document view (after zooming, fullscreen, etc)
+        var loadDocument = function ()
+        {
+            clearViewer();
+
+            // Make sure the zoom level we've been given is valid
+            settings.zoomLevel = getValidZoomLevel(settings.zoomLevel);
+            var z = settings.zoomLevel;
+
+            // Calculate the horizontal and vertical inter-page padding
+            if (settings.adaptivePadding > 0)
+            {
+                settings.horizontalPadding = settings.averageWidths[z] * settings.adaptivePadding;
+                settings.verticalPadding = settings.averageHeights[z] * settings.adaptivePadding;
+            }
+            else
+            {
+                // It's less than or equal to 0; use fixedPadding instead
+                settings.horizontalPadding = settings.fixedPadding;
+                settings.verticalPadding = settings.fixedPadding;
+            }
+
+            // Make sure the vertical padding is at least 40, if plugin icons are enabled
+            if (settings.pageTools.length)
+            {
+                settings.verticalPadding = Math.max(40, settings.horizontalPadding);
+            }
+
+            // Now reset some things that need to be changed after each zoom
+            settings.totalHeight = settings.totalHeights[z] + settings.verticalPadding * (settings.numPages + 1);
+            settings.dimAfterZoom = settings.totalHeight;
+
+            // Determine the width of the inner element (based on the max width)
+            var maxWidthToSet = settings.maxWidths[z] + settings.horizontalPadding * 2;
+            var widthToSet = Math.max(maxWidthToSet, settings.panelWidth);
+
+            // Needed to set settings.heightAbovePages - initially just the top padding
+            var heightSoFar = 0;
+            var i;
+
+            for (i = 0; i < settings.numPages; i++)
+            {
+                // First set the height above that page by adding this height to the previous total
+                // A page includes the padding above it
+                settings.heightAbovePages[i] = heightSoFar;
+
+                // Has to be done this way otherwise you get the height of the page included too
+                heightSoFar = settings.heightAbovePages[i] + getPageData(i, 'h') + settings.verticalPadding;
+
+                // Figure out the pageLeftOffset stuff
+                settings.pageLeftOffsets[i] = (widthToSet - getPageData(i, 'w')) / 2;
+
+                // Now try to load the page ONLY if the page needs to be loaded
+                // Take scrolling into account later, just try this for now
+                if (isPageVisible(i))
+                {
+                    loadPage(i);
+                    settings.lastPageLoaded = i;
+                }
+            }
+
+            // If this is not the initial load, execute the zoom callbacks
+            if (settings.oldZoomLevel >= 0)
+            {
+                if (settings.oldZoomLevel < settings.zoomLevel)
+                {
+                    executeCallback(settings.onZoomIn, z);
+                }
+                else
+                {
+                    executeCallback(settings.onZoomOut, z);
+                }
+
+                executeCallback(settings.onZoom, z);
+            }
+
+            // Set the height and width of documentpane (necessary for dragscrollable)
+            $(settings.innerSelector).height(Math.round(settings.totalHeight));
+            $(settings.innerSelector).width(Math.round(widthToSet));
+
+            // Scroll to the proper place
+            documentScroll();
+
+            // For the iPad - wait until this request finishes before accepting others
+            if (settings.scaleWait)
+            {
+                settings.scaleWait = false;
+            }
+
+            var fileName = settings.pages[settings.currentPageIndex].f;
+            executeCallback(settings.onDocumentLoaded, settings.lastPageLoaded, fileName);
+            Events.publish("DocumentHasFinishedLoading", [settings.lastPageLoaded, fileName]);
+        };
+
+        var loadGrid = function ()
+        {
+            clearViewer();
+
+            // Make sure the pages per row setting is valid
+            settings.pagesPerRow = getValidPagesPerRow(settings.pagesPerRow);
+
+            var horizontalPadding = settings.fixedPadding * (settings.pagesPerRow + 1);
+            var pageWidth = (settings.panelWidth - horizontalPadding) / settings.pagesPerRow;
+            settings.gridPageWidth = pageWidth;
+
+            // Calculate the row height depending on whether we want to fix the width or the height
+            settings.rowHeight = (settings.fixedHeightGrid) ? settings.fixedPadding + settings.minRatio * pageWidth : settings.fixedPadding + settings.maxRatio * pageWidth;
+            settings.numRows = Math.ceil(settings.numPages / settings.pagesPerRow);
+            settings.totalHeight = settings.numRows * settings.rowHeight + settings.fixedPadding;
+
+            $(settings.innerSelector).height(Math.round(settings.totalHeight));
+            $(settings.innerSelector).width(Math.round(settings.panelWidth));
+
+            // First scroll directly to the row containing the current page
+            gridScroll();
+
+            var i, rowIndex;
+
+            // Figure out the row each page is in
+            var np = settings.numPages;
+            for (i = 0; i < np; i += settings.pagesPerRow)
+            {
+                rowIndex = Math.floor(i / settings.pagesPerRow);
+
+                if (isRowVisible(rowIndex))
+                {
+                    settings.firstRowLoaded = (settings.firstRowLoaded < 0) ? rowIndex : settings.firstRowLoaded;
+                    loadRow(rowIndex);
+                    settings.lastRowLoaded = rowIndex;
+                }
+            }
+        };
+
+        // Handles switching in and out of fullscreen mode
+        // Should only be called after changing settings.inFullscreen
+        var handleModeChange = function (changeView)
+        {
+            // Save some offsets (required for scrolling properly), if it's not the initial load
+            if (settings.oldZoomLevel >= 0)
+            {
+                if (!settings.inGrid)
+                {
+                    var pageOffset = $(settings.selector + 'page-' + settings.currentPageIndex).offset();
+                    var topOffset = -(pageOffset.top - settings.verticalPadding - settings.viewerYOffset);
+                    var expectedLeft = (settings.panelWidth - getPageData(settings.currentPageIndex, 'w')) / 2;
+                    var leftOffset = -(pageOffset.left - settings.viewerXOffset - expectedLeft);
+                    settings.verticalOffset = topOffset;
+                    settings.horizontalOffset = leftOffset;
+                }
+            }
+
+            // Change the look of the toolbar
+            Events.publish("ModeDidSwitch", null);
+
+            // Toggle the classes
+            $(settings.selector + 'fullscreen').toggleClass('diva-in-fullscreen');
+            $(settings.outerSelector).toggleClass('diva-fullscreen');
+            $('body').toggleClass('diva-hide-scrollbar');
+            $(settings.parentSelector).toggleClass('diva-full-width');
+
+            // Reset the panel dimensions
+            settings.panelHeight = $(settings.outerSelector).height();
+            settings.panelWidth = $(settings.outerSelector).width() - settings.scrollbarWidth;
+            $(settings.innerSelector).width(settings.panelWidth);
+
+            // Recalculate the viewer offsets
+            settings.viewerXOffset = $(settings.outerSelector).offset().left;
+            settings.viewerYOffset = $(settings.outerSelector).offset().top;
+
+            // Used by setState when we need to change the view and the mode
+            if (changeView)
+            {
+                settings.inGrid = !settings.inGrid;
+                handleViewChange();
+            }
+            else
+            {
+                loadViewer();
+            }
+
+            // Execute callbacks
+            executeCallback(settings.onModeToggle, settings.inFullscreen);
+            Events.publish("ModeHasChanged", [settings.inFullScreen]);
+        };
+
+        // Handles switching in and out of grid view
+        // Should only be called after changing settings.inGrid
+        var handleViewChange = function ()
+        {
+            // Switch the slider
+            // Events.publish("ViewDidSwitch", null);
+
+            loadViewer();
+            executeCallback(settings.onViewToggle, settings.inGrid);
+            Events.publish("ViewDidSwitch", [settings.inGrid]);
+        };
+
+        // Called when the fullscreen icon is clicked
+        var toggleFullscreen = function ()
+        {
+            settings.goDirectlyTo = settings.currentPageIndex;
+            settings.inFullscreen = !settings.inFullscreen;
+            handleModeChange(false);
+        };
+
+        // Called when the grid icon is clicked
+        var toggleGrid = function ()
+        {
+            settings.goDirectlyTo = settings.currentPageIndex;
+            settings.inGrid = !settings.inGrid;
+            handleViewChange();
+        };
+
+        // Called after double-click or ctrl+double-click events on pages in document view
+        var handleDocumentDoubleClick = function (event)
+        {
+            var pageOffset = $(this).offset();
+            var offsetX = event.pageX - pageOffset.left;
+            var offsetY = event.pageY - pageOffset.top;
+
+            // Store the offset information so that it can be used in documentScroll()
+            settings.preZoomOffset = {
+                x: offsetX,
+                y: offsetY,
+                originalX: event.pageX,
+                originalY: event.pageY,
+                i: $(this).attr('data-index')
+            };
+
+            // Hold control to zoom out, otherwise, zoom in
+            var newZoomLevel = (event.ctrlKey) ? settings.zoomLevel - 1 : settings.zoomLevel + 1;
+
+            handleZoom(newZoomLevel);
+        };
+
+        // Called after double-clicking on a page in grid view
+        var handleGridDoubleClick = function (event)
+        {
+            // Figure out the page that was clicked, scroll to that page
+            var sel = document.getElementById(settings.ID + "outer");
+            var centerX = (event.pageX - settings.viewerXOffset) + sel.scrollLeft;
+            var centerY = (event.pageY - settings.viewerYOffset) + sel.scrollTop;
+            var rowIndex = Math.floor(centerY / settings.rowHeight);
+            var colIndex = Math.floor(centerX / (settings.panelWidth / settings.pagesPerRow));
+            var pageIndex = rowIndex * settings.pagesPerRow + colIndex;
+            settings.goDirectlyTo = pageIndex;
+
+            // Leave grid view, jump directly to the desired page
+            settings.inGrid = false;
+            handleViewChange();
+        };
+
+        // Handles pinch-zooming for mobile devices
+        var handlePinchZoom = function (event)
+        {
+            var newZoomLevel = settings.zoomLevel;
+
+            // First figure out the new zoom level:
+            if (event.scale > 1 && newZoomLevel < settings.maxZoomLevel)
+            {
+                newZoomLevel++;
+            }
+            else if (event.scale < 1 && newZoomLevel > settings.minZoomLevel)
+            {
+                newZoomLevel--;
+            }
+            else
+            {
+                return;
+            }
+
+            // Set it to true so we have to wait for this one to finish
+            settings.scaleWait = true;
+
+            // Has to call handleZoomSlide so that the coordinates are kept
+            handleZoom(newZoomLevel);
+        };
+
+        // Called to handle any zoom level
+        var handleZoom = function (newValue)
+        {
+            var newZoomLevel = getValidZoomLevel(newValue);
+
+            // If the zoom level provided is invalid, return false
+            if (newZoomLevel !== newValue)
+            {
+                return false;
+            }
+
+            settings.oldZoomLevel = settings.zoomLevel;
+            settings.zoomLevel = newZoomLevel;
+
+            // Update the slider
+            Events.publish("ZoomLevelDidChange", null);
+
+            loadDocument();
+
+            return true;
+        };
+
+        // Called to handle changing the pages per row slider
+        var handleGrid = function (newValue)
+        {
+            var newPagesPerRow = getValidPagesPerRow(newValue);
+
+            // If the value provided is invalid, return false
+            if (newPagesPerRow !== newValue)
+            {
+                return false;
+            }
+
+            settings.oldPagesPerRow = settings.zoomLevel;
+            settings.pagesPerRow = newPagesPerRow;
+
+            // Update the slider
+            Events.publish("GridRowNumberDidChange", null);
+
+            loadGrid();
+        };
+
+        var getYOffset = function ()
+        {
+            var yScroll = document.getElementById(settings.ID + "outer").scrollTop;
+            var topOfPage = settings.heightAbovePages[settings.currentPageIndex];
+
+            return parseInt(yScroll - topOfPage, 10);
+        };
+
+        var getXOffset = function ()
+        {
+            var innerWidth = settings.maxWidths[settings.zoomLevel] + settings.horizontalPadding * 2;
+            var centerX = (innerWidth - settings.panelWidth) / 2;
+            var xoff = document.getElementById(settings.ID + "outer").scrollLeft - centerX;
+            return parseInt(xoff, 10);
+        };
+
+        var getState = function ()
+        {
+            var state = {
+                'f': settings.inFullscreen,
+                'g': settings.inGrid,
+                'z': settings.zoomLevel,
+                'n': settings.pagesPerRow,
+                'i': (settings.enableFilename) ? settings.pages[settings.currentPageIndex].f : false,
+                'p': (settings.enableFilename) ? false : settings.currentPageIndex + 1,
+                'y': (settings.inGrid) ? false : getYOffset(),
+                'x': (settings.inGrid) ? false : getXOffset(),
+                'h': (settings.inFullscreen) ? false : settings.panelHeight,
+                'w': (settings.inFullscreen) ? false : $(settings.outerSelector).width()
+            };
+
+            return state;
+        };
+
+        var getURLHash = function ()
+        {
+            var hashParams = getState();
+            var hashStringBuilder = [];
+            var param;
+
+            for (param in hashParams)
+            {
+                if (hashParams[param] !== false)
+                {
+                    hashStringBuilder.push(param + settings.hashParamSuffix + '=' + hashParams[param]);
+                }
+            }
+
+            return hashStringBuilder.join('&');
+        };
+
+        // Returns the URL to the current state of the document viewer (so it should be an exact replica)
+        var getCurrentURL = function ()
+        {
+            return location.protocol + '//' + location.host + location.pathname + '#' + getURLHash();
+        };
+
+        // Called in init and when the orientation changes
+        var adjustMobileWebkitDims = function ()
+        {
+            var outerOffset = $(settings.outerSelector).offset().top;
+            settings.panelHeight = window.innerHeight - outerOffset - settings.viewerHeightPadding;
+            settings.panelWidth = window.innerWidth - settings.viewerWidthPadding;
+
+            // $(settings.parentSelector).width(settings.panelWidth);
+            // document.getElementById(settings.parentSelector.substring(1)).style.width = settings.panelWidth + "px";
+            settings.parentSelector.style.width = settings.panelWidth + "px";
+
+            if (settings.enableAutoHeight)
+            {
+                document.getElementById(settings.ID + "outer").style.height = settings.panelHeight + "px";
+            }
+
+            if (settings.enableAutoWidth)
+            {
+                document.getElementById(settings.ID + "outer").style.width = settings.panelWidth + "px";
+            }
+        };
+
+        // Will return true if something has changed, false otherwise
+        var adjustBrowserDims = function ()
+        {
+            // Only resize if the browser viewport is too small
+            var newHeight = $(settings.outerSelector).height();
+            var newWidth = $(settings.parentSelector).width() - settings.scrollbarWidth;
+            var outerOffset = $(settings.outerSelector).offset().top;
+
+            var windowHeight = window.innerHeight || document.documentElement.clientHeight;
+            var windowWidth = window.innerWidth || document.documentElement.clientWidth;
+            // 2 or 1 pixels for the border
+            var desiredWidth = windowWidth - settings.viewerWidthPadding - settings.scrollbarWidth - 2;
+            var desiredHeight = windowHeight - outerOffset - settings.viewerHeightPadding - 1;
+
+            if (settings.enableAutoHeight)
+            {
+                if (newHeight + outerOffset + 16 > window.innerHeight)
+                {
+                    newHeight = desiredHeight;
+                }
+                else if (newHeight <= settings.originalHeight)
+                {
+                    newHeight = Math.min(desiredHeight, settings.originalHeight);
+                }
+            }
+
+            if (settings.enableAutoWidth)
+            {
+                if (newWidth + 32 > window.innerWidth)
+                {
+                    newWidth = desiredWidth;
+                }
+                else if (newWidth <= settings.originalWidth)
+                {
+                    newWidth = Math.min(desiredWidth, settings.originalWidth);
+                }
+
+                settings.parentSelector[0].style.width = newWidth + settings.scrollbarWidth;
+            }
+
+            if (newWidth !== settings.panelWidth || newHeight !== settings.panelHeight)
+            {
+                var el = document.getElementById(settings.ID + "outer");
+                el.style.height = newHeight + "px";
+                el.style.width = newWidth + settings.scrollbarWidth + "px";
+                settings.panelWidth = newWidth;
+                settings.panelHeight = newHeight;
+                return true;
+            }
+
+            return false;
+        };
+
+        // Update the panelHeight and panelWidth based on the window size
+        var adjustFullscreenDims = function ()
+        {
+            settings.panelWidth = window.innerWidth - settings.scrollbarWidth;
+            settings.panelHeight = window.innerHeight;
+
+            return true;
+        };
+
+        var resizeViewer = function (newWidth, newHeight)
+        {
+            if (newWidth >= settings.minWidth)
+            {
+                settings.originalWidth = newWidth;
+                $(settings.outerSelector).width(newWidth);
+                document.getElementById(settings.ID + "outer").style.width = newWidth + "px";
+
+                settings.panelWidth = newWidth - settings.scrollbarWidth;
+
+                // Should also change the width of the container
+                settings.parentSelector[0].style.width = newWidth + "px";
+            }
+
+            if (newHeight >= settings.minHeight)
+            {
+                settings.originalHeight = newHeight;
+                document.getElementById(settings.ID + "outer").style.height = newHeight + "px";
+
+                settings.panelHeight = newHeight;
+            }
+        };
+
+        // Binds most of the event handlers (some more in createToolbar)
+        var handleEvents = function ()
+        {
+            // Create the fullscreen toggle icon if fullscreen is enabled
+            if (settings.enableFullscreen)
+            {
+                // Event handler for fullscreen toggling
+                $(settings.selector + 'fullscreen').click(function ()
+                {
+                    toggleFullscreen();
+                });
+            }
+
+            // Change the cursor for dragging
+            $(settings.innerSelector).mouseover(function ()
+            {
+                $(this).removeClass('diva-grabbing').addClass('diva-grab');
+            });
+
+            $(settings.innerSelector).mouseout(function ()
+            {
+                $(this).removeClass('diva-grab');
+            });
+
+            $(settings.innerSelector).mousedown(function ()
+            {
+                $(this).removeClass('diva-grab').addClass('diva-grabbing');
+            });
+
+            $(settings.innerSelector).mouseup(function ()
+            {
+                $(this).removeClass('diva-grabbing').addClass('diva-grab');
+            });
+
+            // Set drag scroll on first descendant of class dragger on both selected elements
+            $(settings.outerSelector + ', ' + settings.innerSelector).dragscrollable({dragSelector: '.diva-dragger', acceptPropagatedEvent: true});
+
+            // Handle the scroll
+            $(settings.outerSelector).scroll(function ()
+            {
+                settings.topScrollSoFar = document.getElementById(settings.ID + "outer").scrollTop;
+                var direction = settings.topScrollSoFar - settings.previousTopScroll;
+
+                if (settings.inGrid)
+                {
+                    adjustRows(direction);
+                }
+                else
+                {
+                    adjustPages(direction);
+                    settings.leftScrollSoFar = $(this).scrollLeft();
+                }
+
+                settings.previousTopScroll = settings.topScrollSoFar;
+            });
+
+            // Double-click to zoom
+            $(settings.outerSelector).on('dblclick', '.diva-document-page', function (event)
+            {
+                handleDocumentDoubleClick.call(this, event);
+            });
+
+            // Handle the control key for macs (in conjunction with double-clicking)
+            $(settings.outerSelector).on('contextmenu', '.diva-document-page', function (event)
+            {
+                if (event.ctrlKey)
+                {
+                    // In Firefox, this doesn't trigger a double-click, so we apply one manually
+                    clearTimeout(settings.singleClickTimeout);
+
+                    if (settings.singleClick)
+                    {
+                        handleDocumentDoubleClick.call(this, event);
+                        settings.singleClick = false;
+                    }
+                    else
+                    {
+                        settings.singleClick = true;
+
+                        // Set it to false again after 500 milliseconds (standard double-click timeout)
+                        settings.singleClickTimeout = setTimeout(function ()
+                        {
+                            settings.singleClick = false;
+                        }, 500);
+                    }
+
+                    return false;
+                }
+            });
+
+            $(settings.outerSelector).on('dblclick', '.diva-row', function (event)
+            {
+                handleGridDoubleClick.call(this, event);
+            });
+
+            // Check if the user is on a iPhone or iPod touch or iPad
+            if (settings.mobileWebkit)
+            {
+                // Prevent resizing (below from http://matt.might.net/articles/how-to-native-iphone-ipad-apps-in-javascript/)
+                var toAppend = [];
+                toAppend.push('<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1" />');
+
+                // Eliminate URL and button bars if added to home screen
+                toAppend.push('<meta name="apple-mobile-web-app-capable" content="yes" />');
+
+                // Choose how to handle the phone status bar
+                toAppend.push('<meta name="apple-mobile-web-app-status-bar-style" content="black" />');
+                $('head').append(toAppend.join('\n'));
+
+                // Block the user from moving the window only if it's not integrated
+                if (settings.blockMobileMove)
+                {
+                    $('body').bind('touchmove', function (event)
+                    {
+                        var e = event.originalEvent;
+                        e.preventDefault();
+
+                        return false;
+                    });
+                }
+
+                // Allow pinch-zooming
+                $('body').bind('gestureend', function (event)
+                {
+                    var e = event.originalEvent;
+
+                    if (!settings.scaleWait)
+                    {
+                        // Save the page we're currently on so we scroll there
+                        settings.goDirectlyTo = settings.currentPageIndex;
+
+                        if (settings.inGrid)
+                        {
+                            settings.inGrid = false;
+
+                            handleViewChange();
+                        }
+                        else
+                        {
+                            handlePinchZoom(e);
+                        }
+                    }
+                    return false;
+                });
+
+                // Listen to orientation change event
+                $(window).bind('orientationchange', function (event)
+                {
+                    settings.orientationChange = true;
+                    adjustMobileWebkitDims();
+
+                    // Reload the viewer to account for the resized viewport
+                    settings.goDirectlyTo = settings.currentPageIndex;
+                    loadViewer();
+                });
+
+                // Inertial scrolling
+                $(settings.outerSelector).kinetic();
+            }
+
+            // Only check if either scrollBySpace or scrollByKeys is enabled
+            if (settings.enableSpaceScroll || settings.enableKeyScroll)
+            {
+                var spaceKey = $.ui.keyCode.SPACE;
+                var pageUpKey = $.ui.keyCode.PAGE_UP;
+                var pageDownKey = $.ui.keyCode.PAGE_DOWN;
+                var homeKey = $.ui.keyCode.HOME;
+                var endKey = $.ui.keyCode.END;
+
+                // Catch the key presses in document
+                $(document).keydown(function (event)
+                {
+                    // Space or page down - go to the next page
+                    if ((settings.enableSpaceScroll && event.keyCode === spaceKey) || (settings.enableKeyScroll && event.keyCode === pageDownKey))
+                    {
+                        $(settings.outerSelector).scrollTop(settings.topScrollSoFar + settings.panelHeight);
+                        return false;
+                    }
+
+                    // Page up - go to the previous page
+                    if (settings.enableKeyScroll && event.keyCode === pageUpKey)
+                    {
+                        $(settings.outerSelector).scrollTop(settings.topScrollSoFar - settings.panelHeight);
+                        return false;
+                    }
+
+                    // Home key - go to the beginning of the document
+                    if (settings.enableKeyScroll && event.keyCode === homeKey)
+                    {
+                        $(settings.outerSelector).scrollTop(0);
+                        return false;
+                    }
+
+                    // End key - go to the end of the document
+                    if (settings.enableKeyScroll && event.keyCode === endKey)
+                    {
+                        $(settings.outerSelector).scrollTop(settings.totalHeight);
+                        return false;
+                    }
+                });
+
+                // Handle window resizing events
+                if (!settings.mobileWebkit)
+                {
+                    $(window).resize(function ()
+                    {
+                        var adjustSuccess = (settings.inFullscreen) ? adjustFullscreenDims() : adjustBrowserDims();
+
+                        if (adjustSuccess)
+                        {
+                            // Cancel any previously-set resize timeouts
+                            clearTimeout(settings.resizeTimer);
+
+                            settings.resizeTimer = setTimeout(function ()
+                            {
+                                settings.goDirectlyTo = settings.currentPageIndex;
+                                loadViewer();
+                            }, 200);
+                        }
+                    });
+                }
+            }
+        };
+
+        // Handles all status updating etc (both fullscreen and not)
+        var createToolbar = function () {
+            // Prepare the HTML for the various components
+            var gridIconHTML = (settings.enableGridIcon) ? '<div class="diva-grid-icon' + (settings.inGrid ? ' diva-in-grid' : '') + '" id="' + settings.ID + 'grid-icon" title="Toggle grid view"></div>' : '';
+            var linkIconHTML = (settings.enableLinkIcon) ? '<div class="diva-link-icon" id="' + settings.ID + 'link-icon" style="' + (settings.enableGridIcon ? 'border-left: 0px' : '') + '" title="Link to this page"></div>' : '';
+            var zoomSliderHTML = (settings.enableZoomSlider) ? '<div id="' + settings.ID + 'zoom-slider"></div>' : '';
+            var gridSliderHTML = (settings.enableGridSlider) ? '<div id="' + settings.ID + 'grid-slider"></div>' : '';
+            var gotoPageHTML = (settings.enableGotoPage) ? '<form id="' + settings.ID + 'goto-page" class="diva-goto-form"><input type="text" id="' + settings.ID + 'goto-page-input" / class="diva-input"> <input type="submit" value="Go" style="margin-top: 0px;" /></form>' : '';
+            var zoomSliderLabelHTML = (settings.enableZoomSlider) ? '<div id="' + settings.ID + 'zoom-slider-label" class="diva-slider-label">Zoom level: <span id="' + settings.ID + 'zoom-level">' + settings.zoomLevel + '</span></div>' : '';
+            var gridSliderLabelHTML = (settings.enableGridSlider) ? '<div id="' + settings.ID + 'grid-slider-label" class="diva-slider-label">Pages per row: <span id="' + settings.ID + 'pages-per-row">' + settings.pagesPerRow + '</span></div>' : '';
+            var pageNumberHTML = '<div class="diva-page-label">Page <span id="' + settings.ID + 'current-page">1</span> of <span id="' + settings.ID + 'num-pages">' + settings.numPages + '</span></div>';
+
+            // If the viewer is specified to be "contained", we make room for the fullscreen icon
+            var otherToolbarClass = '';
+
+            if (settings.contained)
+            {
+                // Make sure the container element does not have a static position
+                // (Needed for the fullscreen icon to be contained)
+                if ($(settings.parentSelector).css('position') === 'static')
+                {
+                    $(settings.parentSelector).addClass('diva-relative-position');
+                }
+
+                otherToolbarClass = ' diva-fullscreen-space';
+
+                // If enableAutoTitle is set to TRUE, move it down
+                if (settings.enableAutoTitle)
+                {
+                    $(settings.selector + 'fullscreen').addClass('diva-contained');
+                }
+            }
+
+            var toolbarHTML = '<div id="' + settings.ID + 'tools-left" class="diva-tools-left' + otherToolbarClass + '">' + zoomSliderHTML + gridSliderHTML + zoomSliderLabelHTML + gridSliderLabelHTML + '</div><div id="' + settings.ID + 'tools-right" class="diva-tools-right">' + linkIconHTML + gridIconHTML + '<div class="diva-page-nav">' + gotoPageHTML + pageNumberHTML + '</div></div>';
+
+            if (settings.toolbarParentSelector)
+            {
+                $(settings.toolbarParentSelector).prepend('<div id="' + settings.ID + 'tools" class="diva-tools">' + toolbarHTML + '</div>');
+            }
+            else
+            {
+                $(settings.parentSelector).prepend('<div id="' + settings.ID + 'tools" class="diva-tools">' + toolbarHTML + '</div>');
+            }
+
+            // Create the zoom slider
+            $(settings.selector + 'zoom-slider').slider({
+                value: settings.zoomLevel,
+                min: settings.minZoomLevel,
+                max: settings.maxZoomLevel,
+                step: 1,
+                slide: function (event, ui)
+                {
+                    var i = settings.currentPageIndex;
+                    settings.goDirectlyTo = i;
+
+                    // Figure out the horizontal and vertical offsets
+                    // (Try to zoom in on the current center)
+                    var zoomRatio = Math.pow(2, ui.value - settings.zoomLevel);
+                    var innerWidth = settings.maxWidths[settings.zoomLevel] + settings.horizontalPadding * 2;
+                    var centerX = $(settings.outerSelector).scrollLeft() - (innerWidth - settings.panelWidth) / 2;
+                    settings.horizontalOffset = (innerWidth > settings.panelWidth) ? centerX * zoomRatio : 0;
+                    settings.verticalOffset = zoomRatio * ($(settings.outerSelector).scrollTop() - settings.heightAbovePages[i]);
+
+                    handleZoom(ui.value);
+                },
+                change: function (event, ui)
+                {
+                    if (ui.value !== settings.zoomLevel)
+                    {
+                        handleZoom(ui.value);
+                    }
+                }
+            });
+
+            // Create the grid slider
+            $(settings.selector + 'grid-slider').slider(
+            {
+                value: settings.pagesPerRow,
+                min: settings.minPagesPerRow,
+                max: settings.maxPagesPerRow,
+                step: 1,
+                slide: function (event, ui)
+                {
+                    handleGrid(ui.value);
+                },
+                change: function (event, ui)
+                {
+                    if (ui.value !== settings.pagesPerRow)
+                    {
+                        handleGrid(ui.value);
+                    }
+                }
+            });
+
+            // Handle clicking of the grid icon
+            $(settings.selector + 'grid-icon').click(function ()
+            {
+                toggleGrid();
+            });
+
+            // Handle going to a specific page using the input box
+            $(settings.selector + 'goto-page').submit(function ()
+            {
+                var desiredPage = parseInt($(settings.selector + 'goto-page-input').val(), 10);
+                var pageIndex = desiredPage - 1;
+
+                if (!isPageValid(pageIndex))
+                {
+                    alert("Invalid page number");
+                }
+                else
+                {
+                    if (settings.inGrid)
+                    {
+                        gotoRow(pageIndex);
+                    }
+                    else
+                    {
+                        gotoPage(pageIndex, 0, 0);
+                    }
+                }
+
+                // Prevent the default action of reloading the page
+                return false;
+            });
+
+            // Handle the creation of the link popup box
+            $(settings.selector + 'link-icon').click(function ()
+            {
+                $('body').prepend('<div id="' + settings.ID + 'link-popup" class="diva-link-popup"><input id="' + settings.ID + 'link-popup-input" class="diva-input" type="text" value="' + getCurrentURL() + '"/></div>');
+
+                if (settings.inFullscreen)
+                {
+                    $(settings.selector + 'link-popup').addClass('in-fullscreen');
+                }
+                else
+                {
+                    // Calculate the left and top offsets
+                    // Compensate for border, popup width
+                    var leftOffset = $(settings.outerSelector).offset().left + settings.panelWidth;
+                    leftOffset += settings.scrollbarWidth - 240 - 1;
+                    var topOffset = $(settings.outerSelector).offset().top + 1;
+
+                    $(settings.selector + 'link-popup').removeClass('in-fullscreen').css(
+                    {
+                        'top': topOffset + 'px',
+                        'left': leftOffset + 'px'
+                    });
+                }
+
+                // Catch onmouseup events outside of this div
+                $('body').mouseup(function (event)
+                {
+                    var targetID = event.target.id;
+
+                    if (targetID !== settings.ID + 'link-popup' && targetID !== settings.ID + 'link-popup-input')
+                    {
+                        $(settings.selector + 'link-popup').remove();
+                    }
+                });
+
+                // Also delete it upon scroll and page up/down key events
+                $(settings.outerSelector).scroll(function ()
+                {
+                    $(settings.selector + 'link-popup').remove();
+                });
+                $(settings.selector + 'link-popup input').click(function ()
+                {
+                    $(this).focus().select();
+                });
+                return false;
+            });
+
+            // Show the relevant slider
+            var currentSlider = (settings.inGrid) ? 'grid' : 'zoom';
+            $(settings.selector + currentSlider + '-slider').show();
+            $(settings.selector + currentSlider + '-slider-label').show();
+
+            var switchMode = function ()
+            {
+                // Switch from fullscreen to not
+                $(settings.selector + 'tools').toggleClass('diva-fullscreen-tools');
+
+                if (!settings.inFullscreen)
+                {
+                    // Leaving fullscreen
+                    $(settings.selector + 'tools-left').after($(settings.selector + 'tools-right'));
+                    $(settings.selector + 'tools-left').removeClass('in-fullscreen');
+                }
+                else
+                {
+                    // Entering fullscreen
+                    $(settings.selector + 'tools-right').after($(settings.selector + 'tools-left'));
+                    $(settings.selector + 'tools-left').addClass('in-fullscreen');
+                }
+            };
+
+            var switchView = function ()
+            {
+                // Switch from grid to document view etc
+                $(settings.selector + currentSlider + '-slider').hide();
+                $(settings.selector + currentSlider + '-slider-label').hide();
+                currentSlider = (settings.inGrid) ? 'grid' : 'zoom';
+                $(settings.selector + currentSlider + '-slider').show();
+                $(settings.selector + currentSlider + '-slider-label').show();
+
+                // Also change the image for the grid icon
+                $(settings.selector + 'grid-icon').toggleClass('diva-in-grid');
+            };
+
+            var toolbar =
+            {
+                updateCurrentPage: function ()
+                {
+                    $(settings.selector + 'current-page').text(settings.currentPageIndex + 1);
+                },
+                setNumPages: function (newNumber)
+                {
+                    $(settings.selector + 'num-pages').text(newNumber);
+                },
+                updateZoomSlider: function ()
+                {
+                    // Update the position of the handle within the slider
+                    if (settings.zoomLevel !== $(settings.selector + 'zoom-slider').slider('value'))
+                    {
+                        $(settings.selector + 'zoom-slider').slider(
+                        {
+                            value: settings.zoomLevel
+                        });
+                    }
+
+                    // Update the slider label
+                    $(settings.selector + 'zoom-level').text(settings.zoomLevel);
+                },
+                updateGridSlider: function ()
+                {
+                    // Update the position of the handle within the slider
+                    if (settings.pagesPerRow !== $(settings.selector + 'grid-slider').slider('value'))
+                    {
+                        $(settings.selector + 'grid-slider').slider(
+                        {
+                            value: settings.pagesPerRow
+                        });
+                    }
+
+                    // Update the slider label
+                    $(settings.selector + 'pages-per-row').text(settings.pagesPerRow);
+                },
+                switchView: switchView,
+                switchMode: switchMode
+            };
+            return toolbar;
+        };
+
+        var initPlugins = function ()
+        {
+            if (window.divaPlugins)
+            {
+                var pageTools = [];
+
+                // Add all the plugins that have not been explicitly disabled to settings.plugins
+                $.each(window.divaPlugins, function (index, plugin)
+                {
+                    var pluginProperName = plugin.pluginName[0].toUpperCase() + plugin.pluginName.substring(1);
+
+                    if (settings['enable' + pluginProperName])
+                    {
+                        // Call the init function and check return value
+                        var enablePlugin = plugin.init(settings, self);
+
+                        // If int returns false, consider the plugin disabled
+                        if (!enablePlugin)
+                        {
+                            return;
+                        }
+
+                        // If the title text is undefined, use the name of the plugin
+                        var titleText = plugin.titleText || pluginProperName + " plugin";
+
+                        // Create the pageTools bar if handleClick is set to a function
+                        if (typeof plugin.handleClick === 'function')
+                        {
+                            pageTools.push('<div class="diva-' + plugin.pluginName + '-icon" title="' + titleText + '"></div>');
+
+                            // Delegate the click event - pass it the settings
+                            $(settings.outerSelector).delegate('.diva-' + plugin.pluginName + '-icon', 'click', function (event)
+                            {
+                                plugin.handleClick.call(this, event, settings);
+                            });
+                        }
+
+                        // Add it to settings.plugins so it can be used later
+                        settings.plugins.push(plugin);
+                    }
+                });
+
+                // Save the page tools bar so it can be added for each page
+                if (pageTools.length)
+                {
+                    settings.pageTools = '<div class="diva-page-tools">' + pageTools.join('') + '</div>';
+                }
+            }
+        };
+
+        var hideThrobber = function ()
+        {
+            // Clear the timeout, if it hasn't executed yet
+            clearTimeout(settings.throbberTimeoutID);
+
+            // Hide the throbber if it has already executed
+            $(settings.selector + 'throbber').hide();
+        };
+
+        var setupViewer = function ()
+        {
+            // Create the throbber element
+            var throbberHTML = '<div id="' + settings.ID + 'throbber" class="diva-throbber"></div>';
+            $(settings.outerSelector).append(throbberHTML);
+
+            // If the request hasn't completed after a specified time, show it
+            settings.throbberTimeoutID = setTimeout(function ()
+            {
+                $(settings.selector + 'throbber').show();
+            }, settings.throbberTimeout);
+
+            $.ajax({
+                url: settings.objectData,
+                cache: true,
+                dataType: 'json',
+                error: function (jqxhr, status, error)
+                {
+                    hideThrobber();
+
+                    // Show a basic error message within the document viewer pane
+                    $(settings.outerSelector).text("Invalid URL. Error code: " + status + " " + error);
+                },
+                success: function (data, status, jqxhr)
+                {
+                    hideThrobber();
+
+                    // Save all the data we need
+                    settings.pages = data.pgs;
+                    settings.maxRatio = data.dims.max_ratio;
+                    settings.minRatio = data.dims.min_ratio;
+                    settings.itemTitle = data.item_title;
+                    settings.numPages = data.pgs.length;
+
+                    // These are arrays, the index corresponding to the zoom level
+                    settings.maxWidths = data.dims.max_w;
+                    settings.averageWidths = data.dims.a_wid;
+                    settings.averageHeights = data.dims.a_hei;
+                    settings.totalHeights = data.dims.t_hei;
+
+                    // Make sure the set max and min values are valid
+                    settings.realMaxZoom = data.max_zoom;
+                    settings.maxZoomLevel = (settings.maxZoomLevel >= 0 && settings.maxZoomLevel <= data.max_zoom) ? settings.maxZoomLevel : data.max_zoom;
+                    settings.minZoomLevel = (settings.minZoomLevel >= 0 && settings.minZoomLevel <= settings.maxZoomLevel) ? settings.minZoomLevel : 0;
+                    settings.minPagesPerRow = Math.max(2, settings.minPagesPerRow);
+                    settings.maxPagesPerRow = Math.max(settings.minPagesPerRow, settings.maxPagesPerRow);
+
+                    // Check that the desired page is in range
+                    if (settings.enableFilename)
+                    {
+                        var iParam = $.getHashParam('i' + settings.hashParamSuffix);
+                        var iParamPage = getPageIndex(iParam);
+
+                        if (isPageValid(iParamPage))
+                        {
+                            settings.goDirectlyTo = iParamPage;
+                        }
+                    }
+                    else
+                    {
+                        // Not using the i parameter, check the p parameter
+                        // Subtract 1 to get the page index
+                        var pParam = parseInt($.getHashParam('p' + settings.hashParamSuffix), 10) - 1;
+
+                        if (isPageValid(pParam))
+                        {
+                            settings.goDirectlyTo = pParam;
+                        }
+                    }
+
+                    // Execute the setup hook for each plugin (if defined)
+                    $.each(settings.plugins, function (index, plugin)
+                    {
+                        executeCallback(plugin.setupHook, settings);
+                    });
+
+                    // Create the toolbar and display the title + total number of pages
+                    if (settings.enableToolbar)
+                    {
+                        settings.toolbar = createToolbar();
+                        Events.subscribe("VisiblePageDidChange", settings.toolbar.updateCurrentPage);
+                        Events.subscribe("ModeDidSwitch", settings.toolbar.switchMode);
+                        Events.subscribe("ViewDidSwitch", settings.toolbar.switchView);
+                        Events.subscribe("ZoomLevelDidChange", settings.toolbar.updateZoomSlider);
+                        Events.subscribe("GridRowNumberDidChange", settings.toolbar.updateGridSlider);
+                    }
+
+                    $(settings.selector + 'current label').text(settings.numPages);
+
+                    if (settings.enableAutoTitle)
+                    {
+                        $(settings.parentSelector).prepend('<div id="' + settings.ID + 'title" class="diva-title">' + settings.itemTitle + '</div>');
+                    }
+
+                    // Adjust the document panel dimensions for touch devices
+                    if (settings.mobileWebkit)
+                    {
+                        adjustMobileWebkitDims();
+                    }
+                    else
+                    {
+                        settings.originalWidth = $(settings.parentSelector).width() - settings.scrollbarWidth;
+                        settings.originalHeight = $(settings.outerSelector).height();
+                        adjustBrowserDims();
+                    }
+
+                    // Calculate the viewer x and y offsets
+                    var viewerOffset = $(settings.outerSelector).offset();
+                    settings.viewerXOffset = viewerOffset.left;
+                    settings.viewerYOffset = viewerOffset.top;
+
+                    if (settings.inFullscreen)
+                    {
+                        handleModeChange(false);
+                    }
+                    else
+                    {
+                        loadViewer();
+                    }
+
+                    // Execute the callback
+                    executeCallback(settings.onReady, settings);
+                    Events.publish("ViewerHasFinishedLoading", [settings]);
+
+                    // signal that everything should be set up and ready to go.
+                    settings.loaded = true;
+                }
+            });
+        };
+
+        var checkLoaded = function()
+        {
+            if (!settings.loaded)
+            {
+                console.warn("The viewer is not completely initialized. This is likely because it is still downloading data. To fix this, only call this function if the isReady() method returns true.");
+                return false;
+            }
+            return true;
+        };
+
+        var init = function ()
+        {
+            // First figure out the width of the scrollbar in this browser
+            settings.scrollbarWidth = $.getScrollbarWidth();
+
+            // If window.orientation is defined, then it's probably mobileWebkit
+            settings.mobileWebkit = window.orientation !== undefined;
+
+            // Generate an ID that can be used as a prefix for all the other IDs
+            settings.ID = $.generateId('diva-');
+            settings.selector = '#' + settings.ID;
+
+            // Figure out the hashParamSuffix from the ID
+            var divaNumber = parseInt(settings.ID, 10);
+
+            if (divaNumber > 1)
+            {
+                // If this is document viewer #1, don't use a suffix; otherwise, use the document viewer number
+                settings.hashParamSuffix = divaNumber;
+            }
+
+            // Since we need to reference these two a lot
+            settings.outerSelector = settings.selector + 'outer';
+            settings.innerSelector = settings.selector + 'inner';
+
+            // Create the inner and outer panels
+            $(settings.parentSelector).append('<div id="' + settings.ID + 'outer" class="diva-outer"></div>');
+            $(settings.outerSelector).append('<div id="' + settings.ID + 'inner" class="diva-inner diva-dragger"></div>');
+
+            // Create the fullscreen icon
+            if (settings.enableFullscreen)
+            {
+                $(settings.parentSelector).prepend('<div id="' + settings.ID + 'fullscreen" class="diva-fullscreen-icon" title="Toggle fullscreen mode"></div>');
+            }
+
+            // First, n - check if it's in range
+            var nParam = parseInt($.getHashParam('n' + settings.hashParamSuffix), 10);
+
+            if (nParam >= settings.minPagesPerRow && nParam <= settings.maxPagesPerRow)
+            {
+                settings.pagesPerRow = nParam;
+            }
+
+            // Now z - check that it's in range
+            var zParam = $.getHashParam('z' + settings.hashParamSuffix);
+
+            if (zParam !== '')
+            {
+                // If it's empty, we don't want to change the default zoom level
+                zParam = parseInt(zParam, 10);
+
+                // Can't check if it exceeds the max zoom level or not because that data is not available yet ...
+                if (zParam >= settings.minZoomLevel)
+                {
+                    settings.zoomLevel = zParam;
+                }
+            }
+
+            // y - vertical offset from the top of the relevant page
+            var yParam = parseInt($.getHashParam('y' + settings.hashParamSuffix), 10);
+
+            if (!isNaN(yParam))
+            {
+                settings.verticalOffset = yParam;
+            }
+
+            // x - horizontal offset from the center of the page
+            var xParam = parseInt($.getHashParam('x' + settings.hashParamSuffix), 10);
+
+            if (!isNaN(xParam))
+            {
+                settings.horizontalOffset = xParam;
+            }
+
+            // If the "fullscreen" hash param is true, go to fullscreen initially
+            // If the grid hash param is true, go to grid view initially
+            var gridParam = $.getHashParam('g' + settings.hashParamSuffix);
+            var goIntoGrid = gridParam === 'true';
+            var fullscreenParam = $.getHashParam('f' + settings.hashParamSuffix);
+            var goIntoFullscreen = fullscreenParam === 'true';
+
+            settings.inGrid = (settings.inGrid && gridParam !== 'false') || goIntoGrid;
+            settings.inFullscreen = (settings.inFullscreen && fullscreenParam !== 'false') || goIntoFullscreen;
+
+            // Store the height and width of the viewer (the outer div), if present
+            var desiredHeight = parseInt($.getHashParam('h' + settings.hashParamSuffix), 10);
+            var desiredWidth = parseInt($.getHashParam('w' + settings.hashParamSuffix), 10);
+
+            // Store the minimum and maximum height too
+            settings.minHeight = parseInt($(settings.outerSelector).css('min-height'), 10);
+            settings.minWidth = parseInt($(settings.outerSelector).css('min-width'), 10);
+
+            // Just call resize, it'll take care of bounds-checking etc
+            if (desiredHeight > 0 || desiredWidth > 0)
+            {
+                resizeViewer(desiredWidth, desiredHeight);
+            }
+
+            // Do the initial AJAX request and viewer loading
+            setupViewer();
+
+            // Do all the plugin initialisation
+            initPlugins();
+
+            handleEvents();
+        };
+
+        // Call the init function when this object is created.
+        init();
+
+        /* PUBLIC FUNCTIONS
+===============================================
+*/
+
+        // Returns the title of the document, based on the directory name
+        this.getItemTitle = function ()
+        {
+            return settings.itemTitle;
+        };
+
+        // Go to a particular page by its page number (with indexing starting at 1)
+        // returns True if the page number passed is valid; false if it is not.
+        this.gotoPageByNumber = function (pageNumber)
+        {
+            var pageIndex = pageNumber - 1;
+            if (isPageValid(pageIndex))
+            {
+                gotoPage(pageIndex, 0, 0);
+                return true;
+            }
+            return false;
+        };
+
+        // Go to a particular page (with indexing starting at 0)
+        // returns True if the page index is valid; false if it is not.
+        this.gotoPageByIndex = function (pageIndex)
+        {
+            if (isPageValid(pageIndex))
+            {
+                gotoPage(pageIndex, 0, 0);
+                return true;
+            }
+            return false;
+        };
+
+        // Returns the page index (with indexing starting at 0)
+        this.getCurrentPage = function ()
+        {
+            console.warn("Deprecated. Use getCurrentPageIndex instead.");
+            return settings.currentPageIndex;
+        };
+
+        this.getNumberOfPages = function()
+        {
+            if (!checkLoaded())
+            {
+                return false;
+            }
+
+            return settings.numPages;
+        }
+
+        // Returns the dimensions of a given page index at a given zoom level
+        this.getPageDimensionsAtZoomLevel = function(pageIdx, zoomLevel)
+        {
+            if (!checkLoaded())
+            {
+                return false;
+            }
+
+            var zoomLevel = zoomLevel - 1; // zoom levels are 1-based, but our array is 0-based;
+            var pg = settings.pages[pageIdx];
+            var pgAtZoom = pg.d[parseInt(zoomLevel, 10)];
+            return {'width': pgAtZoom.w, 'height': pgAtZoom.h}
+        };
+
+        // Returns the dimensions of the current page at the current zoom level
+        this.getCurrentPageDimensionsAtCurrentZoomLevel = function()
+        {
+            return this.getPageDimensionsAtZoomLevel(settings.currentPageIndex, settings.zoomLevel);
+        };
+
+        this.isReady = function()
+        {
+            return settings.loaded;
+        };
+
+        this.getCurrentPageIndex = function ()
+        {
+            return settings.currentPageIndex;
+        };
+
+        this.getCurrentPageFilename = function ()
+        {
+            return settings.pages[settings.currentPageIndex].f;
+        };
+
+        this.getCurrentPageNumber = function ()
+        {
+            return settings.currentPageIndex + 1;
+        };
+
+        // Returns the current zoom level
+        this.getZoomLevel = function ()
+        {
+            return settings.zoomLevel;
+        };
+
+        // gets the maximum zoom level for the entire document
+        this.getMaxZoomLevel = function ()
+        {
+            return settings.maxZoomLevel;
+        };
+
+        // gets the max zoom level for a given page
+        this.getMaxZoomLevelForPage = function(pageIdx)
+        {
+            if (!checkLoaded)
+            {
+                return false;
+            }
+
+            return settings.pages[pageIdx].m;
+        }
+
+        this.getMinZoomLevel = function ()
+        {
+            return settings.minZoomLevel;
+        };
+
+        // Use the provided zoom level (will check for validity first)
+        // Returns false if the zoom level is invalid, true otherwise
+        this.setZoomLevel = function (zoomLevel)
+        {
+            if (settings.inGrid)
+            {
+                toggleGrid();
+            }
+
+            return handleZoom(zoomLevel);
+        };
+
+        // Zoom in. Will return false if it's at the maximum zoom
+        this.zoomIn = function ()
+        {
+            return this.setZoomLevel(settings.zoomLevel + 1);
+        };
+
+        // Zoom out. Will return false if it's at the minimum zoom
+        this.zoomOut = function ()
+        {
+            return this.setZoomLevel(settings.zoomLevel - 1);
+        };
+
+        // Uses the isVerticallyInViewport() function, but relative to a page
+        // Check if something (e.g. a highlight box on a particular page) is visible
+        this.inViewport = function (pageNumber, topOffset, height)
+        {
+            var pageIndex = pageNumber - 1;
+            var top = settings.heightAbovePages[pageIndex] + topOffset;
+            var bottom = top + height;
+
+            return isVerticallyInViewport(top, bottom);
+        };
+
+        // Toggle fullscreen mode
+        this.toggleFullscreenMode = function ()
+        {
+            toggleFullscreen();
+        };
+
+        // Enter fullscreen mode if currently not in fullscreen mode
+        // Returns false if in fullscreen mode initially, true otherwise
+        // This function will work even if enableFullscreen is set to false
+        this.enterFullscreenMode = function ()
+        {
+            if (!settings.inFullscreen)
+            {
+                toggleFullscreen();
+                return true;
+            }
+
+            return false;
+        };
+
+        // Leave fullscreen mode if currently in fullscreen mode
+        // Returns true if in fullscreen mode intitially, false otherwise
+        this.leaveFullscreenMode = function ()
+        {
+            if (settings.inFullscreen)
+            {
+                toggleFullscreen();
+                return true;
+            }
+
+            return false;
+        };
+
+        // Toggle grid view
+        this.toggleGridView = function ()
+        {
+            toggleGrid();
+        };
+
+        // Enter grid view if currently not in grid view
+        // Returns false if in grid view initially, true otherwise
+        this.enterGridView = function ()
+        {
+            if (!settings.inGrid) {
+                toggleGrid();
+                return true;
+            }
+
+            return false;
+        };
+
+        // Leave grid view if currently in grid view
+        // Returns true if in grid view initially, false otherwise
+        this.leaveGridView = function ()
+        {
+            if (settings.inGrid)
+            {
+                toggleGrid();
+                return true;
+            }
+
+            return false;
+        };
+
+        // Jump to a page based on its filename
+        // Returns true if successful and false if the filename is invalid
+        this.gotoPageByName = function (filename)
+        {
+            var pageIndex = getPageIndex(filename);
+            if (isPageValid(pageIndex))
+            {
+                gotoPage(pageIndex, 0, 0);
+                return true;
+            }
+
+            return false;
+        };
+
+        // Get the page index (0-based) corresponding to a given filename
+        // If the page index doesn't exist, this will return -1
+        this.getPageIndex = function (filename)
+        {
+            return getPageIndex(filename);
+        };
+
+        // Get the current URL (exposes the private method)
+        this.getCurrentURL = function ()
+        {
+            return getCurrentURL();
+        };
+
+        // Get the hash part only of the current URL (without the leading #)
+        this.getURLHash = function ()
+        {
+            return getURLHash();
+        };
+
+        // Get an object representing the state of this diva instance (for setState)
+        this.getState = function ()
+        {
+            return getState();
+        };
+
+        // Get the instance selector for this instance, since it's auto-generated.
+        this.getInstanceSelector = function ()
+        {
+            return settings.selector;
+        };
+
+        // Get the instance ID -- essentially the selector without the leading '#'.
+        this.getInstanceId = function()
+        {
+            return settings.ID;
+        };
+
+        this.getSettings = function()
+        {
+            return settings;
+        };
+
+        // Align this diva instance with a state object (as returned by getState)
+        this.setState = function (state)
+        {
+            var pageIndex;
+
+            // If we need to resize the viewer, do that first
+            resizeViewer(state.w, state.h);
+
+            // Only change settings.goDirectlyTo if state.i or state.p is valid
+            pageIndex = getPageIndex(state.i);
+
+            if (isPageValid(pageIndex))
+            {
+                settings.goDirectlyTo = pageIndex;
+            }
+            else if (isPageValid(state.p))
+            {
+                settings.goDirectlyTo = state.p;
+            }
+
+            settings.horizontalOffset = parseInt(state.x, 10);
+            settings.verticalOffset = parseInt(state.y, 10);
+
+            // Only change the zoom if state.z is valid
+            if (state.z >= settings.minZoomLevel && state.z <= settings.maxZoomLevel)
+            {
+                settings.zoomLevel = state.z;
+            }
+
+            // Only change the pages per row setting if state.n is valid
+            if (state.n >= settings.minPagesPerRow && state.n <= settings.maxPagesPerRow)
+            {
+                settings.pagesPerRow = state.n;
+            }
+
+            if (settings.inFullscreen !== state.f)
+            {
+                // The parameter determines if we need to change the view as well
+                settings.inFullscreen = state.f;
+                handleModeChange(settings.inGrid !== state.g);
+            }
+            else
+            {
+                // Don't need to change the mode, may need to change view
+                if (settings.inGrid !== state.g)
+                {
+                    settings.inGrid = state.g;
+                    handleViewChange();
+                }
+                else
+                {
+                    // Reload the viewer, just in case
+                    loadViewer();
+                }
+            }
+        };
+
+        // Resizes the outer div to the specified width and height
+        this.resize = function (newWidth, newHeight)
+        {
+            resizeViewer(newWidth, newHeight);
+            loadViewer();
+        };
+
+        // Destroys this instance, tells plugins to do the same (for testing)
+        this.destroy = function ()
+        {
+            // Removes the hide-scrollbar class from the body
+            $('body').removeClass('diva-hide-scrollbar');
+
+            // Empty the parent container and remove any diva-related data
+            $(settings.parentSelector).empty().removeData('diva');
+
+            // Call the destroy function for all the enabled plugins (if it exists)
+            $.each(settings.plugins, function (index, plugin)
+            {
+                executeCallback(plugin.destroy);
+            });
+
+            // Remove any additional styling on the parent element
+            $(settings.parentSelector).removeAttr('style').removeAttr('class');
+        };
+    };
+
+    $.fn.diva = function (options)
+    {
+        return this.each(function ()
+        {
+            var element = $(this);
+
+            // Return early if this element already has a plugin instance
+            if (element.data('diva'))
+            {
+                return;
+            }
+
+            // Save the reference to the container element
+            options.parentSelector = element;
+
+            // Otherwise, instantiate the document viewer
+            var diva = new Diva(this, options);
+            element.data('diva', diva);
+        });
+    };
+
+})(jQuery);
\ No newline at end of file