X-Git-Url: http://git.rot13.org/?a=blobdiff_plain;f=BookReader%2FBookReader.js;h=e2e1de7eaedc065da8f5c41e8e83dedf3f7d0488;hb=2f18c355b86fad952ddbecc2c623823405cec310;hp=0c101a3e84bc00f6e8d21bd01d2691209954e3cc;hpb=dfb5a3ab7ef6be1a92ecbfd65935534980e6ff7d;p=bookreader.git diff --git a/BookReader/BookReader.js b/BookReader/BookReader.js index 0c101a3..e2e1de7 100644 --- a/BookReader/BookReader.js +++ b/BookReader/BookReader.js @@ -36,14 +36,28 @@ This file is part of BookReader. // You must also add a numLeafs property before calling init(). function BookReader() { + + // Mode constants + this.constMode1up = 1; + this.constMode2up = 2; + this.constModeThumb = 3; + this.reduce = 4; - this.padding = 10; - this.mode = 1; //1, 2, 3 - this.ui = 'full'; // UI mode - this.thumbWidth = 100; - this.thumbRowBuffer = 3; // number of rows to pre-cache out a view + this.padding = 10; // Padding in 1up + + this.mode = this.constMode1up; + this.ui = 'full'; // UI mode + this.uiAutoHide = true; // Controls whether nav/toolbar will autohide + + // thumbnail mode + this.thumbWidth = 100; // will be overridden during prepareThumbnailView + this.thumbRowBuffer = 2; // number of rows to pre-cache out a view + this.thumbColumns = 6; // default + this.thumbMaxLoading = 4; // number of thumbnails to load at once + this.thumbPadding = 10; // spacing between thumbnails this.displayedRows=[]; - this.displayedIndices = []; + + this.displayedIndices = []; //this.indicesToDisplay = []; this.imgs = {}; this.prefetchedImgs = {}; //an object with numeric keys cooresponding to page index @@ -67,30 +81,58 @@ function BookReader() { this.lastDisplayableIndex2up = null; // We link to index.php to avoid redirect which breaks back button + // Should be overriden (before init) by custom implmentations. this.logoURL = 'http://www.archive.org/index.php'; - // Base URL for images + // Base URL for UI images - should be overriden (before init) by + // custom implementations. + // $$$ This is the same directory as the images referenced by relative + // path in the CSS. Would be better to automagically find that path. this.imagesBaseURL = '/bookreader/images/'; - // Mode constants - this.constMode1up = 1; - this.constMode2up = 2; - this.constModeThumb = 3; // Zoom levels - this.reductionFactors = [0.5, 1, 2, 4, 8, 16]; - + // $$$ provide finer grained zooming + this.reductionFactors = [ {reduce: 0.5, autofit: null}, + {reduce: 1, autofit: null}, + {reduce: 2, autofit: null}, + {reduce: 4, autofit: null}, + {reduce: 8, autofit: null}, + {reduce: 16, autofit: null} ]; + + // Object to hold parameters related to 1up mode + this.onePage = { + autofit: 'height' // valid values are height, width, none + }; + // Object to hold parameters related to 2up mode this.twoPage = { - coverInternalPadding: 10, // Width of cover - coverExternalPadding: 10, // Padding outside of cover - bookSpineDivWidth: 30, // Width of book spine $$$ consider sizing based on book length - autofit: true + coverInternalPadding: 0, // Width of cover + coverExternalPadding: 0, // Padding outside of cover + bookSpineDivWidth: 0, // Width of book spine $$$ consider sizing based on book length + autofit: 'auto' + }; + + // This object/dictionary controls which optional features are enabled + // XXXmang in progress + this.features = { + // search + // read aloud + // open library entry + // table of contents + // embed/share ui + // info ui }; + + // Text-to-Speech params + this.ttsPlaying = false; + this.ttsIndex = null; //leaf index + this.ttsPosition = -1; //chunk (paragraph) number + this.ttsBuffering = false; + this.ttsPoller = null; + this.ttsFormat = null; - // Background color for pages (e.g. when loading page image) - // $$$ TODO dynamically calculate based on page images - this.pageDefaultBackgroundColor = 'rgb(234, 226, 205)'; + return this; }; // init() @@ -102,16 +144,25 @@ BookReader.prototype.init = function() { // Find start index and mode if set in location hash var params = this.paramsFromFragment(window.location.hash); + + // Sanitize/process parameters + + if ( !this.canSwitchToMode( this.mode ) ) { + this.mode = this.constMode1up; + } if ('undefined' != typeof(params.index)) { startIndex = params.index; } else if ('undefined' != typeof(params.page)) { startIndex = this.getPageIndex(params.page); } - + if ('undefined' == typeof(startIndex)) { if ('undefined' != typeof(this.titleLeaf)) { - startIndex = this.leafNumToIndex(this.titleLeaf); + // title leaf is known - but only use as default if book has a few pages + if (this.numLeafs > 2) { + startIndex = this.leafNumToIndex(this.titleLeaf); + } } } @@ -127,20 +178,28 @@ BookReader.prototype.init = function() { // search engine visibility document.title = this.shortTitle(50); - // Sanitize parameters - if ( !this.canSwitchToMode( this.mode ) ) { - this.mode = this.constMode1up; - } - $("#BookReader").empty(); + this.initToolbar(this.mode, this.ui); // Build inside of toolbar div + $("#BookReader").append("
"); $("#BRcontainer").append("
"); + + this.initNavbar(); + this.bindNavigationHandlers(); + + // Autohide nav after showing for awhile + var self = this; + if (this.uiAutoHide) { + $(window).bind('load', function() { + setTimeout(function() { self.hideNavigation(); }, 3000); + }); + }; $("#BRcontainer").bind('scroll', this, function(e) { e.data.loadLeafs(); }); - + this.setupKeyListeners(); this.startLocationPolling(); @@ -148,6 +207,9 @@ BookReader.prototype.init = function() { //console.log('resize!'); if (1 == e.data.mode) { //console.log('centering 1page view'); + if (e.data.autofit) { + e.data.resizePageView(); + } e.data.centerPageView(); $('#BRpageview').empty() e.data.displayedIndices = []; @@ -183,11 +245,14 @@ BookReader.prototype.init = function() { $('.BRpagediv1up').bind('mousedown', this, function(e) { // $$$ the purpose of this is to disable selection of the image (makes it turn blue) // but this also interferes with right-click. See https://bugs.edge.launchpad.net/gnubook/+bug/362626 + return false; }); + // $$$ refactor this so it's enough to set the first index and call preparePageView + // (get rid of mode-specific logic at this point) if (1 == this.mode) { - this.resizePageView(); this.firstIndex = startIndex; + this.prepareOnePageView(); this.jumpToIndex(startIndex); } else if (3 == this.mode) { this.firstIndex = startIndex; @@ -206,6 +271,11 @@ BookReader.prototype.init = function() { // Enact other parts of initial params this.updateFromParams(params); + + // Start AJAX request for OL data + if (this.getOpenLibraryRecord) { + this.getOpenLibraryRecord(this.gotOpenLibraryRecord); + } } BookReader.prototype.setupKeyListeners = function() { @@ -272,169 +342,35 @@ BookReader.prototype.setupKeyListeners = function() { BookReader.prototype.drawLeafs = function() { if (1 == this.mode) { this.drawLeafsOnePage(); - } else if(3 == this.mode) { + } else if (3 == this.mode) { this.drawLeafsThumbnail(); } else { this.drawLeafsTwoPage(); } -} - -// setDragHandler() -//______________________________________________________________________________ -BookReader.prototype.setDragHandler = function(div) { - div.dragging = false; - - $(div).unbind('mousedown').bind('mousedown', function(e) { - e.preventDefault(); - - //console.log('mousedown at ' + e.pageY); - - this.dragging = true; - this.prevMouseX = e.pageX; - this.prevMouseY = e.pageY; - - var startX = e.pageX; - var startY = e.pageY; - var startTop = $('#BRcontainer').attr('scrollTop'); - var startLeft = $('#BRcontainer').attr('scrollLeft'); - - }); - - $(div).unbind('mousemove').bind('mousemove', function(ee) { - ee.preventDefault(); - - // console.log('mousemove ' + ee.pageX + ',' + ee.pageY); - - var offsetX = ee.pageX - this.prevMouseX; - var offsetY = ee.pageY - this.prevMouseY; - - if (this.dragging) { - $('#BRcontainer').attr('scrollTop', $('#BRcontainer').attr('scrollTop') - offsetY); - $('#BRcontainer').attr('scrollLeft', $('#BRcontainer').attr('scrollLeft') - offsetX); - } - - this.prevMouseX = ee.pageX; - this.prevMouseY = ee.pageY; - - }); - - $(div).unbind('mouseup').bind('mouseup', function(ee) { - ee.preventDefault(); - //console.log('mouseup'); - - this.dragging = false; - }); - - $(div).unbind('mouseleave').bind('mouseleave', function(e) { - e.preventDefault(); - //console.log('mouseleave'); - - this.dragging = false; - }); - $(div).unbind('mouseenter').bind('mouseenter', function(e) { - e.preventDefault(); - //console.log('mouseenter'); - - this.dragging = false; - }); } -// setDragHandler2UP() +// bindGestures(jElement) //______________________________________________________________________________ -BookReader.prototype.setDragHandler2UP = function(div) { - div.dragging = false; - - $(div).unbind('mousedown').bind('mousedown', function(e) { - e.preventDefault(); - - //console.log('mousedown at ' + e.pageY); - - this.dragStart = {x: e.pageX, y: e.pageY }; - this.mouseDown = true; - this.dragging = false; // wait until drag distance - this.prevMouseX = e.pageX; - this.prevMouseY = e.pageY; - - var startX = e.pageX; - var startY = e.pageY; - var startTop = $('#BRcontainer').attr('scrollTop'); - var startLeft = $('#BRcontainer').attr('scrollLeft'); - - }); - - $(div).unbind('mousemove').bind('mousemove', function(ee) { - ee.preventDefault(); +BookReader.prototype.bindGestures = function(jElement) { - // console.log('mousemove ' + ee.pageX + ',' + ee.pageY); - - var offsetX = ee.pageX - this.prevMouseX; - var offsetY = ee.pageY - this.prevMouseY; - - var minDragDistance = 5; // $$$ constant - - var distance = Math.max(Math.abs(offsetX), Math.abs(offsetY)); - - if (this.mouseDown && (distance > minDragDistance)) { - //console.log('drag start!'); - - this.dragging = true; - } - - if (this.dragging) { - $('#BRcontainer').attr('scrollTop', $('#BRcontainer').attr('scrollTop') - offsetY); - $('#BRcontainer').attr('scrollLeft', $('#BRcontainer').attr('scrollLeft') - offsetX); - this.prevMouseX = ee.pageX; - this.prevMouseY = ee.pageY; - } - - - }); - - /* - $(div).unbind('mouseup').bind('mouseup', function(ee) { - ee.preventDefault(); - //console.log('mouseup'); - - this.dragging = false; - this.mouseDown = false; - }); - */ - - - $(div).unbind('mouseleave').bind('mouseleave', function(e) { + jElement.unbind('gesturechange').bind('gesturechange', function(e) { e.preventDefault(); - //console.log('mouseleave'); - - this.dragging = false; - this.mouseDown = false; + if (e.originalEvent.scale > 1.5) { + br.zoom(1); + } else if (e.originalEvent.scale < 0.6) { + br.zoom(-1); + } }); - - $(div).unbind('mouseenter').bind('mouseenter', function(e) { - e.preventDefault(); - //console.log('mouseenter'); - this.dragging = false; - this.mouseDown = false; - }); } BookReader.prototype.setClickHandler2UP = function( element, data, handler) { //console.log('setting handler'); //console.log(element.tagName); - $(element).unbind('click').bind('click', data, function(e) { - e.preventDefault(); - - //console.log('click!'); - - if (this.mouseDown && (!this.dragging)) { - //console.log('click not dragging!'); - handler(e); - } - - this.dragging = false; - this.mouseDown = false; + $(element).unbind('tap').bind('tap', data, function(e) { + handler(e); }); } @@ -470,7 +406,7 @@ BookReader.prototype.drawLeafsOnePage = function() { leafTop += height +10; leafBottom += 10; } - + var firstIndexToDraw = indicesToDisplay[0]; this.firstIndex = firstIndexToDraw; @@ -504,7 +440,7 @@ BookReader.prototype.drawLeafsOnePage = function() { var index = indicesToDisplay[i]; var height = parseInt(this._getPageHeight(index)/this.reduce); - if(-1 == jQuery.inArray(indicesToDisplay[i], this.displayedIndices)) { + if (BookReader.util.notInArray(indicesToDisplay[i], this.displayedIndices)) { var width = parseInt(this._getPageWidth(index)/this.reduce); //console.log("displaying leaf " + indicesToDisplay[i] + ' leafTop=' +leafTop); var div = document.createElement("div"); @@ -519,8 +455,6 @@ BookReader.prototype.drawLeafsOnePage = function() { $(div).css('height', height+'px'); //$(div).text('loading...'); - this.setDragHandler(div); - $('#BRpageview').append(div); var img = document.createElement("img"); @@ -538,7 +472,7 @@ BookReader.prototype.drawLeafsOnePage = function() { } for (i=0; i= scrollTop) && (leafTop <= scrollBottom); var bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom); var middleInView = (leafTop <=scrollTop) && (leafBottom>=scrollBottom); if (topInView | bottomInView | middleInView) { //console.log('row to display: ' + j); rowsToDisplay.push(i); + if (leafMap[i].leafs[0].num < leastVisible) { + leastVisible = leafMap[i].leafs[0].num; + } + if (leafMap[i].leafs[leafMap[i].leafs.length - 1].num > mostVisible) { + mostVisible = leafMap[i].leafs[leafMap[i].leafs.length - 1].num; + } } - if(leafTop > leafMap[i].top) { leafMap[i].top = leafTop; } + if (leafTop > leafMap[i].top) { leafMap[i].top = leafTop; } leafTop = leafBottom; } @@ -641,12 +606,7 @@ BookReader.prototype.drawLeafsThumbnail = function() { if (firstRow-i >= 0) { rowsToDisplay.push(firstRow-i); } } - // Update hash, but only if we're currently displaying a leaf - // Hack that fixes #365790 - if (this.displayedRows.length > 0) { - this.updateLocationHash(); - } - + // Create the thumbnail divs and images (lazy loaded) var j; var row; var left; @@ -656,7 +616,7 @@ BookReader.prototype.drawLeafsThumbnail = function() { var img; var page; for (i=0; i0) { - div = $('.BRpagedivthumb_highlight') - div.attr({className: 'BRpagedivthumb' }); - } - // highlight current page - $('#pagediv'+this.currentIndex()).attr({className: 'BRpagedivthumb_highlight' }); - + + // Remove thumbnails that are not to be displayed var k; for (i=0; i mostVisible) { + this.setCurrentIndex(mostVisible); + } this.displayedRows = rowsToDisplay.slice(); + + // Update hash, but only if we're currently displaying a leaf + // Hack that fixes #365790 + if (this.displayedRows.length > 0) { + this.updateLocationHash(); + } + + // remove previous highlights + $('.BRpagedivthumb_highlight').removeClass('BRpagedivthumb_highlight'); + + // highlight current page + $('#pagediv'+this.currentIndex()).addClass('BRpagedivthumb_highlight'); + + this.lazyLoadThumbnails(); + // Update page number box. $$$ refactor to function if (null !== this.getPageNum(this.currentIndex())) { $("#BRpagenum").val(this.getPageNum(this.currentIndex())); } else { @@ -735,6 +732,63 @@ BookReader.prototype.drawLeafsThumbnail = function() { this.updateToolbarZoom(this.reduce); } +BookReader.prototype.lazyLoadThumbnails = function() { + + // console.log('lazy load'); + + // We check the complete property since load may not be fired if loading from the cache + $('.BRlazyloading').filter('[complete=true]').removeClass('BRlazyloading'); + + var loading = $('.BRlazyloading').length; + var toLoad = this.thumbMaxLoading - loading; + + // console.log(' ' + loading + ' thumbnails loading'); + // console.log(' this.thumbMaxLoading ' + this.thumbMaxLoading); + + var self = this; + + if (toLoad > 0) { + // $$$ TODO load those near top (but not beyond) page view first + $('#BRpageview img.BRlazyload').filter(':lt(' + toLoad + ')').each( function() { + self.lazyLoadImage(this); + }); + } +} + +BookReader.prototype.lazyLoadImage = function (dummyImage) { + //console.log(' lazy load started for ' + $(dummyImage).data('srcURL').match('([0-9]{4}).jp2')[1] ); + + var img = new Image(); + var self = this; + + $(img) + .addClass('BRlazyloading') + .one('load', function() { + //if (console) { console.log(' onload ' + $(this).attr('src').match('([0-9]{4}).jp2')[1]); }; + + $(this).removeClass('BRlazyloading'); + + // $$$ Calling lazyLoadThumbnails here was causing stack overflow on IE so + // we call the function after a slight delay. Also the img.complete property + // is not yet set in IE8 inside this onload handler + setTimeout(function() { self.lazyLoadThumbnails(); }, 100); + }) + .one('error', function() { + // Remove class so we no longer count as loading + $(this).removeClass('BRlazyloading'); + }) + .attr( { width: $(dummyImage).width(), + height: $(dummyImage).height(), + src: $(dummyImage).data('srcURL') + }); + + // replace with the new img + $(dummyImage).before(img).remove(); + + img = null; // tidy up closure +} + + // drawLeafsTwoPage() //______________________________________________________________________________ BookReader.prototype.drawLeafsTwoPage = function() { @@ -767,10 +821,8 @@ BookReader.prototype.drawLeafsTwoPage = function() { left: this.twoPage.gutter-this.twoPage.scaledWL+'px', right: '', top: top+'px', - backgroundColor: this.getPageBackgroundColor(indexL), height: this.twoPage.height +'px', // $$$ height forced the same for both pages width: this.twoPage.scaledWL + 'px', - borderRight: '1px solid black', zIndex: 2 }).appendTo('#BRtwopageview'); @@ -787,10 +839,8 @@ BookReader.prototype.drawLeafsTwoPage = function() { left: this.twoPage.gutter+'px', right: '', top: top+'px', - backgroundColor: this.getPageBackgroundColor(indexR), height: this.twoPage.height + 'px', // $$$ height forced the same for both pages width: this.twoPage.scaledWR + 'px', - borderLeft: '1px solid black', zIndex: 2 }).appendTo('#BRtwopageview'); @@ -838,31 +888,47 @@ BookReader.prototype.loadLeafs = function() { BookReader.prototype.zoom = function(direction) { switch (this.mode) { case this.constMode1up: - return this.zoom1up(direction); + if (direction == 1) { + // XXX other cases + return this.zoom1up('in'); + } else { + return this.zoom1up('out'); + } + case this.constMode2up: - return this.zoom2up(direction); + if (direction == 1) { + // XXX other cases + return this.zoom2up('in'); + } else { + return this.zoom2up('out'); + } + + case this.constModeThumb: + // XXX update zoomThumb for named directions + return this.zoomThumb(direction); + } } // zoom1up(dir) //______________________________________________________________________________ -BookReader.prototype.zoom1up = function(dir) { +BookReader.prototype.zoom1up = function(direction) { if (2 == this.mode) { //can only zoom in 1-page mode this.switchMode(1); return; } - // $$$ with flexible zoom we could "snap" to /2 page reductions - // for better scaling - if (1 == dir) { - if (this.reduce <= 0.5) return; - this.reduce*=0.5; //zoom in - } else { - if (this.reduce >= 8) return; - this.reduce*=2; //zoom out + var reduceFactor = this.nextReduce(this.reduce, direction, this.onePage.reductionFactors); + + if (this.reduce == reduceFactor.reduce) { + // Already at this level + return; } - + + this.reduce = reduceFactor.reduce; // $$$ incorporate into function + this.onePage.autofit = reduceFactor.autofit; + this.pageScale = this.reduce; // preserve current reduce this.resizePageView(); @@ -881,6 +947,25 @@ BookReader.prototype.zoom1up = function(dir) { // resizePageView() //______________________________________________________________________________ BookReader.prototype.resizePageView = function() { + + // $$$ This code assumes 1up mode + // e.g. does not preserve position in thumbnail mode + // See http://bugs.launchpad.net/bookreader/+bug/552972 + + switch (this.mode) { + case this.constMode1up: + case this.constMode2up: + this.resizePageView1up(); + break; + case this.constModeThumb: + this.prepareThumbnailView( this.currentIndex() ); + break; + default: + alert('Resize not implemented for this mode'); + } +} + +BookReader.prototype.resizePageView1up = function() { var i; var viewHeight = 0; //var viewWidth = $('#BRcontainer').width(); //includes scrollBar @@ -900,13 +985,22 @@ BookReader.prototype.resizePageView = function() { var scrollRatio = 0; } + // Recalculate 1up reduction factors + this.onePageCalculateReductionFactors( $('#BRcontainer').attr('clientWidth'), + $('#BRcontainer').attr('clientHeight') ); + // Update current reduce (if in autofit) + if (this.onePage.autofit) { + var reductionFactor = this.nextReduce(this.reduce, this.onePage.autofit, this.onePage.reductionFactors); + this.reduce = reductionFactor.reduce; + } + for (i=0; iviewWidth) viewWidth=width; } $('#BRpageview').height(viewHeight); - $('#BRpageview').width(viewWidth); + $('#BRpageview').width(viewWidth); var newCenterY = scrollRatio*viewHeight; var newTop = Math.max(0, Math.floor( newCenterY - $('#BRcontainer').height()/2 )); @@ -921,12 +1015,12 @@ BookReader.prototype.resizePageView = function() { //this.centerPageView(); this.loadLeafs(); - - // Not really needed until there is 1up autofit + this.removeSearchHilites(); this.updateSearchHilites(); } + // centerX1up() //______________________________________________________________________________ // Returns the current offset of the viewport center in scaled document coordinates. @@ -969,13 +1063,17 @@ BookReader.prototype.zoom2up = function(direction) { // Hard stop autoplay this.stopFlipAnimations(); - // Get new zoom state - var newZoom = this.twoPageNextReduce(this.reduce, direction); - if ((this.reduce == newZoom.reduce) && (this.twoPage.autofit == newZoom.autofit)) { + // Recalculate autofit factors + this.twoPageCalculateReductionFactors(); + + // Get new zoom state + var reductionFactor = this.nextReduce(this.reduce, direction, this.twoPage.reductionFactors); + if ((this.reduce == reductionFactor.reduce) && (this.twoPage.autofit == reductionFactor.autofit)) { + // Same zoom return; } - this.twoPage.autofit = newZoom.autofit; - this.reduce = newZoom.reduce; + this.twoPage.autofit = reductionFactor.autofit; + this.reduce = reductionFactor.reduce; this.pageScale = this.reduce; // preserve current reduce // Preserve view center position @@ -993,74 +1091,97 @@ BookReader.prototype.zoom2up = function(direction) { this.prepareTwoPageView(oldCenter.percentageX, oldCenter.percentageY); } +BookReader.prototype.zoomThumb = function(direction) { + var oldColumns = this.thumbColumns; + switch (direction) { + case -1: + this.thumbColumns += 1; + break; + case 1: + this.thumbColumns -= 1; + break; + } + + // clamp + if (this.thumbColumns < 2) { + this.thumbColumns = 2; + } else if (this.thumbColumns > 8) { + this.thumbColumns = 8; + } + + if (this.thumbColumns != oldColumns) { + this.prepareThumbnailView(); + } +} + +// Returns the width per thumbnail to display the requested number of columns +// Note: #BRpageview must already exist since its width is used to calculate the +// thumbnail width +BookReader.prototype.getThumbnailWidth = function(thumbnailColumns) { + var padding = (thumbnailColumns + 1) * this.thumbPadding; + var width = ($('#BRpageview').width() - padding) / (thumbnailColumns + 0.5); // extra 0.5 is for some space at sides + return parseInt(width); +} // quantizeReduce(reduce) //______________________________________________________________________________ // Quantizes the given reduction factor to closest power of two from set from 12.5% to 200% -BookReader.prototype.quantizeReduce = function(reduce) { - var quantized = this.reductionFactors[0]; +BookReader.prototype.quantizeReduce = function(reduce, reductionFactors) { + var quantized = reductionFactors[0].reduce; var distance = Math.abs(reduce - quantized); - for (var i = 1; i < this.reductionFactors.length; i++) { - newDistance = Math.abs(reduce - this.reductionFactors[i]); + for (var i = 1; i < reductionFactors.length; i++) { + newDistance = Math.abs(reduce - reductionFactors[i].reduce); if (newDistance < distance) { distance = newDistance; - quantized = this.reductionFactors[i]; + quantized = reductionFactors[i].reduce; } } return quantized; } -// twoPageNextReduce() -//______________________________________________________________________________ -// Returns the next reduction level -BookReader.prototype.twoPageNextReduce = function(reduce, direction) { - var result = {}; - var autofitReduce = this.twoPageGetAutofitReduce(); +// reductionFactors should be array of sorted reduction factors +// e.g. [ {reduce: 0.25, autofit: null}, {reduce: 0.3, autofit: 'width'}, {reduce: 1, autofit: null} ] +BookReader.prototype.nextReduce = function( currentReduce, direction, reductionFactors ) { - if (0 == direction) { // autofit - result.autofit = true; - result.reduce = autofitReduce; - - } else if (1 == direction) { // zoom in - var newReduce = this.reductionFactors[0]; + // XXX add 'closest', to replace quantize function - for (var i = 1; i < this.reductionFactors.length; i++) { - if (this.reductionFactors[i] < reduce) { - newReduce = this.reductionFactors[i]; + if (direction == 'in') { + var newReduceIndex = 0; + + for (var i = 1; i < reductionFactors.length; i++) { + if (reductionFactors[i].reduce < currentReduce) { + newReduceIndex = i; } } + return reductionFactors[newReduceIndex]; - if (!this.twoPage.autofit && (autofitReduce < reduce && autofitReduce > newReduce)) { - // use autofit - result.autofit = true; - result.reduce = autofitReduce; - } else { - result.autofit = false; - result.reduce = newReduce; - } - - } else { // zoom out - var lastIndex = this.reductionFactors.length - 1; - var newReduce = this.reductionFactors[lastIndex]; + } else if (direction == 'out') { // zoom out + var lastIndex = reductionFactors.length - 1; + var newReduceIndex = lastIndex; for (var i = lastIndex; i >= 0; i--) { - if (this.reductionFactors[i] > reduce) { - newReduce = this.reductionFactors[i]; + if (reductionFactors[i].reduce > currentReduce) { + newReduceIndex = i; } } - - if (!this.twoPage.autofit && (autofitReduce > reduce && autofitReduce < newReduce)) { - // use autofit - result.autofit = true; - result.reduce = autofitReduce; - } else { - result.autofit = false; - result.reduce = newReduce; + return reductionFactors[newReduceIndex]; + } + + // Asked for specific autofit mode + for (var i = 0; i < reductionFactors.length; i++) { + if (reductionFactors[i].autofit == direction) { + return reductionFactors[i]; } } - return result; + alert('Could not find reduction factor for direction ' + direction); + return reductionFactors[0]; + +} + +BookReader.prototype._reduceSort = function(a, b) { + return a.reduce - b.reduce; } // jumpToPage() @@ -1086,7 +1207,7 @@ BookReader.prototype.jumpToPage = function(pageNum) { //______________________________________________________________________________ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) { - if (2 == this.mode) { + if (this.constMode2up == this.mode) { this.autoStop(); // By checking against min/max we do nothing if requested index @@ -1097,7 +1218,7 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) { this.flipFwdToIndex(index); } - } else if (3 == this.mode){ + } else if (this.constModeThumb == this.mode) { var viewWidth = $('#BRcontainer').attr('scrollWidth') - 20; // width minus buffer var i; var leafWidth = 0; @@ -1110,16 +1231,17 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) { for (i=0; i<(index+1); i++) { leafWidth = this.thumbWidth; - if (rightPos + (leafWidth + this.padding) > viewWidth){ + if (rightPos + (leafWidth + this.thumbPadding) > viewWidth){ rightPos = 0; rowHeight = 0; leafIndex = 0; } - leafHeight = parseInt((this.getPageHeight(leaf)*this.thumbWidth)/this.getPageWidth(leaf), 10); - if(leafHeight > rowHeight) { rowHeight = leafHeight; } + + leafHeight = parseInt((this.getPageHeight(leafIndex)*this.thumbWidth)/this.getPageWidth(leafIndex), 10); + if (leafHeight > rowHeight) { rowHeight = leafHeight; } if (leafIndex==0) { leafTop = bottomPos; } - if (leafIndex==0) { bottomPos += this.padding + rowHeight; } - rightPos += leafWidth + this.padding; + if (leafIndex==0) { bottomPos += this.thumbPadding + rowHeight; } + rightPos += leafWidth + this.thumbPadding; leafIndex++; } this.firstIndex=index; @@ -1129,6 +1251,7 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) { $('#BRcontainer').animate({scrollTop: leafTop },'fast'); } } else { + // 1up var i; var leafTop = 0; var leafLeft = 0; @@ -1144,12 +1267,18 @@ BookReader.prototype.jumpToIndex = function(index, pageX, pageY) { offset -= $('#BRcontainer').attr('clientHeight') >> 1; //console.log( 'jumping to ' + leafTop + ' ' + offset); leafTop += offset; + } else { + // Show page just a little below the top + leafTop -= this.padding / 2; } if (pageX) { var offset = parseInt( (pageX) / this.reduce); offset -= $('#BRcontainer').attr('clientWidth') >> 1; leafLeft += offset; + } else { + // Preserve left position + leafLeft = $('#BRcontainer').scrollLeft(); } //$('#BRcontainer').attr('scrollTop', leafTop); @@ -1164,34 +1293,46 @@ BookReader.prototype.switchMode = function(mode) { //console.log(' asked to switch to mode ' + mode + ' from ' + this.mode); - if (mode == this.mode) return; + if (mode == this.mode) { + return; + } if (!this.canSwitchToMode(mode)) { return; } this.autoStop(); + this.ttsStop(); this.removeSearchHilites(); this.mode = mode; - this.switchToolbarMode(mode); + //this.switchToolbarMode(mode); // reinstate scale if moving from thumbnail view - if (this.pageScale != this.reduce) this.reduce = this.pageScale; + if (this.pageScale != this.reduce) { + this.reduce = this.pageScale; + } // $$$ TODO preserve center of view when switching between mode // See https://bugs.edge.launchpad.net/gnubook/+bug/416682 + // XXX maybe better to preserve zoom in each mode if (1 == mode) { - this.reduce = this.quantizeReduce(this.reduce); + this.onePageCalculateReductionFactors( $('#BRcontainer').attr('clientWidth'), $('#BRcontainer').attr('clientHeight')); + this.reduce = this.quantizeReduce(this.reduce, this.onePage.reductionFactors); this.prepareOnePageView(); } else if (3 == mode) { - this.reduce = this.quantizeReduce(this.reduce); + $('button.thumb').hide(); + $('button.twopg').show(); + this.reduce = this.quantizeReduce(this.reduce, this.reductionFactors); this.prepareThumbnailView(); - this.jumpToIndex(this.currentIndex()); } else { - this.twoPage.autofit = false; // Take zoom level from other mode - this.reduce = this.quantizeReduce(this.reduce); + // $$$ why don't we save autofit? + // 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 } @@ -1204,63 +1345,58 @@ BookReader.prototype.prepareOnePageView = function() { // var startLeaf = this.displayedIndices[0]; var startLeaf = this.currentIndex(); - + $('#BRcontainer').empty(); $('#BRcontainer').css({ overflowY: 'scroll', overflowX: 'auto' }); + + $("#BRcontainer").append("
"); + + // Attaches to first child - child must be present + $('#BRcontainer').dragscrollable(); + this.bindGestures($('#BRcontainer')); + + // $$$ keep select enabled for now since disabling it breaks keyboard + // nav in FF 3.6 (https://bugs.edge.launchpad.net/bookreader/+bug/544666) + // BookReader.util.disableSelect($('#BRpageview')); - var brPageView = $("#BRcontainer").append("
"); - - this.resizePageView(); + this.resizePageView(); this.jumpToIndex(startLeaf); this.displayedIndices = []; this.drawLeafsOnePage(); - - // Bind mouse handlers - // Disable mouse click to avoid selected/highlighted page images - bug 354239 - brPageView.bind('mousedown', function(e) { - // $$$ check here for right-click and don't disable. Also use jQuery style - // for stopping propagation. See https://bugs.edge.launchpad.net/gnubook/+bug/362626 - return false; - }) - // Special hack for IE7 - brPageView[0].onselectstart = function(e) { return false; }; } //prepareThumbnailView() //______________________________________________________________________________ BookReader.prototype.prepareThumbnailView = function() { - - // var startLeaf = this.displayedIndices[0]; - var startLeaf = this.currentIndex(); - this.reduce = this.getPageWidth(0)/this.thumbWidth; $('#BRcontainer').empty(); $('#BRcontainer').css({ overflowY: 'scroll', overflowX: 'auto' }); + + $("#BRcontainer").append("
"); - var brPageView = $("#BRcontainer").append("
"); + $('#BRcontainer').dragscrollable(); + this.bindGestures($('#BRcontainer')); + + // $$$ keep select enabled for now since disabling it breaks keyboard + // nav in FF 3.6 (https://bugs.edge.launchpad.net/bookreader/+bug/544666) + // BookReader.util.disableSelect($('#BRpageview')); - this.resizePageView(); + this.thumbWidth = this.getThumbnailWidth(this.thumbColumns); + this.reduce = this.getPageWidth(0)/this.thumbWidth; + + this.displayedRows = []; + + // Draw leafs with current index directly in view (no animating to the index) + this.drawLeafsThumbnail( this.currentIndex() ); - this.displayedRows = []; - this.drawLeafsThumbnail(); - - // Bind mouse handlers - // Disable mouse click to avoid selected/highlighted page images - bug 354239 - brPageView.bind('mousedown', function(e) { - // $$$ check here for right-click and don't disable. Also use jQuery style - // for stopping propagation. See https://bugs.edge.launchpad.net/gnubook/+bug/362626 - return false; - }) - // Special hack for IE7 - brPageView[0].onselectstart = function(e) { return false; }; } // prepareTwoPageView() @@ -1313,6 +1449,10 @@ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPerc // Add the two page view // $$$ Can we get everything set up and then append? $('#BRcontainer').append('
'); + + // Attaches to first child, so must come after we add the page view + $('#BRcontainer').dragscrollable(); + this.bindGestures($('#BRcontainer')); // $$$ calculate first then set $('#BRtwopageview').css( { @@ -1334,56 +1474,33 @@ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPerc this.twoPage.coverDiv = document.createElement('div'); $(this.twoPage.coverDiv).attr('id', 'BRbookcover').css({ - border: '1px solid rgb(68, 25, 17)', width: this.twoPage.bookCoverDivWidth + 'px', height: this.twoPage.bookCoverDivHeight+'px', visibility: 'visible', - position: 'absolute', - backgroundColor: '#663929', - left: this.twoPage.bookCoverDivLeft + 'px', - top: this.twoPage.bookCoverDivTop+'px', - MozBorderRadiusTopleft: '7px', - MozBorderRadiusTopright: '7px', - MozBorderRadiusBottomright: '7px', - MozBorderRadiusBottomleft: '7px' }).appendTo('#BRtwopageview'); this.leafEdgeR = document.createElement('div'); - this.leafEdgeR.className = 'leafEdgeR'; // $$$ the static CSS should be moved into the .css file + this.leafEdgeR.className = 'BRleafEdgeR'; $(this.leafEdgeR).css({ - borderStyle: 'solid solid solid none', - borderColor: 'rgb(51, 51, 34)', - borderWidth: '1px 1px 1px 0px', - background: 'transparent url(' + this.imagesBaseURL + 'right_edges.png) repeat scroll 0% 0%', width: this.twoPage.leafEdgeWidthR + 'px', - height: this.twoPage.height-1 + 'px', - /*right: '10px',*/ + height: this.twoPage.height + 'px', left: this.twoPage.gutter+this.twoPage.scaledWR+'px', - top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px', - position: 'absolute' + top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px' }).appendTo('#BRtwopageview'); this.leafEdgeL = document.createElement('div'); - this.leafEdgeL.className = 'leafEdgeL'; - $(this.leafEdgeL).css({ // $$$ static CSS should be moved to file - borderStyle: 'solid none solid solid', - borderColor: 'rgb(51, 51, 34)', - borderWidth: '1px 0px 1px 1px', - background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%', + this.leafEdgeL.className = 'BRleafEdgeL'; + $(this.leafEdgeL).css({ width: this.twoPage.leafEdgeWidthL + 'px', - height: this.twoPage.height-1 + 'px', + height: this.twoPage.height + 'px', left: this.twoPage.bookCoverDivLeft+this.twoPage.coverInternalPadding+'px', - top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px', - position: 'absolute' + top: this.twoPage.bookCoverDivTop+this.twoPage.coverInternalPadding+'px' }).appendTo('#BRtwopageview'); div = document.createElement('div'); $(div).attr('id', 'BRbookspine').css({ - border: '1px solid rgb(68, 25, 17)', width: this.twoPage.bookSpineDivWidth+'px', height: this.twoPage.bookSpineDivHeight+'px', - position: 'absolute', - backgroundColor: 'rgb(68, 25, 17)', left: this.twoPage.bookSpineDivLeft+'px', top: this.twoPage.bookSpineDivTop+'px' }).appendTo('#BRtwopageview'); @@ -1402,7 +1519,7 @@ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPerc top: this.twoPageFlipAreaTop() + 'px', cursor: 'w-resize', zIndex: 100 - }).bind('click', function(e) { + }).click(function(e) { self.left(); }).bind('mousedown', function(e) { e.preventDefault(); @@ -1419,7 +1536,7 @@ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPerc top: this.twoPageFlipAreaTop() + 'px', cursor: 'e-resize', zIndex: 100 - }).bind('click', function(e) { + }).click(function(e) { self.right(); }).bind('mousedown', function(e) { e.preventDefault(); @@ -1452,15 +1569,9 @@ BookReader.prototype.prepareTwoPageView = function(centerPercentageX, centerPerc BookReader.prototype.prepareTwoPagePopUp = function() { this.twoPagePopUp = document.createElement('div'); + this.twoPagePopUp.className = 'BRtwoPagePopUp'; $(this.twoPagePopUp).css({ - border: '1px solid black', - padding: '2px 6px', - position: 'absolute', - fontFamily: 'sans-serif', - fontSize: '14px', - zIndex: '1000', - backgroundColor: 'rgb(255, 255, 238)', - opacity: 0.85 + zIndex: '1000' }).appendTo('#BRcontainer'); $(this.twoPagePopUp).hide(); @@ -1474,12 +1585,14 @@ BookReader.prototype.prepareTwoPagePopUp = function() { $(this.leafEdgeL).bind('click', this, function(e) { e.data.autoStop(); + e.data.ttsStop(); var jumpIndex = e.data.jumpIndexForLeftEdgePageX(e.pageX); e.data.jumpToIndex(jumpIndex); }); $(this.leafEdgeR).bind('click', this, function(e) { e.data.autoStop(); + e.data.ttsStop(); var jumpIndex = e.data.jumpIndexForRightEdgePageX(e.pageX); e.data.jumpToIndex(jumpIndex); }); @@ -1492,7 +1605,7 @@ BookReader.prototype.prepareTwoPagePopUp = function() { // $$$ TODO: Make sure popup is positioned so that it is in view // (https://bugs.edge.launchpad.net/gnubook/+bug/327456) $(e.data.twoPagePopUp).css({ - left: e.pageX- $('#BRcontainer').offset().left + $('#BRcontainer').scrollLeft() + 20 + 'px', + left: e.pageX- $('#BRcontainer').offset().left + $('#BRcontainer').scrollLeft() - 100 + 'px', top: e.pageY - $('#BRcontainer').offset().top + $('#BRcontainer').scrollTop() + 'px' }); }); @@ -1505,7 +1618,7 @@ BookReader.prototype.prepareTwoPagePopUp = function() { // $$$ TODO: Make sure popup is positioned so that it is in view // (https://bugs.edge.launchpad.net/gnubook/+bug/327456) $(e.data.twoPagePopUp).css({ - left: e.pageX - $('#BRcontainer').offset().left + $('#BRcontainer').scrollLeft() - $(e.data.twoPagePopUp).width() - 25 + 'px', + left: e.pageX - $('#BRcontainer').offset().left + $('#BRcontainer').scrollLeft() - $(e.data.twoPagePopUp).width() + 100 + 'px', top: e.pageY-$('#BRcontainer').offset().top + $('#BRcontainer').scrollTop() + 'px' }); }); @@ -1549,7 +1662,7 @@ BookReader.prototype.calculateSpreadSize = function() { // Book cover // The width of the book cover div. The combined width of both pages, twice the width // of the book cover internal padding (2*10) and the page edges - this.twoPage.bookCoverDivWidth = this.twoPage.scaledWL + this.twoPage.scaledWR + 2 * this.twoPage.coverInternalPadding + this.twoPage.edgeWidth; + this.twoPage.bookCoverDivWidth = this.twoPageCoverWidth(this.twoPage.scaledWL + this.twoPage.scaledWR); // The height of the book cover div this.twoPage.bookCoverDivHeight = this.twoPage.height + 2 * this.twoPage.coverInternalPadding; @@ -1672,6 +1785,55 @@ BookReader.prototype.twoPageGetAutofitReduce = function() { return spreadSize.reduce; } +BookReader.prototype.onePageGetAutofitWidth = function() { + var widthPadding = 20; + return (this.getMedianPageSize().width + 0.0) / ($('#BRcontainer').attr('clientWidth') - widthPadding * 2); +} + +BookReader.prototype.onePageGetAutofitHeight = function() { + return (this.getMedianPageSize().height + 0.0) / ($('#BRcontainer').attr('clientHeight') - this.padding * 2); // make sure a little of adjacent pages show +} + +BookReader.prototype.getMedianPageSize = function() { + if (this._medianPageSize) { + return this._medianPageSize; + } + + // A little expensive but we just do it once + var widths = []; + var heights = []; + for (var i = 0; i < this.numLeafs; i++) { + widths.push(this.getPageWidth(i)); + heights.push(this.getPageHeight(i)); + } + + widths.sort(); + heights.sort(); + + this._medianPageSize = { width: widths[parseInt(widths.length / 2)], height: heights[parseInt(heights.length / 2)] }; + return this._medianPageSize; +} + +// Update the reduction factors for 1up mode given the available width and height. Recalculates +// the autofit reduction factors. +BookReader.prototype.onePageCalculateReductionFactors = function( width, height ) { + this.onePage.reductionFactors = this.reductionFactors.concat( + [ + { reduce: this.onePageGetAutofitWidth(), autofit: 'width' }, + { reduce: this.onePageGetAutofitHeight(), autofit: 'height'} + ]); + this.onePage.reductionFactors.sort(this._reduceSort); +} + +BookReader.prototype.twoPageCalculateReductionFactors = function() { + this.twoPage.reductionFactors = this.reductionFactors.concat( + [ + { reduce: this.getIdealSpreadSize( this.twoPage.currentIndexL, this.twoPage.currentIndexR ).reduce, + autofit: 'auto' } + ]); + this.twoPage.reductionFactors.sort(this._reduceSort); +} + // twoPageSetCursor() //______________________________________________________________________________ // Set the cursor for two page view @@ -1702,6 +1864,15 @@ BookReader.prototype.currentIndex = function() { } } +// setCurrentIndex(index) +//______________________________________________________________________________ +// Sets the idea of current index without triggering other actions such as animation. +// Compare to jumpToIndex which animates to that index +BookReader.prototype.setCurrentIndex = function(index) { + this.firstIndex = index; +} + + // right() //______________________________________________________________________________ // Flip the right page over onto the left @@ -1784,6 +1955,59 @@ BookReader.prototype.last = function() { this.jumpToIndex(this.lastDisplayableIndex()); } +// scrollDown() +//______________________________________________________________________________ +// Scrolls down one screen view +BookReader.prototype.scrollDown = function() { + if ($.inArray(this.mode, [this.constMode1up, this.constModeThumb]) >= 0) { + if ( this.mode == this.constMode1up && (this.reduce >= this.onePageGetAutofitHeight()) ) { + // Whole pages are visible, scroll whole page only + return this.next(); + } + + $('#BRcontainer').animate( + { scrollTop: '+=' + this._scrollAmount() + 'px'}, + 400, 'easeInOutExpo' + ); + return true; + } else { + return false; + } +} + +// scrollUp() +//______________________________________________________________________________ +// Scrolls up one screen view +BookReader.prototype.scrollUp = function() { + if ($.inArray(this.mode, [this.constMode1up, this.constModeThumb]) >= 0) { + if ( this.mode == this.constMode1up && (this.reduce >= this.onePageGetAutofitHeight()) ) { + // Whole pages are visible, scroll whole page only + return this.prev(); + } + + $('#BRcontainer').animate( + { scrollTop: '-=' + this._scrollAmount() + 'px'}, + 400, 'easeInOutExpo' + ); + return true; + } else { + return false; + } +} + +// _scrollAmount() +//______________________________________________________________________________ +// The amount to scroll vertically in integer pixels +BookReader.prototype._scrollAmount = function() { + if (this.constMode1up == this.mode) { + // Overlap by % of page size + return parseInt($('#BRcontainer').attr('clientHeight') - this.getPageHeight(this.currentIndex()) / this.reduce * 0.03); + } + + return parseInt(0.9 * $('#BRcontainer').attr('clientHeight')); +} + + // flipBackToIndex() //______________________________________________________________________________ // to flip back one spread, pass index=null @@ -1875,16 +2099,12 @@ BookReader.prototype.flipLeftToRight = function(newIndexL, newIndexR) { var leftEdgeTmpLeft = gutter - currWidthL - leafEdgeTmpW; this.leafEdgeTmp = document.createElement('div'); + this.leafEdgeTmp.className = 'BRleafEdgeTmp'; $(this.leafEdgeTmp).css({ - borderStyle: 'solid none solid solid', - borderColor: 'rgb(51, 51, 34)', - borderWidth: '1px 0px 1px 1px', - background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%', width: leafEdgeTmpW + 'px', - height: this.twoPage.height-1 + 'px', + height: this.twoPage.height + 'px', left: leftEdgeTmpLeft + 'px', - top: top+'px', - position: 'absolute', + top: top+'px', zIndex:1000 }).appendTo('#BRtwopageview'); @@ -2028,16 +2248,12 @@ BookReader.prototype.flipRightToLeft = function(newIndexL, newIndexR) { var gutter = middle + this.gutterOffsetForIndex(newIndexL); this.leafEdgeTmp = document.createElement('div'); + this.leafEdgeTmp.className = 'BRleafEdgeTmp'; $(this.leafEdgeTmp).css({ - borderStyle: 'solid none solid solid', - borderColor: 'rgb(51, 51, 34)', - borderWidth: '1px 0px 1px 1px', - background: 'transparent url(' + this.imagesBaseURL + 'left_edges.png) repeat scroll 0% 0%', width: leafEdgeTmpW + 'px', - height: this.twoPage.height-1 + 'px', + height: this.twoPage.height + 'px', left: gutter+scaledW+'px', top: top+'px', - position: 'absolute', zIndex:1000 }).appendTo('#BRtwopageview'); @@ -2108,32 +2324,21 @@ BookReader.prototype.flipRightToLeft = function(newIndexL, newIndexR) { // setMouseHandlers2UP //______________________________________________________________________________ BookReader.prototype.setMouseHandlers2UP = function() { - /* - $(this.prefetchedImgs[this.twoPage.currentIndexL]).bind('dblclick', function() { - //self.prevPage(); - self.autoStop(); - self.left(); - }); - $(this.prefetchedImgs[this.twoPage.currentIndexR]).bind('dblclick', function() { - //self.nextPage();' - self.autoStop(); - self.right(); - }); - */ - - this.setDragHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexL] ); this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexL], { self: this }, function(e) { + e.data.self.ttsStop(); e.data.self.left(); + e.preventDefault(); } ); - this.setDragHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexR] ); this.setClickHandler2UP( this.prefetchedImgs[this.twoPage.currentIndexR], { self: this }, function(e) { + e.data.self.ttsStop(); e.data.self.right(); + e.preventDefault(); } ); } @@ -2156,6 +2361,13 @@ BookReader.prototype.prefetchImg = function(index) { if (loadImage) { //console.log('prefetching ' + index); var img = document.createElement("img"); + img.className = 'BRpageimage'; + if (index < 0 || index > (this.numLeafs - 1) ) { + // Facing page at beginning or end, or beyond + $(img).css({ + 'background-color': 'transparent' + }); + } img.src = pageURI; img.uri = pageURI; // browser may rewrite src so we stash raw URI here this.prefetchedImgs[index] = img; @@ -2197,8 +2409,7 @@ BookReader.prototype.prepareFlipLeftToRight = function(prevL, prevR) { top: top+'px', height: this.twoPage.height, width: scaledW+'px', - backgroundColor: this.getPageBackgroundColor(prevL), - borderRight: '1px solid black', + borderRight: '1px solid black', // XXXmang check zIndex: 1 } @@ -2214,9 +2425,8 @@ BookReader.prototype.prepareFlipLeftToRight = function(prevL, prevR) { right: '', top: top+'px', height: this.twoPage.height, - width: '0px', - backgroundColor: this.getPageBackgroundColor(prevR), - borderLeft: '1px solid black', + borderLeft: '1px solid black', // XXXmang check + width: '0', zIndex: 2 } @@ -2250,10 +2460,8 @@ BookReader.prototype.prepareFlipRightToLeft = function(nextL, nextR) { position: 'absolute', left: gutter+'px', top: top+'px', - backgroundColor: this.getPageBackgroundColor(nextR), height: this.twoPage.height, width: scaledW+'px', - borderLeft: '1px solid black', zIndex: 1 }); @@ -2268,10 +2476,8 @@ BookReader.prototype.prepareFlipRightToLeft = function(nextL, nextR) { position: 'absolute', right: $('#BRtwopageview').attr('clientWidth')-gutter+'px', top: top+'px', - backgroundColor: this.getPageBackgroundColor(nextL), height: this.twoPage.height, width: 0+'px', // Start at 0 width, then grow to the left - borderRight: '1px solid black', zIndex: 2 }); @@ -2322,10 +2528,14 @@ BookReader.prototype.pruneUnusedImgs = function() { //______________________________________________________________________________ BookReader.prototype.prefetch = function() { + // $$$ We should check here if the current indices have finished + // loading (with some timeout) before loading more page images + // See https://bugs.edge.launchpad.net/bookreader/+bug/511391 + // prefetch visible pages first this.prefetchImg(this.twoPage.currentIndexL); this.prefetchImg(this.twoPage.currentIndexR); - + var adjacentPagesToLoad = 3; var lowCurrent = Math.min(this.twoPage.currentIndexL, this.twoPage.currentIndexR); @@ -2420,7 +2630,7 @@ BookReader.prototype.BRSearchCallback = function(txt) { //console.log(pages[i].getAttribute('file').substr(1) +'-'+ parseInt(pages[i].getAttribute('file').substr(1), 10)); - var re = new RegExp (/_(\d{4})/); + 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); @@ -2477,9 +2687,9 @@ BookReader.prototype.updateSearchHilites1UP = function() { for (var key in this.searchResults) { - if (-1 != jQuery.inArray(parseInt(key), this.displayedIndices)) { + if (jQuery.inArray(parseInt(key), this.displayedIndices) >= 0) { var result = this.searchResults[key]; - if(null == result.div) { + if (null == result.div) { result.div = document.createElement('div'); $(result.div).attr('className', 'BookReaderSearchHilite').appendTo('#pagediv'+key); //console.log('appending ' + key); @@ -2631,36 +2841,15 @@ BookReader.prototype.updateSearchHilites2UP = function() { for (var key in this.searchResults) { key = parseInt(key, 10); - if (-1 != jQuery.inArray(key, this.displayedIndices)) { + if (jQuery.inArray(key, this.displayedIndices) >= 0) { var result = this.searchResults[key]; - if(null == result.div) { + if (null == result.div) { result.div = document.createElement('div'); $(result.div).attr('className', 'BookReaderSearchHilite').css('zIndex', 3).appendTo('#BRtwopageview'); //console.log('appending ' + key); } - // We calculate the reduction factor for the specific page because it can be different - // for each page in the spread - var height = this._getPageHeight(key); - var width = this._getPageWidth(key) - var reduce = this.twoPage.height/height; - var scaledW = parseInt(width*reduce); - - var gutter = this.twoPageGutter(); - var pageL; - if ('L' == this.getPageSide(key)) { - pageL = gutter-scaledW; - } else { - pageL = gutter; - } - var pageT = this.twoPageTop(); - - $(result.div).css({ - width: (result.r-result.l)*reduce + 'px', - height: (result.b-result.t)*reduce + 'px', - left: pageL+(result.l)*reduce + 'px', - top: pageT+(result.t)*reduce +'px' - }); + this.setHilightCss2UP(result.div, key, result.l, result.r, result.t, result.b); } else { //console.log(key + ' not displayed'); @@ -2673,6 +2862,35 @@ BookReader.prototype.updateSearchHilites2UP = function() { } } +// setHilightCss2UP() +//______________________________________________________________________________ +//position calculation shared between search and text-to-speech functions +BookReader.prototype.setHilightCss2UP = function(div, index, left, right, top, bottom) { + + // We calculate the reduction factor for the specific page because it can be different + // for each page in the spread + var height = this._getPageHeight(index); + var width = this._getPageWidth(index) + var reduce = this.twoPage.height/height; + var scaledW = parseInt(width*reduce); + + var gutter = this.twoPageGutter(); + var pageL; + if ('L' == this.getPageSide(index)) { + pageL = gutter-scaledW; + } else { + pageL = gutter; + } + var pageT = this.twoPageTop(); + + $(div).css({ + width: (right-left)*reduce + 'px', + height: (bottom-top)*reduce + 'px', + left: pageL+left*reduce + 'px', + top: pageT+top*reduce +'px' + }); +} + // removeSearchHilites() //______________________________________________________________________________ BookReader.prototype.removeSearchHilites = function() { @@ -2688,62 +2906,6 @@ BookReader.prototype.removeSearchHilites = function() { //______________________________________________________________________________ BookReader.prototype.printPage = function() { window.open(this.getPrintURI(), 'printpage', 'width=400, height=500, resizable=yes, scrollbars=no, toolbar=no, location=no'); - - /* iframe implementation - - if (null != this.printPopup) { // check if already showing - return; - } - this.printPopup = document.createElement("div"); - $(this.printPopup).css({ - position: 'absolute', - top: '20px', - left: ($('#BRcontainer').width()-400)/2 + 'px', - width: '400px', - padding: "20px", - border: "3px double #999999", - zIndex: 3, - backgroundColor: "#fff" - }).appendTo('#BookReader'); - - var indexToPrint; - if (this.constMode1up == this.mode) { - indexToPrint = this.firstIndex; - } else { - indexToPrint = this.twoPage.currentIndexL; - } - - this.indexToPrint = indexToPrint; - - var htmlStr = '
'; - htmlStr = '

Click here to print this page

'; - htmlStr += '
' - htmlStr += '

'; - //htmlStr += ' '; - //htmlStr += ''; - htmlStr += 'Prev Next'; - htmlStr += '

'; - htmlStr += '
'; - htmlStr += '

Close popup

'; - htmlStr += '
'; - - this.printPopup.innerHTML = htmlStr; - - var iframe = document.createElement('iframe'); - iframe.id = 'printFrame'; - iframe.name = 'printFrame'; - iframe.width = '233px'; // 8.5 x 11 aspect - iframe.height = '300px'; - - var self = this; // closure - - $(iframe).load(function() { - var doc = BookReader.util.getIFrameDocument(this); - $('body', doc).html(self.getPrintFrameContent(self.indexToPrint)); - }); - - $('#printDiv').prepend(iframe); - */ } // Get print URI from current indices and mode @@ -2755,7 +2917,7 @@ BookReader.prototype.getPrintURI = function() { indexToPrint = this.firstIndex; // $$$ the index in the middle of the viewport would make more sense } - var options = 'id=' + this.bookId + '&server=' + this.server + '&zip=' + this.zip + var options = 'id=' + this.subPrefix + '&server=' + this.server + '&zip=' + this.zip + '&format=' + this.imageFormat + '&file=' + this._getPageFile(indexToPrint) + '&width=' + this._getPageWidth(indexToPrint) + '&height=' + this._getPageHeight(indexToPrint); @@ -2826,29 +2988,76 @@ BookReader.prototype.showEmbedCode = function() { return; } this.autoStop(); + this.ttsStop(); + this.embedPopup = document.createElement("div"); $(this.embedPopup).css({ position: 'absolute', - top: '20px', + top: ($('#BRcontainer').attr('clientHeight')-250)/2 + 'px', left: ($('#BRcontainer').attr('clientWidth')-400)/2 + 'px', width: '400px', - padding: "20px", - border: "3px double #999999", - zIndex: 3, - backgroundColor: "#fff" + height: '250px', + padding: '0', + fontSize: '12px', + color: '#333', + zIndex: 300, + border: '10px solid #615132', + backgroundColor: "#fff", + MozBorderRadius: '8px', + MozBoxShadow: '0 0 6px #000', + WebkitBorderRadius: '8px', + WebkitBoxShadow: '0 0 6px #000' }).appendTo('#BookReader'); - htmlStr = '

Embed Bookreader in your blog!

'; - htmlStr += '

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.

'; - htmlStr += '

Embed Code:

'; - htmlStr += '

Close popup

'; + htmlStr = '

Embed Bookreader

'; + htmlStr += '

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.

'; + htmlStr += ''; + htmlStr += 'Close'; this.embedPopup.innerHTML = htmlStr; - $(this.embedPopup).find('input').bind('click', function() { + $('#BookReader').append('
'); + $(this.embedPopup).find('textarea').click(function() { + this.select(); + }) + $(this.embedPopup).addClass("popped"); +} + +// showBookmarkCode() +//______________________________________________________________________________ +BookReader.prototype.showBookmarkCode = function() { + this.bookmarkPopup = document.createElement("div"); + $(this.bookmarkPopup).css({ + position: 'absolute', + top: ($('#BRcontainer').attr('clientHeight')-250)/2 + 'px', + left: ($('#BRcontainer').attr('clientWidth')-400)/2 + 'px', + width: '400px', + height: '250px', + padding: '0', + fontSize: '12px', + color: '#333', + zIndex: 300, + border: '10px solid #615132', + backgroundColor: "#fff", + MozBorderRadius: '8px', + MozBoxShadow: '0 0 6px #000', + WebkitBorderRadius: '8px', + WebkitBoxShadow: '0 0 6px #000' + }).appendTo('#BookReader'); + + htmlStr = '

Add a bookmark

'; + htmlStr += '

You can add a bookmark to any page in any book. If you elect to make your bookmark public, other readers will be able to see it. You must be logged in to your Open Library account to add bookmarks.

'; + htmlStr += '



'; + htmlStr += 'Close'; + + this.bookmarkPopup.innerHTML = htmlStr; + $('#BookReader').append('
'); + $(this.bookmarkPopup).find('textarea').click(function() { this.select(); }) + $(this.bookmarkPopup).addClass("popped"); } + // autoToggle() //______________________________________________________________________________ BookReader.prototype.autoToggle = function() { @@ -2861,7 +3070,7 @@ BookReader.prototype.autoToggle = function() { // Change to autofit if book is too large if (this.reduce < this.twoPageGetAutofitReduce()) { - this.zoom2up(0); + this.zoom2up('auto'); } var self = this; @@ -3009,35 +3218,309 @@ BookReader.prototype.jumpIndexForRightEdgePageX = function(pageX) { } } +// initNavbar +//______________________________________________________________________________ +// Initialize the navigation bar. +// $$$ this could also add the base elements to the DOM, so disabling the nav bar +// could be as simple as not calling this function +BookReader.prototype.initNavbar = function() { + // Setup nav / chapter / search results bar + + // $$$ should make this work inside the BookReader div (self-contained), rather than after + $('#BookReader').after( + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + // XXXmang update code to update pagenum + + '
n141 / 325
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + +/* +
+
+ A related distinction is between the emotion and the results of the emotion, principally behaviors and emotional expressions. People often behave in certain ways as a direct result of their emotional state, such as crying, fighting or fleeing. Page 163 +
IV. The Witch | Page 163
+
+
+*/ + + /* $$$mang search results and chapters should automatically coalesce + $('.searchChap').bt({ + contentSelector: '$(this).find(".query")', + trigger: 'click', + closeWhenOthersOpen: true, + cssStyles: { + width: '250px', + padding: '10px 10px 15px', + backgroundColor: '#fff', + border: '3px solid #e2dcc5', + borderBottom: 'none', + fontFamily: '"Lucida Grande","Arial",sans-serif', + fontSize: '12px', + lineHeight: '18px', + color: '#615132' + }, + shrinkToFit: false, + width: '230px', + padding: 0, + spikeGirth: 0, + spikeLength: 0, + overlap: '10px', + overlay: false, + killTitle: true, + textzIndex: 9999, + boxzIndex: 9998, + wrapperzIndex: 9997, + offsetParent: null, + positions: ['top'], + fill: 'white', + windowMargin: 10, + strokeWidth: 3, + strokeStyle: '#e2dcc5', + cornerRadius: 0, + centerPointX: 0, + centerPointY: 0, + shadow: false + }); + $('.searchChap').each(function(){ + $(this).hover(function(){ + $(this).addClass('front'); + },function(){ + $(this).removeClass('front'); + }); + }); + */ +} + +BookReader.prototype.addSearchResult = function(queryString, pageNumber, pageIndex) { + var uiStringSearch = "Search result"; // i18n + var uiStringPage = "Page"; // i18n + + var percentThrough = BookReader.util.cssPercentage(pageIndex, this.numLeafs); + + // $$$mang add click-through to page + $('" + + "
" + + "
" + + "" + + "
" + + "
" + + "
" + + "
" + + "" + + "
" + "
"); this.updateToolbarZoom(this.reduce); // Pretty format - if (ui == "embed") { + if (ui == "embed" || ui == "touch") { $("#BookReader a.logo").attr("target","_blank"); } @@ -3053,11 +3536,15 @@ BookReader.prototype.initToolbar = function(mode, ui) { var titles = { '.logo': 'Go to Archive.org', '.zoom_in': 'Zoom in', '.zoom_out': 'Zoom out', - '.one_page_mode': 'One-page view', - '.two_page_mode': 'Two-page view', - '.thumbnail_mode': 'Thumbnail view', + '.onepg': 'One-page view', + '.twopg': 'Two-page view', + '.thumb': 'Thumbnail view', '.print': 'Print this page', - '.embed': 'Embed bookreader', + '.embed': 'Embed BookReader', + '.link': 'Link to this book (and page)', + '.bookmark': 'Bookmark this page', + '.read': 'Allow BookReader to read this aloud', + '.full': 'Show fullscreen', '.book_left': 'Flip left', '.book_right': 'Flip right', '.book_up': 'Page up', @@ -3082,11 +3569,19 @@ BookReader.prototype.initToolbar = function(mode, ui) { // Hide mode buttons and autoplay if 2up is not available // $$$ if we end up with more than two modes we should show the applicable buttons if ( !this.canSwitchToMode(this.constMode2up) ) { - jToolbar.find('.one_page_mode, .two_page_mode, .play, .pause').hide(); + jToolbar.find('.two_page_mode, .play, .pause').hide(); + } + if ( !this.canSwitchToMode(this.constModeThumb) ) { + jToolbar.find('.thumbnail_mode').hide(); + } + + // Hide one page button if it is the only mode available + if ( ! (this.canSwitchToMode(this.constMode2up) || this.canSwitchToMode(this.constModeThumb)) ) { + jToolbar.find('.one_page_mode').hide(); } // Switch to requested mode -- binds other click handlers - this.switchToolbarMode(mode); + //this.switchToolbarMode(mode); } @@ -3124,62 +3619,75 @@ BookReader.prototype.bindToolbarNavHandlers = function(jToolbar) { var self = this; // closure - jToolbar.find('.book_left').bind('click', function(e) { + jToolbar.find('.book_left').click(function(e) { self.left(); return false; }); - jToolbar.find('.book_right').bind('click', function(e) { + jToolbar.find('.book_right').click(function(e) { self.right(); return false; }); jToolbar.find('.book_up').bind('click', function(e) { - self.prev(); + if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) { + self.scrollUp(); + } else { + self.prev(); + } return false; }); jToolbar.find('.book_down').bind('click', function(e) { - self.next(); + if ($.inArray(self.mode, [self.constMode1up, self.constModeThumb]) >= 0) { + self.scrollDown(); + } else { + self.next(); + } return false; }); - jToolbar.find('.print').bind('click', function(e) { + jToolbar.find('.print').click(function(e) { self.printPage(); return false; }); - jToolbar.find('.embed').bind('click', function(e) { + jToolbar.find('.embed').click(function(e) { self.showEmbedCode(); return false; }); - jToolbar.find('.play').bind('click', function(e) { + jToolbar.find('.bookmark').click(function(e) { + self.showBookmarkCode(); + return false; + }); + + jToolbar.find('.play').click(function(e) { self.autoToggle(); return false; }); - jToolbar.find('.pause').bind('click', function(e) { + jToolbar.find('.pause').click(function(e) { self.autoToggle(); return false; }); - jToolbar.find('.book_top').bind('click', function(e) { + jToolbar.find('.book_top').click(function(e) { self.first(); return false; }); - jToolbar.find('.book_bottom').bind('click', function(e) { + jToolbar.find('.book_bottom').click(function(e) { self.last(); return false; }); - jToolbar.find('.book_leftmost').bind('click', function(e) { + jToolbar.find('.book_leftmost').click(function(e) { self.leftmost(); return false; }); - jToolbar.find('.book_rightmost').bind('click', function(e) { + jToolbar.find('.book_rightmost').click(function(e) { self.rightmost(); return false; }); @@ -3190,8 +3698,17 @@ BookReader.prototype.bindToolbarNavHandlers = function(jToolbar) { // Update the displayed zoom factor based on reduction factor BookReader.prototype.updateToolbarZoom = function(reduce) { var value; - if (this.constMode2up == this.mode && this.twoPage.autofit) { - value = 'Auto'; + 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 @@ -3202,6 +3719,78 @@ BookReader.prototype.updateToolbarZoom = function(reduce) { $('#BRzoom').text(value); } +// bindNavigationHandlers +//______________________________________________________________________________ +// Bind navigation handlers +BookReader.prototype.bindNavigationHandlers = function() { + $('#BookReader').die('mousemove.navigation').live('mousemove.navigation', + { 'br': this }, + this.navigationMousemoveHandler + ); +} + +// unbindNavigationHandlers +//______________________________________________________________________________ +// Unbind navigation handlers +BookReader.prototype.unbindNavigationHandlers = function() { + $('#BookReader').die('mousemove.navigation'); +} + +// navigationMousemoveHandler +//______________________________________________________________________________ +// Handle mousemove related to navigation. Bind at #BookReader level to allow autohide. +BookReader.prototype.navigationMousemoveHandler = function(event) { + // $$$ possibly not great to be calling this for every mousemove + + if (event.data['br'].uiAutoHide) { + var navkey = $(document).height() - 75; + if ((event.pageY < 76) || (event.pageY > navkey)) { + // inside or near navigation elements + event.data['br'].hideNavigation(); + } else { + event.data['br'].showNavigation(); + } + } +} + +// navigationIsVisible +//______________________________________________________________________________ +// Returns true if the navigation elements are currently visible +BookReader.prototype.navigationIsVisible = function() { + // $$$ doesn't account for transitioning states, nav must be fully visible to return true + var toolpos = $('#BRtoolbar').offset(); + var tooltop = toolpos.top; + if (tooltop == 0) { + return true; + } + return false; +} + +// hideNavigation +//______________________________________________________________________________ +// Hide navigation elements, if visible +BookReader.prototype.hideNavigation = function() { + // Check if navigation is showing + if (this.navigationIsVisible()) { + // $$$ don't hardcode height + $('#BRtoolbar').animate({top:-60}); + $('#BRnav').animate({bottom:-60}); + $('#BRzoomer').animate({right:-26}); + } +} + +// showNavigation +//______________________________________________________________________________ +// Show navigation elements +BookReader.prototype.showNavigation = function() { + // Check if navigation is hidden + if (!this.navigationIsVisible()) { + $('#BRtoolbar').animate({top:0}); + $('#BRnav').animate({bottom:0}); + $('#BRzoomer').animate({right:0}); + } +} + // firstDisplayableIndex //______________________________________________________________________________ // Returns the index of the first visible page, dependent on the mode. @@ -3514,6 +4103,8 @@ BookReader.prototype.startLocationPolling = function() { if (newHash != self.oldUserHash) { // Only process new user hash once //console.log('url change detected ' + self.oldLocationHash + " -> " + newHash); + self.ttsStop(); + // Queue change if animating if (self.animating) { self.autoStop(); @@ -3533,11 +4124,11 @@ BookReader.prototype.startLocationPolling = function() { //________ // Returns true if we can switch to the requested mode BookReader.prototype.canSwitchToMode = function(mode) { - if (mode == this.constMode2up) { + if (mode == this.constMode2up || mode == this.constModeThumb) { // check there are enough pages to display // $$$ this is a workaround for the mis-feature that we can't display // short books in 2up mode - if (this.numLeafs < 6) { + if (this.numLeafs < 2) { return false; } } @@ -3562,19 +4153,6 @@ BookReader.prototype.searchHighlightVisible = function() { return false; } -// getPageBackgroundColor -//-------- -// Returns a CSS property string for the background color for the given page -// $$$ turn into regular CSS? -BookReader.prototype.getPageBackgroundColor = function(index) { - if (index >= 0 && index < this.numLeafs) { - // normal page - return this.pageDefaultBackgroundColor; - } - - return ''; -} - // _getPageWidth //-------- // Returns the page width for the given index, or first or last page if out of range @@ -3604,6 +4182,7 @@ BookReader.prototype._getPageURI = function(index, reduce, rotate) { if ('undefined' == typeof(reduce)) { // reduce not passed in + // $$$ this probably won't work for thumbnail mode var ratio = this.getPageHeight(index) / this.twoPage.height; var scale; // $$$ we make an assumption here that the scales are available pow2 (like kakadu) @@ -3626,11 +4205,45 @@ BookReader.prototype._getPageURI = function(index, reduce, rotate) { return this.getPageURI(index, reduce, rotate); } +/* + * Update based on received record from Open Library. + */ +BookReader.prototype.gotOpenLibraryRecord = function(self, olObject) { + // $$$ could refactor this so that 'this' is available + if (olObject) { + if (olObject['table_of_contents']) { + self.updateTOC(olObject['table_of_contents']); + } + } +} + // Library functions BookReader.util = { + disableSelect: function(jObject) { + // Bind mouse handlers + // Disable mouse click to avoid selected/highlighted page images - bug 354239 + jObject.bind('mousedown', function(e) { + // $$$ check here for right-click and don't disable. Also use jQuery style + // for stopping propagation. See https://bugs.edge.launchpad.net/gnubook/+bug/362626 + return false; + }); + // Special hack for IE7 + jObject[0].onselectstart = function(e) { return false; }; + }, + clamp: function(value, min, max) { return Math.min(Math.max(value, min), max); }, + + // Given value and maximum, calculate a percentage suitable for CSS + cssPercentage: function(value, max) { + return parseInt(((value + 0.0) / max) * 100) + '%'; + }, + + notInArray: function(value, array) { + // inArray returns -1 or undefined if value not in array + return ! (jQuery.inArray(value, array) >= 0); + }, getIFrameDocument: function(iframe) { // Adapted from http://xkr.us/articles/dom/iframe-document/ @@ -3649,3 +4262,413 @@ BookReader.util = { } // The final property here must NOT have a comma after it - IE7 } + + +// ttsToggle() +//______________________________________________________________________________ +BookReader.prototype.ttsToggle = function () { + if (false == this.ttsPlaying) { + if(soundManager.supported()) { + this.ttsStart(); + } else { + soundManager.onready(function(oStatus) { + if (oStatus.success) { + this.ttsStart(); + } else { + alert('Could not load soundManger2, possibly due to FlashBlock. Audio playback is disabled'); + } + }, this); + } + } else { + this.ttsStop(); + } +} + +// ttsStart() +//______________________________________________________________________________ +BookReader.prototype.ttsStart = function () { + if (soundManager.debugMode) console.log('starting readAloud'); + if (this.constModeThumb == this.mode) this.switchMode(this.constMode1up); + + this.ttsPlaying = true; + this.ttsIndex = this.currentIndex(); + this.ttsFormat = 'mp3'; + if ($.browser.mozilla) { + this.ttsFormat = 'ogg'; + } + this.ttsGetText(this.ttsIndex, 'ttsStartCB'); +} + +// ttsStop() +//______________________________________________________________________________ +BookReader.prototype.ttsStop = function () { + if (false == this.ttsPlaying) return; + + if (soundManager.debugMode) console.log('stopping readaloud'); + soundManager.stopAll(); + soundManager.destroySound('chunk'+this.ttsIndex+'-'+this.ttsPosition); + this.ttsRemoveHilites(); + this.ttsRemovePopup(); + + this.ttsPlaying = false; + this.ttsIndex = null; //leaf index + this.ttsPosition = -1; //chunk (paragraph) number + this.ttsBuffering = false; + this.ttsPoller = null; +} + +// ttsGetText() +//______________________________________________________________________________ +BookReader.prototype.ttsGetText = function(index, callback) { + var url = 'http://'+this.server+'/BookReader/BookReaderGetTextWrapper.php?path='+this.bookPath+'_djvu.xml&page='+index; + this.ttsAjax = $.ajax({url:url, dataType:'jsonp', jsonpCallback:callback}); +} + +// ttsStartCB(): text-to-speech callback +//______________________________________________________________________________ +BookReader.prototype.ttsStartCB = function (data) { + if (soundManager.debugMode) console.log('ttsStartCB got data: ' + data); + this.ttsChunks = data; + this.ttsHilites = []; + + //deal with the page being blank + if (0 == data.length) { + if (soundManager.debugMode) console.log('first page is blank!'); + if(this.ttsAdvance(true)) { + this.ttsGetText(this.ttsIndex, 'ttsStartCB'); + } + return; + } + + this.ttsShowPopup(); + + ///// whileloading: broken on safari + ///// onload fires on safari, but *after* the sound starts playing.. + this.ttsPosition = -1; + var snd = soundManager.createSound({ + 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... + }); + snd.br = this; + snd.load(); + + this.ttsNextChunk(); +} + +// ttsShowPopup +//______________________________________________________________________________ +BookReader.prototype.ttsShowPopup = function() { + if (soundManager.debugMode) console.log('ttsShowPopup index='+this.ttsIndex+' pos='+this.ttsPosition); + + 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'); + + htmlStr = ' '; + + this.popup.innerHTML = htmlStr; +} + +// ttsRemovePopup +//______________________________________________________________________________ +BookReader.prototype.ttsRemovePopup = function() { + $(this.popup).remove(); + this.popup=null; +} + +// ttsNextPageCB +//______________________________________________________________________________ +BookReader.prototype.ttsNextPageCB = function (data) { + this.ttsNextChunks = data; + if (soundManager.debugMode) console.log('preloaded next chunks.. data is ' + data); + + if (true == this.ttsBuffering) { + if (soundManager.debugMode) console.log('ttsNextPageCB: ttsBuffering is true'); + this.ttsBuffering = false; + } +} + +// ttsLoadChunk +//______________________________________________________________________________ +BookReader.prototype.ttsLoadChunk = function (page, pos, string) { + var snd = soundManager.createSound({ + id: 'chunk'+page+'-'+pos, + url: 'http://'+this.server+'/BookReader/BookReaderGetTTS.php?string=' + escape(string) + '&format=.'+this.ttsFormat //the .ogg is to trick SoundManager2 to use the HTML5 audio player + }); + snd.br = this; + snd.load() +} + + +// ttsNextChunk() +//______________________________________________________________________________ +// This function into two parts: ttsNextChunk gets run before page flip animation +// and ttsNextChunkPhase2 get run after page flip animation. +// If a page flip is necessary, ttsAdvance() will return false so Phase2 isn't +// called. Instead, this.animationFinishedCallback is set, so that Phase2 +// continues after animation is finished. + +BookReader.prototype.ttsNextChunk = function () { + if (soundManager.debugMode) console.log('nextchunk pos=' + this.ttsPosition); + + if (-1 != this.ttsPosition) { + soundManager.destroySound('chunk'+this.ttsIndex+'-'+this.ttsPosition); + } + + this.ttsRemoveHilites(); //remove old hilights + + var moreToPlay = this.ttsAdvance(); + + if (moreToPlay) { + this.ttsNextChunkPhase2(); + } + + //This function is called again when ttsPlay() has finished playback. + //If the next chunk of text has not yet finished loading, ttsPlay() + //will start polling until the next chunk is ready. +} + +// ttsNextChunkPhase2() +//______________________________________________________________________________ +// page flip animation has now completed +BookReader.prototype.ttsNextChunkPhase2 = function () { + if (null == this.ttsChunks) { + alert('error: ttsChunks is null?'); //TODO + return; + } + + if (0 == this.ttsChunks.length) { + if (soundManager.debugMode) console.log('ttsNextChunk2: ttsChunks.length is zero.. hacking...'); + this.ttsStartCB(this.ttsChunks); + return; + } + + if (soundManager.debugMode) console.log('next chunk is ' + this.ttsPosition); + + //prefetch next page of text + if (0 == this.ttsPosition) { + if (this.ttsIndex<(this.numLeafs-1)) { + this.ttsGetText(this.ttsIndex+1, 'ttsNextPageCB'); + } + } + + this.ttsPrefetchAudio(); + + this.ttsPlay(); +} + +// ttsAdvance() +//______________________________________________________________________________ +// 1. advance ttsPosition +// 2. if necessary, advance ttsIndex, and copy ttsNextChunks to ttsChunks +// 3. if necessary, flip to current page, or scroll so chunk is visible +// 4. do something smart is ttsNextChunks has not yet finished preloading (TODO) +// 5. stop playing at end of book + +BookReader.prototype.ttsAdvance = function (starting) { + this.ttsPosition++; + + if (this.ttsPosition >= this.ttsChunks.length) { + + if (this.ttsIndex == (this.numLeafs-1)) { + if (soundManager.debugMode) console.log('tts stop'); + return false; + } else { + if ((null != this.ttsNextChunks) || (starting)) { + if (soundManager.debugMode) console.log('moving to next page!'); + this.ttsIndex++; + this.ttsPosition = 0; + this.ttsChunks = this.ttsNextChunks; + this.ttsNextChunks = null; + + //A page flip might be necessary. This code is confusing since + //ttsNextChunks might be null if we are starting on a blank page. + if (2 == this.mode) { + if ((this.ttsIndex != this.twoPage.currentIndexL) && (this.ttsIndex != this.twoPage.currentIndexR)) { + if (!starting) { + this.animationFinishedCallback = this.ttsNextChunkPhase2; + this.next(); + return false; + } else { + this.next(); + return true; + } + } else { + return true; + } + } + } else { + if (soundManager.debugMode) console.log('ttsAdvance: ttsNextChunks is null'); + return false; + } + } + } + + return true; +} + +// ttsPrefetchAudio() +//______________________________________________________________________________ +BookReader.prototype.ttsPrefetchAudio = function () { + + if(false != this.ttsBuffering) { + alert('TTS Error: prefetch() called while content still buffering!'); + return; + } + + //preload next chunk + var nextPos = this.ttsPosition+1; + if (nextPos < this.ttsChunks.length) { + this.ttsLoadChunk(this.ttsIndex, nextPos, this.ttsChunks[nextPos][0]); + } else { + //for a short page, preload might nt have yet returned.. + if (soundManager.debugMode) console.log('preloading chunk 0 from next page, index='+(this.ttsIndex+1)); + if (null != this.ttsNextChunks) { + if (0 != this.ttsNextChunks.length) { + this.ttsLoadChunk(this.ttsIndex+1, 0, this.ttsNextChunks[0][0]); + } else { + if (soundManager.debugMode) console.log('prefetchAudio(): ttsNextChunks is zero length!'); + } + } else { + if (soundManager.debugMode) console.log('ttsNextChunks is null, not preloading next page'); + this.ttsBuffering = true; + } + } + +} + +// ttsPlay() +//______________________________________________________________________________ +BookReader.prototype.ttsPlay = function () { + + var chunk = this.ttsChunks[this.ttsPosition]; + if (soundManager.debugMode) { + console.log('ttsPlay position = ' + this.ttsPosition); + console.log('chunk = ' + chunk); + console.log(this.ttsChunks); + } + + //add new hilights + if (2 == this.mode) { + this.ttsHilite2UP(chunk); + } else { + this.ttsHilite1UP(chunk); + } + + this.ttsScrollToChunk(chunk); + + //play current chunk + if (false == this.ttsBuffering) { + soundManager.play('chunk'+this.ttsIndex+'-'+this.ttsPosition,{onfinish:function(){br.ttsNextChunk();}}); + } else { + soundManager.play('chunk'+this.ttsIndex+'-'+this.ttsPosition,{onfinish:function(){br.ttsStartPolling();}}); + } +} + +// scrollToChunk() +//______________________________________________________________________________ +BookReader.prototype.ttsScrollToChunk = function(chunk) { + if (this.constMode1up != this.mode) return; + + var leafTop = 0; + var h; + var i; + for (i=0; i