Merge branch 'master' of git://github.com/openlibrary/bookreader
[bookreader.git] / GnuBook / GnuBook.js
index 9587910..a9c1376 100644 (file)
@@ -18,7 +18,7 @@ This file is part of GnuBook.
     
     The GnuBook source is hosted at http://github.com/openlibrary/bookreader/
 
-    archive.org cvs $Revision: 1.68 $ $Date: 2009-03-04 22:00:31 $
+    archive.org cvs $Revision: 1.2 $ $Date: 2009-06-22 18:42:51 $
 */
 
 // GnuBook()
@@ -34,6 +34,7 @@ function GnuBook() {
     this.reduce  = 4;
     this.padding = 10;
     this.mode    = 1; //1 or 2
+    this.ui = 'full'; // UI mode
     
     this.displayedLeafs = [];  
     //this.leafsToDisplay = [];
@@ -56,28 +57,51 @@ function GnuBook() {
     
     this.lastDisplayableIndex2up = null;
     
+    // We link to index.php to avoid redirect which breaks back button
+    this.logoURL = 'http://www.archive.org/index.php';
+    
+    // Base URL for images
+    this.imagesBaseURL = '/bookreader/images/';
+    
+    // Mode constants
+    this.constMode1up = 1;
+    this.constMode2up = 2;
+    
 };
 
 // init()
 //______________________________________________________________________________
 GnuBook.prototype.init = function() {
-    var startLeaf = window.location.hash;
-    //console.log("startLeaf from location.hash: %s", startLeaf);
-    if ('' == startLeaf) {
-        if (this.titleLeaf) {
-            startLeaf = "#" + this.leafNumToIndex(this.titleLeaf);
-        }
+
+    var startIndex = undefined;
+    
+    // Find start index and mode if set in location hash
+    var params = this.paramsFromFragment(window.location.hash);
+        
+    if ('undefined' != typeof(params.index)) {
+        startIndex = params.index;
+    } else if ('undefined' != typeof(params.page)) {
+        startIndex = this.getPageIndex(params.page);
     }
-    var title = this.bookTitle.substr(0,50);
-    if (this.bookTitle.length>50) title += '...';
     
-    // Ideally this would be set in the HTML/PHP for better search engine visibility but
-    // it takes some time to locate the item and retrieve the metadata
-    document.title = title;
+    if ('undefined' == typeof(startIndex)) {    
+        startIndex = this.leafNumToIndex(this.titleLeaf);
+    }
+    
+    if ('undefined' == typeof(startIndex)) {
+        startIndex = 0;
+    }
+    
+    if ('undefined' != typeof(params.mode)) {
+        this.mode = params.mode;
+    }
+    
+    // Set document title -- may have already been set in enclosing html for
+    // search engine visibility
+    document.title = this.shortTitle(50);
     
     $("#GnuBook").empty();
-    $("#GnuBook").append("<div id='GBtoolbar'><span style='float:left;'><button class='GBicon zoom_out' onclick='gb.zoom1up(-1); return false;'/> <button class='GBicon zoom_in' onclick='gb.zoom1up(1); return false;'/> zoom: <span id='GBzoom'>25</span>% <button class='GBicon script' onclick='gb.switchMode(1); return false;'/> <button class='GBicon book_open' onclick='gb.switchMode(2); return false;'/>  &nbsp;&nbsp; <a href='"+this.bookUrl+"' target='_blank'>"+title+"</a></span></div>");
-    this.initToolbar(this.mode); // Build inside of toolbar div
+    this.initToolbar(this.mode, this.ui); // Build inside of toolbar div
     $("#GnuBook").append("<div id='GBcontainer'></div>");
     $("#GBcontainer").append("<div id='GBpageview'></div>");
 
@@ -86,6 +110,7 @@ GnuBook.prototype.init = function() {
     });
 
     this.setupKeyListeners();
+    this.startLocationPolling();
 
     $(window).bind('resize', this, function(e) {
         //console.log('resize!');
@@ -101,26 +126,29 @@ GnuBook.prototype.init = function() {
             e.data.prepareTwoPageView();
         }
     });
+    
+    $('.GBpagediv1up').bind('mousedown', this, function(e) {
+        //console.log('mousedown!');
+    });
 
     if (1 == this.mode) {
         this.resizePageView();
-    
-        if ('' != startLeaf) { // Jump to the leaf specified in the URL
-            this.jumpToIndex(parseInt(startLeaf.substr(1)));
-            //console.log('jump to ' + parseInt(startLeaf.substr(1)));
-        }
+        this.firstIndex = startIndex;
+        this.jumpToIndex(startIndex);
     } else {
         //this.resizePageView();
         
         this.displayedLeafs=[0];
-        if ('' != startLeaf) {
-            this.displayedLeafs = [parseInt(startLeaf.substr(1))];
-        }
+        this.firstIndex = startIndex;
+        this.displayedLeafs = [this.firstIndex];
         //console.log('titleLeaf: %d', this.titleLeaf);
         //console.log('displayedLeafs: %s', this.displayedLeafs);
         this.prepareTwoPageView();
         //if (this.auto) this.nextPage();
     }
+    
+    // Enact other parts of initial params
+    this.updateFromParams(params);
 }
 
 GnuBook.prototype.setupKeyListeners = function() {
@@ -155,10 +183,10 @@ GnuBook.prototype.setupKeyListeners = function() {
                 }
                 break;
             case KEY_END:
-                self.end();
+                self.last();
                 break;
             case KEY_HOME:
-                self.home();
+                self.first();
                 break;
             case KEY_LEFT:
                 if (self.keyboardNavigationIsDisabled(e)) {
@@ -190,6 +218,58 @@ GnuBook.prototype.drawLeafs = function() {
     }
 }
 
+// setDragHandler1up()
+//______________________________________________________________________________
+GnuBook.prototype.setDragHandler1up = function(div) {
+    div.dragging = false;
+
+    $(div).bind('mousedown', function(e) {
+        //console.log('mousedown at ' + e.pageY);
+
+        this.dragging = true;
+        this.prevMouseX = e.pageX;
+        this.prevMouseY = e.pageY;
+    
+        var startX    = e.pageX;
+        var startY    = e.pageY;
+        var startTop  = $('#GBcontainer').attr('scrollTop');
+        var startLeft =  $('#GBcontainer').attr('scrollLeft');
+
+        return false;
+    });
+        
+    $(div).bind('mousemove', function(ee) {
+        //console.log('mousemove ' + startY);
+        
+        var offsetX = ee.pageX - this.prevMouseX;
+        var offsetY = ee.pageY - this.prevMouseY;
+        
+        if (this.dragging) {
+            $('#GBcontainer').attr('scrollTop', $('#GBcontainer').attr('scrollTop') - offsetY);
+            $('#GBcontainer').attr('scrollLeft', $('#GBcontainer').attr('scrollLeft') - offsetX);
+        }
+        
+        this.prevMouseX = ee.pageX;
+        this.prevMouseY = ee.pageY;
+        
+        return false;
+    });
+    
+    $(div).bind('mouseup', function(ee) {
+        //console.log('mouseup');
+
+        this.dragging = false;
+        return false;
+    });
+    
+    $(div).bind('mouseleave', function(e) {
+        //console.log('mouseleave');
+
+        //$(this).unbind('mousemove mouseup');
+        this.dragging = false;
+        
+    });
+}
 
 // drawLeafsOnePage()
 //______________________________________________________________________________
@@ -216,6 +296,7 @@ GnuBook.prototype.drawLeafsOnePage = function() {
         var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
         var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
         if (topInView | bottomInView | middleInView) {
+            //console.log('displayed: ' + this.displayedLeafs);
             //console.log('to display: ' + i);
             leafsToDisplay.push(i);
         }
@@ -224,8 +305,13 @@ GnuBook.prototype.drawLeafsOnePage = function() {
     }
 
     var firstLeafToDraw  = leafsToDisplay[0];
-    window.location.replace('#' + firstLeafToDraw);
     this.firstIndex      = firstLeafToDraw;
+    
+    // Update hash, but only if we're currently displaying a leaf
+    // Hack that fixes #365790
+    if (this.displayedLeafs.length > 0) {
+        this.updateLocationHash();
+    }
 
     if ((0 != firstLeafToDraw) && (1 < this.reduce)) {
         firstLeafToDraw--;
@@ -266,6 +352,8 @@ GnuBook.prototype.drawLeafsOnePage = function() {
             $(div).css('height', height+'px');
             //$(div).text('loading...');
             
+            this.setDragHandler1up(div);
+            
             $('#GBpageview').append(div);
 
             var img = document.createElement("img");
@@ -297,7 +385,7 @@ GnuBook.prototype.drawLeafsOnePage = function() {
     this.updateSearchHilites();
     
     if (null != this.getPageNum(firstLeafToDraw))  {
-        $("#GBpagenum").val(this.getPageNum(firstLeafToDraw));
+        $("#GBpagenum").val(this.getPageNum(this.currentIndex()));
     } else {
         $("#GBpagenum").val('');
     }
@@ -387,7 +475,7 @@ GnuBook.prototype.updatePageNumBox2UP = function() {
     } else {
         $("#GBpagenum").val('');
     }
-    window.location.replace('#' + this.currentLeafL); 
+    this.updateLocationHash();
 }
 
 // loadLeafs()
@@ -430,7 +518,6 @@ GnuBook.prototype.zoom1up = function(dir) {
     $('#GBzoom').text(100/this.reduce);
 }
 
-
 // resizePageView()
 //______________________________________________________________________________
 GnuBook.prototype.resizePageView = function() {
@@ -477,27 +564,21 @@ GnuBook.prototype.centerPageView = function() {
 
 // jumpToPage()
 //______________________________________________________________________________
+// Attempts to jump to page.  Returns true if page could be found, false otherwise.
 GnuBook.prototype.jumpToPage = function(pageNum) {
 
-    var i;
-    var foundPage = false;
-    var foundLeaf = null;
-    for (i=0; i<this.numLeafs; i++) {
-        if (this.getPageNum(i) == pageNum) {
-            foundPage = true;
-            foundLeaf = i;
-            break;
-        }
-    }
-    
-    if (foundPage) {
+    var pageIndex = this.getPageIndex(pageNum);
+
+    if ('undefined' != typeof(pageIndex)) {
         var leafTop = 0;
         var h;
-        this.jumpToIndex(foundLeaf);
+        this.jumpToIndex(pageIndex);
         $('#GBcontainer').attr('scrollTop', leafTop);
-    } else {
-        alert('Page not found. This book might not have pageNumbers in scandata.');
+        return true;
     }
+    
+    // Page not found
+    return false;
 }
 
 // jumpToIndex()
@@ -524,7 +605,7 @@ GnuBook.prototype.jumpToIndex = function(index) {
             leafTop += h + this.padding;
         }
         //$('#GBcontainer').attr('scrollTop', leafTop);
-        $('#GBcontainer').animate({scrollTop: leafTop },'fast');    
+        $('#GBcontainer').animate({scrollTop: leafTop },'fast');
     }
 }
 
@@ -557,7 +638,8 @@ GnuBook.prototype.switchMode = function(mode) {
 //______________________________________________________________________________
 GnuBook.prototype.prepareOnePageView = function() {
 
-    var startLeaf = this.displayedLeafs[0];
+    // var startLeaf = this.displayedLeafs[0];
+    var startLeaf = this.currentIndex();
     
     $('#GBcontainer').empty();
     $('#GBcontainer').css({
@@ -565,12 +647,22 @@ GnuBook.prototype.prepareOnePageView = function() {
         overflowX: 'auto'
     });
     
-    $("#GBcontainer").append("<div id='GBpageview'></div>");
+    var gbPageView = $("#GBcontainer").append("<div id='GBpageview'></div>");
     this.resizePageView();
+    
     this.jumpToIndex(startLeaf);
-    this.displayedLeafs = [];    
+    this.displayedLeafs = [];
+    
     this.drawLeafsOnePage();
-    $('#GBzoom').text(100/this.reduce);    
+    $('#GBzoom').text(100/this.reduce);
+        
+    // Bind mouse handlers
+    // Disable mouse click to avoid selected/highlighted page images - bug 354239
+    gbPageView.bind('mousedown', function(e) {
+        return false;
+    })
+    // Special hack for IE7
+    gbPageView[0].onselectstart = function(e) { return false; };
 }
 
 // prepareTwoPageView()
@@ -582,8 +674,9 @@ GnuBook.prototype.prepareTwoPageView = function() {
     // one side of the spread because it is the first/last leaf,
     // foldouts, missing pages, etc
 
-    var targetLeaf = this.displayedLeafs[0];
-    
+    //var targetLeaf = this.displayedLeafs[0];
+    var targetLeaf = this.firstIndex;
+
     if (targetLeaf < this.firstDisplayableIndex()) {
         targetLeaf = this.firstDisplayableIndex();
     }
@@ -599,6 +692,7 @@ GnuBook.prototype.prepareTwoPageView = function() {
     var currentSpreadIndices = this.getSpreadIndices(targetLeaf);
     this.currentLeafL = currentSpreadIndices[0];
     this.currentLeafR = currentSpreadIndices[1];
+    this.firstIndex = this.currentLeafL;
     
     this.calculateSpreadSize(); //sets this.twoPageW, twoPageH, and twoPageRatio
 
@@ -639,7 +733,7 @@ GnuBook.prototype.prepareTwoPageView = function() {
         height: bookCoverDivHeight+'px',
         visibility: 'visible',
         position: 'absolute',
-        backgroundColor: 'rgb(136, 51, 34)',
+        backgroundColor: '#663929',
         left: bookCoverDivLeft + 'px',
         top: bookCoverDivTop+'px',
         MozBorderRadiusTopleft: '7px',
@@ -660,7 +754,7 @@ GnuBook.prototype.prepareTwoPageView = function() {
         borderStyle: 'solid solid solid none',
         borderColor: 'rgb(51, 51, 34)',
         borderWidth: '1px 1px 1px 0px',
-        background: 'transparent url(images/right-edges.png) repeat scroll 0% 0%',
+        background: 'transparent url(' + this.imagesBaseURL + 'right_edges.png) repeat scroll 0% 0%',
         width: leafEdgeWidthR + 'px',
         height: this.twoPageH-1 + 'px',
         /*right: '10px',*/
@@ -675,7 +769,7 @@ GnuBook.prototype.prepareTwoPageView = function() {
         borderStyle: 'solid none solid solid',
         borderColor: 'rgb(51, 51, 34)',
         borderWidth: '1px 0px 1px 1px',
-        background: 'transparent url(images/left-edges.png) repeat scroll 0% 0%',
+        background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
         width: leafEdgeWidthL + 'px',
         height: this.twoPageH-1 + 'px',
         left: bookCoverDivLeft+10+'px',
@@ -836,6 +930,18 @@ GnuBook.prototype.calculateSpreadSize = function() {
 
 }
 
+// currentIndex()
+//______________________________________________________________________________
+// Returns the currently active index.
+GnuBook.prototype.currentIndex = function() {
+    // $$$ we should be cleaner with our idea of which index is active in 1up/2up
+    if (this.mode == this.constMode1up || this.mode == this.constMode2up) {
+        return this.firstIndex;
+    } else {
+        throw 'currentIndex called for unimplemented mode ' + this.mode;
+    }
+}
+
 // right()
 //______________________________________________________________________________
 // Flip the right page over onto the left
@@ -849,6 +955,17 @@ GnuBook.prototype.right = function() {
     }
 }
 
+// rightmost()
+//______________________________________________________________________________
+// Flip to the rightmost page
+GnuBook.prototype.rightmost = function() {
+    if ('rl' != this.pageProgression) {
+        gb.last();
+    } else {
+        gb.first();
+    }
+}
+
 // left()
 //______________________________________________________________________________
 // Flip the left page over onto the right.
@@ -862,6 +979,17 @@ GnuBook.prototype.left = function() {
     }
 }
 
+// leftmost()
+//______________________________________________________________________________
+// Flip to the leftmost page
+GnuBook.prototype.leftmost = function() {
+    if ('rl' != this.pageProgression) {
+        gb.first();
+    } else {
+        gb.last();
+    }
+}
+
 // next()
 //______________________________________________________________________________
 GnuBook.prototype.next = function() {
@@ -888,7 +1016,7 @@ GnuBook.prototype.prev = function() {
     }
 }
 
-GnuBook.prototype.home = function() {
+GnuBook.prototype.first = function() {
     if (2 == this.mode) {
         this.jumpToIndex(2);
     }
@@ -897,7 +1025,7 @@ GnuBook.prototype.home = function() {
     }
 }
 
-GnuBook.prototype.end = function() {
+GnuBook.prototype.last = function() {
     if (2 == this.mode) {
         this.jumpToIndex(this.lastDisplayableIndex());
     }
@@ -1005,7 +1133,7 @@ GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
         borderStyle: 'solid none solid solid',
         borderColor: 'rgb(51, 51, 34)',
         borderWidth: '1px 0px 1px 1px',
-        background: 'transparent url(images/left-edges.png) repeat scroll 0% 0%',
+        background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
         width: leafEdgeTmpW + 'px',
         height: this.twoPageH-1 + 'px',
         left: leftEdgeTmpLeft + 'px',
@@ -1075,15 +1203,21 @@ GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
             
             self.currentLeafL = newIndexL;
             self.currentLeafR = newIndexR;
+            self.firstIndex = self.currentLeafL;
             self.displayedLeafs = [newIndexL, newIndexR];
             self.setClickHandlers();
             self.pruneUnusedImgs();
-            self.prefetch();
+            self.prefetch();            
             self.animating = false;
             
             self.updateSearchHilites2UP();
             self.updatePageNumBox2UP();
             //$('#GBzoom').text((self.twoPageH/self.getPageHeight(newIndexL)).toString().substr(0,4));            
+            
+            if (self.animationFinishedCallback) {
+                self.animationFinishedCallback();
+                self.animationFinishedCallback = null;
+            }
         });
     });        
     
@@ -1148,7 +1282,7 @@ GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
         borderStyle: 'solid none solid solid',
         borderColor: 'rgb(51, 51, 34)',
         borderWidth: '1px 0px 1px 1px',
-        background: 'transparent url(images/left-edges.png) repeat scroll 0% 0%',
+        background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%',
         width: leafEdgeTmpW + 'px',
         height: this.twoPageH-1 + 'px',
         left: currGutter+scaledW+'px',
@@ -1194,15 +1328,22 @@ GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
             
             self.currentLeafL = newIndexL;
             self.currentLeafR = newIndexR;
+            self.firstIndex = self.currentLeafL;
             self.displayedLeafs = [newIndexL, newIndexR];
             self.setClickHandlers();            
             self.pruneUnusedImgs();
             self.prefetch();
             self.animating = false;
 
+
             self.updateSearchHilites2UP();
             self.updatePageNumBox2UP();
             //$('#GBzoom').text((self.twoPageH/self.getPageHeight(newIndexL)).toString().substr(0,4));
+            
+            if (self.animationFinishedCallback) {
+                self.animationFinishedCallback();
+                self.animationFinishedCallback = null;
+            }
         });
     });    
 }
@@ -1614,12 +1755,14 @@ GnuBook.prototype.showEmbedCode = function() {
     }).appendTo('#GnuBook');
 
     htmlStr =  '<p style="text-align:center;"><b>Embed Bookreader in your blog!</b></p>';
-    htmlStr += '<p><b>Note:</b> The bookreader is still in beta testing. URLs may change in the future, breaking embedded books. This feature is just for testing!</b></p>';
     htmlStr += '<p>The bookreader uses iframes for embedding. It will not work on web hosts that block iframes. The embed feature has been tested on blogspot.com blogs as well as self-hosted Wordpress blogs. This feature will NOT work on wordpress.com blogs.</p>';
-    htmlStr += '<p>Embed Code: <input type="text" size="40" value="<iframe src=\'http://www.us.archive.org/GnuBook/GnuBookEmbed.php?id='+this.bookId+'\' width=\'430px\' height=\'430px\'></iframe>"></p>';
+    htmlStr += '<p>Embed Code: <input type="text" size="40" value="' + this.getEmbedCode() + '"></p>';
     htmlStr += '<p style="text-align:center;"><a href="" onclick="gb.embedPopup = null; $(this.parentNode.parentNode).remove(); return false">Close popup</a></p>';    
 
-    this.embedPopup.innerHTML = htmlStr;    
+    this.embedPopup.innerHTML = htmlStr;
+    $(this.embedPopup).find('input').bind('click', function() {
+        this.select();
+    })
 }
 
 // autoToggle()
@@ -1647,7 +1790,8 @@ GnuBook.prototype.autoToggle = function() {
             this.flipFwdToIndex();        
         }
 
-        $('#autoImg').removeClass('play').addClass('pause');
+        $('#GBtoolbar .play').hide();
+        $('#GBtoolbar .pause').show();
         this.autoTimer=setInterval(function(){
             if (self.animating) {return;}
             
@@ -1668,7 +1812,8 @@ GnuBook.prototype.autoStop = function() {
     if (null != this.autoTimer) {
         clearInterval(this.autoTimer);
         this.flipSpeed = 'fast';
-        $('#autoImg').removeClass('pause').addClass('play');
+        $('#GBtoolbar .pause').hide();
+        $('#GBtoolbar .play').show();
         this.autoTimer = null;
     }
 }
@@ -1751,22 +1896,61 @@ GnuBook.prototype.jumpIndexForRightEdgePageX = function(pageX) {
     }
 }
 
-GnuBook.prototype.initToolbar = function(mode) {
+GnuBook.prototype.initToolbar = function(mode, ui) {
+    $("#GnuBook").append("<div id='GBtoolbar'><span style='float:left;'>"
+        + "<a class='GBicon logo rollover' href='" + this.logoURL + "'>&nbsp;</a>"
+        + " <button class='GBicon rollover zoom_out' onclick='gb.zoom1up(-1); return false;'/>" 
+        + "<button class='GBicon rollover zoom_in' onclick='gb.zoom1up(1); return false;'/>"
+        + " <span class='label'>Zoom: <span id='GBzoom'>"+100/this.reduce+"</span>%</span>"
+        + " <button class='GBicon rollover one_page_mode' onclick='gb.switchMode(1); return false;'/>"
+        + " <button class='GBicon rollover two_page_mode' onclick='gb.switchMode(2); return false;'/>"
+        + "&nbsp;&nbsp;<a class='GBblack title' href='"+this.bookUrl+"' target='_blank'>"+this.shortTitle(50)+"</a>"
+        + "</span></div>");
+        
+    if (ui == "embed") {
+        $("#GnuBook a.logo").attr("target","_blank");
+    }
+
     // $$$ turn this into a member variable
     var jToolbar = $('#GBtoolbar'); // j prefix indicates jQuery object
     
     // We build in mode 2
-    jToolbar.append("<span id='GBtoolbarbuttons' style='float: right'><button class='GBicon page_code' /><form class='GBpageform' action='javascript:' onsubmit='gb.jumpToPage(this.elements[0].value)'> page:<input id='GBpagenum' type='text' size='3' onfocus='gb.autoStop();'></input></form> <button class='GBicon book_left'/> <button class='GBicon book_right' /> <button class='GBicon play' id='autoImg' /></span>");
-
-    // Bind the non-changing click handlers
-    jToolbar.find('.page_code').bind('click', function(e) {
-        gb.showEmbedCode();
-        return false;
-    });
-    jToolbar.find('.play').bind('click', function(e) {
-        gb.autoToggle();
-        return false;
-    });
+    jToolbar.append("<span id='GBtoolbarbuttons' style='float: right'>"
+        + "<button class='GBicon rollover embed' />"
+        + "<form class='GBpageform' action='javascript:' onsubmit='gb.jumpToPage(this.elements[0].value)'> <span class='label'>Page:<input id='GBpagenum' type='text' size='3' onfocus='gb.autoStop();'></input></span></form>"
+        + "<div class='GBtoolbarmode2' style='display: inline'><button class='GBicon rollover book_leftmost' /><button class='GBicon rollover book_left' /><button class='GBicon rollover book_right' /><button class='GBicon rollover book_rightmost' /></div>"
+        + "<div class='GBtoolbarmode1' style='display: hidden'><button class='GBicon rollover book_top' /><button class='GBicon rollover book_up' /> <button class='GBicon rollover book_down' /><button class='GBicon rollover book_bottom' /></div>"
+        + "<button class='GBicon rollover play' /><button class='GBicon rollover pause' style='display: none' /></span>");
+
+    this.bindToolbarNavHandlers(jToolbar);
+    
+    // Setup tooltips -- later we could load these from a file for i18n
+    var titles = { '.logo': 'Go to Archive.org',
+                   '.zoom_in': 'Zoom in',
+                   '.zoom_out': 'Zoom out',
+                   '.one_page_mode': 'One-page view',
+                   '.two_page_mode': 'Two-page view',
+                   '.embed': 'Embed bookreader',
+                   '.book_left': 'Flip left',
+                   '.book_right': 'Flip right',
+                   '.book_up': 'Page up',
+                   '.book_down': 'Page down',
+                   '.play': 'Play',
+                   '.pause': 'Pause',
+                   '.book_top': 'First page',
+                   '.book_bottom': 'Last page'
+                  };
+    if ('rl' == this.pageProgression) {
+        titles['.book_leftmost'] = 'Last page';
+        titles['.book_rightmost'] = 'First page';
+    } else { // LTR
+        titles['.book_leftmost'] = 'First page';
+        titles['.book_rightmost'] = 'Last page';
+    }
+                  
+    for (var icon in titles) {
+        jToolbar.find(icon).attr('title', titles[icon]);
+    }
 
     // Switch to requested mode -- binds other click handlers
     this.switchToolbarMode(mode);
@@ -1780,20 +1964,14 @@ GnuBook.prototype.initToolbar = function(mode) {
 // $$$ we should soon split the toolbar out into its own module
 GnuBook.prototype.switchToolbarMode = function(mode) {
     if (1 == mode) {
-        // 1-up        
-      $('#GBtoolbar .GBicon.book_left').removeClass('book_left').addClass('book_up');
-        $('#GBtoolbar .GBicon.book_right').removeClass('book_right').addClass('book_down');
+        // 1-up     
+        $('#GBtoolbar .GBtoolbarmode2').hide();
+        $('#GBtoolbar .GBtoolbarmode1').css('display', 'inline').show();
     } else {
         // 2-up
-        $('#GBtoolbar .GBicon.book_up').removeClass('book_up').addClass('book_left');
-        $('#GBtoolbar .GBicon.book_down').removeClass('book_down').addClass('book_right');
+        $('#GBtoolbar .GBtoolbarmode1').hide();
+        $('#GBtoolbar .GBtoolbarmode2').css('display', 'inline').show();
     }
-    
-    this.bindToolbarNavHandlers($('#GBtoolbar'));
-}
-
-GnuBook.prototype.bindToolbarHandlers = function(jToolbar) {
-
 }
 
 // bindToolbarNavHandlers
@@ -1801,32 +1979,60 @@ GnuBook.prototype.bindToolbarHandlers = function(jToolbar) {
 // Binds the toolbar handlers
 GnuBook.prototype.bindToolbarNavHandlers = function(jToolbar) {
 
-    // $$$ TODO understand why an anonymous function is required instead of just
-    //          setting handler to e.g. gb.prev
-
-    jToolbar.find('.book_left').unbind('click')
-        .bind('click', function(e) {
-            gb.left();
-            return false;
-         });
+    jToolbar.find('.book_left').bind('click', function(e) {
+        gb.left();
+        return false;
+    });
          
-    jToolbar.find('.book_right').unbind('click')
-        .bind('click', function(e) {
-            gb.right();
-            return false;
-        });
+    jToolbar.find('.book_right').bind('click', function(e) {
+        gb.right();
+        return false;
+    });
+        
+    jToolbar.find('.book_up').bind('click', function(e) {
+        gb.prev();
+        return false;
+    });        
         
-    jToolbar.find('.book_up').unbind('click')
-        .bind('click', function(e) {
-            gb.prev();
-            return false;
-        });        
+    jToolbar.find('.book_down').bind('click', function(e) {
+        gb.next();
+        return false;
+    });
         
-    jToolbar.find('.book_down').unbind('click')
-        .bind('click', function(e) {
-            gb.next();
-            return false;
-        });      
+    jToolbar.find('.embed').bind('click', function(e) {
+        gb.showEmbedCode();
+        return false;
+    });
+
+    jToolbar.find('.play').bind('click', function(e) {
+        gb.autoToggle();
+        return false;
+    });
+
+    jToolbar.find('.pause').bind('click', function(e) {
+        gb.autoToggle();
+        return false;
+    });
+    
+    jToolbar.find('.book_top').bind('click', function(e) {
+        gb.first();
+        return false;
+    });
+
+    jToolbar.find('.book_bottom').bind('click', function(e) {
+        gb.last();
+        return false;
+    });
+    
+    jToolbar.find('.book_leftmost').bind('click', function(e) {
+        gb.leftmost();
+        return false;
+    });
+  
+    jToolbar.find('.book_rightmost').bind('click', function(e) {
+        gb.rightmost();
+        return false;
+    });
 }
 
 // firstDisplayableIndex
@@ -1865,3 +2071,245 @@ GnuBook.prototype.lastDisplayableIndex = function() {
         return this.numLeafs - 1;
     }
 }
+
+// shortTitle(maximumCharacters)
+//________
+// Returns a shortened version of the title with the maximum number of characters
+GnuBook.prototype.shortTitle = function(maximumCharacters) {
+    if (this.bookTitle.length < maximumCharacters) {
+        return this.bookTitle;
+    }
+    
+    var title = this.bookTitle.substr(0, maximumCharacters - 3);
+    title += '...';
+    return title;
+}
+
+
+
+// Parameter related functions
+
+// updateFromParams(params)
+//________
+// Update ourselves from the params object.
+//
+// e.g. this.updateFromParams(this.paramsFromFragment(window.location.hash))
+GnuBook.prototype.updateFromParams = function(params) {
+    if ('undefined' != typeof(params.mode)) {
+        this.switchMode(params.mode);
+    }
+
+    // $$$ process /search
+    // $$$ process /zoom
+    
+    // We only respect page if index is not set
+    if ('undefined' != typeof(params.index)) {
+        if (params.index != this.currentIndex()) {
+            this.jumpToIndex(params.index);
+        }
+    } else if ('undefined' != typeof(params.page)) {
+        if (params.page != this.getPageNum(this.currentIndex())) {
+            this.jumpToPage(params.page);
+        }
+    }
+    
+    // $$$ process /region
+    // $$$ process /highlight
+}
+
+// paramsFromFragment(urlFragment)
+//________
+// Returns a object with configuration parametes from a URL fragment.
+//
+// E.g paramsFromFragment(window.location.hash)
+GnuBook.prototype.paramsFromFragment = function(urlFragment) {
+    // URL fragment syntax specification: http://openlibrary.org/dev/docs/bookurls
+    
+    var params = {};
+    
+    // For convenience we allow an initial # character (as from window.location.hash)
+    // but don't require it
+    if (urlFragment.substr(0,1) == '#') {
+        urlFragment = urlFragment.substr(1);
+    }
+    
+    // Simple #nn syntax
+    var oldStyleLeafNum = parseInt( /^\d+$/.exec(urlFragment) );
+    if ( !isNaN(oldStyleLeafNum) ) {
+        params.index = oldStyleLeafNum;
+        
+        // Done processing if using old-style syntax
+        return params;
+    }
+    
+    // Split into key-value pairs
+    var urlArray = urlFragment.split('/');
+    var urlHash = {};
+    for (var i = 0; i < urlArray.length; i += 2) {
+        urlHash[urlArray[i]] = urlArray[i+1];
+    }
+    
+    // Mode
+    if ('1up' == urlHash['mode']) {
+        params.mode = this.constMode1up;
+    } else if ('2up' == urlHash['mode']) {
+        params.mode = this.constMode2up;
+    }
+    
+    // Index and page
+    if ('undefined' != typeof(urlHash['page'])) {
+        // page was set -- may not be int
+        params.page = urlHash['page'];
+    }
+    
+    // $$$ process /region
+    // $$$ process /search
+    // $$$ process /highlight
+        
+    return params;
+}
+
+// paramsFromCurrent()
+//________
+// Create a params object from the current parameters.
+GnuBook.prototype.paramsFromCurrent = function() {
+
+    var params = {};
+
+    var pageNum = this.getPageNum(this.currentIndex());
+    if ((pageNum === 0) || pageNum) {
+        params.page = pageNum;
+    }
+    
+    params.index = this.currentIndex();
+    params.mode = this.mode;
+    
+    // $$$ highlight
+    // $$$ region
+    // $$$ search
+    
+    return params;
+}
+
+// fragmentFromParams(params)
+//________
+// Create a fragment string from the params object.
+// See http://openlibrary.org/dev/docs/bookurls for an explanation of the fragment syntax.
+GnuBook.prototype.fragmentFromParams = function(params) {
+    var separator = '/';
+    
+    var fragments = [];
+    
+    if ('undefined' != typeof(params.page)) {
+        fragments.push('page', params.page);
+    } else {
+        // Don't have page numbering -- use index instead
+        fragments.push('page', 'n' + params.index);
+    }
+    
+    // $$$ highlight
+    // $$$ region
+    // $$$ search
+    
+    // mode
+    if ('undefined' != typeof(params.mode)) {    
+        if (params.mode == this.constMode1up) {
+            fragments.push('mode', '1up');
+        } else if (params.mode == this.constMode2up) {
+            fragments.push('mode', '2up');
+        } else {
+            throw 'fragmentFromParams called with unknown mode ' + params.mode;
+        }
+    }
+    
+    return fragments.join(separator);
+}
+
+// getPageIndex(pageNum)
+//________
+// Returns the index of the given page number, or undefined
+GnuBook.prototype.getPageIndex = function(pageNum) {
+    var pageIndex = undefined;
+    
+    // Check for special "nXX" page number
+    if (pageNum.slice(0,1) == 'n') {
+        try {
+            var pageIntStr = pageNum.slice(1, pageNum.length);
+            pageIndex = parseInt(pageIntStr);
+            return pageIndex;
+        } catch(err) {
+            // Do nothing... will run through page names and see if one matches
+        }
+    }
+
+    var i;
+    for (i=0; i<this.numLeafs; i++) {
+        if (this.getPageNum(i) == pageNum) {
+            pageIndex = i;
+            return pageIndex;
+        }
+    }
+
+    return pageIndex;
+}
+
+// updateLocationHash
+//________
+// Update the location hash from the current parameters.  Call this instead of manually
+// using window.location.replace
+GnuBook.prototype.updateLocationHash = function() {
+    var newHash = '#' + this.fragmentFromParams(this.paramsFromCurrent());
+    window.location.replace(newHash);
+    
+    // This is the variable checked in the timer.  Only user-generated changes
+    // to the URL will trigger the event.
+    this.oldLocationHash = newHash;
+}
+
+// startLocationPolling
+//________
+// Starts polling of window.location to see hash fragment changes
+GnuBook.prototype.startLocationPolling = function() {
+    var self = this; // remember who I am
+    self.oldLocationHash = window.location.hash;
+    
+    if (this.locationPollId) {
+        clearInterval(this.locationPollID);
+        this.locationPollId = null;
+    }
+    
+    this.locationPollId = setInterval(function() {
+        var newHash = window.location.hash;
+        if (newHash != self.oldLocationHash) {
+            if (newHash != self.oldUserHash) { // Only process new user hash once
+                //console.log('url change detected ' + self.oldLocationHash + " -> " + newHash);
+                
+                // Queue change if animating
+                if (self.animating) {
+                    self.autoStop();
+                    self.animationFinishedCallback = function() {
+                        self.updateFromParams(self.paramsFromFragment(newHash));
+                    }                        
+                } else { // update immediately
+                    self.updateFromParams(self.paramsFromFragment(newHash));
+                }
+                self.oldUserHash = newHash;
+            }
+        }
+    }, 500);
+}
+
+// getEmbedURL
+//________
+// Returns a URL for an embedded version of the current book
+GnuBook.prototype.getEmbedURL = function() {
+    // We could generate a URL hash fragment here but for now we just leave at defaults
+    return 'http://' + window.location.host + '/stream/'+this.bookId + '?ui=embed';
+}
+
+// getEmbedCode
+//________
+// Returns the embed code HTML fragment suitable for copy and paste
+GnuBook.prototype.getEmbedCode = function() {
+    return "<iframe src='" + this.getEmbedURL() + "' width='480px' height='430px'></iframe>";
+}
\ No newline at end of file