Increase minimum search box width. Enable auto-play mode. Change read-aloud icon...
[bookreader.git] / BookReader / BookReader.js
index 463c042..1efa4df 100644 (file)
@@ -74,7 +74,7 @@ function BookReader() {
     this.printPopup = null;
     
     this.searchTerm = '';
-    this.searchResults = {};
+    this.searchResults = null;
     
     this.firstIndex = null;
     
@@ -370,7 +370,7 @@ BookReader.prototype.setClickHandler2UP = function( element, data, handler) {
     //console.log('setting handler');
     //console.log(element.tagName);
     
-    $(element).unbind('tap').bind('tap', data, function(e) {
+    $(element).unbind('click').bind('click', data, function(e) {
         handler(e);
     });
 }
@@ -707,8 +707,10 @@ BookReader.prototype.drawLeafsThumbnail = function( seekIndex ) {
     // console.log('current ' + currentIndex);
     // console.log('least visible ' + leastVisible + ' most visible ' + mostVisible);
     if (currentIndex < leastVisible) {
+        this.willChangeToIndex(leastVisible);
         this.setCurrentIndex(leastVisible);
     } else if (currentIndex > mostVisible) {
+        this.willChangeToIndex(mostVisible);
         this.setCurrentIndex(mostVisible);
     }
 
@@ -1237,6 +1239,8 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) {
 
     this.willChangeToIndex(index);
 
+    this.ttsStop();
+
     if (this.constMode2up == this.mode) {
         this.autoStop();
         
@@ -1342,8 +1346,6 @@ BookReader.prototype.switchMode = function(mode) {
         this.reduce = this.quantizeReduce(this.reduce, this.onePage.reductionFactors);
         this.prepareOnePageView();
     } else if (3 == mode) {
-        $('button.thumb').hide();
-        $('button.twopg').show();
         this.reduce = this.quantizeReduce(this.reduce, this.reductionFactors);
         this.prepareThumbnailView();
     } else {
@@ -1351,8 +1353,6 @@ BookReader.prototype.switchMode = function(mode) {
         // this.twoPage.autofit = null; // Take zoom level from other mode
         this.twoPageCalculateReductionFactors();
         this.reduce = this.quantizeReduce(this.reduce, this.twoPage.reductionFactors);
-        $('button.thumb').show();
-        $('button.twopg').hide();
         this.prepareTwoPageView();
         this.twoPageCenterView(0.5, 0.5); // $$$ TODO preserve center
     }
@@ -1471,7 +1471,7 @@ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPerc
     $('#BRcontainer').append('<div id="BRtwopageview"></div>');
     
     // Attaches to first child, so must come after we add the page view
-    $('#BRcontainer').dragscrollable();
+    //$('#BRcontainer').dragscrollable();
     this.bindGestures($('#BRcontainer'));
 
     // $$$ calculate first then set
@@ -2375,8 +2375,13 @@ BookReader.prototype.setMouseHandlers2UP = function() {
     this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexL],
         { self: this },
         function(e) {
+            if (e.button == 2) {
+                // right click
+                return;
+            }
             e.data.self.ttsStop();
             e.data.self.left();
+            
             e.preventDefault();
         }
     );
@@ -2384,6 +2389,10 @@ BookReader.prototype.setMouseHandlers2UP = function() {
     this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexR],
         { self: this },
         function(e) {
+            if (e.button == 2) {
+                // right click
+                return;
+            }
             e.data.self.ttsStop();
             e.data.self.right();
             e.preventDefault();
@@ -2409,11 +2418,11 @@ BookReader.prototype.prefetchImg = function(index) {
     if (loadImage) {
         //console.log('prefetching ' + index);
         var img = document.createElement("img");
-        img.className = 'BRpageimage';
+        $(img).addClass('BRpageimage').addClass('BRnoselect');
         if (index < 0 || index > (this.numLeafs - 1) ) {
             // Facing page at beginning or end, or beyond
             $(img).css({
-                'background-color': 'transparent'
+                'background-color': '#efefef'
             });
         }
         img.src = pageURI;
@@ -2633,92 +2642,51 @@ BookReader.prototype.getPageWidth2UP = function(index) {
 // search()
 //______________________________________________________________________________
 BookReader.prototype.search = function(term) {
-    term = term.replace(/\//g, ' '); // strip slashes
+    //console.log('search called with term=' + term);
+    var url = 'http://'+this.server.replace(/:.+/, ''); //remove the port and userdir
+    url    += '/~edward/inside_jsonp.php?item_id='+this.bookId;
+    url    += '&doc='+this.subPrefix;   //TODO: test with subitem
+    url    += '&path='+this.bookPath.replace(new RegExp('/'+this.subPrefix+'$'), ''); //remove subPrefix from end of path
+    url    += '&q='+escape(term);
+    //console.log('search url='+url);
+    
+    term = term.replace(/\//g, ' '); // strip slashes, since this goes in the url
     this.searchTerm = term;
-    $('#BookReaderSearchScript').remove();
-    var script  = document.createElement("script");
-    script.setAttribute('id', 'BookReaderSearchScript');
-    script.setAttribute("type", "text/javascript");
-    script.setAttribute("src", 'http://'+this.server+'/BookReader/flipbook_search_br.php?url='+escape(this.bookPath + '_djvu.xml')+'&term='+term+'&format=XML&callback=br.BRSearchCallback');
-    document.getElementsByTagName('head')[0].appendChild(script);
-    $('#BookReaderSearchBox').val(term);
-    $('#BookReaderSearchResults').html('Searching...');
+    
+    this.removeSearchResults();
+    this.showProgressPopup('<img id="searchmarker" src="'+this.imagesBaseURL + 'marker_srch-on.png'+'"> Search results will appear below...');
+    this.ttsAjax = $.ajax({url:url, dataType:'jsonp', jsonpCallback:'BRSearchCallback'});    
 }
 
 // BRSearchCallback()
 //______________________________________________________________________________
-BookReader.prototype.BRSearchCallback = function(txt) {
-    //alert(txt);
-    if (jQuery.browser.msie) {
-        var dom=new ActiveXObject("Microsoft.XMLDOM");
-        dom.async="false";
-        dom.loadXML(txt);    
-    } else {
-        var parser = new DOMParser();
-        var dom = parser.parseFromString(txt, "text/xml");    
-    }
-    
-    $('#BookReaderSearchResults').empty();    
-    $('#BookReaderSearchResults').append('<ul>');
-    
-    for (var key in this.searchResults) {
-        if (null != this.searchResults[key].div) {
-            $(this.searchResults[key].div).remove();
-        }
-        delete this.searchResults[key];
+// Unfortunately, we can't pass 'br.searchCallback' to our search service,
+// because it can't handle the '.'
+function BRSearchCallback(results) {    
+    //console.log('got ' + results.matches.length + ' results');
+    br.removeSearchResults();
+    br.searchResults = results; 
+    //console.log(br.searchResults);
+    
+    if (0 == results.matches.length) {
+        $(br.popup).text('No matches were found.');
+        setTimeout(function(){
+            $(br.popup).fadeOut('slow', function() {
+                br.removeProgressPopup();
+            })        
+        },1000);
+        return;
     }
     
-    var pages = dom.getElementsByTagName('PAGE');
-    
-    if (0 == pages.length) {
-        // $$$ it would be nice to echo the (sanitized) search result here
-        $('#BookReaderSearchResults').append('<li>No search results found</li>');
-    } else {    
-        for (var i = 0; i < pages.length; i++){
-            //console.log(pages[i].getAttribute('file').substr(1) +'-'+ parseInt(pages[i].getAttribute('file').substr(1), 10));
-    
-            
-            var re = new RegExp (/_(\d{4})\.djvu/);
-            var reMatch = re.exec(pages[i].getAttribute('file'));
-            var index = parseInt(reMatch[1], 10);
-            //var index = parseInt(pages[i].getAttribute('file').substr(1), 10);
-            
-            var children = pages[i].childNodes;
-            var context = '';
-            for (var j=0; j<children.length; j++) {
-                //console.log(j + ' - ' + children[j].nodeName);
-                //console.log(children[j].firstChild.nodeValue);
-                if ('CONTEXT' == children[j].nodeName) {
-                    context += children[j].firstChild.nodeValue;
-                } else if ('WORD' == children[j].nodeName) {
-                    context += '<b>'+children[j].firstChild.nodeValue+'</b>';
-                    
-                    var index = this.leafNumToIndex(index);
-                    if (null != index) {
-                        //coordinates are [left, bottom, right, top, [baseline]]
-                        //we'll skip baseline for now...
-                        var coords = children[j].getAttribute('coords').split(',',4);
-                        if (4 == coords.length) {
-                            this.searchResults[index] = {'l':parseInt(coords[0]), 'b':parseInt(coords[1]), 'r':parseInt(coords[2]), 't':parseInt(coords[3]), 'div':null};
-                        }
-                    }
-                }
-            }
-            var pageName = this.getPageName(index);
-            var middleX = (this.searchResults[index].l + this.searchResults[index].r) >> 1;
-            var middleY = (this.searchResults[index].t + this.searchResults[index].b) >> 1;
-            //TODO: remove hardcoded instance name
-            $('#BookReaderSearchResults').append('<li><b><a href="javascript:br.jumpToIndex('+index+','+middleX+','+middleY+');">' + pageName + '</a></b> - ' + context + '</li>');
-        }
+    var i;    
+    for (i=0; i<results.matches.length; i++) {        
+        br.addSearchResult(results.matches[i].text, br.leafNumToIndex(results.matches[i].par[0].page));
     }
-    $('#BookReaderSearchResults').append('</ul>');
-
-    // $$$ update again for case of loading search URL in new browser window (search box may not have been ready yet)
-    $('#BookReaderSearchBox').val(this.searchTerm);
-
-    this.updateSearchHilites();
+    br.updateSearchHilites();
+    br.removeProgressPopup();
 }
 
+
 // updateSearchHilites()
 //______________________________________________________________________________
 BookReader.prototype.updateSearchHilites = function() {
@@ -2732,30 +2700,39 @@ BookReader.prototype.updateSearchHilites = function() {
 // showSearchHilites1UP()
 //______________________________________________________________________________
 BookReader.prototype.updateSearchHilites1UP = function() {
-
-    for (var key in this.searchResults) {
-        
-        if (jQuery.inArray(parseInt(key), this.displayedIndices) >= 0) {
-            var result = this.searchResults[key];
-            if (null == result.div) {
-                result.div = document.createElement('div');
-                $(result.div).attr('className', 'BookReaderSearchHilite').appendTo('#pagediv'+key);
-                //console.log('appending ' + key);
-            }    
-            $(result.div).css({
-                width:  (result.r-result.l)/this.reduce + 'px',
-                height: (result.b-result.t)/this.reduce + 'px',
-                left:   (result.l)/this.reduce + 'px',
-                top:    (result.t)/this.reduce +'px'
-            });
-
-        } else {
-            //console.log(key + ' not displayed');
-            this.searchResults[key].div=null;
+    var results = this.searchResults;
+    if (null == results) return;
+    var i, j;
+    for (i=0; i<results.matches.length; i++) {
+        //console.log(results.matches[i].par[0]);
+        for (j=0; j<results.matches[i].par[0].boxes.length; j++) {
+            var box = results.matches[i].par[0].boxes[j];
+            var pageIndex = this.leafNumToIndex(box.page);
+            if (jQuery.inArray(pageIndex, this.displayedIndices) >= 0) {
+                if (null == box.div) {
+                    //create a div for the search highlight, and stash it in the box object
+                    box.div = document.createElement('div');
+                    $(box.div).attr('className', 'BookReaderSearchHilite').appendTo('#pagediv'+pageIndex);
+                }
+                $(box.div).css({
+                    width:  (box.r-box.l)/this.reduce + 'px',
+                    height: (box.b-box.t)/this.reduce + 'px',
+                    left:   (box.l)/this.reduce + 'px',
+                    top:    (box.t)/this.reduce +'px'
+                });                
+            } else {
+                if (null != box.div) {
+                    //console.log('removing search highlight div');
+                    $(box.div).remove();
+                    box.div=null;
+                }                
+            }
         }
     }
+    
 }
 
+
 // twoPageGutter()
 //______________________________________________________________________________
 // Returns the position of the gutter (line between the page images)
@@ -2883,31 +2860,36 @@ BookReader.prototype.twoPagePlaceFlipAreas = function() {
     });
 }
     
-// showSearchHilites2UP()
+// showSearchHilites2UPNew()
 //______________________________________________________________________________
 BookReader.prototype.updateSearchHilites2UP = function() {
-
-    for (var key in this.searchResults) {
-        key = parseInt(key, 10);
-        if (jQuery.inArray(key, this.displayedIndices) >= 0) {
-            var result = this.searchResults[key];
-            if (null == result.div) {
-                result.div = document.createElement('div');
-                $(result.div).attr('className', 'BookReaderSearchHilite').css('zIndex', 3).appendTo('#BRtwopageview');
-                //console.log('appending ' + key);
-            }
-
-            this.setHilightCss2UP(result.div, key, result.l, result.r, result.t, result.b);
-
-        } else {
-            //console.log(key + ' not displayed');
-            if (null != this.searchResults[key].div) {
-                //console.log('removing ' + key);
-                $(this.searchResults[key].div).remove();
+    //console.log('updateSearchHilites2UP results = ' + this.searchResults); 
+    var results = this.searchResults;
+    if (null == results) return;
+    var i, j;
+    for (i=0; i<results.matches.length; i++) {
+        //console.log(results.matches[i].par[0]);
+        for (j=0; j<results.matches[i].par[0].boxes.length; j++) {
+            var box = results.matches[i].par[0].boxes[j];
+            var pageIndex = this.leafNumToIndex(box.page);
+            if (jQuery.inArray(pageIndex, this.displayedIndices) >= 0) {
+                if (null == box.div) {
+                    //create a div for the search highlight, and stash it in the box object
+                    box.div = document.createElement('div');
+                    $(box.div).attr('className', 'BookReaderSearchHilite').css('zIndex', 3).appendTo('#BRtwopageview');
+                    //console.log('appending new div');
+                }
+                this.setHilightCss2UP(box.div, pageIndex, box.l, box.r, box.t, box.b);
+            } else {
+                if (null != box.div) {
+                    //console.log('removing search highlight div');
+                    $(box.div).remove();
+                    box.div=null;
+                }                
             }
-            this.searchResults[key].div=null;
         }
     }
+    
 }
 
 // setHilightCss2UP()
@@ -2942,14 +2924,21 @@ BookReader.prototype.setHilightCss2UP = function(div, index, left, right, top, b
 // removeSearchHilites()
 //______________________________________________________________________________
 BookReader.prototype.removeSearchHilites = function() {
-    for (var key in this.searchResults) {
-        if (null != this.searchResults[key].div) {
-            $(this.searchResults[key].div).remove();
-            this.searchResults[key].div=null;
-        }        
-    }
+    var results = this.searchResults;
+    if (null == results) return;
+    var i, j;
+    for (i=0; i<results.matches.length; i++) {
+        for (j=0; j<results.matches[i].par[0].boxes.length; j++) {
+            var box = results.matches[i].par[0].boxes[j];
+            if (null != box.div) {
+                $(box.div).remove();
+                box.div=null;                
+            }
+        }
+    }    
 }
 
+
 // printPage
 //______________________________________________________________________________
 BookReader.prototype.printPage = function() {
@@ -3278,6 +3267,12 @@ BookReader.prototype.initNavbar = function() {
     $('#BookReader').after(
         '<div id="BRnav">'
         +     '<div id="BRpage">'   // Page turn buttons
+        +         '<button class="BRicon onepg"></button>'
+        +         '<button class="BRicon twopg"></button>'
+        +         '<button class="BRicon thumb"></button>'
+        +         '<button class="BRicon fit"></button>'
+        +         '<button class="BRicon zoom_in"></button>'
+        +         '<button class="BRicon zoom_out"></button>'
         +         '<button class="BRicon book_left"></button>'
         +         '<button class="BRicon book_right"></button>'
         +     '</div>'
@@ -3289,6 +3284,7 @@ BookReader.prototype.initNavbar = function() {
         +             '<div class="BRnavend" id="BRnavright"></div>'
         +         '</div>'     
         +     '</div>'
+        +     '<div id="BRnavCntlBtm" class="BRnavCntl BRdn"></div>'
         + '</div>'
     );
     
@@ -3384,10 +3380,13 @@ BookReader.prototype.initNavbar = function() {
     .append('<div id="pagenum"><span class="currentpage"></span></div>');
     //.wrap('<div class="ui-handle-helper-parent"></div>').parent(); // XXXmang is this used for hiding the tooltip?
     
+    // $$$mang, why are these set both here and in bindNavigationHandlers?
     $('.BRicon.book_left').bind('click', function() {
+        self.ttsStop();
         self.left();
     });
     $('.BRicon.book_right').bind('click', function() {
+        self.ttsStop();
         self.right();
     });
     
@@ -3422,7 +3421,8 @@ BookReader.prototype.updateNavIndex = function(index) {
     $('#BRpager').data('swallowchange', true).slider('value', index);
 }
 
-BookReader.prototype.addSearchResult = function(queryString, pageNumber, pageIndex) {
+BookReader.prototype.addSearchResult = function(queryString, pageIndex) {
+    var pageNumber = this.getPageNum(pageIndex);
     var uiStringSearch = "Search result"; // i18n
     var uiStringPage = "Page"; // i18n
     
@@ -3432,7 +3432,10 @@ BookReader.prototype.addSearchResult = function(queryString, pageNumber, pageInd
         pageDisplayString = uiStringPage + ' ' + pageNumber;
     }
     
-    $('<div class="search" style="left:' + percentThrough + ';" title="' + uiStringSearch + '"><div class="query">'
+    var re = new RegExp('{{{(.+?)}}}', 'g');    
+    queryString = queryString.replace(re, '<a href="#" onclick="br.jumpToIndex('+pageIndex+'); return false;">$1</a>')
+
+    var marker = $('<div class="search" style="top:'+(-$('#BRcontainer').height())+'px; left:' + percentThrough + ';" title="' + uiStringSearch + '"><div class="query">'
         + queryString + '<span>' + uiStringPage + ' ' + pageNumber + '</span></div>')
     .data({'self': this, 'pageIndex': pageIndex })
     .appendTo('#BRnavline').bt({
@@ -3478,10 +3481,13 @@ BookReader.prototype.addSearchResult = function(queryString, pageNumber, pageInd
     .bind('click', function() {
         $(this).data('self').jumpToIndex($(this).data('pageIndex'));
     });
+    
+    $(marker).animate({top:'-25px'}, 'slow');
 
 }
 
 BookReader.prototype.removeSearchResults = function() {
+    this.removeSearchHilites(); //be sure to set all box.divs to null
     $('#BRnavpos .search').remove();
 }
 
@@ -3500,8 +3506,7 @@ BookReader.prototype.addChapter = function(chapterTitle, pageNumber, pageIndex)
         closeWhenOthersOpen: true,
         cssStyles: {
             padding: '12px 14px',
-            //backgroundColor: '#000',
-            backgroundColor: '#444', // To set it off slightly from the chapter marker
+            backgroundColor: '#000',
             border: '4px solid #e2dcc5',
             //borderBottom: 'none',
             fontFamily: '"Arial", sans-serif',
@@ -3601,21 +3606,29 @@ BookReader.prototype.addChapterFromEntry = function(tocEntryObject) {
 BookReader.prototype.initToolbar = function(mode, ui) {
 
     // $$$mang should be contained within the BookReader div instead of body
+
+    var readIcon = '';
+    if (!navigator.userAgent.match(/mobile/i)) {
+        readIcon = "<button class='BRicon read modal'></button>";
+    }
+    
     $("body").append(
           "<div id='BRtoolbar'>"
         +   "<span id='BRtoolbarbuttons'>"
         /* XXXmang integrate search */
-        +     "<form method='get' id='booksearch'><input type='search' id='textSrch' name='textSrch' val='' placeholder='Search inside'/><button type='submit' id='btnSrch' name='btnSrch'>GO</button></form>"
+        +     "<form action='javascript:' id='booksearch'><input type='search' id='textSrch' name='textSrch' val='' placeholder='Search inside'/><button type='submit' id='btnSrch' name='btnSrch'>GO</button></form>"
         // XXXmang icons incorrect or handlers wrong
+        +     "<button class='BRicon play'></button>"
+        +     "<button class='BRicon pause'></button>"
         +     "<button class='BRicon info'></button>"
         +     "<button class='BRicon share'></button>"
-        +     "<button class='BRicon read modal'></button>"
-        +     "<button class='BRicon full'></button>"
+        +     readIcon
+        //+     "<button class='BRicon full'></button>"
         +   "</span>"
         +   "<span><a class='logo' href='" + this.logoURL + "'></a></span>"
         +   "<span id='BRreturn'><span>Back to</span><a href='" + this.bookUrl + "'>" + this.bookTitle + "</a></span>"
         + "</div>"
-        
+        /*
         + "<div id='BRzoomer'>"
         +   "<div id='BRzoompos'>"
         +     "<button class='BRicon zoom_out'></button>"
@@ -3625,7 +3638,11 @@ BookReader.prototype.initToolbar = function(mode, ui) {
         +     "</div>"
         +     "<button class='BRicon zoom_in'></button>"
         +   "</div>"
-        + "</div>");
+        + "</div>"
+        */
+        );
+    
+    $('#BRtoolbar .pause').hide();
     
     this.updateToolbarZoom(this.reduce); // Pretty format
         
@@ -3638,8 +3655,9 @@ BookReader.prototype.initToolbar = function(mode, ui) {
     
     // We build in mode 2
     jToolbar.append();
-
-    this.bindToolbarNavHandlers(jToolbar);
+    
+    // Navigation handlers will be bound after all UI is in place -- makes moving icons between
+    // the toolbar and nav bar easier
     
     // Setup tooltips -- later we could load these from a file for i18n
     var titles = { '.logo': 'Go to Archive.org',
@@ -3652,7 +3670,7 @@ BookReader.prototype.initToolbar = function(mode, ui) {
                    '.embed': 'Embed BookReader',
                    '.link': 'Link to this book (and page)',
                    '.bookmark': 'Bookmark this page',
-                   '.read': 'Allow BookReader to read this aloud',
+                   '.read': 'Read this book aloud',
                    '.full': 'Show fullscreen',
                    '.book_left': 'Flip left',
                    '.book_right': 'Flip right',
@@ -3721,24 +3739,67 @@ BookReader.prototype.switchToolbarMode = function(mode) {
     }
 }
 
-// bindToolbarNavHandlers
+// updateToolbarZoom(reduce)
 //______________________________________________________________________________
-// Binds the toolbar handlers
-BookReader.prototype.bindToolbarNavHandlers = function(jToolbar) {
+// Update the displayed zoom factor based on reduction factor
+BookReader.prototype.updateToolbarZoom = function(reduce) {
+    var value;
+    var autofit = null;
+
+    // $$$ TODO preserve zoom/fit for each mode
+    if (this.mode == this.constMode2up) {
+        autofit = this.twoPage.autofit;
+    } else {
+        autofit = this.onePage.autofit;
+    }
+    
+    if (autofit) {
+        value = autofit.slice(0,1).toUpperCase() + autofit.slice(1);
+    } else {
+        value = (100 / reduce).toFixed(2);
+        // Strip trailing zeroes and decimal if all zeroes
+        value = value.replace(/0+$/,'');
+        value = value.replace(/\.$/,'');
+        value += '%';
+    }
+    $('#BRzoom').text(value);
+}
+
+// bindNavigationHandlers
+//______________________________________________________________________________
+// Bind navigation handlers
+BookReader.prototype.bindNavigationHandlers = function() {
 
     var self = this; // closure
+    jIcons = $('.BRicon');
+
+    jIcons.filter('.onepg').bind('click', function(e) {
+        self.switchMode(self.constMode1up);
+    });
+    
+    jIcons.filter('.twopg').bind('click', function(e) {
+        self.switchMode(self.constMode2up);
+    });
+
+    jIcons.filter('.thumb').bind('click', function(e) {
+        self.switchMode(self.constModeThumb);
+    });
+    
+    jIcons.filter('.fit').bind('fit', function(e) {
+        // XXXmang implement autofit zoom
+    });
 
-    jToolbar.find('.book_left').click(function(e) {
+    jIcons.filter('.book_left').click(function(e) {
         self.left();
         return false;
     });
          
-    jToolbar.find('.book_right').click(function(e) {
+    jIcons.filter('.book_right').click(function(e) {
         self.right();
         return false;
     });
         
-    jToolbar.find('.book_up').bind('click', function(e) {
+    jIcons.filter('.book_up').bind('click', function(e) {
         if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) {
             self.scrollUp();
         } else {
@@ -3747,7 +3808,7 @@ BookReader.prototype.bindToolbarNavHandlers = function(jToolbar) {
         return false;
     });        
         
-    jToolbar.find('.book_down').bind('click', function(e) {
+    jIcons.filter('.book_down').bind('click', function(e) {
         if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) {
             self.scrollDown();
         } else {
@@ -3756,102 +3817,93 @@ BookReader.prototype.bindToolbarNavHandlers = function(jToolbar) {
         return false;
     });
 
-    jToolbar.find('.print').click(function(e) {
+    jIcons.filter('.print').click(function(e) {
         self.printPage();
         return false;
     });
         
-    jToolbar.find('.embed').click(function(e) {
+    jIcons.filter('.embed').click(function(e) {
         self.showEmbedCode();
         return false;
     });
 
-    jToolbar.find('.bookmark').click(function(e) {
+    jIcons.filter('.bookmark').click(function(e) {
         self.showBookmarkCode();
         return false;
     });
 
-    jToolbar.find('.play').click(function(e) {
+    jIcons.filter('.play').click(function(e) {
         self.autoToggle();
         return false;
     });
 
-    jToolbar.find('.pause').click(function(e) {
+    jIcons.filter('.pause').click(function(e) {
         self.autoToggle();
         return false;
     });
     
-    jToolbar.find('.book_top').click(function(e) {
+    jIcons.filter('.book_top').click(function(e) {
         self.first();
         return false;
     });
 
-    jToolbar.find('.book_bottom').click(function(e) {
+    jIcons.filter('.book_bottom').click(function(e) {
         self.last();
         return false;
     });
     
-    jToolbar.find('.book_leftmost').click(function(e) {
+    jIcons.filter('.book_leftmost').click(function(e) {
         self.leftmost();
         return false;
     });
   
-    jToolbar.find('.book_rightmost').click(function(e) {
+    jIcons.filter('.book_rightmost').click(function(e) {
         self.rightmost();
         return false;
     });
 
-    jToolbar.find('.read').click(function(e) {
+    jIcons.filter('.read').click(function(e) {
         self.ttsToggle();
         return false;
     });
     
-    // $$$mang cleanup
-    $('#BRzoomer .zoom_in').bind('click', function() {
+    jIcons.filter('.zoom_in').bind('click', function() {
+        self.ttsStop();
         self.zoom(1);
         return false;
     });
     
-    $('#BRzoomer .zoom_out').bind('click', function() {
+    jIcons.filter('.zoom_out').bind('click', function() {
+        self.ttsStop();
         self.zoom(-1);
         return false;
     });
-}
-
-// updateToolbarZoom(reduce)
-//______________________________________________________________________________
-// Update the displayed zoom factor based on reduction factor
-BookReader.prototype.updateToolbarZoom = function(reduce) {
-    var value;
-    var autofit = null;
-
-    // $$$ TODO preserve zoom/fit for each mode
-    if (this.mode == this.constMode2up) {
-        autofit = this.twoPage.autofit;
-    } else {
-        autofit = this.onePage.autofit;
-    }
     
-    if (autofit) {
-        value = autofit.slice(0,1).toUpperCase() + autofit.slice(1);
-    } else {
-        value = (100 / reduce).toFixed(2);
-        // Strip trailing zeroes and decimal if all zeroes
-        value = value.replace(/0+$/,'');
-        value = value.replace(/\.$/,'');
-        value += '%';
-    }
-    $('#BRzoom').text(value);
-}
+    // XXX fix integration
+    $('#booksearch').bind('submit', function() {
+        self.search($('#textSrch').val());
+    });
 
-// bindNavigationHandlers
-//______________________________________________________________________________
-// Bind navigation handlers
-BookReader.prototype.bindNavigationHandlers = function() {
+    this.initSwipeData();
     $('#BookReader').die('mousemove.navigation').live('mousemove.navigation',
         { 'br': this },
         this.navigationMousemoveHandler
     );
+    
+    $('.BRpageimage').die('mousedown.swipe').live('mousedown.swipe',
+        { 'br': this },
+        this.swipeMousedownHandler
+    )
+    .die('mousemove.swipe').live('mousemove.swipe',
+        { 'br': this },
+        this.swipeMousemoveHandler
+    )
+    .die('mouseup.swipe').live('mouseup.swipe',
+        { 'br': this },
+        this.swipeMouseupHandler
+    );
+    
+    this.bindMozTouchHandlers();
 }
 
 // unbindNavigationHandlers
@@ -3878,6 +3930,100 @@ BookReader.prototype.navigationMousemoveHandler = function(event) {
     }
 }
 
+BookReader.prototype.initSwipeData = function(clientX, clientY) {
+    /*
+     * Based on the really quite awesome "Today's Guardian" at http://guardian.gyford.com/
+     */
+    this._swipe = {
+        mightBeSwiping: false,
+        startTime: (new Date).getTime(),
+        startX: clientX,
+        startY: clientY,
+        deltaX: 0,
+        deltaY: 0,
+        deltaT: 0
+    }
+}
+
+BookReader.prototype.swipeMousedownHandler = function(event) {
+    //console.log('swipe mousedown');
+    //console.log(event);
+    
+    var self = event.data['br'];
+    self.initSwipeData(event.clientX, event.clientY);
+    self._swipe.mightBeSwiping = true;
+    
+    // We should be the last bubble point for the page images
+    // Disable image drag and select, but keep right-click
+    if ($(event.originalTarget).hasClass('BRpageimage') && event.button != 2) {
+        event.preventDefault();
+    }
+}
+
+BookReader.prototype.swipeMousemoveHandler = function(event) {
+    //console.log('swipe move ' + event.clientX + ',' + event.clientY);
+
+    var _swipe = event.data['br']._swipe;
+    if (! _swipe.mightBeSwiping) {
+        return;
+    }
+    
+    // Update swipe data
+    _swipe.deltaX = event.clientX - _swipe.startX;
+    _swipe.deltaY = event.clientY - _swipe.startY;
+    _swipe.deltaT = (new Date).getTime() - _swipe.startTime;
+    
+    var absX = Math.abs(_swipe.deltaX);
+    var absY = Math.abs(_swipe.deltaY);
+    
+    // Minimum distance in the amount of tim to trigger the swipe
+    var minSwipeLength = Math.max($('#BookReader').width() / 5, 100);
+    var maxSwipeTime = 1000;
+    
+    // Check for horizontal swipe
+    if (absX > absY && (absX > minSwipeLength) && _swipe.deltaT < maxSwipeTime) {
+        //console.log('swipe! ' + _swipe.deltaX + ',' + _swipe.deltaY + ' ' + _swipe.deltaT + 'ms');
+        
+        _swipe.mightBeSwiping = false; // only trigger once
+        if (event.data['br'].mode == event.data['br'].constMode2up) {
+            if (_swipe.deltaX < 0) {
+                event.data['br'].right();
+            } else {
+                event.data['br'].left();
+            }
+        }
+    }
+}
+BookReader.prototype.swipeMouseupHandler = function(event) {
+    //console.log('swipe mouseup');
+    //console.log(event);
+    event.data['br']._swipe.mightBeSwiping = false;
+}
+
+BookReader.prototype.bindMozTouchHandlers = function() {
+    var self = this;
+    
+    // Currently only want touch handlers in 2up
+    $('#BookReader').bind('MozTouchDown', function(event) {
+        //console.log('MozTouchDown ' + event.streamId + ' ' + event.clientX + ',' + event.clientY);
+        if (this.mode == this.constMode2up) {
+            event.preventDefault();
+        }
+    })
+    .bind('MozTouchMove', function(event) {
+        //console.log('MozTouchMove - ' + event.streamId + ' ' + event.clientX + ',' + event.clientY)
+        if (this.mode == this.constMode2up) { 
+            event.preventDefault();
+        }
+    })
+    .bind('MozTouchUp', function(event) {
+        //console.log('MozTouchUp - ' + event.streamId + ' ' + event.clientX + ',' + event.clientY);
+        if (this.mode = this.constMode2up) {
+            event.preventDefault();
+        }
+    });
+}
+
 // navigationIsVisible
 //______________________________________________________________________________
 // Returns true if the navigation elements are currently visible
@@ -3900,7 +4046,7 @@ BookReader.prototype.hideNavigation = function() {
         // $$$ don't hardcode height
         $('#BRtoolbar').animate({top:-60});
         $('#BRnav').animate({bottom:-60});
-        $('#BRzoomer').animate({right:-26});
+        //$('#BRzoomer').animate({right:-26});
     }
 }
 
@@ -3912,7 +4058,7 @@ BookReader.prototype.showNavigation = function() {
     if (!this.navigationIsVisible()) {
         $('#BRtoolbar').animate({top:0});
         $('#BRnav').animate({bottom:0});
-        $('#BRzoomer').animate({right:0});
+        //$('#BRzoomer').animate({right:0});
     }
 }
 
@@ -4264,17 +4410,31 @@ BookReader.prototype.canSwitchToMode = function(mode) {
 // searchHighlightVisible
 //________
 // Returns true if a search highlight is currently being displayed
-BookReader.prototype.searchHighlightVisible = function() {
+BookReader.prototype.searchHighlightVisible = function() {    
+    var results = this.searchResults;
+    if (null == results) return false;    
+    
     if (this.constMode2up == this.mode) {
-        if (this.searchResults[this.twoPage.currentIndexL]
-                || this.searchResults[this.twoPage.currentIndexR]) {
-            return true;
-        }
-    } else { // 1up
-        if (this.searchResults[this.currentIndex()]) {
-            return true;
+        var visiblePages = Array(this.twoPage.currentIndexL, this.twoPage.currentIndexR);
+    } else if (this.constMode1up == this.mode) {
+        var visiblePages = Array();
+        visiblePages[0] = this.currentIndex();
+    } else {
+        return false;
+    }
+    
+    var i, j;
+    for (i=0; i<results.matches.length; i++) {
+        //console.log(results.matches[i].par[0]);
+        for (j=0; j<results.matches[i].par[0].boxes.length; j++) {
+            var box = results.matches[i].par[0].boxes[j];
+            var pageIndex = this.leafNumToIndex(box.page);
+            if (jQuery.inArray(pageIndex, visiblePages) >= 0) {
+                return true;
+            }
         }
     }
+    
     return false;
 }
 
@@ -4302,7 +4462,7 @@ BookReader.prototype._getPageHeight= function(index) {
 // Returns the page URI or transparent image if out of range
 BookReader.prototype._getPageURI = function(index, reduce, rotate) {
     if (index < 0 || index >= this.numLeafs) { // Synthesize page
-        return this.imagesBaseURL + "/transparent.png";
+        return this.imagesBaseURL + "transparent.png";
     }
     
     if ('undefined' == typeof(reduce)) {
@@ -4337,6 +4497,7 @@ BookReader.prototype.gotOpenLibraryRecord = function(self, olObject) {
     // $$$ could refactor this so that 'this' is available
     if (olObject) {
         if (olObject['table_of_contents']) {
+            // XXX check here that TOC is valid
             self.updateTOC(olObject['table_of_contents']);
         }
     }
@@ -4396,7 +4557,9 @@ BookReader.util = {
 // ttsToggle()
 //______________________________________________________________________________
 BookReader.prototype.ttsToggle = function () {
-    if (false == this.ttsPlaying) {        
+    if (false == this.ttsPlaying) {
+        this.ttsPlaying = true;
+        this.showProgressPopup('Loading audio...');    
         if(soundManager.supported()) {
             this.ttsStart();            
         } else {               
@@ -4404,7 +4567,7 @@ BookReader.prototype.ttsToggle = function () {
               if (oStatus.success) {                
                 this.ttsStart();
               } else {
-                alert('Could not load soundManger2, possibly due to FlashBlock. Audio playback is disabled');
+                alert('Could not load soundManager2, possibly due to FlashBlock. Audio playback is disabled');
               }
             }, this);        
         }
@@ -4419,7 +4582,7 @@ BookReader.prototype.ttsStart = function () {
     if (soundManager.debugMode) console.log('starting readAloud');
     if (this.constModeThumb == this.mode) this.switchMode(this.constMode1up);
     
-    this.ttsPlaying = true;
+    //this.ttsPlaying = true; //set this in ttsToggle()
     this.ttsIndex = this.currentIndex();
     this.ttsFormat = 'mp3';
     if ($.browser.mozilla) {
@@ -4437,7 +4600,7 @@ BookReader.prototype.ttsStop = function () {
     soundManager.stopAll();
     soundManager.destroySound('chunk'+this.ttsIndex+'-'+this.ttsPosition);
     this.ttsRemoveHilites();
-    this.ttsRemovePopup();
+    this.removeProgressPopup();
 
     this.ttsPlaying     = false;
     this.ttsIndex       = null;  //leaf index
@@ -4469,7 +4632,7 @@ BookReader.prototype.ttsStartCB = function (data) {
         return;
     }
     
-    this.ttsShowPopup();
+    this.showProgressPopup('Loading audio...');
     
     ///// whileloading: broken on safari
     ///// onload fires on safari, but *after* the sound starts playing..
@@ -4478,8 +4641,8 @@ BookReader.prototype.ttsStartCB = function (data) {
      id: 'chunk'+this.ttsIndex+'-0',
      //url: 'http://home.us.archive.org/~rkumar/arctic.ogg',
      url: 'http://'+this.server+'/BookReader/BookReaderGetTTS.php?string=' + escape(data[0][0]) + '&format=.'+this.ttsFormat, //the .ogg is to trick SoundManager2 to use the HTML5 audio player
-     whileloading: function(){if (this.bytesLoaded == this.bytesTotal) this.br.ttsRemovePopup();}, //onload never fires in FF...
-     onload: function(){this.br.ttsRemovePopup();} //whileloading never fires in safari...
+     whileloading: function(){if (this.bytesLoaded == this.bytesTotal) this.br.removeProgressPopup();}, //onload never fires in FF...
+     onload: function(){this.br.removeProgressPopup();} //whileloading never fires in safari...
     });    
     snd.br = this;
     snd.load();
@@ -4487,27 +4650,38 @@ BookReader.prototype.ttsStartCB = function (data) {
     this.ttsNextChunk();
 }
 
-// ttsShowPopup
+// showProgressPopup
 //______________________________________________________________________________
-BookReader.prototype.ttsShowPopup = function() {
-    if (soundManager.debugMode) console.log('ttsShowPopup index='+this.ttsIndex+' pos='+this.ttsPosition);
+BookReader.prototype.showProgressPopup = function(msg) {
+    if (soundManager.debugMode) console.log('showProgressPopup index='+this.ttsIndex+' pos='+this.ttsPosition);
+    if (this.popup) return;
     
     this.popup = document.createElement("div");
     $(this.popup).css({
-        top:      $('#BRtoolbar').height() + 'px',
-        left:     $('#BookReader').width()-220 + 'px',
-        width:    '220px',
-        height:   '20px',
-    }).attr('className', 'BRttsPopUp').appendTo('#BookReader');
+        top:      ($('#BookReader').height()*0.5-100) + 'px',
+        left:     ($('#BookReader').width()-300)*0.5 + 'px',
+        width:    '300px',
+        border:   '2px solid black'
+    }).attr('className', 'BRprogresspopup');
 
-    htmlStr =  '&nbsp;';
+    var bar = document.createElement("div");
+    $(bar).css({
+        height:   '20px'
+    }).attr('className', 'BRprogressbar');
+    $(this.popup).append(bar);
 
-    this.popup.innerHTML = htmlStr;
+    if (msg) {
+        var msgdiv = document.createElement("div");
+        msgdiv.innerHTML = msg;
+        $(this.popup).append(msgdiv);
+    }
+    
+    $(this.popup).appendTo('#BookReader');
 }
 
-// ttsRemovePopup
+// removeProgressPopup
 //______________________________________________________________________________
-BookReader.prototype.ttsRemovePopup = function() {
+BookReader.prototype.removeProgressPopup = function() {
     $(this.popup).remove(); 
     this.popup=null;
 }
@@ -4801,3 +4975,41 @@ BookReader.prototype.ttsStartPolling = function () {
         self.ttsNextChunk();
     },500);    
 }
+//FADING, ETC.
+    function changeArrow(){
+        setTimeout(function(){
+            $('#BRnavCntlBtm').removeClass('BRdn').addClass('BRup');
+        },3000);
+    };
+    $().ready(function(){
+        $('#BRtoolbar').animate({top:0},3000).animate({top:-40});
+        $('#BRnav').animate({bottom:0},3000).animate({bottom:-53});
+        changeArrow();
+        $('.BRnavCntl').animate({opacity:1},3000).animate({height:'43px'}).animate({opacity:1},1000).animate({opacity:.25},1000);
+        $('.BRnavCntl').click(
+            function(){
+                if ($('#BRnavCntlBtm').hasClass('BRdn')) {
+                    $('#BRtoolbar').animate({top:-40});
+                    $('#BRnav').animate({bottom:-53});
+                    $('#BRnavCntlBtm').addClass('BRup').removeClass('BRdn');
+                    $('.BRnavCntl').animate({height:'43px'}).animate({opacity:1},1000).animate({opacity:.25},1000);
+                } else {
+                    $('#BRtoolbar').animate({top:0});
+                    $('#BRnav').animate({bottom:0});
+                    $('#BRnavCntlBtm').addClass('BRdn').removeClass('BRup');
+                    $('.BRnavCntl').animate({opacity:1,height:'30px'});
+                };
+            }
+        );
+        $('#BRnavCntlBtm').mouseover(function(){
+            if ($(this).hasClass('BRup')) {
+                $('.BRnavCntl').animate({opacity:1},250);
+            };
+        });
+        $('#BRnavCntlBtm').mouseleave(function(){
+            if ($(this).hasClass('BRup')) {
+                $('.BRnavCntl').animate({opacity:.25},250);
+            };
+        });
+    });
+