IE7 doesn't like it when a dictionary literal ends with ,}
[bookreader.git] / GnuBook / GnuBook.js
1 /*
2 Copyright(c)2008 Internet Archive. Software license AGPL version 3.
3
4 This file is part of GnuBook.
5
6     GnuBook is free software: you can redistribute it and/or modify
7     it under the terms of the GNU Affero General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10
11     GnuBook is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU Affero General Public License for more details.
15
16     You should have received a copy of the GNU Affero General Public License
17     along with GnuBook.  If not, see <http://www.gnu.org/licenses/>.
18     
19     The GnuBook source is hosted at http://github.com/openlibrary/bookreader/
20
21     archive.org cvs $Revision: 1.69 $ $Date: 2009-04-01 22:00:51 $
22 */
23
24 // GnuBook()
25 //______________________________________________________________________________
26 // After you instantiate this object, you must supply the following
27 // book-specific functions, before calling init():
28 //  - getPageWidth()
29 //  - getPageHeight()
30 //  - getPageURI()
31 // You must also add a numLeafs property before calling init().
32
33 function GnuBook() {
34     this.reduce  = 4;
35     this.padding = 10;
36     this.mode    = 1; //1 or 2
37     
38     this.displayedLeafs = [];   
39     //this.leafsToDisplay = [];
40     this.imgs = {};
41     this.prefetchedImgs = {}; //an object with numeric keys cooresponding to leafNum
42     
43     this.timer     = null;
44     this.animating = false;
45     this.auto      = false;
46     this.autoTimer = null;
47     this.flipSpeed = 'fast';
48
49     this.twoPagePopUp = null;
50     this.leafEdgeTmp  = null;
51     this.embedPopup = null;
52     
53     this.searchResults = {};
54     
55     this.firstIndex = null;
56     
57     this.lastDisplayableIndex2up = null;
58     
59     // We link to index.php to avoid redirect which breaks back button
60     this.logoURL = 'http://www.archive.org/index.php';
61     
62 };
63
64 // init()
65 //______________________________________________________________________________
66 GnuBook.prototype.init = function() {
67     var startLeaf = window.location.hash;
68     //console.log("startLeaf from location.hash: %s", startLeaf);
69     if ('' == startLeaf) {
70         if (this.titleLeaf) {
71             startLeaf = "#" + this.leafNumToIndex(this.titleLeaf);
72         }
73     }
74     
75     // Ideally this would be set in the HTML/PHP for better search engine visibility but
76     // it takes some time to locate the item and retrieve the metadata
77     document.title = this.shortTitle(50);
78     
79     $("#GnuBook").empty();
80     this.initToolbar(this.mode); // Build inside of toolbar div
81     $("#GnuBook").append("<div id='GBcontainer'></div>");
82     $("#GBcontainer").append("<div id='GBpageview'></div>");
83
84     $("#GBcontainer").bind('scroll', this, function(e) {
85         e.data.loadLeafs();
86     });
87
88     this.setupKeyListeners();
89
90     $(window).bind('resize', this, function(e) {
91         //console.log('resize!');
92         if (1 == e.data.mode) {
93             //console.log('centering 1page view');
94             e.data.centerPageView();
95             $('#GBpageview').empty()
96             e.data.displayedLeafs = [];
97             e.data.updateSearchHilites(); //deletes hilights but does not call remove()            
98             e.data.loadLeafs();
99         } else {
100             //console.log('drawing 2 page view');
101             e.data.prepareTwoPageView();
102         }
103     });
104
105     if (1 == this.mode) {
106         this.resizePageView();
107     
108         if ('' != startLeaf) { // Jump to the leaf specified in the URL
109             this.jumpToIndex(parseInt(startLeaf.substr(1)));
110             //console.log('jump to ' + parseInt(startLeaf.substr(1)));
111         }
112     } else {
113         //this.resizePageView();
114         
115         this.displayedLeafs=[0];
116         if ('' != startLeaf) {
117             this.displayedLeafs = [parseInt(startLeaf.substr(1))];
118         }
119         //console.log('titleLeaf: %d', this.titleLeaf);
120         //console.log('displayedLeafs: %s', this.displayedLeafs);
121         this.prepareTwoPageView();
122         //if (this.auto) this.nextPage();
123     }
124 }
125
126 GnuBook.prototype.setupKeyListeners = function() {
127     var self = this;
128
129     var KEY_PGUP = 33;
130     var KEY_PGDOWN = 34;
131     var KEY_END = 35;
132     var KEY_HOME = 36;
133
134     var KEY_LEFT = 37;
135     var KEY_UP = 38;
136     var KEY_RIGHT = 39;
137     var KEY_DOWN = 40;
138
139     // We use document here instead of window to avoid a bug in jQuery on IE7
140     $(document).keydown(function(e) {
141         
142         // Keyboard navigation        
143         switch(e.keyCode) {
144             case KEY_PGUP:
145             case KEY_UP:            
146                 // In 1up mode page scrolling is handled by browser
147                 if (2 == self.mode) {
148                     self.prev();
149                 }
150                 break;
151             case KEY_DOWN:
152             case KEY_PGDOWN:
153                 if (2 == self.mode) {
154                     self.next();
155                 }
156                 break;
157             case KEY_END:
158                 self.end();
159                 break;
160             case KEY_HOME:
161                 self.home();
162                 break;
163             case KEY_LEFT:
164                 if (self.keyboardNavigationIsDisabled(e)) {
165                     break;
166                 }
167                 if (2 == self.mode) {
168                     self.left();
169                 }
170                 break;
171             case KEY_RIGHT:
172                 if (self.keyboardNavigationIsDisabled(e)) {
173                     break;
174                 }
175                 if (2 == self.mode) {
176                     self.right();
177                 }
178                 break;
179         }
180     });
181 }
182
183 // drawLeafs()
184 //______________________________________________________________________________
185 GnuBook.prototype.drawLeafs = function() {
186     if (1 == this.mode) {
187         this.drawLeafsOnePage();
188     } else {
189         this.drawLeafsTwoPage();
190     }
191 }
192
193
194 // drawLeafsOnePage()
195 //______________________________________________________________________________
196 GnuBook.prototype.drawLeafsOnePage = function() {
197     //alert('drawing leafs!');
198     this.timer = null;
199
200
201     var scrollTop = $('#GBcontainer').attr('scrollTop');
202     var scrollBottom = scrollTop + $('#GBcontainer').height();
203     //console.log('top=' + scrollTop + ' bottom='+scrollBottom);
204     
205     var leafsToDisplay = [];
206     
207     var i;
208     var leafTop = 0;
209     var leafBottom = 0;
210     for (i=0; i<this.numLeafs; i++) {
211         var height  = parseInt(this.getPageHeight(i)/this.reduce); 
212     
213         leafBottom += height;
214         //console.log('leafTop = '+leafTop+ ' pageH = ' + this.pageH[i] + 'leafTop>=scrollTop=' + (leafTop>=scrollTop));
215         var topInView    = (leafTop >= scrollTop) && (leafTop <= scrollBottom);
216         var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom);
217         var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom);
218         if (topInView | bottomInView | middleInView) {
219             //console.log('to display: ' + i);
220             leafsToDisplay.push(i);
221         }
222         leafTop += height +10;      
223         leafBottom += 10;
224     }
225
226     var firstLeafToDraw  = leafsToDisplay[0];
227     window.location.replace('#' + firstLeafToDraw);
228     this.firstIndex      = firstLeafToDraw;
229
230     if ((0 != firstLeafToDraw) && (1 < this.reduce)) {
231         firstLeafToDraw--;
232         leafsToDisplay.unshift(firstLeafToDraw);
233     }
234     
235     var lastLeafToDraw = leafsToDisplay[leafsToDisplay.length-1];
236     if ( ((this.numLeafs-1) != lastLeafToDraw) && (1 < this.reduce) ) {
237         leafsToDisplay.push(lastLeafToDraw+1);
238     }
239     
240     leafTop = 0;
241     var i;
242     for (i=0; i<firstLeafToDraw; i++) {
243         leafTop += parseInt(this.getPageHeight(i)/this.reduce) +10;
244     }
245
246     //var viewWidth = $('#GBpageview').width(); //includes scroll bar width
247     var viewWidth = $('#GBcontainer').attr('scrollWidth');
248
249
250     for (i=0; i<leafsToDisplay.length; i++) {
251         var leafNum = leafsToDisplay[i];    
252         var height  = parseInt(this.getPageHeight(leafNum)/this.reduce); 
253
254         if(-1 == jQuery.inArray(leafsToDisplay[i], this.displayedLeafs)) {            
255             var width   = parseInt(this.getPageWidth(leafNum)/this.reduce); 
256             //console.log("displaying leaf " + leafsToDisplay[i] + ' leafTop=' +leafTop);
257             var div = document.createElement("div");
258             div.className = 'GBpagediv1up';
259             div.id = 'pagediv'+leafNum;
260             div.style.position = "absolute";
261             $(div).css('top', leafTop + 'px');
262             var left = (viewWidth-width)>>1;
263             if (left<0) left = 0;
264             $(div).css('left', left+'px');
265             $(div).css('width', width+'px');
266             $(div).css('height', height+'px');
267             //$(div).text('loading...');
268             
269             $('#GBpageview').append(div);
270
271             var img = document.createElement("img");
272             img.src = this.getPageURI(leafNum);
273             $(img).css('width', width+'px');
274             $(img).css('height', height+'px');
275             $(div).append(img);
276
277         } else {
278             //console.log("not displaying " + leafsToDisplay[i] + ' score=' + jQuery.inArray(leafsToDisplay[i], this.displayedLeafs));            
279         }
280
281         leafTop += height +10;
282
283     }
284     
285     for (i=0; i<this.displayedLeafs.length; i++) {
286         if (-1 == jQuery.inArray(this.displayedLeafs[i], leafsToDisplay)) {
287             var leafNum = this.displayedLeafs[i];
288             //console.log('Removing leaf ' + leafNum);
289             //console.log('id='+'#pagediv'+leafNum+ ' top = ' +$('#pagediv'+leafNum).css('top'));
290             $('#pagediv'+leafNum).remove();
291         } else {
292             //console.log('NOT Removing leaf ' + this.displayedLeafs[i]);
293         }
294     }
295     
296     this.displayedLeafs = leafsToDisplay.slice();
297     this.updateSearchHilites();
298     
299     if (null != this.getPageNum(firstLeafToDraw))  {
300         $("#GBpagenum").val(this.getPageNum(firstLeafToDraw));
301     } else {
302         $("#GBpagenum").val('');
303     }
304 }
305
306 // drawLeafsTwoPage()
307 //______________________________________________________________________________
308 GnuBook.prototype.drawLeafsTwoPage = function() {
309     //alert('drawing two leafs!');
310
311     var scrollTop = $('#GBcontainer').attr('scrollTop');
312     var scrollBottom = scrollTop + $('#GBcontainer').height();
313     
314     //console.log('drawLeafsTwoPage: this.currrentLeafL ' + this.currentLeafL);
315     
316     var leafNum = this.currentLeafL;
317     var height  = this.getPageHeight(leafNum); 
318     var width   = this.getPageWidth(leafNum);
319     var handSide= this.getPageSide(leafNum);
320
321     var leafEdgeWidthL = this.leafEdgeWidth(leafNum);
322     var leafEdgeWidthR = this.twoPageEdgeW - leafEdgeWidthL;
323     var bookCoverDivWidth = this.twoPageW*2+20 + this.twoPageEdgeW;
324     var bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
325     //console.log(leafEdgeWidthL);
326
327     var middle = ($('#GBcontainer').width() >> 1);            
328     var left = middle - this.twoPageW;
329     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
330
331     var scaledW = parseInt(this.twoPageH*width/height);
332     left = 10+leafEdgeWidthL;
333     //var right = left+scaledW;
334     var right = $(this.twoPageDiv).width()-11-$(this.leafEdgeL).width()-scaledW;
335
336     var gutter = middle + this.gutterOffsetForIndex(this.currentLeafL);
337     
338     this.prefetchImg(leafNum);
339     $(this.prefetchedImgs[leafNum]).css({
340         position: 'absolute',
341         /*right:   gutter+'px',*/
342         left: gutter-scaledW+'px',
343         right: '',
344         top:    top+'px',
345         backgroundColor: 'rgb(234, 226, 205)',
346         height: this.twoPageH +'px',
347         width:  scaledW + 'px',
348         borderRight: '1px solid black',
349         zIndex: 2
350     }).appendTo('#GBcontainer');
351     //$('#GBcontainer').append(this.prefetchedImgs[leafNum]);
352
353
354     var leafNum = this.currentLeafR;
355     var height  = this.getPageHeight(leafNum); 
356     var width   = this.getPageWidth(leafNum);
357     //    var left = ($('#GBcontainer').width() >> 1);
358     left += scaledW;
359
360     var scaledW = this.twoPageH*width/height;
361     this.prefetchImg(leafNum);
362     $(this.prefetchedImgs[leafNum]).css({
363         position: 'absolute',
364         left:   gutter+'px',
365         right: '',
366         top:    top+'px',
367         backgroundColor: 'rgb(234, 226, 205)',
368         height: this.twoPageH + 'px',
369         width:  scaledW + 'px',
370         borderLeft: '1px solid black',
371         zIndex: 2
372     }).appendTo('#GBcontainer');
373     //$('#GBcontainer').append(this.prefetchedImgs[leafNum]);
374         
375
376     this.displayedLeafs = [this.currentLeafL, this.currentLeafR];
377     this.setClickHandlers();
378
379     this.updatePageNumBox2UP();
380 }
381
382 // updatePageNumBox2UP
383 //______________________________________________________________________________
384 GnuBook.prototype.updatePageNumBox2UP = function() {
385     if (null != this.getPageNum(this.currentLeafL))  {
386         $("#GBpagenum").val(this.getPageNum(this.currentLeafL));
387     } else {
388         $("#GBpagenum").val('');
389     }
390     window.location.replace('#' + this.currentLeafL); 
391 }
392
393 // loadLeafs()
394 //______________________________________________________________________________
395 GnuBook.prototype.loadLeafs = function() {
396
397
398     var self = this;
399     if (null == this.timer) {
400         this.timer=setTimeout(function(){self.drawLeafs()},250);
401     } else {
402         clearTimeout(this.timer);
403         this.timer=setTimeout(function(){self.drawLeafs()},250);    
404     }
405 }
406
407
408 // zoom1up()
409 //______________________________________________________________________________
410 GnuBook.prototype.zoom1up = function(dir) {
411     if (2 == this.mode) {     //can only zoom in 1-page mode
412         this.switchMode(1);
413         return;
414     }
415     
416     if (1 == dir) {
417         if (this.reduce <= 0.5) return;
418         this.reduce*=0.5;           //zoom in
419     } else {
420         if (this.reduce >= 8) return;
421         this.reduce*=2;             //zoom out
422     }
423     
424     this.resizePageView();
425
426     $('#GBpageview').empty()
427     this.displayedLeafs = [];
428     this.loadLeafs();
429     
430     $('#GBzoom').text(100/this.reduce);
431 }
432
433
434 // resizePageView()
435 //______________________________________________________________________________
436 GnuBook.prototype.resizePageView = function() {
437     var i;
438     var viewHeight = 0;
439     //var viewWidth  = $('#GBcontainer').width(); //includes scrollBar
440     var viewWidth  = $('#GBcontainer').attr('clientWidth');   
441
442     var oldScrollTop  = $('#GBcontainer').attr('scrollTop');
443     var oldViewHeight = $('#GBpageview').height();
444     if (0 != oldViewHeight) {
445         var scrollRatio = oldScrollTop / oldViewHeight;
446     } else {
447         var scrollRatio = 0;
448     }
449     
450     for (i=0; i<this.numLeafs; i++) {
451         viewHeight += parseInt(this.getPageHeight(i)/this.reduce) + this.padding; 
452         var width = parseInt(this.getPageWidth(i)/this.reduce);
453         if (width>viewWidth) viewWidth=width;
454     }
455     $('#GBpageview').height(viewHeight);
456     $('#GBpageview').width(viewWidth);    
457
458     $('#GBcontainer').attr('scrollTop', Math.floor(scrollRatio*viewHeight));
459     
460     this.centerPageView();
461     this.loadLeafs();
462     
463 }
464
465 // centerPageView()
466 //______________________________________________________________________________
467 GnuBook.prototype.centerPageView = function() {
468
469     var scrollWidth  = $('#GBcontainer').attr('scrollWidth');
470     var clientWidth  =  $('#GBcontainer').attr('clientWidth');
471     //console.log('sW='+scrollWidth+' cW='+clientWidth);
472     if (scrollWidth > clientWidth) {
473         $('#GBcontainer').attr('scrollLeft', (scrollWidth-clientWidth)/2);
474     }
475
476 }
477
478 // jumpToPage()
479 //______________________________________________________________________________
480 GnuBook.prototype.jumpToPage = function(pageNum) {
481
482     var i;
483     var foundPage = false;
484     var foundLeaf = null;
485     for (i=0; i<this.numLeafs; i++) {
486         if (this.getPageNum(i) == pageNum) {
487             foundPage = true;
488             foundLeaf = i;
489             break;
490         }
491     }
492     
493     if (foundPage) {
494         var leafTop = 0;
495         var h;
496         this.jumpToIndex(foundLeaf);
497         $('#GBcontainer').attr('scrollTop', leafTop);
498     } else {
499         alert('Page not found. This book might not have pageNumbers in scandata.');
500     }
501 }
502
503 // jumpToIndex()
504 //______________________________________________________________________________
505 GnuBook.prototype.jumpToIndex = function(index) {
506
507     if (2 == this.mode) {
508         this.autoStop();
509         
510         // By checking against min/max we do nothing if requested index
511         // is current
512         if (index < Math.min(this.currentLeafL, this.currentLeafR)) {
513             this.flipBackToIndex(index);
514         } else if (index > Math.max(this.currentLeafL, this.currentLeafR)) {
515             this.flipFwdToIndex(index);
516         }
517
518     } else {        
519         var i;
520         var leafTop = 0;
521         var h;
522         for (i=0; i<index; i++) {
523             h = parseInt(this.getPageHeight(i)/this.reduce); 
524             leafTop += h + this.padding;
525         }
526         //$('#GBcontainer').attr('scrollTop', leafTop);
527         $('#GBcontainer').animate({scrollTop: leafTop },'fast');    
528     }
529 }
530
531
532
533 // switchMode()
534 //______________________________________________________________________________
535 GnuBook.prototype.switchMode = function(mode) {
536
537     //console.log('  asked to switch to mode ' + mode + ' from ' + this.mode);
538     
539     if (mode == this.mode) return;
540
541     this.autoStop();
542     this.removeSearchHilites();
543
544     this.mode = mode;
545     
546     this.switchToolbarMode(mode);
547     
548     if (1 == mode) {
549         this.prepareOnePageView();
550     } else {
551         this.prepareTwoPageView();
552     }
553
554 }
555
556 //prepareOnePageView()
557 //______________________________________________________________________________
558 GnuBook.prototype.prepareOnePageView = function() {
559
560     var startLeaf = this.displayedLeafs[0];
561     
562     $('#GBcontainer').empty();
563     $('#GBcontainer').css({
564         overflowY: 'scroll',
565         overflowX: 'auto'
566     });
567     
568     $("#GBcontainer").append("<div id='GBpageview'></div>");
569     this.resizePageView();
570     this.jumpToIndex(startLeaf);
571     this.displayedLeafs = [];    
572     this.drawLeafsOnePage();
573     $('#GBzoom').text(100/this.reduce);    
574 }
575
576 // prepareTwoPageView()
577 //______________________________________________________________________________
578 GnuBook.prototype.prepareTwoPageView = function() {
579     $('#GBcontainer').empty();
580
581     // We want to display two facing pages.  We may be missing
582     // one side of the spread because it is the first/last leaf,
583     // foldouts, missing pages, etc
584
585     var targetLeaf = this.displayedLeafs[0];
586     
587     if (targetLeaf < this.firstDisplayableIndex()) {
588         targetLeaf = this.firstDisplayableIndex();
589     }
590     
591     if (targetLeaf > this.lastDisplayableIndex()) {
592         targetLeaf = this.lastDisplayableIndex();
593     }
594     
595     this.currentLeafL = null;
596     this.currentLeafR = null;
597     this.pruneUnusedImgs();
598     
599     var currentSpreadIndices = this.getSpreadIndices(targetLeaf);
600     this.currentLeafL = currentSpreadIndices[0];
601     this.currentLeafR = currentSpreadIndices[1];
602     
603     this.calculateSpreadSize(); //sets this.twoPageW, twoPageH, and twoPageRatio
604
605     // We want to minimize the unused space in two-up mode (maximize the amount of page
606     // shown).  We give width to the leaf edges and these widths change (though the sum
607     // of the two remains constant) as we flip through the book.  With the book
608     // cover centered and fixed in the GBcontainer div the page images will meet
609     // at the "gutter" which is generally offset from the center.
610     var middle = ($('#GBcontainer').width() >> 1); // Middle of the GBcontainer div
611     //var gutter = middle+parseInt((2*this.currentLeafL - this.numLeafs)*this.twoPageEdgeW/this.numLeafs/2);
612     
613     var gutter = middle + this.gutterOffsetForIndex(this.currentLeafL);
614     
615     var scaledWL = this.getPageWidth2UP(this.currentLeafL);
616     var scaledWR = this.getPageWidth2UP(this.currentLeafR);
617     var leafEdgeWidthL = this.leafEdgeWidth(this.currentLeafL);
618     var leafEdgeWidthR = this.twoPageEdgeW - leafEdgeWidthL;
619
620     //console.log('idealWidth='+idealWidth+' idealHeight='+idealHeight);
621     //var bookCoverDivWidth = this.twoPageW*2+20 + this.twoPageEdgeW;
622     
623     // The width of the book cover div.  The combined width of both pages, twice the width
624     // of the book cover internal padding (2*10) and the page edges
625     var bookCoverDivWidth = scaledWL + scaledWR + 20 + this.twoPageEdgeW;
626     
627     // The height of the book cover div
628     var bookCoverDivHeight = this.twoPageH+20;
629     
630     //var bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
631     var bookCoverDivLeft = gutter-scaledWL-leafEdgeWidthL-10;
632     var bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
633     //console.log('bookCoverDivWidth='+bookCoverDivWidth+' bookCoverDivHeight='+bookCoverDivHeight+ ' bookCoverDivLeft='+bookCoverDivLeft+' bookCoverDivTop='+bookCoverDivTop);
634
635     this.twoPageDiv = document.createElement('div');
636     $(this.twoPageDiv).attr('id', 'book_div_1').css({
637         border: '1px solid rgb(68, 25, 17)',
638         width:  bookCoverDivWidth + 'px',
639         height: bookCoverDivHeight+'px',
640         visibility: 'visible',
641         position: 'absolute',
642         backgroundColor: '#663929',
643         left: bookCoverDivLeft + 'px',
644         top: bookCoverDivTop+'px',
645         MozBorderRadiusTopleft: '7px',
646         MozBorderRadiusTopright: '7px',
647         MozBorderRadiusBottomright: '7px',
648         MozBorderRadiusBottomleft: '7px'
649     }).appendTo('#GBcontainer');
650     //$('#GBcontainer').append('<div id="book_div_1" style="border: 1px solid rgb(68, 25, 17); width: ' + bookCoverDivWidth + 'px; height: '+bookCoverDivHeight+'px; visibility: visible; position: absolute; background-color: rgb(136, 51, 34); left: ' + bookCoverDivLeft + 'px; top: '+bookCoverDivTop+'px; -moz-border-radius-topleft: 7px; -moz-border-radius-topright: 7px; -moz-border-radius-bottomright: 7px; -moz-border-radius-bottomleft: 7px;"/>');
651
652
653     var height  = this.getPageHeight(this.currentLeafR); 
654     var width   = this.getPageWidth(this.currentLeafR);    
655     var scaledW = this.twoPageH*width/height;
656     
657     this.leafEdgeR = document.createElement('div');
658     this.leafEdgeR.className = 'leafEdgeR';
659     $(this.leafEdgeR).css({
660         borderStyle: 'solid solid solid none',
661         borderColor: 'rgb(51, 51, 34)',
662         borderWidth: '1px 1px 1px 0px',
663         background: 'transparent url(images/right_edges.png) repeat scroll 0% 0%',
664         width: leafEdgeWidthR + 'px',
665         height: this.twoPageH-1 + 'px',
666         /*right: '10px',*/
667         left: gutter+scaledW+'px',
668         top: bookCoverDivTop+10+'px',
669         position: 'absolute'
670     }).appendTo('#GBcontainer');
671     
672     this.leafEdgeL = document.createElement('div');
673     this.leafEdgeL.className = 'leafEdgeL';
674     $(this.leafEdgeL).css({
675         borderStyle: 'solid none solid solid',
676         borderColor: 'rgb(51, 51, 34)',
677         borderWidth: '1px 0px 1px 1px',
678         background: 'transparent url(images/left_edges.png) repeat scroll 0% 0%',
679         width: leafEdgeWidthL + 'px',
680         height: this.twoPageH-1 + 'px',
681         left: bookCoverDivLeft+10+'px',
682         top: bookCoverDivTop+10+'px',    
683         position: 'absolute'
684     }).appendTo('#GBcontainer');
685
686
687
688     bookCoverDivWidth = 30;
689     bookCoverDivHeight = this.twoPageH+20;
690     bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
691     bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
692
693     div = document.createElement('div');
694     $(div).attr('id', 'book_div_2').css({
695         border:          '1px solid rgb(68, 25, 17)',
696         width:           bookCoverDivWidth+'px',
697         height:          bookCoverDivHeight+'px',
698         position:        'absolute',
699         backgroundColor: 'rgb(68, 25, 17)',
700         left:            bookCoverDivLeft+'px',
701         top:             bookCoverDivTop+'px'
702     }).appendTo('#GBcontainer');
703     //$('#GBcontainer').append('<div id="book_div_2" style="border: 1px solid rgb(68, 25, 17); width: '+bookCoverDivWidth+'px; height: '+bookCoverDivHeight+'px; visibility: visible; position: absolute; background-color: rgb(68, 25, 17); left: '+bookCoverDivLeft+'px; top: '+bookCoverDivTop+'px;"/>');
704
705     bookCoverDivWidth = this.twoPageW*2;
706     bookCoverDivHeight = this.twoPageH;
707     bookCoverDivLeft = ($('#GBcontainer').width() - bookCoverDivWidth) >> 1;
708     bookCoverDivTop = ($('#GBcontainer').height() - bookCoverDivHeight) >> 1;
709
710
711     this.prepareTwoPagePopUp();
712
713     this.displayedLeafs = [];
714     
715     //this.leafsToDisplay=[firstLeaf, firstLeaf+1];
716     //console.log('leafsToDisplay: ' + this.leafsToDisplay[0] + ' ' + this.leafsToDisplay[1]);
717     
718     this.drawLeafsTwoPage();
719     this.updateSearchHilites2UP();
720     
721     this.prefetch();
722     $('#GBzoom').text((100*this.twoPageH/this.getPageHeight(this.currentLeafL)).toString().substr(0,4));
723 }
724
725 // prepareTwoPagePopUp()
726 //
727 // This function prepares the "View leaf n" popup that shows while the mouse is
728 // over the left/right "stack of sheets" edges.  It also binds the mouse
729 // events for these divs.
730 //______________________________________________________________________________
731 GnuBook.prototype.prepareTwoPagePopUp = function() {
732     this.twoPagePopUp = document.createElement('div');
733     $(this.twoPagePopUp).css({
734         border: '1px solid black',
735         padding: '2px 6px',
736         position: 'absolute',
737         fontFamily: 'sans-serif',
738         fontSize: '14px',
739         zIndex: '1000',
740         backgroundColor: 'rgb(255, 255, 238)',
741         opacity: 0.85
742     }).appendTo('#GBcontainer');
743     $(this.twoPagePopUp).hide();
744     
745     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseenter', this, function(e) {
746         $(e.data.twoPagePopUp).show();
747     });
748
749     $(this.leafEdgeL).add(this.leafEdgeR).bind('mouseleave', this, function(e) {
750         $(e.data.twoPagePopUp).hide();
751     });
752
753     $(this.leafEdgeL).bind('click', this, function(e) { 
754         e.data.autoStop();
755         var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX);
756         e.data.jumpToIndex(jumpIndex);
757     });
758
759     $(this.leafEdgeR).bind('click', this, function(e) { 
760         e.data.autoStop();
761         var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX);
762         e.data.jumpToIndex(jumpIndex);    
763     });
764
765     $(this.leafEdgeR).bind('mousemove', this, function(e) {
766
767         var jumpLeaf = e.data.jumpIndexForRightEdgePageX(e.pageX);
768         $(e.data.twoPagePopUp).text('View Leaf '+jumpLeaf);
769         
770         $(e.data.twoPagePopUp).css({
771             left: e.pageX +5+ 'px',
772             top: e.pageY-$('#GBcontainer').offset().top+ 'px'
773         });
774     });
775
776     $(this.leafEdgeL).bind('mousemove', this, function(e) {
777     
778         var jumpLeaf = e.data.jumpIndexForLeftEdgePageX(e.pageX);
779         $(e.data.twoPagePopUp).text('View Leaf '+jumpLeaf);
780         
781         $(e.data.twoPagePopUp).css({
782             left: e.pageX - $(e.data.twoPagePopUp).width() - 30 + 'px',
783             top: e.pageY-$('#GBcontainer').offset().top+ 'px'
784         });
785     });
786 }
787
788 // calculateSpreadSize()
789 //______________________________________________________________________________
790 // Calculates 2-page spread dimensions based on this.currentLeafL and
791 // this.currentLeafR
792 // This function sets this.twoPageH, twoPageW, and twoPageRatio
793
794 GnuBook.prototype.calculateSpreadSize = function() {
795     var firstLeaf  = this.currentLeafL;
796     var secondLeaf = this.currentLeafR;
797     //console.log('first page is ' + firstLeaf);
798
799     var canon5Dratio = 1.5;
800     
801     var firstLeafRatio  = this.getPageHeight(firstLeaf) / this.getPageWidth(firstLeaf);
802     var secondLeafRatio = this.getPageHeight(secondLeaf) / this.getPageWidth(secondLeaf);
803     //console.log('firstLeafRatio = ' + firstLeafRatio + ' secondLeafRatio = ' + secondLeafRatio);
804
805     var ratio;
806     if (Math.abs(firstLeafRatio - canon5Dratio) < Math.abs(secondLeafRatio - canon5Dratio)) {
807         ratio = firstLeafRatio;
808         //console.log('using firstLeafRatio ' + ratio);
809     } else {
810         ratio = secondLeafRatio;
811         //console.log('using secondLeafRatio ' + ratio);
812     }
813
814     var totalLeafEdgeWidth = parseInt(this.numLeafs * 0.1);
815     var maxLeafEdgeWidth   = parseInt($('#GBcontainer').width() * 0.1);
816     totalLeafEdgeWidth     = Math.min(totalLeafEdgeWidth, maxLeafEdgeWidth);
817     
818     $('#GBcontainer').css('overflow', 'hidden');
819
820     var idealWidth  = ($('#GBcontainer').width() - 30 - totalLeafEdgeWidth)>>1;
821     var idealHeight = $('#GBcontainer').height() - 30;
822     //console.log('init idealWidth='+idealWidth+' idealHeight='+idealHeight + ' ratio='+ratio);
823
824     if (idealHeight/ratio <= idealWidth) {
825         //use height
826         idealWidth = parseInt(idealHeight/ratio);
827     } else {
828         //use width
829         idealHeight = parseInt(idealWidth*ratio);
830     }
831
832     this.twoPageH     = idealHeight;
833     this.twoPageW     = idealWidth;
834     this.twoPageRatio = ratio;
835     this.twoPageEdgeW = totalLeafEdgeWidth; // The combined width of both edges
836
837 }
838
839 // right()
840 //______________________________________________________________________________
841 // Flip the right page over onto the left
842 GnuBook.prototype.right = function() {
843     if ('rl' != this.pageProgression) {
844         // LTR
845         gb.next();
846     } else {
847         // RTL
848         gb.prev();
849     }
850 }
851
852 // left()
853 //______________________________________________________________________________
854 // Flip the left page over onto the right.
855 GnuBook.prototype.left = function() {
856     if ('rl' != this.pageProgression) {
857         // LTR
858         gb.prev();
859     } else {
860         // RTL
861         gb.next();
862     }
863 }
864
865 // next()
866 //______________________________________________________________________________
867 GnuBook.prototype.next = function() {
868     if (2 == this.mode) {
869         this.autoStop();
870         this.flipFwdToIndex(null);
871     } else {
872         if (this.firstIndex < this.lastDisplayableIndex()) {
873             this.jumpToIndex(this.firstIndex+1);
874         }
875     }
876 }
877
878 // prev()
879 //______________________________________________________________________________
880 GnuBook.prototype.prev = function() {
881     if (2 == this.mode) {
882         this.autoStop();
883         this.flipBackToIndex(null);
884     } else {
885         if (this.firstIndex >= 1) {
886             this.jumpToIndex(this.firstIndex-1);
887         }    
888     }
889 }
890
891 GnuBook.prototype.home = function() {
892     if (2 == this.mode) {
893         this.jumpToIndex(2);
894     }
895     else {
896         this.jumpToIndex(0);
897     }
898 }
899
900 GnuBook.prototype.end = function() {
901     if (2 == this.mode) {
902         this.jumpToIndex(this.lastDisplayableIndex());
903     }
904     else {
905         this.jumpToIndex(this.lastDisplayableIndex());
906     }
907 }
908
909 // flipBackToIndex()
910 //______________________________________________________________________________
911 // to flip back one spread, pass index=null
912 GnuBook.prototype.flipBackToIndex = function(index) {
913     if (1 == this.mode) return;
914
915     var leftIndex = this.currentLeafL;
916     
917     // $$$ Need to change this to be able to see first spread.
918     //     See https://bugs.launchpad.net/gnubook/+bug/296788
919     if (leftIndex <= 2) return;
920     if (this.animating) return;
921
922     if (null != this.leafEdgeTmp) {
923         alert('error: leafEdgeTmp should be null!');
924         return;
925     }
926     
927     if (null == index) {
928         index = leftIndex-2;
929     }
930     //if (index<0) return;
931     
932     var previousIndices = this.getSpreadIndices(index);
933     
934     if (previousIndices[0] < 0 || previousIndices[1] < 0) {
935         return;
936     }
937     
938     //console.log("flipping back to " + previousIndices[0] + ',' + previousIndices[1]);
939
940     this.animating = true;
941     
942     if ('rl' != this.pageProgression) {
943         // Assume LTR and we are going backward    
944         var gutter = this.prepareFlipLeftToRight(previousIndices[0], previousIndices[1]);        
945         this.flipLeftToRight(previousIndices[0], previousIndices[1], gutter);
946         
947     } else {
948         // RTL and going backward
949         var gutter = this.prepareFlipRightToLeft(previousIndices[0], previousIndices[1]);
950         this.flipRightToLeft(previousIndices[0], previousIndices[1], gutter);
951     }
952 }
953
954 // flipLeftToRight()
955 //______________________________________________________________________________
956 // Flips the page on the left towards the page on the right
957 GnuBook.prototype.flipLeftToRight = function(newIndexL, newIndexR, gutter) {
958
959     var leftLeaf = this.currentLeafL;
960     
961     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.currentLeafL);
962     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);    
963     var leafEdgeTmpW = oldLeafEdgeWidthL - newLeafEdgeWidthL;
964     
965     var currWidthL   = this.getPageWidth2UP(leftLeaf);
966     var newWidthL    = this.getPageWidth2UP(newIndexL);
967     var newWidthR    = this.getPageWidth2UP(newIndexR);
968
969     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
970
971     //console.log('leftEdgeTmpW ' + leafEdgeTmpW);
972     //console.log('  gutter ' + gutter + ', scaledWL ' + scaledWL + ', newLeafEdgeWL ' + newLeafEdgeWidthL);
973     
974     //animation strategy:
975     // 0. remove search highlight, if any.
976     // 1. create a new div, called leafEdgeTmp to represent the leaf edge between the leftmost edge 
977     //    of the left leaf and where the user clicked in the leaf edge.
978     //    Note that if this function was triggered by left() and not a
979     //    mouse click, the width of leafEdgeTmp is very small (zero px).
980     // 2. animate both leafEdgeTmp to the gutter (without changing its width) and animate
981     //    leftLeaf to width=0.
982     // 3. When step 2 is finished, animate leafEdgeTmp to right-hand side of new right leaf
983     //    (left=gutter+newWidthR) while also animating the new right leaf from width=0 to
984     //    its new full width.
985     // 4. After step 3 is finished, do the following:
986     //      - remove leafEdgeTmp from the dom.
987     //      - resize and move the right leaf edge (leafEdgeR) to left=gutter+newWidthR
988     //          and width=twoPageEdgeW-newLeafEdgeWidthL.
989     //      - resize and move the left leaf edge (leafEdgeL) to left=gutter-newWidthL-newLeafEdgeWidthL
990     //          and width=newLeafEdgeWidthL.
991     //      - resize the back cover (twoPageDiv) to left=gutter-newWidthL-newLeafEdgeWidthL-10
992     //          and width=newWidthL+newWidthR+twoPageEdgeW+20
993     //      - move new left leaf (newIndexL) forward to zindex=2 so it can receive clicks.
994     //      - remove old left and right leafs from the dom [pruneUnusedImgs()].
995     //      - prefetch new adjacent leafs.
996     //      - set up click handlers for both new left and right leafs.
997     //      - redraw the search highlight.
998     //      - update the pagenum box and the url.
999     
1000     
1001     var leftEdgeTmpLeft = gutter - currWidthL - leafEdgeTmpW;
1002
1003     this.leafEdgeTmp = document.createElement('div');
1004     $(this.leafEdgeTmp).css({
1005         borderStyle: 'solid none solid solid',
1006         borderColor: 'rgb(51, 51, 34)',
1007         borderWidth: '1px 0px 1px 1px',
1008         background: 'transparent url(images/left_edges.png) repeat scroll 0% 0%',
1009         width: leafEdgeTmpW + 'px',
1010         height: this.twoPageH-1 + 'px',
1011         left: leftEdgeTmpLeft + 'px',
1012         top: top+'px',    
1013         position: 'absolute',
1014         zIndex:1000
1015     }).appendTo('#GBcontainer');
1016     
1017     //$(this.leafEdgeL).css('width', newLeafEdgeWidthL+'px');
1018     $(this.leafEdgeL).css({
1019         width: newLeafEdgeWidthL+'px', 
1020         left: gutter-currWidthL-newLeafEdgeWidthL+'px'
1021     });   
1022
1023     // Left gets the offset of the current left leaf from the document
1024     var left = $(this.prefetchedImgs[leftLeaf]).offset().left;
1025     // $$$ This seems very similar to the gutter.  May be able to consolidate the logic.
1026     var right = $('#GBcontainer').width()-left-$(this.prefetchedImgs[leftLeaf]).width()+$('#GBcontainer').offset().left-2+'px';
1027     // We change the left leaf to right positioning
1028     $(this.prefetchedImgs[leftLeaf]).css({
1029         right: right,
1030         left: ''
1031     });
1032
1033      left = $(this.prefetchedImgs[leftLeaf]).offset().left - $('#book_div_1').offset().left;
1034      
1035      right = left+$(this.prefetchedImgs[leftLeaf]).width()+'px';
1036
1037     $(this.leafEdgeTmp).animate({left: gutter}, this.flipSpeed, 'easeInSine');    
1038     //$(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, 'slow', 'easeInSine');
1039     
1040     var self = this;
1041
1042     this.removeSearchHilites();
1043
1044     //console.log('animating leafLeaf ' + leftLeaf + ' to 0px');
1045     $(this.prefetchedImgs[leftLeaf]).animate({width: '0px'}, self.flipSpeed, 'easeInSine', function() {
1046     
1047         //console.log('     and now leafEdgeTmp to left: gutter+newWidthR ' + (gutter + newWidthR));
1048         $(self.leafEdgeTmp).animate({left: gutter+newWidthR+'px'}, self.flipSpeed, 'easeOutSine');
1049
1050         //console.log('  animating newIndexR ' + newIndexR + ' to ' + newWidthR + ' from ' + $(self.prefetchedImgs[newIndexR]).width());
1051         $(self.prefetchedImgs[newIndexR]).animate({width: newWidthR+'px'}, self.flipSpeed, 'easeOutSine', function() {
1052             $(self.prefetchedImgs[newIndexL]).css('zIndex', 2);
1053
1054             $(self.leafEdgeR).css({
1055                 // Moves the right leaf edge
1056                 width: self.twoPageEdgeW-newLeafEdgeWidthL+'px',
1057                 left:  gutter+newWidthR+'px'
1058             });
1059
1060             $(self.leafEdgeL).css({
1061                 // Moves and resizes the left leaf edge
1062                 width: newLeafEdgeWidthL+'px',
1063                 left:  gutter-newWidthL-newLeafEdgeWidthL+'px'
1064             });
1065
1066             
1067             $(self.twoPageDiv).css({
1068                 // Resizes the brown border div
1069                 width: newWidthL+newWidthR+self.twoPageEdgeW+20+'px',
1070                 left: gutter-newWidthL-newLeafEdgeWidthL-10+'px'
1071             });
1072             
1073             $(self.leafEdgeTmp).remove();
1074             self.leafEdgeTmp = null;
1075             
1076             self.currentLeafL = newIndexL;
1077             self.currentLeafR = newIndexR;
1078             self.displayedLeafs = [newIndexL, newIndexR];
1079             self.setClickHandlers();
1080             self.pruneUnusedImgs();
1081             self.prefetch();
1082             self.animating = false;
1083             
1084             self.updateSearchHilites2UP();
1085             self.updatePageNumBox2UP();
1086             //$('#GBzoom').text((self.twoPageH/self.getPageHeight(newIndexL)).toString().substr(0,4));            
1087         });
1088     });        
1089     
1090 }
1091
1092 // flipFwdToIndex()
1093 //______________________________________________________________________________
1094 // Whether we flip left or right is dependent on the page progression
1095 // to flip forward one spread, pass index=null
1096 GnuBook.prototype.flipFwdToIndex = function(index) {
1097
1098     if (this.animating) return;
1099
1100     if (null != this.leafEdgeTmp) {
1101         alert('error: leafEdgeTmp should be null!');
1102         return;
1103     }
1104
1105     if (null == index) {
1106         index = this.currentLeafR+2; // $$$ assumes indices are continuous
1107     }
1108     if (index > this.lastDisplayableIndex()) return;
1109
1110     this.animating = true;
1111     
1112     var nextIndices = this.getSpreadIndices(index);
1113     
1114     //console.log('flipfwd to indices ' + nextIndices[0] + ',' + nextIndices[1]);
1115
1116     if ('rl' != this.pageProgression) {
1117         // We did not specify RTL
1118         var gutter = this.prepareFlipRightToLeft(nextIndices[0], nextIndices[1]);
1119         this.flipRightToLeft(nextIndices[0], nextIndices[1], gutter);
1120     } else {
1121         // RTL
1122         var gutter = this.prepareFlipLeftToRight(nextIndices[0], nextIndices[1]);
1123         this.flipLeftToRight(nextIndices[0], nextIndices[1], gutter);
1124     }
1125 }
1126
1127 // flipRightToLeft(nextL, nextR, gutter)
1128 // $$$ better not to have to pass gutter in
1129 //______________________________________________________________________________
1130 // Flip from left to right and show the nextL and nextR indices on those sides
1131 GnuBook.prototype.flipRightToLeft = function(newIndexL, newIndexR, gutter) {
1132     var oldLeafEdgeWidthL = this.leafEdgeWidth(this.currentLeafL);
1133     var oldLeafEdgeWidthR = this.twoPageEdgeW-oldLeafEdgeWidthL;
1134     var newLeafEdgeWidthL = this.leafEdgeWidth(newIndexL);  
1135     var newLeafEdgeWidthR = this.twoPageEdgeW-newLeafEdgeWidthL;
1136
1137     var leafEdgeTmpW = oldLeafEdgeWidthR - newLeafEdgeWidthR;
1138
1139     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1140
1141     var scaledW = this.getPageWidth2UP(this.currentLeafR);
1142
1143     var middle     = ($('#GBcontainer').width() >> 1);
1144     var currGutter = middle + this.gutterOffsetForIndex(this.currentLeafL);
1145
1146     this.leafEdgeTmp = document.createElement('div');
1147     $(this.leafEdgeTmp).css({
1148         borderStyle: 'solid none solid solid',
1149         borderColor: 'rgb(51, 51, 34)',
1150         borderWidth: '1px 0px 1px 1px',
1151         background: 'transparent url(images/left_edges.png) repeat scroll 0% 0%',
1152         width: leafEdgeTmpW + 'px',
1153         height: this.twoPageH-1 + 'px',
1154         left: currGutter+scaledW+'px',
1155         top: top+'px',    
1156         position: 'absolute',
1157         zIndex:1000
1158     }).appendTo('#GBcontainer');
1159
1160     //var scaledWR = this.getPageWidth2UP(newIndexR); // $$$ should be current instead?
1161     //var scaledWL = this.getPageWidth2UP(newIndexL); // $$$ should be current instead?
1162     
1163     var currWidthL = this.getPageWidth2UP(this.currentLeafL);
1164     var currWidthR = this.getPageWidth2UP(this.currentLeafR);
1165     var newWidthL = this.getPageWidth2UP(newIndexL);
1166     var newWidthR = this.getPageWidth2UP(newIndexR);
1167
1168     $(this.leafEdgeR).css({width: newLeafEdgeWidthR+'px', left: gutter+newWidthR+'px' });
1169
1170     var self = this; // closure-tastic!
1171
1172     var speed = this.flipSpeed;
1173
1174     this.removeSearchHilites();
1175     
1176     $(this.leafEdgeTmp).animate({left: gutter}, speed, 'easeInSine');    
1177     $(this.prefetchedImgs[this.currentLeafR]).animate({width: '0px'}, speed, 'easeInSine', function() {
1178         $(self.leafEdgeTmp).animate({left: gutter-newWidthL-leafEdgeTmpW+'px'}, speed, 'easeOutSine');    
1179         $(self.prefetchedImgs[newIndexL]).animate({width: newWidthL+'px'}, speed, 'easeOutSine', function() {
1180             $(self.prefetchedImgs[newIndexR]).css('zIndex', 2);
1181
1182             $(self.leafEdgeL).css({
1183                 width: newLeafEdgeWidthL+'px', 
1184                 left: gutter-newWidthL-newLeafEdgeWidthL+'px'
1185             });
1186             
1187             $(self.twoPageDiv).css({
1188                 width: newWidthL+newWidthR+self.twoPageEdgeW+20+'px',
1189                 left: gutter-newWidthL-newLeafEdgeWidthL-10+'px'
1190             });
1191             
1192             $(self.leafEdgeTmp).remove();
1193             self.leafEdgeTmp = null;
1194             
1195             self.currentLeafL = newIndexL;
1196             self.currentLeafR = newIndexR;
1197             self.displayedLeafs = [newIndexL, newIndexR];
1198             self.setClickHandlers();            
1199             self.pruneUnusedImgs();
1200             self.prefetch();
1201             self.animating = false;
1202
1203             self.updateSearchHilites2UP();
1204             self.updatePageNumBox2UP();
1205             //$('#GBzoom').text((self.twoPageH/self.getPageHeight(newIndexL)).toString().substr(0,4));
1206         });
1207     });    
1208 }
1209
1210 // setClickHandlers
1211 //______________________________________________________________________________
1212 GnuBook.prototype.setClickHandlers = function() {
1213     var self = this;
1214     $(this.prefetchedImgs[this.currentLeafL]).click(function() {
1215         //self.prevPage();
1216         self.autoStop();
1217         self.left();
1218     });
1219     $(this.prefetchedImgs[this.currentLeafR]).click(function() {
1220         //self.nextPage();'
1221         self.autoStop();
1222         self.right();        
1223     });
1224 }
1225
1226 // prefetchImg()
1227 //______________________________________________________________________________
1228 GnuBook.prototype.prefetchImg = function(leafNum) {
1229     if (undefined == this.prefetchedImgs[leafNum]) {    
1230         //console.log('prefetching ' + leafNum);
1231         var img = document.createElement("img");
1232         img.src = this.getPageURI(leafNum);
1233         this.prefetchedImgs[leafNum] = img;
1234     }
1235 }
1236
1237
1238 // prepareFlipLeftToRight()
1239 //
1240 //______________________________________________________________________________
1241 //
1242 // Prepare to flip the left page towards the right.  This corresponds to moving
1243 // backward when the page progression is left to right.
1244 GnuBook.prototype.prepareFlipLeftToRight = function(prevL, prevR) {
1245
1246     //console.log('  preparing left->right for ' + prevL + ',' + prevR);
1247
1248     this.prefetchImg(prevL);
1249     this.prefetchImg(prevR);
1250     
1251     var height  = this.getPageHeight(prevL); 
1252     var width   = this.getPageWidth(prevL);    
1253     var middle = ($('#GBcontainer').width() >> 1);
1254     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1255     var scaledW = this.twoPageH*width/height;
1256
1257     // The gutter is the dividing line between the left and right pages.
1258     // It is offset from the middle to create the illusion of thickness to the pages
1259     var gutter = middle + this.gutterOffsetForIndex(prevL);
1260     
1261     //console.log('    gutter for ' + prevL + ' is ' + gutter);
1262     //console.log('    prevL.left: ' + (gutter - scaledW) + 'px');
1263     //console.log('    changing prevL ' + prevL + ' to left: ' + (gutter-scaledW) + ' width: ' + scaledW);
1264     
1265     $(this.prefetchedImgs[prevL]).css({
1266         position: 'absolute',
1267         /*right:   middle+'px',*/
1268         left: gutter-scaledW+'px',
1269         right: '',
1270         top:    top+'px',
1271         backgroundColor: 'rgb(234, 226, 205)',
1272         height: this.twoPageH,
1273         width:  scaledW+'px',
1274         borderRight: '1px solid black',
1275         zIndex: 1
1276     });
1277
1278     $('#GBcontainer').append(this.prefetchedImgs[prevL]);
1279
1280     //console.log('    changing prevR ' + prevR + ' to left: ' + gutter + ' width: 0');
1281
1282     $(this.prefetchedImgs[prevR]).css({
1283         position: 'absolute',
1284         left:   gutter+'px',
1285         right: '',
1286         top:    top+'px',
1287         backgroundColor: 'rgb(234, 226, 205)',
1288         height: this.twoPageH,
1289         width:  '0px',
1290         borderLeft: '1px solid black',
1291         zIndex: 2
1292     });
1293
1294     $('#GBcontainer').append(this.prefetchedImgs[prevR]);
1295
1296
1297     return gutter;
1298             
1299 }
1300
1301 // prepareFlipRightToLeft()
1302 //______________________________________________________________________________
1303 GnuBook.prototype.prepareFlipRightToLeft = function(nextL, nextR) {
1304
1305     //console.log('  preparing left<-right for ' + nextL + ',' + nextR);
1306
1307     this.prefetchImg(nextL);
1308     this.prefetchImg(nextR);
1309
1310     var height  = this.getPageHeight(nextR); 
1311     var width   = this.getPageWidth(nextR);    
1312     var middle = ($('#GBcontainer').width() >> 1);
1313     var top  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1314     var scaledW = this.twoPageH*width/height;
1315
1316     var gutter = middle + this.gutterOffsetForIndex(nextL);
1317     
1318     //console.log('right to left to %d gutter is %d', nextL, gutter);
1319     
1320     //console.log(' prepareRTL changing nextR ' + nextR + ' to left: ' + gutter);
1321     $(this.prefetchedImgs[nextR]).css({
1322         position: 'absolute',
1323         left:   gutter+'px',
1324         top:    top+'px',
1325         backgroundColor: 'rgb(234, 226, 205)',
1326         height: this.twoPageH,
1327         width:  scaledW+'px',
1328         borderLeft: '1px solid black',
1329         zIndex: 1
1330     });
1331
1332     $('#GBcontainer').append(this.prefetchedImgs[nextR]);
1333
1334     height  = this.getPageHeight(nextL); 
1335     width   = this.getPageWidth(nextL);      
1336     scaledW = this.twoPageH*width/height;
1337
1338     //console.log(' prepareRTL changing nextL ' + nextL + ' to right: ' + $('#GBcontainer').width()-gutter);
1339     $(this.prefetchedImgs[nextL]).css({
1340         position: 'absolute',
1341         right:   $('#GBcontainer').width()-gutter+'px',
1342         top:    top+'px',
1343         backgroundColor: 'rgb(234, 226, 205)',
1344         height: this.twoPageH,
1345         width:  0+'px',
1346         borderRight: '1px solid black',
1347         zIndex: 2
1348     });
1349
1350     $('#GBcontainer').append(this.prefetchedImgs[nextL]);    
1351
1352     return gutter;
1353             
1354 }
1355
1356 // getNextLeafs() -- NOT RTL AWARE
1357 //______________________________________________________________________________
1358 // GnuBook.prototype.getNextLeafs = function(o) {
1359 //     //TODO: we might have two left or two right leafs in a row (damaged book)
1360 //     //For now, assume that leafs are contiguous.
1361 //     
1362 //     //return [this.currentLeafL+2, this.currentLeafL+3];
1363 //     o.L = this.currentLeafL+2;
1364 //     o.R = this.currentLeafL+3;
1365 // }
1366
1367 // getprevLeafs() -- NOT RTL AWARE
1368 //______________________________________________________________________________
1369 // GnuBook.prototype.getPrevLeafs = function(o) {
1370 //     //TODO: we might have two left or two right leafs in a row (damaged book)
1371 //     //For now, assume that leafs are contiguous.
1372 //     
1373 //     //return [this.currentLeafL-2, this.currentLeafL-1];
1374 //     o.L = this.currentLeafL-2;
1375 //     o.R = this.currentLeafL-1;
1376 // }
1377
1378 // pruneUnusedImgs()
1379 //______________________________________________________________________________
1380 GnuBook.prototype.pruneUnusedImgs = function() {
1381     //console.log('current: ' + this.currentLeafL + ' ' + this.currentLeafR);
1382     for (var key in this.prefetchedImgs) {
1383         //console.log('key is ' + key);
1384         if ((key != this.currentLeafL) && (key != this.currentLeafR)) {
1385             //console.log('removing key '+ key);
1386             $(this.prefetchedImgs[key]).remove();
1387         }
1388         if ((key < this.currentLeafL-4) || (key > this.currentLeafR+4)) {
1389             //console.log('deleting key '+ key);
1390             delete this.prefetchedImgs[key];
1391         }
1392     }
1393 }
1394
1395 // prefetch()
1396 //______________________________________________________________________________
1397 GnuBook.prototype.prefetch = function() {
1398
1399     var lim = this.currentLeafL-4;
1400     var i;
1401     lim = Math.max(lim, 0);
1402     for (i = lim; i < this.currentLeafL; i++) {
1403         this.prefetchImg(i);
1404     }
1405     
1406     if (this.numLeafs > (this.currentLeafR+1)) {
1407         lim = Math.min(this.currentLeafR+4, this.numLeafs-1);
1408         for (i=this.currentLeafR+1; i<=lim; i++) {
1409             this.prefetchImg(i);
1410         }
1411     }
1412 }
1413
1414 // getPageWidth2UP()
1415 //______________________________________________________________________________
1416 GnuBook.prototype.getPageWidth2UP = function(index) {
1417     var height  = this.getPageHeight(index); 
1418     var width   = this.getPageWidth(index);    
1419     return Math.floor(this.twoPageH*width/height);
1420 }    
1421
1422 // search()
1423 //______________________________________________________________________________
1424 GnuBook.prototype.search = function(term) {
1425     $('#GnuBookSearchScript').remove();
1426         var script  = document.createElement("script");
1427         script.setAttribute('id', 'GnuBookSearchScript');
1428         script.setAttribute("type", "text/javascript");
1429         script.setAttribute("src", 'http://'+this.server+'/GnuBook/flipbook_search_gb.php?url='+escape(this.bookPath+'/'+this.bookId+'_djvu.xml')+'&term='+term+'&format=XML&callback=gb.GBSearchCallback');
1430         document.getElementsByTagName('head')[0].appendChild(script);
1431 }
1432
1433 // GBSearchCallback()
1434 //______________________________________________________________________________
1435 GnuBook.prototype.GBSearchCallback = function(txt) {
1436     //alert(txt);
1437     if (jQuery.browser.msie) {
1438         var dom=new ActiveXObject("Microsoft.XMLDOM");
1439         dom.async="false";
1440         dom.loadXML(txt);    
1441     } else {
1442         var parser = new DOMParser();
1443         var dom = parser.parseFromString(txt, "text/xml");    
1444     }
1445     
1446     $('#GnuBookSearchResults').empty();    
1447     $('#GnuBookSearchResults').append('<ul>');
1448     
1449     for (var key in this.searchResults) {
1450         if (null != this.searchResults[key].div) {
1451             $(this.searchResults[key].div).remove();
1452         }
1453         delete this.searchResults[key];
1454     }
1455     
1456     var pages = dom.getElementsByTagName('PAGE');
1457     
1458     if (0 == pages.length) {
1459         // $$$ it would be nice to echo the (sanitized) search result here
1460         $('#GnuBookSearchResults').append('<li>No search results found</li>');
1461     } else {    
1462         for (var i = 0; i < pages.length; i++){
1463             //console.log(pages[i].getAttribute('file').substr(1) +'-'+ parseInt(pages[i].getAttribute('file').substr(1), 10));
1464     
1465             
1466             var re = new RegExp (/_(\d{4})/);
1467             var reMatch = re.exec(pages[i].getAttribute('file'));
1468             var leafNum = parseInt(reMatch[1], 10);
1469             //var leafNum = parseInt(pages[i].getAttribute('file').substr(1), 10);
1470             
1471             var children = pages[i].childNodes;
1472             var context = '';
1473             for (var j=0; j<children.length; j++) {
1474                 //console.log(j + ' - ' + children[j].nodeName);
1475                 //console.log(children[j].firstChild.nodeValue);
1476                 if ('CONTEXT' == children[j].nodeName) {
1477                     context += children[j].firstChild.nodeValue;
1478                 } else if ('WORD' == children[j].nodeName) {
1479                     context += '<b>'+children[j].firstChild.nodeValue+'</b>';
1480                     
1481                     var index = this.leafNumToIndex(leafNum);
1482                     if (null != index) {
1483                         //coordinates are [left, bottom, right, top, [baseline]]
1484                         //we'll skip baseline for now...
1485                         var coords = children[j].getAttribute('coords').split(',',4);
1486                         if (4 == coords.length) {
1487                             this.searchResults[index] = {'l':coords[0], 'b':coords[1], 'r':coords[2], 't':coords[3], 'div':null};
1488                         }
1489                     }
1490                 }
1491             }
1492             //TODO: remove hardcoded instance name
1493             $('#GnuBookSearchResults').append('<li><b><a href="javascript:gb.jumpToIndex('+index+');">Leaf ' + leafNum + '</a></b> - ' + context+'</li>');
1494         }
1495     }
1496     $('#GnuBookSearchResults').append('</ul>');
1497
1498     this.updateSearchHilites();
1499 }
1500
1501 // updateSearchHilites()
1502 //______________________________________________________________________________
1503 GnuBook.prototype.updateSearchHilites = function() {
1504     if (2 == this.mode) {
1505         this.updateSearchHilites2UP();
1506     } else {
1507         this.updateSearchHilites1UP();
1508     }
1509 }
1510
1511 // showSearchHilites1UP()
1512 //______________________________________________________________________________
1513 GnuBook.prototype.updateSearchHilites1UP = function() {
1514
1515     for (var key in this.searchResults) {
1516         
1517         if (-1 != jQuery.inArray(parseInt(key), this.displayedLeafs)) {
1518             var result = this.searchResults[key];
1519             if(null == result.div) {
1520                 result.div = document.createElement('div');
1521                 $(result.div).attr('className', 'GnuBookSearchHilite').appendTo('#pagediv'+key);
1522                 //console.log('appending ' + key);
1523             }    
1524             $(result.div).css({
1525                 width:  (result.r-result.l)/this.reduce + 'px',
1526                 height: (result.b-result.t)/this.reduce + 'px',
1527                 left:   (result.l)/this.reduce + 'px',
1528                 top:    (result.t)/this.reduce +'px'
1529             });
1530
1531         } else {
1532             //console.log(key + ' not displayed');
1533             this.searchResults[key].div=null;
1534         }
1535     }
1536 }
1537
1538 // showSearchHilites2UP()
1539 //______________________________________________________________________________
1540 GnuBook.prototype.updateSearchHilites2UP = function() {
1541
1542     var middle = ($('#GBcontainer').width() >> 1);
1543
1544     for (var key in this.searchResults) {
1545         key = parseInt(key, 10);
1546         if (-1 != jQuery.inArray(key, this.displayedLeafs)) {
1547             var result = this.searchResults[key];
1548             if(null == result.div) {
1549                 result.div = document.createElement('div');
1550                 $(result.div).attr('className', 'GnuBookSearchHilite').css('zIndex', 3).appendTo('#GBcontainer');
1551                 //console.log('appending ' + key);
1552             }
1553
1554             var height = this.getPageHeight(key);
1555             var width  = this.getPageWidth(key)
1556             var reduce = this.twoPageH/height;
1557             var scaledW = parseInt(width*reduce);
1558             
1559             var gutter = middle + this.gutterOffsetForIndex(this.currentLeafL);
1560             
1561             if ('L' == this.getPageSide(key)) {
1562                 var pageL = gutter-scaledW;
1563             } else {
1564                 var pageL = gutter;
1565             }
1566             var pageT  = ($('#GBcontainer').height() - this.twoPageH) >> 1;                
1567                         
1568             $(result.div).css({
1569                 width:  (result.r-result.l)*reduce + 'px',
1570                 height: (result.b-result.t)*reduce + 'px',
1571                 left:   pageL+(result.l)*reduce + 'px',
1572                 top:    pageT+(result.t)*reduce +'px'
1573             });
1574
1575         } else {
1576             //console.log(key + ' not displayed');
1577             if (null != this.searchResults[key].div) {
1578                 //console.log('removing ' + key);
1579                 $(this.searchResults[key].div).remove();
1580             }
1581             this.searchResults[key].div=null;
1582         }
1583     }
1584 }
1585
1586 // removeSearchHilites()
1587 //______________________________________________________________________________
1588 GnuBook.prototype.removeSearchHilites = function() {
1589     for (var key in this.searchResults) {
1590         if (null != this.searchResults[key].div) {
1591             $(this.searchResults[key].div).remove();
1592             this.searchResults[key].div=null;
1593         }        
1594     }
1595 }
1596
1597 // showEmbedCode()
1598 //______________________________________________________________________________
1599 GnuBook.prototype.showEmbedCode = function() {
1600     if (null != this.embedPopup) { // check if already showing
1601         return;
1602     }
1603     this.autoStop();
1604     this.embedPopup = document.createElement("div");
1605     $(this.embedPopup).css({
1606         position: 'absolute',
1607         top:      '20px',
1608         left:     ($('#GBcontainer').width()-400)/2 + 'px',
1609         width:    '400px',
1610         padding:  "20px",
1611         border:   "3px double #999999",
1612         zIndex:   3,
1613         backgroundColor: "#fff"
1614     }).appendTo('#GnuBook');
1615
1616     htmlStr =  '<p style="text-align:center;"><b>Embed Bookreader in your blog!</b></p>';
1617     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>';
1618     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>';
1619     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>';
1620     htmlStr += '<p style="text-align:center;"><a href="" onclick="gb.embedPopup = null; $(this.parentNode.parentNode).remove(); return false">Close popup</a></p>';    
1621
1622     this.embedPopup.innerHTML = htmlStr;    
1623 }
1624
1625 // autoToggle()
1626 //______________________________________________________________________________
1627 GnuBook.prototype.autoToggle = function() {
1628
1629     var bComingFrom1up = false;
1630     if (2 != this.mode) {
1631         bComingFrom1up = true;
1632         this.switchMode(2);
1633     }
1634
1635     var self = this;
1636     if (null == this.autoTimer) {
1637         this.flipSpeed = 2000;
1638         
1639         // $$$ Draw events currently cause layout problems when they occur during animation.
1640         //     There is a specific problem when changing from 1-up immediately to autoplay in RTL so
1641         //     we workaround for now by not triggering immediate animation in that case.
1642         //     See https://bugs.launchpad.net/gnubook/+bug/328327
1643         if (('rl' == this.pageProgression) && bComingFrom1up) {
1644             // don't flip immediately -- wait until timer fires
1645         } else {
1646             // flip immediately
1647             this.flipFwdToIndex();        
1648         }
1649
1650         $('#GBtoolbar .play').hide();
1651         $('#GBtoolbar .pause').show();
1652         this.autoTimer=setInterval(function(){
1653             if (self.animating) {return;}
1654             
1655             if (Math.max(self.currentLeafL, self.currentLeafR) >= self.lastDisplayableIndex()) {
1656                 self.flipBackToIndex(1); // $$$ really what we want?
1657             } else {            
1658                 self.flipFwdToIndex();
1659             }
1660         },5000);
1661     } else {
1662         this.autoStop();
1663     }
1664 }
1665
1666 // autoStop()
1667 //______________________________________________________________________________
1668 GnuBook.prototype.autoStop = function() {
1669     if (null != this.autoTimer) {
1670         clearInterval(this.autoTimer);
1671         this.flipSpeed = 'fast';
1672         $('#GBtoolbar .pause').hide();
1673         $('#GBtoolbar .play').show();
1674         this.autoTimer = null;
1675     }
1676 }
1677
1678 // keyboardNavigationIsDisabled(event)
1679 //   - returns true if keyboard navigation should be disabled for the event
1680 //______________________________________________________________________________
1681 GnuBook.prototype.keyboardNavigationIsDisabled = function(event) {
1682     if (event.target.tagName == "INPUT") {
1683         return true;
1684     }   
1685     return false;
1686 }
1687
1688 // gutterOffsetForIndex
1689 //______________________________________________________________________________
1690 //
1691 // Returns the gutter offset for the spread containing the given index.
1692 // This function supports RTL
1693 GnuBook.prototype.gutterOffsetForIndex = function(pindex) {
1694
1695     // To find the offset of the gutter from the middle we calculate our percentage distance
1696     // through the book (0..1), remap to (-0.5..0.5) and multiply by the total page edge width
1697     var offset = parseInt(((pindex / this.numLeafs) - 0.5) * this.twoPageEdgeW);
1698     
1699     // But then again for RTL it's the opposite
1700     if ('rl' == this.pageProgression) {
1701         offset = -offset;
1702     }
1703     
1704     return offset;
1705 }
1706
1707 // leafEdgeWidth
1708 //______________________________________________________________________________
1709 // Returns the width of the leaf edge div for the page with index given
1710 GnuBook.prototype.leafEdgeWidth = function(pindex) {
1711     // $$$ could there be single pixel rounding errors for L vs R?
1712     if ((this.getPageSide(pindex) == 'L') && (this.pageProgression != 'rl')) {
1713         return parseInt( (pindex/this.numLeafs) * this.twoPageEdgeW + 0.5);
1714     } else {
1715         return parseInt( (1 - pindex/this.numLeafs) * this.twoPageEdgeW + 0.5);
1716     }
1717 }
1718
1719 // jumpIndexForLeftEdgePageX
1720 //______________________________________________________________________________
1721 // Returns the target jump leaf given a page coordinate (inside the left page edge div)
1722 GnuBook.prototype.jumpIndexForLeftEdgePageX = function(pageX) {
1723     if ('rl' != this.pageProgression) {
1724         // LTR - flipping backward
1725         var jumpLeaf = this.currentLeafL - ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
1726         // browser may have resized the div due to font size change -- see https://bugs.launchpad.net/gnubook/+bug/333570
1727         jumpLeaf = Math.min(jumpLeaf, this.currentLeafL - 2);
1728         jumpLeaf = Math.max(jumpLeaf, this.firstDisplayableIndex());
1729         return jumpLeaf;
1730     } else {
1731         var jumpLeaf = this.currentLeafL + ($(this.leafEdgeL).offset().left + $(this.leafEdgeL).width() - pageX) * 10;
1732         jumpLeaf = Math.max(jumpLeaf, this.currentLeafL + 2);
1733         jumpLeaf = Math.min(jumpLeaf, this.lastDisplayableIndex());
1734         return jumpLeaf;
1735     }
1736 }
1737
1738 // jumpIndexForRightEdgePageX
1739 //______________________________________________________________________________
1740 // Returns the target jump leaf given a page coordinate (inside the right page edge div)
1741 GnuBook.prototype.jumpIndexForRightEdgePageX = function(pageX) {
1742     if ('rl' != this.pageProgression) {
1743         // LTR
1744         var jumpLeaf = this.currentLeafR + (pageX - $(this.leafEdgeR).offset().left) * 10;
1745         jumpLeaf = Math.max(jumpLeaf, this.currentLeafR + 2);
1746         jumpLeaf = Math.min(jumpLeaf, this.lastDisplayableIndex());
1747         return jumpLeaf;
1748     } else {
1749         var jumpLeaf = this.currentLeafR - (pageX - $(this.leafEdgeR).offset().left) * 10;
1750         jumpLeaf = Math.min(jumpLeaf, this.currentLeafR - 2);
1751         jumpLeaf = Math.max(jumpLeaf, this.firstDisplayableIndex());
1752         return jumpLeaf;
1753     }
1754 }
1755
1756 GnuBook.prototype.initToolbar = function(mode) {
1757     $("#GnuBook").append("<div id='GBtoolbar'><span style='float:left;'>"
1758         + "<a class='GBicon logo rollover' href='" + this.logoURL + "'>&nbsp;</a>"
1759         + " <button class='GBicon rollover zoom_out' onclick='gb.zoom1up(-1); return false;'/>" 
1760         + "<button class='GBicon rollover zoom_in' onclick='gb.zoom1up(1); return false;'/>"
1761         + " <span class='label'>Zoom: <span id='GBzoom'>25</span>%</span>"
1762         + " <button class='GBicon rollover one_page_mode' onclick='gb.switchMode(1); return false;'/>"
1763         + " <button class='GBicon rollover two_page_mode' onclick='gb.switchMode(2); return false;'/>"
1764         + "&nbsp;&nbsp; <a class='GBblack title' href='"+this.bookUrl+"' target='_blank'>"+this.shortTitle(50)+"</a>"
1765         + "</span></div>");
1766
1767     // $$$ turn this into a member variable
1768     var jToolbar = $('#GBtoolbar'); // j prefix indicates jQuery object
1769     
1770     // We build in mode 2
1771     jToolbar.append("<span id='GBtoolbarbuttons' style='float: right'>"
1772         + "<button class='GBicon rollover embed' />"
1773         + "<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>"
1774         + "<div class='GBtoolbarmode2' style='display: inline'><button class='GBicon rollover book_left' /><button class='GBicon rollover book_right' /></div>"
1775         + "<div class='GBtoolbarmode1' style='display: hidden'><button class='GBicon rollover book_up' /> <button class='GBicon rollover book_down' /></div>"
1776         + "<button class='GBicon rollover play' /><button class='GBicon rollover pause' style='display: none' /></span>");
1777
1778     // Bind the non-changing click handlers
1779     jToolbar.find('.embed').bind('click', function(e) {
1780         gb.showEmbedCode();
1781         return false;
1782     });
1783     jToolbar.find('.play').bind('click', function(e) {
1784         gb.autoToggle();
1785         return false;
1786     });
1787     jToolbar.find('.pause').bind('click', function(e) {
1788         gb.autoToggle();
1789         return false;
1790     });
1791     
1792     // Setup tooltips -- later we could load these from a file for i18n
1793     var titles = { '.logo': 'Go to Archive.org',
1794                    '.zoom_in': 'Zoom in',
1795                    '.zoom_out': 'Zoom out',
1796                    '.one_page_mode': 'One-page view',
1797                    '.two_page_mode': 'Two-page view',
1798                    '.embed': 'Embed bookreader',
1799                    '.book_left': 'Flip left',
1800                    '.book_right': 'Flip right',
1801                    '.book_up': 'Page up',
1802                    '.book_down': 'Page down',
1803                    '.play': 'Play',
1804                    '.pause': 'Pause'
1805                   };                  
1806     for (var icon in titles) {
1807         jToolbar.find(icon).attr('title', titles[icon]);
1808     }
1809
1810     // Switch to requested mode -- binds other click handlers
1811     this.switchToolbarMode(mode);
1812
1813 }
1814
1815
1816 // switchToolbarMode
1817 //______________________________________________________________________________
1818 // Update the toolbar for the given mode (changes navigation buttons)
1819 // $$$ we should soon split the toolbar out into its own module
1820 GnuBook.prototype.switchToolbarMode = function(mode) {
1821     if (1 == mode) {
1822         // 1-up     
1823         $('#GBtoolbar .GBtoolbarmode2').hide();
1824         $('#GBtoolbar .GBtoolbarmode1').css('display', 'inline').show();
1825     } else {
1826         // 2-up
1827         $('#GBtoolbar .GBtoolbarmode1').hide();
1828         $('#GBtoolbar .GBtoolbarmode2').css('display', 'inline').show();
1829     }
1830     
1831     this.bindToolbarNavHandlers($('#GBtoolbar'));
1832 }
1833
1834 // bindToolbarNavHandlers
1835 //______________________________________________________________________________
1836 // Binds the toolbar handlers
1837 GnuBook.prototype.bindToolbarNavHandlers = function(jToolbar) {
1838
1839     jToolbar.find('.book_left').unbind('click')
1840         .bind('click', function(e) {
1841             gb.left();
1842             return false;
1843          });
1844          
1845     jToolbar.find('.book_right').unbind('click')
1846         .bind('click', function(e) {
1847             gb.right();
1848             return false;
1849         });
1850         
1851     jToolbar.find('.book_up').unbind('click')
1852         .bind('click', function(e) {
1853             gb.prev();
1854             return false;
1855         });        
1856         
1857     jToolbar.find('.book_down').unbind('click')
1858         .bind('click', function(e) {
1859             gb.next();
1860             return false;
1861         });      
1862 }
1863
1864 // firstDisplayableIndex
1865 //______________________________________________________________________________
1866 // Returns the index of the first visible page, dependent on the mode.
1867 // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
1868 // this function when we can as part of https://bugs.launchpad.net/gnubook/+bug/296788
1869 GnuBook.prototype.firstDisplayableIndex = function() {
1870     if (this.mode == 0) {
1871         return 0;
1872     } else {
1873         return 1; // $$$ we assume there are enough pages... we need logic for very short books
1874     }
1875 }
1876
1877 // lastDisplayableIndex
1878 //______________________________________________________________________________
1879 // Returns the index of the last visible page, dependent on the mode.
1880 // $$$ Currently we cannot display the front/back cover in 2-up and will need to update
1881 // this function when we can as pa  rt of https://bugs.launchpad.net/gnubook/+bug/296788
1882 GnuBook.prototype.lastDisplayableIndex = function() {
1883     if (this.mode == 2) {
1884         if (this.lastDisplayableIndex2up === null) {
1885             // Calculate and cache
1886             var candidate = this.numLeafs - 1;
1887             for ( ; candidate >= 0; candidate--) {
1888                 var spreadIndices = this.getSpreadIndices(candidate);
1889                 if (Math.max(spreadIndices[0], spreadIndices[1]) < (this.numLeafs - 1)) {
1890                     break;
1891                 }
1892             }
1893             this.lastDisplayableIndex2up = candidate;
1894         }
1895         return this.lastDisplayableIndex2up;
1896     } else {
1897         return this.numLeafs - 1;
1898     }
1899 }
1900
1901 // shortTitle(maximumCharacters)
1902 //________
1903 // Returns a shortened version of the title with the maximum number of characters
1904 GnuBook.prototype.shortTitle = function(maximumCharacters) {
1905     if (this.bookTitle.length < maximumCharacters) {
1906         return this.bookTitle;
1907     }
1908     
1909     var title = this.bookTitle.substr(0, maximumCharacters - 3);
1910     title += '...';
1911     return title;
1912 }