diff --git a/src/BookReader.js b/src/BookReader.js index c63d0835a..c10e476ee 100644 --- a/src/BookReader.js +++ b/src/BookReader.js @@ -41,6 +41,7 @@ import { ModeThumb } from './BookReader/ModeThumb'; import { ImageCache } from './BookReader/ImageCache.js'; import { PageContainer } from './BookReader/PageContainer.js'; import { NAMED_REDUCE_SETS } from './BookReader/ReduceSet'; +import { event } from 'jquery'; /** * BookReader @@ -49,8 +50,14 @@ import { NAMED_REDUCE_SETS } from './BookReader/ReduceSet'; * @constructor */ export default function BookReader(overrides = {}) { - const options = jQuery.extend(true, {}, BookReader.defaultOptions, overrides, BookReader.optionOverrides); - this.setup(options); + const options = jQuery.extend( + true, + {}, + BookReader.defaultOptions, + overrides, + BookReader.optionOverrides + ); + this.setup(options); } BookReader.version = PACKAGE_JSON.version; @@ -85,185 +92,202 @@ BookReader.optionOverrides = {}; * It is separate from the constructor, so plugins can extend. * @param {BookReaderOptions} options */ -BookReader.prototype.setup = function(options) { - // Store the options used to setup bookreader - this.options = options; - - /** @type {number} @deprecated some past iterations set this */ - this.numLeafs = undefined; - - /** Overridden by plugin.search.js */ - this.enableSearch = false; - - /** - * Store viewModeOrder states - * @var {boolean} - */ - this.viewModeOrder = []; - - /** - * Used to suppress fragment change for init with canonical URLs - * @var {boolean} - */ - this.suppressFragmentChange = false; - - /** @type {function(): void} */ - this.animationFinishedCallback = null; - - // @deprecated: Instance constants. Use Class constants instead - /** 1 page view */ - this.constMode1up = BookReader.constMode1up; - /** 2 pages view */ - this.constMode2up = BookReader.constMode2up; - /** thumbnails view */ - this.constModeThumb = BookReader.constModeThumb; - - // Private properties below. Configuration should be done with options. - /** @type {number} TODO: Make private */ - this.reduce = 8; /* start very small */ - this.defaults = options.defaults; - this.padding = options.padding; - - this.reduceSet = NAMED_REDUCE_SETS[options.reduceSet]; - if (!this.reduceSet) { - console.warn(`Invalid reduceSet ${options.reduceSet}. Ignoring.`); - this.reduceSet = NAMED_REDUCE_SETS[DEFAULT_OPTIONS.reduceSet]; - } - - /** @type {number} - * can be 1 or 2 or 3 based on the display mode const value - */ - this.mode = null; - this.prevReadMode = null; - this.ui = options.ui; - this.uiAutoHide = options.uiAutoHide; - - this.thumbWidth = 100; // will be overridden during this._modes.modeThumb.prepare(); - this.thumbRowBuffer = options.thumbRowBuffer; - this.thumbColumns = options.thumbColumns; - this.thumbMaxLoading = options.thumbMaxLoading; - this.thumbPadding = options.thumbPadding; - this.displayedRows = []; - this.displayedIndices = []; - - this.animating = false; - this.flipSpeed = typeof options.flipSpeed === 'number' ? options.flipSpeed : { - 'fast': 200, - 'slow': 600, - }[options.flipSpeed] || 400; - this.flipDelay = options.flipDelay; - - /** - * Represents the first displayed index - * In 2up mode it will be the left page - * In 1 up mode it is the highest page - * @property {number|null} firstIndex - */ - this.firstIndex = null; - this.isFullscreenActive = options.startFullscreen || false; - this.lastScroll = null; - - this.showLogo = options.showLogo; - this.logoURL = options.logoURL; - this.imagesBaseURL = options.imagesBaseURL; - - this.reductionFactors = options.reductionFactors; - this.onePage = options.onePage; - /** @type {import('./BookReader/Mode2Up').TwoPageState} */ - this.twoPage = options.twoPage; - this.onePageMinBreakpoint = options.onePageMinBreakpoint; - - this.bookTitle = options.bookTitle; - this.bookUrl = options.bookUrl; - this.bookUrlText = options.bookUrlText; - this.bookUrlTitle = options.bookUrlTitle; - this.titleLeaf = options.titleLeaf; - - this.metadata = options.metadata; - this.thumbnail = options.thumbnail; - this.bookUrlMoreInfo = options.bookUrlMoreInfo; - - this.enableExperimentalControls = options.enableExperimentalControls; - this.el = options.el; - - this.pageProgression = options.pageProgression; - this.protected = options.protected; - this.getEmbedCode = options.getEmbedCode; - this.popup = null; - - // Assign the data methods - this.data = options.data; - - /** @type {{[name: string]: JQuery}} */ - this.refs = {}; - - /** The book being displayed in BookReader*/ - this.book = new BookModel(this); - - if (options.getNumLeafs) this.book.getNumLeafs = options.getNumLeafs.bind(this); - if (options.getPageWidth) this.book.getPageWidth = options.getPageWidth.bind(this); - if (options.getPageHeight) this.book.getPageHeight = options.getPageHeight.bind(this); - if (options.getPageURI) this.book.getPageURI = options.getPageURI.bind(this); - if (options.getPageSide) this.book.getPageSide = options.getPageSide.bind(this); - if (options.getPageNum) this.book.getPageNum = options.getPageNum.bind(this); - if (options.getPageProp) this.book.getPageProp = options.getPageProp.bind(this); - if (options.getSpreadIndices) this.book.getSpreadIndices = options.getSpreadIndices.bind(this); - if (options.leafNumToIndex) this.book.leafNumToIndex = options.leafNumToIndex.bind(this); - - /** - * @private Components are 'subchunks' of bookreader functionality, usually UI related - * They should be relatively decoupled from each other/bookreader. - * Note there are no hooks right now; components just provide methods that bookreader - * calls at the correct moments. - **/ - this._components = { - navbar: new Navbar(this), - toolbar: new Toolbar(this), - }; - - this._modes = { - mode1Up: new Mode1Up(this, this.book), - mode2Up: new Mode2Up(this, this.book), - modeThumb: new ModeThumb(this, this.book), - }; - - /** Stores classes which we want to expose (selectively) some methods as overridable */ - this._overrideable = { - 'book': this.book, - '_components.navbar': this._components.navbar, - '_components.toolbar': this._components.toolbar, - '_modes.mode1Up': this._modes.mode1Up, - '_modes.mode2Up': this._modes.mode2Up, - '_modes.modeThumb': this._modes.modeThumb, - }; - - /** Image cache for general image fetching */ - this.imageCache = new ImageCache(this.book, { - useSrcSet: this.options.useSrcSet, - reduceSet: this.reduceSet, - }); - - /** - * Flag if BookReader has "focus" for keyboard shortcuts - * Initially true, set to false when: - * - BookReader scrolled out of view - * Set to true when: - * - BookReader scrolled into view - */ - this.hasKeyFocus = true; +BookReader.prototype.setup = function (options) { + // Store the options used to setup bookreader + this.options = options; + + /** @type {number} @deprecated some past iterations set this */ + this.numLeafs = undefined; + + /** Overridden by plugin.search.js */ + this.enableSearch = false; + + /** + * Store viewModeOrder states + * @var {boolean} + */ + this.viewModeOrder = []; + + /** + * Used to suppress fragment change for init with canonical URLs + * @var {boolean} + */ + this.suppressFragmentChange = false; + + /** @type {function(): void} */ + this.animationFinishedCallback = null; + + // @deprecated: Instance constants. Use Class constants instead + /** 1 page view */ + this.constMode1up = BookReader.constMode1up; + /** 2 pages view */ + this.constMode2up = BookReader.constMode2up; + /** thumbnails view */ + this.constModeThumb = BookReader.constModeThumb; + + // Private properties below. Configuration should be done with options. + /** @type {number} TODO: Make private */ + this.reduce = 8; /* start very small */ + this.defaults = options.defaults; + this.padding = options.padding; + + this.reduceSet = NAMED_REDUCE_SETS[options.reduceSet]; + if (!this.reduceSet) { + console.warn(`Invalid reduceSet ${options.reduceSet}. Ignoring.`); + this.reduceSet = NAMED_REDUCE_SETS[DEFAULT_OPTIONS.reduceSet]; + } + + /** @type {number} + * can be 1 or 2 or 3 based on the display mode const value + */ + this.mode = null; + this.prevReadMode = null; + this.ui = options.ui; + this.uiAutoHide = options.uiAutoHide; + + this.thumbWidth = 100; // will be overridden during this._modes.modeThumb.prepare(); + this.thumbRowBuffer = options.thumbRowBuffer; + this.thumbColumns = options.thumbColumns; + this.thumbMaxLoading = options.thumbMaxLoading; + this.thumbPadding = options.thumbPadding; + this.displayedRows = []; + this.displayedIndices = []; + + this.animating = false; + this.flipSpeed = + typeof options.flipSpeed === 'number' + ? options.flipSpeed + : { + fast: 200, + slow: 600, + }[options.flipSpeed] || 400; + this.flipDelay = options.flipDelay; + + /** + * Represents the first displayed index + * In 2up mode it will be the left page + * In 1 up mode it is the highest page + * @property {number|null} firstIndex + */ + this.firstIndex = null; + this.isFullscreenActive = options.startFullscreen || false; + this.lastScroll = null; + + this.showLogo = options.showLogo; + this.logoURL = options.logoURL; + this.imagesBaseURL = options.imagesBaseURL; + + this.reductionFactors = options.reductionFactors; + this.onePage = options.onePage; + /** @type {import('./BookReader/Mode2Up').TwoPageState} */ + this.twoPage = options.twoPage; + this.onePageMinBreakpoint = options.onePageMinBreakpoint; + + this.bookTitle = options.bookTitle; + this.bookUrl = options.bookUrl; + this.bookUrlText = options.bookUrlText; + this.bookUrlTitle = options.bookUrlTitle; + this.titleLeaf = options.titleLeaf; + + this.metadata = options.metadata; + this.thumbnail = options.thumbnail; + this.bookUrlMoreInfo = options.bookUrlMoreInfo; + + this.enableExperimentalControls = options.enableExperimentalControls; + this.el = options.el; + + this.pageProgression = options.pageProgression; + this.protected = options.protected; + this.getEmbedCode = options.getEmbedCode; + this.popup = null; + + // Assign the data methods + this.data = options.data; + + /** @type {{[name: string]: JQuery}} */ + this.refs = {}; + + /** The book being displayed in BookReader*/ + this.book = new BookModel(this); + + if (options.getNumLeafs) + this.book.getNumLeafs = options.getNumLeafs.bind(this); + if (options.getPageWidth) + this.book.getPageWidth = options.getPageWidth.bind(this); + if (options.getPageHeight) + this.book.getPageHeight = options.getPageHeight.bind(this); + if (options.getPageURI) this.book.getPageURI = options.getPageURI.bind(this); + if (options.getPageSide) + this.book.getPageSide = options.getPageSide.bind(this); + if (options.getPageNum) this.book.getPageNum = options.getPageNum.bind(this); + if (options.getPageProp) + this.book.getPageProp = options.getPageProp.bind(this); + if (options.getSpreadIndices) + this.book.getSpreadIndices = options.getSpreadIndices.bind(this); + if (options.leafNumToIndex) + this.book.leafNumToIndex = options.leafNumToIndex.bind(this); + + /** + * @private Components are 'subchunks' of bookreader functionality, usually UI related + * They should be relatively decoupled from each other/bookreader. + * Note there are no hooks right now; components just provide methods that bookreader + * calls at the correct moments. + **/ + this._components = { + navbar: new Navbar(this), + toolbar: new Toolbar(this), + }; + + this._modes = { + mode1Up: new Mode1Up(this, this.book), + mode2Up: new Mode2Up(this, this.book), + modeThumb: new ModeThumb(this, this.book), + }; + + /** Stores classes which we want to expose (selectively) some methods as overridable */ + this._overrideable = { + book: this.book, + '_components.navbar': this._components.navbar, + '_components.toolbar': this._components.toolbar, + '_modes.mode1Up': this._modes.mode1Up, + '_modes.mode2Up': this._modes.mode2Up, + '_modes.modeThumb': this._modes.modeThumb, + }; + + /** Image cache for general image fetching */ + this.imageCache = new ImageCache(this.book, { + useSrcSet: this.options.useSrcSet, + reduceSet: this.reduceSet, + }); + + /** + * Flag if BookReader has "focus" for keyboard shortcuts + * Initially true, set to false when: + * - BookReader scrolled out of view + * Set to true when: + * - BookReader scrolled into view + */ + this.hasKeyFocus = true; }; /** * Get all the HTML Elements that are being/can be rendered. * Includes cached elements which might be rendered again. */ -BookReader.prototype.getActivePageContainerElements = function() { - let containerEls = Object.values(this._modes.mode2Up.mode2UpLit.pageContainerCache).map(pc => pc.$container[0]) - .concat(Object.values(this._modes.mode1Up.mode1UpLit.pageContainerCache).map(pc => pc.$container[0])); - if (this.mode == this.constModeThumb) { - containerEls = containerEls.concat(this.$('.BRpagecontainer').toArray()); - } - return containerEls; +BookReader.prototype.getActivePageContainerElements = function () { + let containerEls = Object.values( + this._modes.mode2Up.mode2UpLit.pageContainerCache + ) + .map(pc => pc.$container[0]) + .concat( + Object.values(this._modes.mode1Up.mode1UpLit.pageContainerCache).map( + pc => pc.$container[0] + ) + ); + if (this.mode == this.constModeThumb) { + containerEls = containerEls.concat(this.$('.BRpagecontainer').toArray()); + } + return containerEls; }; /** @@ -271,21 +295,29 @@ BookReader.prototype.getActivePageContainerElements = function() { * (at least as of writing) different modes can maintain different caches. * @param {PageIndex} pageIndex */ -BookReader.prototype.getActivePageContainerElementsForIndex = function(pageIndex) { - return [ - this._modes.mode2Up.mode2UpLit.pageContainerCache[pageIndex]?.$container?.[0], - this._modes.mode1Up.mode1UpLit.pageContainerCache[pageIndex]?.$container?.[0], - ...(this.mode == this.constModeThumb ? this.$(`.pagediv${pageIndex}`).toArray() : []) - ].filter(x => x); +BookReader.prototype.getActivePageContainerElementsForIndex = function ( + pageIndex +) { + return [ + this._modes.mode2Up.mode2UpLit.pageContainerCache[pageIndex] + ?.$container?.[0], + this._modes.mode1Up.mode1UpLit.pageContainerCache[pageIndex] + ?.$container?.[0], + ...(this.mode == this.constModeThumb + ? this.$(`.pagediv${pageIndex}`).toArray() + : []), + ].filter(x => x); }; Object.defineProperty(BookReader.prototype, 'activeMode', { - /** @return {Mode1Up | Mode2Up | ModeThumb} */ - get() { return { - 1: this._modes.mode1Up, - 2: this._modes.mode2Up, - 3: this._modes.modeThumb, - }[this.mode]; }, + /** @return {Mode1Up | Mode2Up | ModeThumb} */ + get() { + return { + 1: this._modes.mode1Up, + 2: this._modes.mode2Up, + 3: this._modes.modeThumb, + }[this.mode]; + }, }); /** @@ -299,15 +331,14 @@ BookReader.util = utils; * It normalizes "page" into the "index" field to disambiguate and prevent concflicts * @private */ -BookReader.prototype.extendParams = function(params, newParams) { - const modifiedNewParams = $.extend({}, newParams); - if ('undefined' != typeof(modifiedNewParams.page)) { - const pageIndex = this.book.parsePageString(modifiedNewParams.page); - if (!isNaN(pageIndex)) - modifiedNewParams.index = pageIndex; - delete modifiedNewParams.page; - } - $.extend(params, modifiedNewParams); +BookReader.prototype.extendParams = function (params, newParams) { + const modifiedNewParams = $.extend({}, newParams); + if ('undefined' != typeof modifiedNewParams.page) { + const pageIndex = this.book.parsePageString(modifiedNewParams.page); + if (!isNaN(pageIndex)) modifiedNewParams.index = pageIndex; + delete modifiedNewParams.page; + } + $.extend(params, modifiedNewParams); }; /** @@ -315,129 +346,131 @@ BookReader.prototype.extendParams = function(params, newParams) { * @private * @return {object} the parsed params */ -BookReader.prototype.initParams = function() { - const params = {}; - // Flag initializing for updateFromParams() - params.init = true; - - // Flag if page given in defaults or URL (not cookie) - // Used for overriding goToFirstResult in plugin.search.js - // Note: extendParams() converts params.page to index and gets rid of page - // so check and set before extendParams() - params.pageFound = false; - - // True if changing the URL - params.fragmentChange = false; - - // This is ordered from lowest to highest priority - - // If we have a title leaf, use that as the default instead of index 0, - // but only use as default if book has a few pages - if ('undefined' != typeof(this.titleLeaf) && this.book.getNumLeafs() > 2) { - params.index = this.book.leafNumToIndex(this.titleLeaf); - } else { - params.index = 0; - } - - // this.defaults is a string passed in the url format. eg "page/1/mode/1up" - if (this.defaults) { - const defaultParams = this.paramsFromFragment(this.defaults); - if ('undefined' != typeof(defaultParams.page)) { - params.pageFound = true; - } - this.extendParams(params, defaultParams); - } - - // Check for Resume plugin - if (this.options.enablePageResume) { - // Check cookies - const val = this.getResumeValue(); - if (val !== null) { - // If page index different from default - if (params.index !== val) { - // Show in URL - params.fragmentChange = true; - } - params.index = val; - } - } - - // Check for URL plugin - if (this.options.enableUrlPlugin) { - // Params explicitly set in URL take precedence over all other methods - let urlParams = this.paramsFromFragment(this.urlReadFragment()); - - // Get params if hash fragment available with 'history' urlMode - const hasHashURL = !Object.keys(urlParams).length && this.urlReadHashFragment(); - if (hasHashURL && (this.options.urlMode === 'history')) { - urlParams = this.paramsFromFragment(this.urlReadHashFragment()); - } - - // If there were any parameters - if (Object.keys(urlParams).length) { - if ('undefined' != typeof(urlParams.page)) { - params.pageFound = true; - } - this.extendParams(params, urlParams); - // Show in URL - params.fragmentChange = true; - } - } - - // Check for Search plugin - if (this.options.enableSearch) { - // Go to first result only if no default or URL page - this.options.goToFirstResult = !params.pageFound; - - // If initialSearchTerm not set - if (!this.options.initialSearchTerm) { - // Look for any term in URL - if (params.search) { - // Old style: /search/[term] - this.options.initialSearchTerm = params.search; - this.searchTerm = params.search; - } else { - // If we have a query string: q=[term] - const searchParams = new URLSearchParams(this.readQueryString()); - const searchTerm = searchParams.get('q'); - if (searchTerm) { - this.options.initialSearchTerm = utils.decodeURIComponentPlus(searchTerm); - } - } - } - } - - // Set for init process, return to false at end of init() - this.suppressFragmentChange = !params.fragmentChange; - - return params; +BookReader.prototype.initParams = function () { + const params = {}; + // Flag initializing for updateFromParams() + params.init = true; + + // Flag if page given in defaults or URL (not cookie) + // Used for overriding goToFirstResult in plugin.search.js + // Note: extendParams() converts params.page to index and gets rid of page + // so check and set before extendParams() + params.pageFound = false; + + // True if changing the URL + params.fragmentChange = false; + + // This is ordered from lowest to highest priority + + // If we have a title leaf, use that as the default instead of index 0, + // but only use as default if book has a few pages + if ('undefined' != typeof this.titleLeaf && this.book.getNumLeafs() > 2) { + params.index = this.book.leafNumToIndex(this.titleLeaf); + } else { + params.index = 0; + } + + // this.defaults is a string passed in the url format. eg "page/1/mode/1up" + if (this.defaults) { + const defaultParams = this.paramsFromFragment(this.defaults); + if ('undefined' != typeof defaultParams.page) { + params.pageFound = true; + } + this.extendParams(params, defaultParams); + } + + // Check for Resume plugin + if (this.options.enablePageResume) { + // Check cookies + const val = this.getResumeValue(); + if (val !== null) { + // If page index different from default + if (params.index !== val) { + // Show in URL + params.fragmentChange = true; + } + params.index = val; + } + } + + // Check for URL plugin + if (this.options.enableUrlPlugin) { + // Params explicitly set in URL take precedence over all other methods + let urlParams = this.paramsFromFragment(this.urlReadFragment()); + + // Get params if hash fragment available with 'history' urlMode + const hasHashURL = + !Object.keys(urlParams).length && this.urlReadHashFragment(); + if (hasHashURL && this.options.urlMode === 'history') { + urlParams = this.paramsFromFragment(this.urlReadHashFragment()); + } + + // If there were any parameters + if (Object.keys(urlParams).length) { + if ('undefined' != typeof urlParams.page) { + params.pageFound = true; + } + this.extendParams(params, urlParams); + // Show in URL + params.fragmentChange = true; + } + } + + // Check for Search plugin + if (this.options.enableSearch) { + // Go to first result only if no default or URL page + this.options.goToFirstResult = !params.pageFound; + + // If initialSearchTerm not set + if (!this.options.initialSearchTerm) { + // Look for any term in URL + if (params.search) { + // Old style: /search/[term] + this.options.initialSearchTerm = params.search; + this.searchTerm = params.search; + } else { + // If we have a query string: q=[term] + const searchParams = new URLSearchParams(this.readQueryString()); + const searchTerm = searchParams.get('q'); + if (searchTerm) { + this.options.initialSearchTerm = + utils.decodeURIComponentPlus(searchTerm); + } + } + } + } + + // Set for init process, return to false at end of init() + this.suppressFragmentChange = !params.fragmentChange; + + return params; }; /** * Allow mocking of window.location.search */ BookReader.prototype.getLocationSearch = function () { - return window.location.search; + return window.location.search; }; /** * Allow mocking of window.location.hash */ BookReader.prototype.getLocationHash = function () { - return window.location.hash; + return window.location.hash; }; /** * Return URL or fragment querystring */ -BookReader.prototype.readQueryString = function() { - const queryString = this.getLocationSearch(); - if (queryString) { - return queryString; - } - const hash = this.getLocationHash(); - const found = hash.search(/\?\w+=/); - return found > -1 ? hash.slice(found) : ''; +BookReader.prototype.readQueryString = function () { + const queryString = this.getLocationSearch(); + if (queryString) { + return queryString; + } + const hash = this.getLocationHash(); + const found = hash.search(/\?\w+=/); + return found > -1 ? hash.slice(found) : ''; }; /** @@ -446,34 +479,34 @@ BookReader.prototype.readQueryString = function() { * @param {object} params * @return {1 | 2 | 3} the initial mode */ -BookReader.prototype.getInitialMode = function(params) { - // if mobile breakpoint, we always show this.constMode1up mode - const windowWidth = $(window).width(); - const isMobile = windowWidth && windowWidth <= this.onePageMinBreakpoint; - - let initialMode; - if (params.mode) { - initialMode = params.mode; - } else if (isMobile) { - initialMode = this.constMode1up; - } else { - initialMode = this.constMode2up; - } - - if (!this.canSwitchToMode(initialMode)) { - initialMode = this.constMode1up; - } - - // override defaults mode via `options.defaults` metadata - if (this.options.defaults) { - try { - initialMode = _modeStringToNumber(this.options.defaults); - } catch (e) { - // Can ignore this error - } - } - - return initialMode; +BookReader.prototype.getInitialMode = function (params) { + // if mobile breakpoint, we always show this.constMode1up mode + const windowWidth = $(window).width(); + const isMobile = windowWidth && windowWidth <= this.onePageMinBreakpoint; + + let initialMode; + if (params.mode) { + initialMode = params.mode; + } else if (isMobile) { + initialMode = this.constMode1up; + } else { + initialMode = this.constMode2up; + } + + if (!this.canSwitchToMode(initialMode)) { + initialMode = this.constMode1up; + } + + // override defaults mode via `options.defaults` metadata + if (this.options.defaults) { + try { + initialMode = _modeStringToNumber(this.options.defaults); + } catch (e) { + // Can ignore this error + } + } + + return initialMode; }; /** @@ -482,354 +515,363 @@ BookReader.prototype.getInitialMode = function(params) { * @return {1 | 2 | 3} */ export function _modeStringToNumber(modeString) { - const MAPPING = { - 'mode/1up': 1, - 'mode/2up': 2, - 'mode/thumb': 3, - }; + const MAPPING = { + 'mode/1up': 1, + 'mode/2up': 2, + 'mode/thumb': 3, + }; - if (!(modeString in MAPPING)) { - throw new OptionsParseError(`Invalid mode string: ${modeString}`); - } + if (!(modeString in MAPPING)) { + throw new OptionsParseError(`Invalid mode string: ${modeString}`); + } - return MAPPING[modeString]; + return MAPPING[modeString]; } /** * This is called by the client to initialize BookReader. * It renders onto the DOM. It should only be called once. */ -BookReader.prototype.init = function() { - this.init.initComplete = false; - this.pageScale = this.reduce; // preserve current reduce - - const params = this.initParams(); - - this.firstIndex = params.index ? params.index : 0; - - // Setup Navbars and other UI - this.isTouchDevice = !!('ontouchstart' in window) || !!('msmaxtouchpoints' in window.navigator); - - this.refs.$br = $(this.el) - .empty() - .removeClass() - .addClass("ui-" + this.ui) - .addClass("br-ui-" + this.ui) - .addClass('BookReader'); - - // Add a class if this is a touch enabled device - if (this.isTouchDevice) { - this.refs.$br.addClass("touch"); - } else { - this.refs.$br.addClass("no-touch"); - } - - this.refs.$brContainer = $("
"); - this.refs.$br.append(this.refs.$brContainer); - - // Explicitly ensure params.mode exists for updateFromParams() below - params.mode = this.getInitialMode(params); - - // Explicitly ensure this.mode exists for initNavbar() below - this.mode = params.mode; - - // Display Navigation - if (this.options.showToolbar) { - this.initToolbar(this.mode, this.ui); // Build inside of toolbar div - } - if (this.options.showNavbar) { // default navigation - this.initNavbar(); - } - - // Switch navbar controls on mobile/desktop - this._components.navbar.switchNavbarControls(); - - this.resizeBRcontainer(); - this.updateFromParams(params); - this.initUIStrings(); - - // Bind to events - - this.bindNavigationHandlers(); - this.setupKeyListeners(); - - this.lastScroll = (new Date().getTime()); - this.refs.$brContainer.on('scroll', this, function(e) { - // Note, this scroll event fires for both user, and js generated calls - // It is functioning in some cases as the primary triggerer for rendering - e.data.lastScroll = (new Date().getTime()); - if (e.data.constModeThumb == e.data.mode) { - e.data.drawLeafsThrottled(); - } - }); - - if (this.options.autoResize) { - $(window).on('resize', this, function(e) { - e.data.resize(); - }); - $(window).on("orientationchange", this, function(e) { - e.data.resize(); - }.bind(this)); - } - - if (this.protected) { - this.$('.BRicon.share').hide(); - } - - // If not searching, set to allow on-going fragment changes - if (!this.options.initialSearchTerm) { - this.suppressFragmentChange = false; - } - - if (this.options.startFullscreen) { - this.enterFullscreen(true); - } - - this.init.initComplete = true; - this.trigger(BookReader.eventNames.PostInit); - - // Must be called after this.init.initComplete set to true to allow - // BookReader.prototype.resize to run. - +BookReader.prototype.init = function () { + this.init.initComplete = false; + this.pageScale = this.reduce; // preserve current reduce + + const params = this.initParams(); + + this.firstIndex = params.index ? params.index : 0; + + // Setup Navbars and other UI + this.isTouchDevice = + !!('ontouchstart' in window) || !!('msmaxtouchpoints' in window.navigator); + + this.refs.$br = $(this.el) + .empty() + .removeClass() + .addClass('ui-' + this.ui) + .addClass('br-ui-' + this.ui) + .addClass('BookReader'); + + // Add a class if this is a touch enabled device + if (this.isTouchDevice) { + this.refs.$br.addClass('touch'); + } else { + this.refs.$br.addClass('no-touch'); + } + + this.refs.$brContainer = $(""); + this.refs.$br.append(this.refs.$brContainer); + + // Explicitly ensure params.mode exists for updateFromParams() below + params.mode = this.getInitialMode(params); + + // Explicitly ensure this.mode exists for initNavbar() below + this.mode = params.mode; + + // Display Navigation + if (this.options.showToolbar) { + this.initToolbar(this.mode, this.ui); // Build inside of toolbar div + } + if (this.options.showNavbar) { + // default navigation + this.initNavbar(); + } + + // Switch navbar controls on mobile/desktop + this._components.navbar.switchNavbarControls(); + + this.resizeBRcontainer(); + this.updateFromParams(params); + this.initUIStrings(); + + // Bind to events + + this.bindNavigationHandlers(); + this.setupKeyListeners(); + + this.lastScroll = new Date().getTime(); + this.refs.$brContainer.on('scroll', this, function (e) { + // Note, this scroll event fires for both user, and js generated calls + // It is functioning in some cases as the primary triggerer for rendering + e.data.lastScroll = new Date().getTime(); + if (e.data.constModeThumb == e.data.mode) { + e.data.drawLeafsThrottled(); + } + }); + + if (this.options.autoResize) { + $(window).on('resize', this, function (e) { + e.data.resize(); + }); + $(window).on( + 'orientationchange', + this, + function (e) { + e.data.resize(); + }.bind(this) + ); + } + + if (this.protected) { + this.$('.BRicon.share').hide(); + } + + // If not searching, set to allow on-going fragment changes + if (!this.options.initialSearchTerm) { + this.suppressFragmentChange = false; + } + + if (this.options.startFullscreen) { + this.enterFullscreen(true); + } + + this.init.initComplete = true; + this.trigger(BookReader.eventNames.PostInit); + + // Must be called after this.init.initComplete set to true to allow + // BookReader.prototype.resize to run. }; /** * @param {EVENTS} name * @param {array | object} [props] */ -BookReader.prototype.trigger = function(name, props = this) { - const eventName = 'BookReader:' + name; - - utils.polyfillCustomEvent(window); - window.dispatchEvent(new CustomEvent(eventName, { - bubbles: true, - composed: true, - detail: { props }, - })); - $(document).trigger(eventName, props); +BookReader.prototype.trigger = function (name, props = this) { + const eventName = 'BookReader:' + name; + + utils.polyfillCustomEvent(window); + window.dispatchEvent( + new CustomEvent(eventName, { + bubbles: true, + composed: true, + detail: { props }, + }) + ); + $(document).trigger(eventName, props); }; -BookReader.prototype.bind = function(name, callback) { - $(document).on('BookReader:' + name, callback); +BookReader.prototype.bind = function (name, callback) { + $(document).on('BookReader:' + name, callback); }; -BookReader.prototype.unbind = function(name, callback) { - $(document).off('BookReader:' + name, callback); +BookReader.prototype.unbind = function (name, callback) { + $(document).off('BookReader:' + name, callback); }; /** * Resizes based on the container width and height */ -BookReader.prototype.resize = function() { - if (!this.init.initComplete) return; - - this.resizeBRcontainer(); - - // Switch navbar controls on mobile/desktop - this._components.navbar.switchNavbarControls(); - - if (this.constMode1up == this.mode) { - if (this.onePage.autofit != 'none') { - this._modes.mode1Up.resizePageView(); - this.centerPageView(); - } else { - this.centerPageView(); - this.displayedIndices = []; - this.drawLeafsThrottled(); - } - } else if (this.constModeThumb == this.mode) { - this._modes.modeThumb.prepare(); - } else { - this._modes.mode2Up.resizePageView(); - } - this.trigger(BookReader.eventNames.resize); +BookReader.prototype.resize = function () { + if (!this.init.initComplete) return; + + this.resizeBRcontainer(); + + // Switch navbar controls on mobile/desktop + this._components.navbar.switchNavbarControls(); + + if (this.constMode1up == this.mode) { + if (this.onePage.autofit != 'none') { + this._modes.mode1Up.resizePageView(); + this.centerPageView(); + } else { + this.centerPageView(); + this.displayedIndices = []; + this.drawLeafsThrottled(); + } + } else if (this.constModeThumb == this.mode) { + this._modes.modeThumb.prepare(); + } else { + this._modes.mode2Up.resizePageView(); + } + this.trigger(BookReader.eventNames.resize); }; /** * Binds keyboard and keyboard focus event listeners */ BookReader.prototype.setupKeyListeners = function () { - - // Keyboard focus by BookReader in viewport - // - // Intersection observer and callback sets BookReader keyboard - // "focus" flag off when the BookReader is not in the viewport. - if (window.IntersectionObserver) { - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.intersectionRatio === 0) { - this.hasKeyFocus = false; - } else { - this.hasKeyFocus = true; - } - }); - }, { - root: null, - rootMargin: '0px', - threshold: [0, 0.05, 1], - }); - observer.observe(this.refs.$br[0]); - } - - // Keyboard listeners - document.addEventListener('keydown', (e) => { - - // Ignore if BookReader "focus" flag not set - if (!this.hasKeyFocus) { - return; - } - - // Ignore if modifiers are active. - if (e.getModifierState('Control') || - e.getModifierState('Alt') || - e.getModifierState('Meta') || - e.getModifierState('Win') /* hack for IE */) { - return; - } - - // Ignore in input elements - if (utils.isInputActive()) { - return; - } - - // KeyboardEvent code values: - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values - switch (e.key) { - - // Page navigation - case "Home": - e.preventDefault(); - this.first(); - break; - case "End": - e.preventDefault(); - this.last(); - break; - case "ArrowDown": - case "PageDown": - case "Down": // hack for IE and old Gecko - // In 1up and thumb mode page scrolling handled by browser - if (this.constMode2up === this.mode) { - e.preventDefault(); - this.next(); - } - break; - case "ArrowUp": - case "PageUp": - case "Up": // hack for IE and old Gecko - // In 1up and thumb mode page scrolling handled by browser - if (this.constMode2up === this.mode) { - e.preventDefault(); - this.prev(); - } - break; - case "ArrowLeft": - case "Left": // hack for IE and old Gecko - // No y-scrolling in thumb mode - if (this.constModeThumb != this.mode) { - e.preventDefault(); - this.left(); - } - break; - case "ArrowRight": - case "Right": // hack for IE and old Gecko - // No y-scrolling in thumb mode - if (this.constModeThumb != this.mode) { - e.preventDefault(); - this.right(); - } - break; - // Zoom - case '-': - case 'Subtract': - e.preventDefault(); - this.zoom(-1); - break; - case '+': - case '=': - case 'Add': - e.preventDefault(); - this.zoom(1); - break; - // Fullscreen - case 'F': - case 'f': - e.preventDefault(); - this.toggleFullscreen(); - break; - } - }); + // Keyboard focus by BookReader in viewport + // + // Intersection observer and callback sets BookReader keyboard + // "focus" flag off when the BookReader is not in the viewport. + if (window.IntersectionObserver) { + const observer = new IntersectionObserver( + entries => { + entries.forEach(entry => { + if (entry.intersectionRatio === 0) { + this.hasKeyFocus = false; + } else { + this.hasKeyFocus = true; + } + }); + }, + { + root: null, + rootMargin: '0px', + threshold: [0, 0.05, 1], + } + ); + observer.observe(this.refs.$br[0]); + } + + // Keyboard listeners + document.addEventListener('keydown', e => { + // Ignore if BookReader "focus" flag not set + if (!this.hasKeyFocus) { + return; + } + + // Ignore if modifiers are active. + if ( + e.getModifierState('Control') || + e.getModifierState('Alt') || + e.getModifierState('Meta') || + e.getModifierState('Win') /* hack for IE */ + ) { + return; + } + + // Ignore in input elements + if (utils.isInputActive()) { + return; + } + + // KeyboardEvent code values: + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values + switch (e.key) { + // Page navigation + case 'Home': + e.preventDefault(); + this.first(); + break; + case 'End': + e.preventDefault(); + this.last(); + break; + case 'ArrowDown': + case 'PageDown': + case 'Down': // hack for IE and old Gecko + // In 1up and thumb mode page scrolling handled by browser + if (this.constMode2up === this.mode) { + e.preventDefault(); + this.next(); + } + break; + case 'ArrowUp': + case 'PageUp': + case 'Up': // hack for IE and old Gecko + // In 1up and thumb mode page scrolling handled by browser + if (this.constMode2up === this.mode) { + e.preventDefault(); + this.prev(); + } + break; + case 'ArrowLeft': + case 'Left': // hack for IE and old Gecko + // No y-scrolling in thumb mode + if (this.constModeThumb != this.mode) { + e.preventDefault(); + this.left(); + } + break; + case 'ArrowRight': + case 'Right': // hack for IE and old Gecko + // No y-scrolling in thumb mode + if (this.constModeThumb != this.mode) { + e.preventDefault(); + this.right(); + } + break; + // Zoom + case '-': + case 'Subtract': + e.preventDefault(); + this.zoom(-1); + break; + case '+': + case '=': + case 'Add': + e.preventDefault(); + this.zoom(1); + break; + // Fullscreen + case 'F': + case 'f': + e.preventDefault(); + this.toggleFullscreen(); + break; + } + }); }; -BookReader.prototype.drawLeafs = function() { - if (this.constMode1up == this.mode) { - // Not needed for Mode1Up anymore - return; - } else { - this.activeMode.drawLeafs(); - } +BookReader.prototype.drawLeafs = function () { + if (this.constMode1up == this.mode) { + // Not needed for Mode1Up anymore + return; + } else { + this.activeMode.drawLeafs(); + } }; /** * @protected * @param {PageIndex} index */ -BookReader.prototype._createPageContainer = function(index) { - return new PageContainer(this.book.getPage(index, false), { - isProtected: this.protected, - imageCache: this.imageCache, - loadingImage: this.imagesBaseURL + 'loading.gif', - }); +BookReader.prototype._createPageContainer = function (index) { + return new PageContainer(this.book.getPage(index, false), { + isProtected: this.protected, + imageCache: this.imageCache, + loadingImage: this.imagesBaseURL + 'loading.gif', + }); }; -BookReader.prototype.bindGestures = function(jElement) { - // TODO support gesture change is only iOS. Support android. - // HACK(2017-01-20) - Momentum scrolling is causing the scroll position - // to jump after zooming in on mobile device. I am able to reproduce - // when you move the book with one finger and then add another - // finger to pinch. Gestures are aware of scroll state. - - const self = this; - let numTouches = 1; - - jElement.unbind('touchmove').bind('touchmove', function(e) { - if (e.originalEvent.cancelable) numTouches = e.originalEvent.touches.length; - e.stopPropagation(); - }); - - jElement.unbind('gesturechange').bind('gesturechange', function(e) { - e.preventDefault(); - // These are two very important fixes to adjust for the scroll position - // issues described below - if (!(numTouches !== 2 || (new Date().getTime()) - self.lastScroll < 500)) { - if (e.originalEvent.scale > 1.5) { - self.zoom(1); - } else if (e.originalEvent.scale < 0.6) { - self.zoom(-1); - } - } - }); +BookReader.prototype.bindGestures = function (jElement) { + // TODO support gesture change is only iOS. Support android. + // HACK(2017-01-20) - Momentum scrolling is causing the scroll position + // to jump after zooming in on mobile device. I am able to reproduce + // when you move the book with one finger and then add another + // finger to pinch. Gestures are aware of scroll state. + + const self = this; + let numTouches = 1; + + jElement.unbind('touchmove').bind('touchmove', function (e) { + if (e.originalEvent.cancelable) numTouches = e.originalEvent.touches.length; + e.stopPropagation(); + }); + + jElement.unbind('gesturechange').bind('gesturechange', function (e) { + e.preventDefault(); + // These are two very important fixes to adjust for the scroll position + // issues described below + if (!(numTouches !== 2 || new Date().getTime() - self.lastScroll < 500)) { + if (e.originalEvent.scale > 1.5) { + self.zoom(1); + } else if (e.originalEvent.scale < 0.6) { + self.zoom(-1); + } + } + }); }; /** * A throttled version of drawLeafs */ BookReader.prototype.drawLeafsThrottled = utils.throttle( - BookReader.prototype.drawLeafs, - 250 // 250 ms gives quick feedback, but doesn't eat cpu + BookReader.prototype.drawLeafs, + 250 // 250 ms gives quick feedback, but doesn't eat cpu ); /** * @param {number} direction Pass 1 to zoom in, anything else to zoom out */ -BookReader.prototype.zoom = function(direction) { - if (direction == 1) { - this.activeMode.zoom('in'); - } else { - this.activeMode.zoom('out'); - } - this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer); - return; +BookReader.prototype.zoom = function (direction) { + if (direction == 1) { + this.activeMode.zoom('in'); + } else { + this.activeMode.zoom('out'); + } + this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer); + return; }; /** @@ -841,26 +883,30 @@ BookReader.prototype.zoom = function(direction) { * This is primarily for 1up view - a follow up animation to the nav animation * So resize isn't perceived sharp/jerky */ -BookReader.prototype.resizeBRcontainer = function(animate) { - if (animate) { - this.refs.$brContainer.animate({ - top: this.getToolBarHeight(), - bottom: this.getFooterHeight() - }, this.constResizeAnimationDuration, 'linear'); - } else { - this.refs.$brContainer.css({ - top: this.getToolBarHeight(), - bottom: this.getFooterHeight() - }); - } +BookReader.prototype.resizeBRcontainer = function (animate) { + if (animate) { + this.refs.$brContainer.animate( + { + top: this.getToolBarHeight(), + bottom: this.getFooterHeight(), + }, + this.constResizeAnimationDuration, + 'linear' + ); + } else { + this.refs.$brContainer.css({ + top: this.getToolBarHeight(), + bottom: this.getFooterHeight(), + }); + } }; -BookReader.prototype.centerPageView = function() { - const scrollWidth = this.refs.$brContainer.prop('scrollWidth'); - const clientWidth = this.refs.$brContainer.prop('clientWidth'); - if (scrollWidth > clientWidth) { - this.refs.$brContainer.prop('scrollLeft', (scrollWidth - clientWidth) / 2); - } +BookReader.prototype.centerPageView = function () { + const scrollWidth = this.refs.$brContainer.prop('scrollWidth'); + const clientWidth = this.refs.$brContainer.prop('clientWidth'); + if (scrollWidth > clientWidth) { + this.refs.$brContainer.prop('scrollLeft', (scrollWidth - clientWidth) / 2); + } }; /** @@ -869,18 +915,18 @@ BookReader.prototype.centerPageView = function() { * @param {ReductionFactor[]} reductionFactors * @return {number} */ -BookReader.prototype.quantizeReduce = function(reduce, reductionFactors) { - let quantized = reductionFactors[0].reduce; - let distance = Math.abs(reduce - quantized); - - for (let i = 1; i < reductionFactors.length; i++) { - const newDistance = Math.abs(reduce - reductionFactors[i].reduce); - if (newDistance < distance) { - distance = newDistance; - quantized = reductionFactors[i].reduce; - } - } - return quantized; +BookReader.prototype.quantizeReduce = function (reduce, reductionFactors) { + let quantized = reductionFactors[0].reduce; + let distance = Math.abs(reduce - quantized); + + for (let i = 1; i < reductionFactors.length; i++) { + const newDistance = Math.abs(reduce - reductionFactors[i].reduce); + if (newDistance < distance) { + distance = newDistance; + quantized = reductionFactors[i].reduce; + } + } + return quantized; }; /** @@ -889,48 +935,54 @@ BookReader.prototype.quantizeReduce = function(reduce, reductionFactors) { * @param {ReductionFactor[]} reductionFactors Must be sorted * @returns {ReductionFactor} */ -BookReader.prototype.nextReduce = function(currentReduce, direction, reductionFactors) { - // XXX add 'closest', to replace quantize function - - if (direction === 'in') { - let newReduceIndex = 0; - for (let i = 1; i < reductionFactors.length; i++) { - if (reductionFactors[i].reduce < currentReduce) { - newReduceIndex = i; - } - } - return reductionFactors[newReduceIndex]; - } else if (direction === 'out') { - const lastIndex = reductionFactors.length - 1; - let newReduceIndex = lastIndex; - - for (let i = lastIndex; i >= 0; i--) { - if (reductionFactors[i].reduce > currentReduce) { - newReduceIndex = i; - } - } - return reductionFactors[newReduceIndex]; - } else if (direction === 'auto') { - // If an 'auto' is specified, use that - const autoMatch = reductionFactors.find(rf => rf.autofit == 'auto'); - if (autoMatch) return autoMatch; - - // Otherwise, choose the least reduction from height/width - const candidates = reductionFactors.filter(({autofit}) => autofit == 'height' || autofit == 'width'); - let choice = null; - for (let i = 0; i < candidates.length; i++) { - if (choice === null || choice.reduce < candidates[i].reduce) { - choice = candidates[i]; - } - } - if (choice) return choice; - } else if (direction === 'height' || direction === 'width') { - // Asked for specific autofit mode - const match = reductionFactors.find(rf => rf.autofit == direction); - if (match) return match; - } - - return reductionFactors[0]; +BookReader.prototype.nextReduce = function ( + currentReduce, + direction, + reductionFactors +) { + // XXX add 'closest', to replace quantize function + + if (direction === 'in') { + let newReduceIndex = 0; + for (let i = 1; i < reductionFactors.length; i++) { + if (reductionFactors[i].reduce < currentReduce) { + newReduceIndex = i; + } + } + return reductionFactors[newReduceIndex]; + } else if (direction === 'out') { + const lastIndex = reductionFactors.length - 1; + let newReduceIndex = lastIndex; + + for (let i = lastIndex; i >= 0; i--) { + if (reductionFactors[i].reduce > currentReduce) { + newReduceIndex = i; + } + } + return reductionFactors[newReduceIndex]; + } else if (direction === 'auto') { + // If an 'auto' is specified, use that + const autoMatch = reductionFactors.find(rf => rf.autofit == 'auto'); + if (autoMatch) return autoMatch; + + // Otherwise, choose the least reduction from height/width + const candidates = reductionFactors.filter( + ({ autofit }) => autofit == 'height' || autofit == 'width' + ); + let choice = null; + for (let i = 0; i < candidates.length; i++) { + if (choice === null || choice.reduce < candidates[i].reduce) { + choice = candidates[i]; + } + } + if (choice) return choice; + } else if (direction === 'height' || direction === 'width') { + // Asked for specific autofit mode + const match = reductionFactors.find(rf => rf.autofit == direction); + if (match) return match; + } + + return reductionFactors[0]; }; /** @@ -944,25 +996,26 @@ BookReader.prototype._reduceSort = (a, b) => a.reduce - b.reduce; * @param {string} * @return {boolean} Returns true if page could be found, false otherwise. */ -BookReader.prototype.jumpToPage = function(pageNum) { - const pageIndex = this.book.parsePageString(pageNum); +BookReader.prototype.jumpToPage = function (pageNum) { + const pageIndex = this.book.parsePageString(pageNum); - if ('undefined' != typeof(pageIndex)) { - this.jumpToIndex(pageIndex); - return true; - } + if ('undefined' != typeof pageIndex) { + this.jumpToIndex(pageIndex); + return true; + } - // Page not found - return false; + // Page not found + return false; }; /** * Check whether the specified index is currently displayed * @param {PageIndex} index */ -BookReader.prototype._isIndexDisplayed = function(index) { - return this.displayedIndices ? this.displayedIndices.includes(index) : - this.currentIndex() == index; +BookReader.prototype._isIndexDisplayed = function (index) { + return this.displayedIndices + ? this.displayedIndices.includes(index) + : this.currentIndex() == index; }; /** @@ -972,32 +1025,34 @@ BookReader.prototype._isIndexDisplayed = function(index) { * @param {number} [pageY] * @param {boolean} [noAnimate] */ -BookReader.prototype.jumpToIndex = function(index, pageX, pageY, noAnimate) { - // Don't jump into specific unviewable page - const page = this.book.getPage(index); - if (!page.isViewable && page.unviewablesStart != page.index) { - // If already in unviewable range, jump to end of that range - const alreadyInPreview = this._isIndexDisplayed(page.unviewablesStart); - const newIndex = alreadyInPreview ? page.findNext({ combineConsecutiveUnviewables: true }).index : page.unviewablesStart; - return this.jumpToIndex(newIndex, pageX, pageY, noAnimate); - } - - this.trigger(BookReader.eventNames.stop); - - this.activeMode.jumpToIndex(index, pageX, pageY, noAnimate); +BookReader.prototype.jumpToIndex = function (index, pageX, pageY, noAnimate) { + // Don't jump into specific unviewable page + const page = this.book.getPage(index); + if (!page.isViewable && page.unviewablesStart != page.index) { + // If already in unviewable range, jump to end of that range + const alreadyInPreview = this._isIndexDisplayed(page.unviewablesStart); + const newIndex = alreadyInPreview + ? page.findNext({ combineConsecutiveUnviewables: true }).index + : page.unviewablesStart; + return this.jumpToIndex(newIndex, pageX, pageY, noAnimate); + } + + this.trigger(BookReader.eventNames.stop); + + this.activeMode.jumpToIndex(index, pageX, pageY, noAnimate); }; /** * Return mode or 1up if initial thumb * @param {number} */ -BookReader.prototype.getPrevReadMode = function(mode) { - if (mode === BookReader.constMode1up || mode === BookReader.constMode2up) { - return mode; - } else if (this.prevReadMode === null) { - // Initial thumb, return 1up - return BookReader.constMode1up; - } +BookReader.prototype.getPrevReadMode = function (mode) { + if (mode === BookReader.constMode1up || mode === BookReader.constMode2up) { + return mode; + } else if (this.prevReadMode === null) { + // Initial thumb, return 1up + return BookReader.constMode1up; + } }; /** @@ -1007,94 +1062,92 @@ BookReader.prototype.getPrevReadMode = function(mode) { * @param {boolean} [options.suppressFragmentChange = false] * @param {boolean} [options.onInit = false] - this */ -BookReader.prototype.switchMode = function( - mode, - { - suppressFragmentChange = false, - init = false, - pageFound = false - } = {} +BookReader.prototype.switchMode = function ( + mode, + { suppressFragmentChange = false, init = false, pageFound = false } = {} ) { - // Skip checks before init() complete - if (this.init.initComplete) { - if (mode === this.mode) { - return; - } - if (!this.canSwitchToMode(mode)) { - return; - } - } - - this.trigger(BookReader.eventNames.stop); - - this.prevReadMode = this.getPrevReadMode(this.mode); - - if (this.mode != mode) { - this.activeMode.unprepare?.(); - } - - this.mode = mode; - - // reinstate scale if moving from thumbnail view - 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 (this.constMode1up == mode) { - this._modes.mode1Up.prepare(); - } else if (this.constModeThumb == mode) { - this.reduce = this.quantizeReduce(this.reduce, this.reductionFactors); - this._modes.modeThumb.prepare(); - } else { - this._modes.mode2Up.prepare(); - } - - if (!(this.suppressFragmentChange || suppressFragmentChange)) { - this.trigger(BookReader.eventNames.fragmentChange); - } - const eventName = mode + 'PageViewSelected'; - this.trigger(BookReader.eventNames[eventName]); - - this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer); + // Skip checks before init() complete + if (this.init.initComplete) { + if (mode === this.mode) { + return; + } + if (!this.canSwitchToMode(mode)) { + return; + } + } + + this.trigger(BookReader.eventNames.stop); + + this.prevReadMode = this.getPrevReadMode(this.mode); + + if (this.mode != mode) { + this.activeMode.unprepare?.(); + } + + this.mode = mode; + + // reinstate scale if moving from thumbnail view + 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 (this.constMode1up == mode) { + this._modes.mode1Up.prepare(); + } else if (this.constModeThumb == mode) { + this.reduce = this.quantizeReduce(this.reduce, this.reductionFactors); + this._modes.modeThumb.prepare(); + } else { + this._modes.mode2Up.prepare(); + } + + if (!(this.suppressFragmentChange || suppressFragmentChange)) { + this.trigger(BookReader.eventNames.fragmentChange); + } + const eventName = mode + 'PageViewSelected'; + this.trigger(BookReader.eventNames[eventName]); + + this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer); }; -BookReader.prototype.updateBrClasses = function() { - const modeToClass = {}; - modeToClass[this.constMode1up] = 'BRmode1up'; - modeToClass[this.constMode2up] = 'BRmode2up'; - modeToClass[this.constModeThumb] = 'BRmodeThumb'; - - this.refs.$br - .removeClass('BRmode1up BRmode2up BRmodeThumb') - .addClass(modeToClass[this.mode]); - - if (this.isFullscreen()) { - this.refs.$br.addClass('fullscreenActive'); - $(document.body).addClass('BRfullscreenActive'); - } else { - this.refs.$br.removeClass('fullscreenActive'); - $(document.body).removeClass('BRfullscreenActive'); - } +BookReader.prototype.updateBrClasses = function () { + const modeToClass = {}; + modeToClass[this.constMode1up] = 'BRmode1up'; + modeToClass[this.constMode2up] = 'BRmode2up'; + modeToClass[this.constModeThumb] = 'BRmodeThumb'; + + this.refs.$br + .removeClass('BRmode1up BRmode2up BRmodeThumb') + .addClass(modeToClass[this.mode]); + + if (this.isFullscreen()) { + this.refs.$br.addClass('fullscreenActive'); + $(document.body).addClass('BRfullscreenActive'); + } else { + this.refs.$br.removeClass('fullscreenActive'); + $(document.body).removeClass('BRfullscreenActive'); + } }; -BookReader.prototype.isFullscreen = function() { - return this.isFullscreenActive; +BookReader.prototype.isFullscreen = function () { + return this.isFullscreenActive; }; /** * Toggles fullscreen * @param { boolean } bindKeyboardControls */ -BookReader.prototype.toggleFullscreen = async function(bindKeyboardControls = true) { - if (this.isFullscreen()) { - await this.exitFullScreen(); - } else { - await this.enterFullscreen(bindKeyboardControls); - } +BookReader.prototype.toggleFullscreen = async function ( + bindKeyboardControls = true +) { + if (this.isFullscreen()) { + await this.exitFullScreen(); + } else { + await this.enterFullscreen(bindKeyboardControls); + } }; /** @@ -1104,45 +1157,50 @@ BookReader.prototype.toggleFullscreen = async function(bindKeyboardControls = tr * - fires custom event * @param { boolean } bindKeyboardControls */ -BookReader.prototype.enterFullscreen = async function(bindKeyboardControls = true) { - this.refs.$br.addClass('BRfullscreenAnimation'); - const currentIndex = this.currentIndex(); - - if (bindKeyboardControls) { - this._fullscreenCloseHandler = (e) => { - if (e.keyCode === 27) this.toggleFullscreen(); - }; - $(document).on("keyup", this._fullscreenCloseHandler); - } - - const windowWidth = $(window).width(); - if (windowWidth <= this.onePageMinBreakpoint) { - this.switchMode(this.constMode1up); - } - - this.isFullscreenActive = true; - // prioritize class updates so CSS can propagate - this.updateBrClasses(); - if (this.activeMode instanceof Mode1Up) { - this.activeMode.mode1UpLit.scale = this.activeMode.mode1UpLit.computeDefaultScale(this.book.getPage(currentIndex)); - // Need the new scale to be applied before calling jumpToIndex - this.activeMode.mode1UpLit.requestUpdate(); - await this.activeMode.mode1UpLit.updateComplete; - } - this.jumpToIndex(currentIndex); - - this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer); - // Add "?view=theater" - this.trigger(BookReader.eventNames.fragmentChange); - // trigger event here, so that animations, - // class updates happen before book-nav relays to web components - this.trigger(BookReader.eventNames.fullscreenToggled); - - // resize book after all events & css updates - await new Promise(resolve => setTimeout(resolve, 0)); - - this.resize(); - this.refs.$br.removeClass('BRfullscreenAnimation'); +BookReader.prototype.enterFullscreen = async function ( + bindKeyboardControls = true +) { + this.refs.$br.addClass('BRfullscreenAnimation'); + const currentIndex = this.currentIndex(); + + if (bindKeyboardControls) { + this._fullscreenCloseHandler = e => { + if (e.keyCode === 27) this.toggleFullscreen(); + }; + $(document).on('keyup', this._fullscreenCloseHandler); + } + + const windowWidth = $(window).width(); + if (windowWidth <= this.onePageMinBreakpoint) { + this.switchMode(this.constMode1up); + } + + this.isFullscreenActive = true; + // prioritize class updates so CSS can propagate + this.updateBrClasses(); + if (this.activeMode instanceof Mode1Up) { + this.activeMode.mode1UpLit.scale = + this.activeMode.mode1UpLit.computeDefaultScale( + this.book.getPage(currentIndex) + ); + // Need the new scale to be applied before calling jumpToIndex + this.activeMode.mode1UpLit.requestUpdate(); + await this.activeMode.mode1UpLit.updateComplete; + } + this.jumpToIndex(currentIndex); + + this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer); + // Add "?view=theater" + this.trigger(BookReader.eventNames.fragmentChange); + // trigger event here, so that animations, + // class updates happen before book-nav relays to web components + this.trigger(BookReader.eventNames.fullscreenToggled); + + // resize book after all events & css updates + await new Promise(resolve => setTimeout(resolve, 0)); + + this.resize(); + this.refs.$br.removeClass('BRfullscreenAnimation'); }; /** @@ -1153,34 +1211,37 @@ BookReader.prototype.enterFullscreen = async function(bindKeyboardControls = tru * @param { boolean } bindKeyboardControls */ BookReader.prototype.exitFullScreen = async function () { - this.refs.$br.addClass('BRfullscreenAnimation'); - $(document).off('keyup', this._fullscreenCloseHandler); - - const windowWidth = $(window).width(); - const canShow2up = this.options.controls.twoPage.visible; - if (canShow2up && (windowWidth <= this.onePageMinBreakpoint)) { - this.switchMode(this.constMode2up); - } - - this.isFullscreenActive = false; - // Trigger fullscreen event immediately - // so that book-nav can relay to web components - this.trigger(BookReader.eventNames.fullscreenToggled); - - this.updateBrClasses(); - await new Promise(resolve => setTimeout(resolve, 0)); - this.resize(); - - if (this.activeMode instanceof Mode1Up) { - this.activeMode.mode1UpLit.scale = this.activeMode.mode1UpLit.computeDefaultScale(this.book.getPage(this.currentIndex())); - this.activeMode.mode1UpLit.requestUpdate(); - await this.activeMode.mode1UpLit.updateComplete; - } - - this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer); - // Remove "?view=theater" - this.trigger(BookReader.eventNames.fragmentChange); - this.refs.$br.removeClass('BRfullscreenAnimation'); + this.refs.$br.addClass('BRfullscreenAnimation'); + $(document).off('keyup', this._fullscreenCloseHandler); + + const windowWidth = $(window).width(); + const canShow2up = this.options.controls.twoPage.visible; + if (canShow2up && windowWidth <= this.onePageMinBreakpoint) { + this.switchMode(this.constMode2up); + } + + this.isFullscreenActive = false; + // Trigger fullscreen event immediately + // so that book-nav can relay to web components + this.trigger(BookReader.eventNames.fullscreenToggled); + + this.updateBrClasses(); + await new Promise(resolve => setTimeout(resolve, 0)); + this.resize(); + + if (this.activeMode instanceof Mode1Up) { + this.activeMode.mode1UpLit.scale = + this.activeMode.mode1UpLit.computeDefaultScale( + this.book.getPage(this.currentIndex()) + ); + this.activeMode.mode1UpLit.requestUpdate(); + await this.activeMode.mode1UpLit.updateComplete; + } + + this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer); + // Remove "?view=theater" + this.trigger(BookReader.eventNames.fragmentChange); + this.refs.$br.removeClass('BRfullscreenAnimation'); }; /** @@ -1188,16 +1249,16 @@ BookReader.prototype.exitFullScreen = async function () { * @return {number} * @throws */ -BookReader.prototype.currentIndex = function() { - // $$$ we should be cleaner with our idea of which index is active in 1up/2up - if (this.mode == this.constMode1up || this.mode == this.constModeThumb) { - return this.firstIndex; // $$$ TODO page in center of view would be better - } else if (this.mode == this.constMode2up) { - // Only allow indices that are actually present in book - return utils.clamp(this.firstIndex, 0, this.book.getNumLeafs() - 1); - } else { - throw 'currentIndex called for unimplemented mode ' + this.mode; - } +BookReader.prototype.currentIndex = function () { + // $$$ we should be cleaner with our idea of which index is active in 1up/2up + if (this.mode == this.constMode1up || this.mode == this.constModeThumb) { + return this.firstIndex; // $$$ TODO page in center of view would be better + } else if (this.mode == this.constMode2up) { + // Only allow indices that are actually present in book + return utils.clamp(this.firstIndex, 0, this.book.getNumLeafs() - 1); + } else { + throw 'currentIndex called for unimplemented mode ' + this.mode; + } }; /** @@ -1207,109 +1268,108 @@ BookReader.prototype.currentIndex = function() { * @param {object} [options] * @param {boolean} [options.suppressFragmentChange = false] */ -BookReader.prototype.updateFirstIndex = function( - index, - { suppressFragmentChange = false } = {} +BookReader.prototype.updateFirstIndex = function ( + index, + { suppressFragmentChange = false } = {} ) { - // If there's no change, do nothing - if (this.firstIndex === index) return; - - this.firstIndex = index; - if (!(this.suppressFragmentChange || suppressFragmentChange)) { - this.trigger(BookReader.eventNames.fragmentChange); - } - // If there's an initial search we stop suppressing global URL changes - // when local suppression ends - // This seems to correctly handle multiple calls during mode/1up - if (this.options.initialSearchTerm && !suppressFragmentChange) { - this.suppressFragmentChange = false; - } - - this.trigger(BookReader.eventNames.pageChanged); - - // event to know if user is actively reading - this.trigger(BookReader.eventNames.userAction); - this._components.navbar.updateNavIndexThrottled(index); + // If there's no change, do nothing + if (this.firstIndex === index) return; + + this.firstIndex = index; + if (!(this.suppressFragmentChange || suppressFragmentChange)) { + this.trigger(BookReader.eventNames.fragmentChange); + } + // If there's an initial search we stop suppressing global URL changes + // when local suppression ends + // This seems to correctly handle multiple calls during mode/1up + if (this.options.initialSearchTerm && !suppressFragmentChange) { + this.suppressFragmentChange = false; + } + + this.trigger(EVENTS.pageChanged); + + // event to know if user is actively reading + this.trigger(BookReader.eventNames.userAction); + this._components.navbar.updateNavIndexThrottled(index); }; /** * Flip the right page over onto the left */ -BookReader.prototype.right = function() { - if ('rl' != this.pageProgression) { - this.next(); - } else { - this.prev(); - } +BookReader.prototype.right = function () { + if ('rl' != this.pageProgression) { + this.next(); + } else { + this.prev(); + } }; /** * Flip to the rightmost page */ -BookReader.prototype.rightmost = function() { - if ('rl' != this.pageProgression) { - this.last(); - } else { - this.first(); - } +BookReader.prototype.rightmost = function () { + if ('rl' != this.pageProgression) { + this.last(); + } else { + this.first(); + } }; /** * Flip the left page over onto the right */ -BookReader.prototype.left = function() { - if ('rl' != this.pageProgression) { - this.prev(); - } else { - this.next(); - } +BookReader.prototype.left = function () { + if ('rl' != this.pageProgression) { + this.prev(); + } else { + this.next(); + } }; /** * Flip to the leftmost page */ -BookReader.prototype.leftmost = function() { - if ('rl' != this.pageProgression) { - this.first(); - } else { - this.last(); - } +BookReader.prototype.leftmost = function () { + if ('rl' != this.pageProgression) { + this.first(); + } else { + this.last(); + } }; -BookReader.prototype.next = function({triggerStop = true} = {}) { - if (this.constMode2up == this.mode) { - if (triggerStop) this.trigger(BookReader.eventNames.stop); - this._modes.mode2Up.mode2UpLit.flipAnimation('next'); - } else { - if (this.firstIndex < this.book.getNumLeafs() - 1) { - this.jumpToIndex(this.firstIndex + 1); - } - } +BookReader.prototype.next = function ({ triggerStop = true } = {}) { + if (this.constMode2up == this.mode) { + if (triggerStop) this.trigger(BookReader.eventNames.stop); + this._modes.mode2Up.mode2UpLit.flipAnimation('next'); + } else { + if (this.firstIndex < this.book.getNumLeafs() - 1) { + this.jumpToIndex(this.firstIndex + 1); + } + } }; -BookReader.prototype.prev = function({triggerStop = true} = {}) { - const isOnFrontPage = this.firstIndex < 1; - if (isOnFrontPage) return; - - if (this.constMode2up == this.mode) { - if (triggerStop) this.trigger(BookReader.eventNames.stop); - this._modes.mode2Up.mode2UpLit.flipAnimation('prev'); - } else { - if (this.firstIndex >= 1) { - this.jumpToIndex(this.firstIndex - 1); - } - } +BookReader.prototype.prev = function ({ triggerStop = true } = {}) { + const isOnFrontPage = this.firstIndex < 1; + if (isOnFrontPage) return; + + if (this.constMode2up == this.mode) { + if (triggerStop) this.trigger(BookReader.eventNames.stop); + this._modes.mode2Up.mode2UpLit.flipAnimation('prev'); + } else { + if (this.firstIndex >= 1) { + this.jumpToIndex(this.firstIndex - 1); + } + } }; -BookReader.prototype.first = function() { - this.jumpToIndex(0); +BookReader.prototype.first = function () { + this.jumpToIndex(0); }; -BookReader.prototype.last = function() { - this.jumpToIndex(this.book.getNumLeafs() - 1); +BookReader.prototype.last = function () { + this.jumpToIndex(this.book.getNumLeafs() - 1); }; - /** * @template TClass extends { br: BookReader } * Helper method to expose a method onto BookReader from a composed class. @@ -1321,14 +1381,13 @@ BookReader.prototype.last = function() { * @param {string} [brMethod] */ function exposeOverrideableMethod(Class, classKey, method, brMethod = method) { - /** @type {function(TClass): BookReader} */ - const classToBr = cls => cls.br; - /** @type {function(BookReader): TClass} */ - const brToClass = br => br._overrideable[classKey]; - exposeOverrideable(Class, method, classToBr, BookReader, brMethod, brToClass); + /** @type {function(TClass): BookReader} */ + const classToBr = cls => cls.br; + /** @type {function(BookReader): TClass} */ + const brToClass = br => br._overrideable[classKey]; + exposeOverrideable(Class, method, classToBr, BookReader, brMethod, brToClass); } - /***********************/ /** Navbar extensions **/ /***********************/ @@ -1339,7 +1398,8 @@ exposeOverrideableMethod(Navbar, '_components.navbar', 'init', 'initNavbar'); /************************/ /** Toolbar extensions **/ /************************/ -BookReader.prototype.buildToolbarElement = Toolbar.prototype.buildToolbarElement; +BookReader.prototype.buildToolbarElement = + Toolbar.prototype.buildToolbarElement; exposeOverrideableMethod(Toolbar, '_components.toolbar', 'buildToolbarElement'); BookReader.prototype.initToolbar = Toolbar.prototype.initToolbar; exposeOverrideableMethod(Toolbar, '_components.toolbar', 'initToolbar'); @@ -1353,131 +1413,141 @@ exposeOverrideableMethod(Toolbar, '_components.toolbar', 'getToolBarHeight'); /** * Bind navigation handlers */ -BookReader.prototype.bindNavigationHandlers = function() { - const self = this; - const jIcons = this.$('.BRicon'); - - // Map of jIcon class -> click handler - const navigationControls = { - book_left: () => { - this.trigger(BookReader.eventNames.stop); - this.left(); - }, - book_right: () => { - this.trigger(BookReader.eventNames.stop); - this.right(); - }, - book_top: this.first.bind(this), - book_bottom: this.last.bind(this), - book_leftmost: this.leftmost.bind(this), - book_rightmost: this.rightmost.bind(this), - onepg: () => { - this.switchMode(self.constMode1up); - }, - thumb: () => { - this.switchMode(self.constModeThumb); - }, - twopg: () => { - this.switchMode(self.constMode2up); - }, - zoom_in: () => { - this.trigger(BookReader.eventNames.stop); - this.zoom(1); - this.trigger(BookReader.eventNames.zoomIn); - }, - zoom_out: () => { - this.trigger(BookReader.eventNames.stop); - this.zoom(-1); - this.trigger(BookReader.eventNames.zoomOut); - }, - full: () => { - if (this.ui == 'embed') { - const url = this.$('.BRembedreturn a').attr('href'); - window.open(url); - } else { - this.toggleFullscreen(); - } - }, - }; - - // custom event for auto-loan-renew in ia-book-actions - // - to know if user is actively reading - this.$('nav.BRcontrols li button').on('click', () => { - this.trigger(BookReader.eventNames.userAction); - }); - - for (const control in navigationControls) { - jIcons.filter(`.${control}`).on('click.bindNavigationHandlers', () => { - navigationControls[control](); - return false; - }); - } - - const $brNavCntlBtmEl = this.$('.BRnavCntlBtm'); - const $brNavCntlTopEl = this.$('.BRnavCntlTop'); - - this.$('.BRnavCntl').click( - function() { - const promises = []; - // TODO don't use magic constants - // TODO move this to a function - if ($brNavCntlBtmEl.hasClass('BRdn')) { - if (self.refs.$BRtoolbar) - promises.push(self.refs.$BRtoolbar.animate( - {top: self.getToolBarHeight() * -1} - ).promise()); - promises.push(self.$('.BRfooter').animate({bottom: self.getFooterHeight() * -1}).promise()); - $brNavCntlBtmEl.addClass('BRup').removeClass('BRdn'); - $brNavCntlTopEl.addClass('BRdn').removeClass('BRup'); - self.$('.BRnavCntlBtm.BRnavCntl').animate({height:'45px'}); - self.$('.BRnavCntl').delay(1000).animate({opacity:.75}, 1000); - } else { - if (self.refs.$BRtoolbar) - promises.push(self.refs.$BRtoolbar.animate({top:0}).promise()); - promises.push(self.$('.BRfooter').animate({bottom:0}).promise()); - $brNavCntlBtmEl.addClass('BRdn').removeClass('BRup'); - $brNavCntlTopEl.addClass('BRup').removeClass('BRdn'); - self.$('.BRnavCntlBtm.BRnavCntl').animate({height:'30px'}); - self.$('.BRvavCntl').animate({opacity:1}); - } - $.when.apply($, promises).done(function() { - // Only do full resize in auto mode and need to recalc. size - if (self.mode == self.constMode2up && self.twoPage.autofit != null - && self.twoPage.autofit != 'none' - ) { - self.resize(); - } else if (self.mode == self.constMode1up && self.onePage.autofit != null - && self.onePage.autofit != 'none') { - self.resize(); - } else { - // Don't do a full resize to avoid redrawing images - self.resizeBRcontainer(); - } - }); - } - ); - $brNavCntlBtmEl - .on("mouseover", function() { - if ($(this).hasClass('BRup')) { - self.$('.BRnavCntl').animate({opacity:1},250); - } - }) - .on("mouseleave", function() { - if ($(this).hasClass('BRup')) { - self.$('.BRnavCntl').animate({opacity:.75},250); - } - }); - $brNavCntlTopEl - .on("mouseover", function() { - if ($(this).hasClass('BRdn')) { - self.$('.BRnavCntl').animate({opacity:1},250); - } - }) - .on("mouseleave", function() { - if ($(this).hasClass('BRdn')) { - self.$('.BRnavCntl').animate({opacity:.75},250); - } - }); +BookReader.prototype.bindNavigationHandlers = function () { + const self = this; + const jIcons = this.$('.BRicon'); + + // Map of jIcon class -> click handler + const navigationControls = { + book_left: () => { + this.trigger(BookReader.eventNames.stop); + this.left(); + }, + book_right: () => { + this.trigger(BookReader.eventNames.stop); + this.right(); + }, + book_top: this.first.bind(this), + book_bottom: this.last.bind(this), + book_leftmost: this.leftmost.bind(this), + book_rightmost: this.rightmost.bind(this), + onepg: () => { + this.switchMode(self.constMode1up); + }, + thumb: () => { + this.switchMode(self.constModeThumb); + }, + twopg: () => { + this.switchMode(self.constMode2up); + }, + zoom_in: () => { + this.trigger(BookReader.eventNames.stop); + this.zoom(1); + this.trigger(BookReader.eventNames.zoomIn); + }, + zoom_out: () => { + this.trigger(BookReader.eventNames.stop); + this.zoom(-1); + this.trigger(BookReader.eventNames.zoomOut); + }, + full: () => { + if (this.ui == 'embed') { + const url = this.$('.BRembedreturn a').attr('href'); + window.open(url); + } else { + this.toggleFullscreen(); + } + }, + }; + + // custom event for auto-loan-renew in ia-book-actions + // - to know if user is actively reading + this.$('nav.BRcontrols li button').on('click', () => { + this.trigger(BookReader.eventNames.userAction); + }); + + for (const control in navigationControls) { + jIcons.filter(`.${control}`).on('click.bindNavigationHandlers', () => { + navigationControls[control](); + return false; + }); + } + + const $brNavCntlBtmEl = this.$('.BRnavCntlBtm'); + const $brNavCntlTopEl = this.$('.BRnavCntlTop'); + + this.$('.BRnavCntl').click(function () { + const promises = []; + // TODO don't use magic constants + // TODO move this to a function + if ($brNavCntlBtmEl.hasClass('BRdn')) { + if (self.refs.$BRtoolbar) + promises.push( + self.refs.$BRtoolbar + .animate({ top: self.getToolBarHeight() * -1 }) + .promise() + ); + promises.push( + self + .$('.BRfooter') + .animate({ bottom: self.getFooterHeight() * -1 }) + .promise() + ); + $brNavCntlBtmEl.addClass('BRup').removeClass('BRdn'); + $brNavCntlTopEl.addClass('BRdn').removeClass('BRup'); + self.$('.BRnavCntlBtm.BRnavCntl').animate({ height: '45px' }); + self.$('.BRnavCntl').delay(1000).animate({ opacity: 0.75 }, 1000); + } else { + if (self.refs.$BRtoolbar) + promises.push(self.refs.$BRtoolbar.animate({ top: 0 }).promise()); + promises.push(self.$('.BRfooter').animate({ bottom: 0 }).promise()); + $brNavCntlBtmEl.addClass('BRdn').removeClass('BRup'); + $brNavCntlTopEl.addClass('BRup').removeClass('BRdn'); + self.$('.BRnavCntlBtm.BRnavCntl').animate({ height: '30px' }); + self.$('.BRvavCntl').animate({ opacity: 1 }); + } + $.when.apply($, promises).done(function () { + // Only do full resize in auto mode and need to recalc. size + if ( + self.mode == self.constMode2up && + self.twoPage.autofit != null && + self.twoPage.autofit != 'none' + ) { + self.resize(); + } else if ( + self.mode == self.constMode1up && + self.onePage.autofit != null && + self.onePage.autofit != 'none' + ) { + self.resize(); + } else { + // Don't do a full resize to avoid redrawing images + self.resizeBRcontainer(); + } + }); + }); + $brNavCntlBtmEl + .on('mouseover', function () { + if ($(this).hasClass('BRup')) { + self.$('.BRnavCntl').animate({ opacity: 1 }, 250); + } + }) + .on('mouseleave', function () { + if ($(this).hasClass('BRup')) { + self.$('.BRnavCntl').animate({ opacity: 0.75 }, 250); + } + }); + $brNavCntlTopEl + .on('mouseover', function () { + if ($(this).hasClass('BRdn')) { + self.$('.BRnavCntl').animate({ opacity: 1 }, 250); + } + }) + .on('mouseleave', function () { + if ($(this).hasClass('BRdn')) { + self.$('.BRnavCntl').animate({ opacity: 0.75 }, 250); + } + }); }; /**************************/ @@ -1488,58 +1558,52 @@ BookReader.prototype.bindNavigationHandlers = function() { BookReader.prototype.getPageURI = BookModel.prototype.getPageURI; exposeOverrideableMethod(BookModel, 'book', 'getPageURI'); - // Parameter related functions /** * Update from the params object * @param {Object} */ -BookReader.prototype.updateFromParams = function(params) { - // Set init, fragment change options for switchMode() - const { - mode = 0, - init = false, - fragmentChange = false, - } = params; - - if (mode) { - this.switchMode( - mode, - { init: init, suppressFragmentChange: !fragmentChange } - ); - } - - // $$$ process /zoom - // We only respect page if index is not set - if ('undefined' != typeof(params.index)) { - if (params.index != this.currentIndex()) { - this.jumpToIndex(params.index); - } - } else if ('undefined' != typeof(params.page)) { - // $$$ this assumes page numbers are unique - if (params.page != this.book.getPageNum(this.currentIndex())) { - this.jumpToPage(params.page); - } - } - - - // process /search - // @deprecated for urlMode 'history' - // Continues to work for urlMode 'hash' - if (this.enableSearch && 'undefined' != typeof(params.search)) { - if (this.searchTerm !== params.search) { - this.$('.BRsearchInput').val(params.search); - } - } - - // $$$ process /region - // $$$ process /highlight - - // $$$ process /theme - if (this.enableThemesPlugin && 'undefined' != typeof(params.theme)) { - this.updateTheme(params.theme); - } +BookReader.prototype.updateFromParams = function (params) { + // Set init, fragment change options for switchMode() + const { mode = 0, init = false, fragmentChange = false } = params; + + if (mode) { + this.switchMode(mode, { + init: init, + suppressFragmentChange: !fragmentChange, + }); + } + + // $$$ process /zoom + // We only respect page if index is not set + if ('undefined' != typeof params.index) { + if (params.index != this.currentIndex()) { + this.jumpToIndex(params.index); + } + } else if ('undefined' != typeof params.page) { + // $$$ this assumes page numbers are unique + if (params.page != this.book.getPageNum(this.currentIndex())) { + this.jumpToPage(params.page); + } + } + + // process /search + // @deprecated for urlMode 'history' + // Continues to work for urlMode 'hash' + if (this.enableSearch && 'undefined' != typeof params.search) { + if (this.searchTerm !== params.search) { + this.$('.BRsearchInput').val(params.search); + } + } + + // $$$ process /region + // $$$ process /highlight + + // $$$ process /theme + if (this.enableThemesPlugin && 'undefined' != typeof params.theme) { + this.updateTheme(params.theme); + } }; /** @@ -1547,17 +1611,17 @@ BookReader.prototype.updateFromParams = function(params) { * @param {number} mode * @return {boolean} */ -BookReader.prototype.canSwitchToMode = function(mode) { - 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.book.getNumLeafs() < 2) { - return false; - } - } - - return true; +BookReader.prototype.canSwitchToMode = function (mode) { + 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.book.getNumLeafs() < 2) { + return false; + } + } + + return true; }; /** @@ -1568,137 +1632,140 @@ BookReader.prototype.canSwitchToMode = function(mode) { * @param {number} [rotate] * @return {string} */ -BookReader.prototype._getPageURI = function(index, reduce, rotate) { - const page = this.book.getPage(index, false); - // Synthesize page - if (!page) return this.imagesBaseURL + "transparent.png"; - - if ('undefined' == typeof(reduce)) { - // reduce not passed in - // $$$ this probably won't work for thumbnail mode - reduce = page.height / this.twoPage.height; - } - - return page.getURI(reduce, rotate); +BookReader.prototype._getPageURI = function (index, reduce, rotate) { + const page = this.book.getPage(index, false); + // Synthesize page + if (!page) return this.imagesBaseURL + 'transparent.png'; + + if ('undefined' == typeof reduce) { + // reduce not passed in + // $$$ this probably won't work for thumbnail mode + reduce = page.height / this.twoPage.height; + } + + return page.getURI(reduce, rotate); }; /** * @param {string} msg * @param {function|undefined} onCloseCallback */ -BookReader.prototype.showProgressPopup = function(msg, onCloseCallback) { - if (this.popup) return; - - this.popup = document.createElement("div"); - $(this.popup).prop('className', 'BRprogresspopup'); - - if (typeof(onCloseCallback) === 'function') { - const closeButton = document.createElement('button'); - closeButton.setAttribute('title', 'close'); - closeButton.setAttribute('class', 'close-popup'); - const icon = document.createElement('span'); - icon.setAttribute('class', 'icon icon-close-dark'); - $(closeButton).append(icon); - closeButton.addEventListener('click', () => { - onCloseCallback(); - this.removeProgressPopup(); - }); - $(this.popup).append(closeButton); - } - - const bar = document.createElement("div"); - $(bar).css({ - height: '20px' - }).prop('className', 'BRprogressbar'); - $(this.popup).append(bar); - - if (msg) { - const msgdiv = document.createElement("div"); - msgdiv.innerHTML = msg; - $(this.popup).append(msgdiv); - } - - $(this.popup).appendTo(this.refs.$br); +BookReader.prototype.showProgressPopup = function (msg, onCloseCallback) { + if (this.popup) return; + + this.popup = document.createElement('div'); + $(this.popup).prop('className', 'BRprogresspopup'); + + if (typeof onCloseCallback === 'function') { + const closeButton = document.createElement('button'); + closeButton.setAttribute('title', 'close'); + closeButton.setAttribute('class', 'close-popup'); + const icon = document.createElement('span'); + icon.setAttribute('class', 'icon icon-close-dark'); + $(closeButton).append(icon); + closeButton.addEventListener('click', () => { + onCloseCallback(); + this.removeProgressPopup(); + }); + $(this.popup).append(closeButton); + } + + const bar = document.createElement('div'); + $(bar) + .css({ + height: '20px', + }) + .prop('className', 'BRprogressbar'); + $(this.popup).append(bar); + + if (msg) { + const msgdiv = document.createElement('div'); + msgdiv.innerHTML = msg; + $(this.popup).append(msgdiv); + } + + $(this.popup).appendTo(this.refs.$br); }; -BookReader.prototype.removeProgressPopup = function() { - $(this.popup).remove(); - this.$('.BRprogresspopup').remove(); - this.popup = null; +BookReader.prototype.removeProgressPopup = function () { + $(this.popup).remove(); + this.$('.BRprogresspopup').remove(); + this.popup = null; }; /** * Can be overridden */ -BookReader.prototype.initUIStrings = function() { - // 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 - const titles = { - '.logo': 'Go to Archive.org', // $$$ update after getting OL record - '.zoom_in': 'Zoom in', - '.zoom_out': 'Zoom out', - '.onepg': 'One-page view', - '.twopg': 'Two-page view', - '.thumb': 'Thumbnail view', - '.print': 'Print this page', - '.embed': 'Embed BookReader', - '.link': 'Link to this book (and page)', - '.bookmark': 'Bookmark this page', - '.share': 'Share this book', - '.info': 'About this book', - '.full': 'Toggle fullscreen', - '.book_left': 'Flip left', - '.book_right': 'Flip right', - '.play': 'Play', - '.pause': 'Pause', - '.BRdn': 'Show/hide nav bar', // Would have to keep updating on state change to have just "Hide nav bar" - '.BRup': 'Show/hide nav bar', - '.book_top': 'First page', - '.book_bottom': 'Last page', - '.book_leftmost': 'First page', - '.book_rightmost': 'Last page', - }; - if ('rl' == this.pageProgression) { - titles['.book_leftmost'] = 'Last page'; - titles['.book_rightmost'] = 'First page'; - } - - for (const icon in titles) { - this.$(icon).prop('title', titles[icon]); - } +BookReader.prototype.initUIStrings = function () { + // 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 + const titles = { + '.logo': 'Go to Archive.org', // $$$ update after getting OL record + '.zoom_in': 'Zoom in', + '.zoom_out': 'Zoom out', + '.onepg': 'One-page view', + '.twopg': 'Two-page view', + '.thumb': 'Thumbnail view', + '.print': 'Print this page', + '.embed': 'Embed BookReader', + '.link': 'Link to this book (and page)', + '.bookmark': 'Bookmark this page', + '.share': 'Share this book', + '.info': 'About this book', + '.full': 'Toggle fullscreen', + '.book_left': 'Flip left', + '.book_right': 'Flip right', + '.play': 'Play', + '.pause': 'Pause', + '.BRdn': 'Show/hide nav bar', // Would have to keep updating on state change to have just "Hide nav bar" + '.BRup': 'Show/hide nav bar', + '.book_top': 'First page', + '.book_bottom': 'Last page', + '.book_leftmost': 'First page', + '.book_rightmost': 'Last page', + }; + if ('rl' == this.pageProgression) { + titles['.book_leftmost'] = 'Last page'; + titles['.book_rightmost'] = 'First page'; + } + + for (const icon in titles) { + this.$(icon).prop('title', titles[icon]); + } }; /** * Reloads images. Useful when some images might have failed. */ -BookReader.prototype.reloadImages = function() { - this.refs.$brContainer.find('img').each(function(index, elem) { - if (!elem.complete || elem.naturalHeight === 0) { - const src = elem.src; - elem.src = ''; - setTimeout(function() { - elem.src = src; - }, 1000); - } - }); +BookReader.prototype.reloadImages = function () { + this.refs.$brContainer.find('img').each(function (index, elem) { + if (!elem.complete || elem.naturalHeight === 0) { + const src = elem.src; + elem.src = ''; + setTimeout(function () { + elem.src = src; + }, 1000); + } + }); }; /** * @param {boolean} ignoreDisplay - bypass the display check * @return {number} */ -BookReader.prototype.getFooterHeight = function() { - const $heightEl = this.mode == this.constMode2up ? this.refs.$BRfooter : this.refs.$BRnav; - if ($heightEl && this.refs.$BRfooter) { - const outerHeight = $heightEl.outerHeight(); - const bottom = parseInt(this.refs.$BRfooter.css('bottom')); - if (!isNaN(outerHeight) && !isNaN(bottom)) { - return outerHeight + bottom; - } - } - return 0; +BookReader.prototype.getFooterHeight = function () { + const $heightEl = + this.mode == this.constMode2up ? this.refs.$BRfooter : this.refs.$BRnav; + if ($heightEl && this.refs.$BRfooter) { + const outerHeight = $heightEl.outerHeight(); + const bottom = parseInt(this.refs.$BRfooter.css('bottom')); + if (!isNaN(outerHeight) && !isNaN(bottom)) { + return outerHeight + bottom; + } + } + return 0; }; // Basic Usage built-in Methods (can be overridden through options) @@ -1708,35 +1775,35 @@ BookReader.prototype.getFooterHeight = function() { * Create a params object from the current parameters. * @return {Object} */ -BookReader.prototype.paramsFromCurrent = function() { - const params = {}; - - // Path params - const index = this.currentIndex(); - const pageNum = this.book.getPageNum(index); - if ((pageNum === 0) || pageNum) { - params.page = pageNum; - } - - params.index = index; - params.mode = this.mode; - - // Unused params - // $$$ highlight - // $$$ region - - // Querystring params - // View - const fullscreenView = 'theater'; - if (this.isFullscreenActive) { - params.view = fullscreenView; - } - // Search - if (this.enableSearch) { - params.search = this.searchTerm; - } - - return params; +BookReader.prototype.paramsFromCurrent = function () { + const params = {}; + + // Path params + const index = this.currentIndex(); + const pageNum = this.book.getPageNum(index); + if (pageNum === 0 || pageNum) { + params.page = pageNum; + } + + params.index = index; + params.mode = this.mode; + + // Unused params + // $$$ highlight + // $$$ region + + // Querystring params + // View + const fullscreenView = 'theater'; + if (this.isFullscreenActive) { + params.view = fullscreenView; + } + // Search + if (this.enableSearch) { + params.search = this.searchTerm; + } + + return params; }; /** @@ -1751,60 +1818,60 @@ BookReader.prototype.paramsFromCurrent = function() { * but is deprecated * @return {Object} */ -BookReader.prototype.paramsFromFragment = function(fragment) { - const params = {}; - - // For backwards compatibility we allow an initial # character - // (as from window.location.hash) but don't require it - if (fragment.substr(0, 1) == '#') { - fragment = fragment.substr(1); - } - - // Simple #nn syntax - const oldStyleLeafNum = parseInt( /^\d+$/.exec(fragment) ); - if ( !isNaN(oldStyleLeafNum) ) { - params.index = oldStyleLeafNum; - - // Done processing if using old-style syntax - return params; - } - - // Split into key-value pairs - const urlArray = fragment.split('/'); - const urlHash = {}; - for (let i = 0; i < urlArray.length; i += 2) { - urlHash[urlArray[i]] = urlArray[i + 1]; - } - - // Mode - if ('1up' == urlHash['mode']) { - params.mode = this.constMode1up; - } else if ('2up' == urlHash['mode']) { - params.mode = this.constMode2up; - } else if ('thumb' == urlHash['mode']) { - params.mode = this.constModeThumb; - } - - // Index and page - if ('undefined' != typeof(urlHash['page'])) { - // page was set -- may not be int - params.page = decodeURIComponent(urlHash['page']); - } - - // $$$ process /region - // $$$ process /search - - if (urlHash['search'] != undefined) { - params.search = utils.decodeURIComponentPlus(urlHash['search']); - } - - // $$$ process /highlight - - // $$$ process /theme - if (urlHash['theme'] != undefined) { - params.theme = urlHash['theme']; - } - return params; +BookReader.prototype.paramsFromFragment = function (fragment) { + const params = {}; + + // For backwards compatibility we allow an initial # character + // (as from window.location.hash) but don't require it + if (fragment.substr(0, 1) == '#') { + fragment = fragment.substr(1); + } + + // Simple #nn syntax + const oldStyleLeafNum = parseInt(/^\d+$/.exec(fragment)); + if (!isNaN(oldStyleLeafNum)) { + params.index = oldStyleLeafNum; + + // Done processing if using old-style syntax + return params; + } + + // Split into key-value pairs + const urlArray = fragment.split('/'); + const urlHash = {}; + for (let i = 0; i < urlArray.length; i += 2) { + urlHash[urlArray[i]] = urlArray[i + 1]; + } + + // Mode + if ('1up' == urlHash['mode']) { + params.mode = this.constMode1up; + } else if ('2up' == urlHash['mode']) { + params.mode = this.constMode2up; + } else if ('thumb' == urlHash['mode']) { + params.mode = this.constModeThumb; + } + + // Index and page + if ('undefined' != typeof urlHash['page']) { + // page was set -- may not be int + params.page = decodeURIComponent(urlHash['page']); + } + + // $$$ process /region + // $$$ process /search + + if (urlHash['search'] != undefined) { + params.search = utils.decodeURIComponentPlus(urlHash['search']); + } + + // $$$ process /highlight + + // $$$ process /theme + if (urlHash['theme'] != undefined) { + params.theme = urlHash['theme']; + } + return params; }; /** @@ -1819,40 +1886,40 @@ BookReader.prototype.paramsFromFragment = function(fragment) { * @param {string} [urlMode] * @return {string} */ -BookReader.prototype.fragmentFromParams = function(params, urlMode = 'hash') { - const fragments = []; - - if ('undefined' != typeof(params.page)) { - fragments.push('page', encodeURIComponent(params.page)); - } else { - if ('undefined' != typeof(params.index)) { - // Don't have page numbering but we do have the index - fragments.push('page', 'n' + params.index); - } - } - - // $$$ highlight - // $$$ region - - // mode - if ('undefined' != typeof(params.mode)) { - if (params.mode == this.constMode1up) { - fragments.push('mode', '1up'); - } else if (params.mode == this.constMode2up) { - fragments.push('mode', '2up'); - } else if (params.mode == this.constModeThumb) { - fragments.push('mode', 'thumb'); - } else { - throw 'fragmentFromParams called with unknown mode ' + params.mode; - } - } - - // search - if (params.search && urlMode === 'hash') { - fragments.push('search', utils.encodeURIComponentPlus(params.search)); - } - - return fragments.join('/'); +BookReader.prototype.fragmentFromParams = function (params, urlMode = 'hash') { + const fragments = []; + + if ('undefined' != typeof params.page) { + fragments.push('page', encodeURIComponent(params.page)); + } else { + if ('undefined' != typeof params.index) { + // Don't have page numbering but we do have the index + fragments.push('page', 'n' + params.index); + } + } + + // $$$ highlight + // $$$ region + + // mode + if ('undefined' != typeof params.mode) { + if (params.mode == this.constMode1up) { + fragments.push('mode', '1up'); + } else if (params.mode == this.constMode2up) { + fragments.push('mode', '2up'); + } else if (params.mode == this.constModeThumb) { + fragments.push('mode', 'thumb'); + } else { + throw 'fragmentFromParams called with unknown mode ' + params.mode; + } + } + + // search + if (params.search && urlMode === 'hash') { + fragments.push('search', utils.encodeURIComponentPlus(params.search)); + } + + return fragments.join('/'); }; /** @@ -1866,35 +1933,35 @@ BookReader.prototype.fragmentFromParams = function(params, urlMode = 'hash') { * @param {string} [urlMode] * @return {string} */ -BookReader.prototype.queryStringFromParams = function( - params, - currQueryString, - urlMode = 'hash' +BookReader.prototype.queryStringFromParams = function ( + params, + currQueryString, + urlMode = 'hash' ) { - const newParams = new URLSearchParams(currQueryString); - - if (params.view) { - // Set ?view=theater when fullscreen - newParams.set('view', params.view); - } else { - // Remove - newParams.delete('view'); - } - - if (params.search && urlMode === 'history') { - newParams.set('q', params.search); - } - // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString - // Note: This method returns the query string without the question mark. - const result = newParams.toString(); - return result ? '?' + result : ''; + const newParams = new URLSearchParams(currQueryString); + + if (params.view) { + // Set ?view=theater when fullscreen + newParams.set('view', params.view); + } else { + // Remove + newParams.delete('view'); + } + + if (params.search && urlMode === 'history') { + newParams.set('q', params.search); + } + // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString + // Note: This method returns the query string without the question mark. + const result = newParams.toString(); + return result ? '?' + result : ''; }; /** * Helper to select within instance's elements */ -BookReader.prototype.$ = function(selector) { - return this.refs.$br.find(selector); +BookReader.prototype.$ = function (selector) { + return this.refs.$br.find(selector); }; window.BookReader = BookReader; diff --git a/src/BookReader/ModeThumb.js b/src/BookReader/ModeThumb.js index 69b411bd1..081dfd2f1 100644 --- a/src/BookReader/ModeThumb.js +++ b/src/BookReader/ModeThumb.js @@ -2,341 +2,389 @@ import { notInArray, clamp } from './utils.js'; import { EVENTS } from './events.js'; import { DragScrollable } from './DragScrollable.js'; -/** @typedef {import('../BookREader.js').default} BookReader */ +/** @typedef {import('../BookReader.js').default} BookReader */ /** @typedef {import('./BookModel.js').PageIndex} PageIndex */ /** @typedef {import('./BookModel.js').BookModel} BookModel */ /** @typedef {JQuery} $lazyLoadImgPlaceholder * jQuery element with data attributes: leaf, reduce */ export class ModeThumb { - /** - * @param {BookReader} br - * @param {BookModel} bookModel - */ - constructor(br, bookModel) { - this.br = br; - this.book = bookModel; - } - - /** - * Draws the thumbnail view - * @param {number} [seekIndex] If seekIndex is defined, the view will be drawn - * with that page visible (without any animated scrolling). - * - * Creates place holder for image to load after gallery has been drawn - */ - drawLeafs(seekIndex) { - const { floor } = Math; - const { book } = this; - const viewWidth = this.br.refs.$brContainer.prop('scrollWidth') - 20; // width minus buffer - - let leafHeight; - let rightPos = 0; - let bottomPos = 0; - let maxRight = 0; - let currentRow = 0; - let leafIndex = 0; - /** @type {Array<{ leafs?: Array<{num: PageIndex, left: number}>, height?: number, top?: number }>} */ - const leafMap = []; - - // Will be set to top of requested seek index, if set - let seekTop; - - // Calculate the position of every thumbnail. $$$ cache instead of calculating on every draw - // make `leafMap` - for (const page of book.pagesIterator({ combineConsecutiveUnviewables: true })) { - const leafWidth = this.br.thumbWidth; - if (rightPos + (leafWidth + this.br.thumbPadding) > viewWidth) { - currentRow++; - rightPos = 0; - leafIndex = 0; - } - - // Init current row in leafMap - if (!leafMap[currentRow]) { - leafMap[currentRow] = {}; - } - if (!leafMap[currentRow].leafs) { - leafMap[currentRow].leafs = []; - leafMap[currentRow].height = 0; - leafMap[currentRow].top = 0; - } - leafMap[currentRow].leafs[leafIndex] = { - num: page.index, - left: rightPos, - }; - - leafHeight = floor((page.height * this.br.thumbWidth) / page.width); - if (leafHeight > leafMap[currentRow].height) { - leafMap[currentRow].height = leafHeight; - } - if (leafIndex === 0) { bottomPos += this.br.thumbPadding + leafMap[currentRow].height; } - rightPos += leafWidth + this.br.thumbPadding; - if (rightPos > maxRight) { maxRight = rightPos; } - leafIndex++; - - if (page.index == seekIndex) { - seekTop = bottomPos - this.br.thumbPadding - leafMap[currentRow].height; - } - } - - // reset the bottom position based on thumbnails - this.br.refs.$brPageViewEl.height(bottomPos); - - const pageViewBuffer = floor((this.br.refs.$brContainer.prop('scrollWidth') - maxRight) / 2) - 14; - - // If seekTop is defined, seeking was requested and target found - if (typeof(seekTop) != 'undefined') { - this.br.refs.$brContainer.scrollTop(seekTop); - } - - const scrollTop = this.br.refs.$brContainer.prop('scrollTop'); - const scrollBottom = scrollTop + this.br.refs.$brContainer.height(); - - let leafTop = 0; - let leafBottom = 0; - const rowsToDisplay = []; - const imagesToDisplay = []; - - // Visible leafs with least/greatest index - let leastVisible = book.getNumLeafs() - 1; - let mostVisible = 0; - - // Determine the thumbnails in view - for (let i = 0; i < leafMap.length; i++) { - if (!leafMap[i]) { continue; } - leafBottom += this.br.thumbPadding + leafMap[i].height; - const topInView = (leafTop >= scrollTop) && (leafTop <= scrollBottom); - const bottomInView = (leafBottom >= scrollTop) && (leafBottom <= scrollBottom); - const middleInView = (leafTop <= scrollTop) && (leafBottom >= scrollBottom); - if (topInView || bottomInView || middleInView) { - 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; } - leafTop = leafBottom; - } - // at this point, `rowsToDisplay` now has all the rows in view - - // create a buffer of preloaded rows before and after the visible rows - const firstRow = rowsToDisplay[0]; - const lastRow = rowsToDisplay[rowsToDisplay.length - 1]; - for (let i = 1; i < this.br.thumbRowBuffer + 1; i++) { - if (lastRow + i < leafMap.length) { rowsToDisplay.push(lastRow + i); } - } - for (let i = 1; i < this.br.thumbRowBuffer; i++) { - if (firstRow - i >= 0) { rowsToDisplay.push(firstRow - i); } - } - rowsToDisplay.sort((a, b) => a - b); - - // Create the thumbnail divs and images (lazy loaded) - for (const row of rowsToDisplay) { - if (notInArray(row, this.br.displayedRows)) { - if (!leafMap[row]) { continue; } - for (const { num: leaf, left: leafLeft } of leafMap[row].leafs) { - const leafWidth = this.br.thumbWidth; - const leafHeight = floor((book.getPageHeight(leaf) * this.br.thumbWidth) / book.getPageWidth(leaf)); - const leafTop = leafMap[row].top; - let left = leafLeft + pageViewBuffer; - if ('rl' == this.br.pageProgression) { - left = viewWidth - leafWidth - left; - } - - left += this.br.thumbPadding; - imagesToDisplay.push(leaf); - - /* get thumbnail's reducer */ - const idealReduce = floor(book.getPageWidth(leaf) / this.br.thumbWidth); - const nearestFactor2 = 2 * Math.round(idealReduce / 2); - const thumbReduce = nearestFactor2; - - const pageContainer = this.br._createPageContainer(leaf) - .update({ - dimensions: { - width: leafWidth, - height: leafHeight, - top: leafTop, - left, - }, - reduce: thumbReduce, - }); - - pageContainer.$container.data('leaf', leaf).on('mouseup', event => { - // We want to suppress the fragmentChange triggers in `updateFirstIndex` and `switchMode` - // because otherwise it repeatedly triggers listeners and we get in an infinite loop. - // We manually trigger the `fragmentChange` once at the end. - this.br.updateFirstIndex(leaf, { suppressFragmentChange: true }); - // as per request in webdev-4042, we want to switch 1-up mode while clicking on thumbnail leafs - this.br.switchMode(this.br.constMode1up, { suppressFragmentChange: true }); - - // shift viewModeOrder after clicking on thumbsnail leaf - const nextModeID = this.br.viewModeOrder.shift(); - this.br.viewModeOrder.push(nextModeID); - this.br._components.navbar.updateViewModeButton($('.viewmode'), 'twopg', 'Two-page view'); - - this.br.trigger(EVENTS.fragmentChange); - event.stopPropagation(); - }); - - this.br.refs.$brPageViewEl.append(pageContainer.$container); - } - } - } - - // Remove thumbnails that are not to be displayed - for (const row of this.br.displayedRows) { - if (notInArray(row, rowsToDisplay)) { - for (const { num: index } of leafMap[row]?.leafs) { - if (!imagesToDisplay?.includes(index)) { - this.br.$(`.pagediv${index}`)?.remove(); - } - } - } - } - - // Update which page is considered current to make sure a visible page is the current one - const currentIndex = this.br.currentIndex(); - if (currentIndex < leastVisible) { - this.br.updateFirstIndex(leastVisible); - } else if (currentIndex > mostVisible) { - this.br.updateFirstIndex(mostVisible); - } - - // remember what rows are displayed - this.br.displayedRows = rowsToDisplay.slice(); - - // remove previous highlights - this.br.$('.BRpagedivthumb_highlight').removeClass('BRpagedivthumb_highlight'); - - // highlight current page - this.br.$('.pagediv' + this.br.currentIndex()).addClass('BRpagedivthumb_highlight'); - } - - /** - * Replaces placeholder image with real one - * - * @param {$lazyLoadImgPlaceholder} imgPlaceholder - */ - lazyLoadImage(imgPlaceholder) { - const leaf = $(imgPlaceholder).data('leaf'); - const reduce = $(imgPlaceholder).data('reduce'); - const $img = this.br.imageCache.image(leaf, reduce); - const $parent = $(imgPlaceholder).parent(); - /* March 16, 2021 (isa) - manually append & remove, `replaceWith` currently loses closure scope */ - $($parent).append($img); - $(imgPlaceholder).remove(); - } - - /** - * @param {'in' | 'out'} direction - */ - zoom(direction) { - const oldColumns = this.br.thumbColumns; - switch (direction) { - case 'in': - this.br.thumbColumns -= 1; - break; - case 'out': - this.br.thumbColumns += 1; - break; - default: - console.error(`Unsupported direction: ${direction}`); - } - - // Limit zoom in/out columns - this.br.thumbColumns = clamp( - this.br.thumbColumns, - this.br.options.thumbMinZoomColumns, - this.br.options.thumbMaxZoomColumns - ); - - if (this.br.thumbColumns != oldColumns) { - this.br.displayedRows = []; /* force a gallery redraw */ - this.prepare(); - } - } - - /** - * 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 - * @param {number} thumbnailColumns - */ - getThumbnailWidth(thumbnailColumns) { - const DEFAULT_THUMBNAIL_WIDTH = 100; - - const padding = (thumbnailColumns + 1) * this.br.thumbPadding; - const width = (this.br.refs.$brPageViewEl.width() - padding) / (thumbnailColumns + 0.5); // extra 0.5 is for some space at sides - const idealThumbnailWidth = Math.floor(width); - return idealThumbnailWidth > 0 ? idealThumbnailWidth : DEFAULT_THUMBNAIL_WIDTH; - } - - prepare() { - this.br.refs.$brContainer.empty(); - this.br.refs.$brContainer.css({ - overflowY: 'scroll', - overflowX: 'auto' - }); - - this.br.refs.$brPageViewEl = $(""); - this.br.refs.$brContainer.append(this.br.refs.$brPageViewEl); - this.dragScrollable = this.dragScrollable || new DragScrollable(this.br.refs.$brContainer[0], {preventDefault: true}); - - this.br.bindGestures(this.br.refs.$brContainer); - - // $$$ keep select enabled for now since disabling it breaks keyboard - // nav in FF 3.6 (https://bugs.edge.launchpad.net/bookreader/+bug/544666) - // disableSelect(this.br.$('#BRpageview')); - this.br.thumbWidth = this.getThumbnailWidth(this.br.thumbColumns); - this.br.reduce = this.book.getPageWidth(0) / this.br.thumbWidth; - this.br.displayedRows = []; - // Draw leafs with current index directly in view (no animating to the index) - this.drawLeafs(this.br.currentIndex()); - this.br.updateBrClasses(); - } - - /** - * @param {PageIndex} index - */ - jumpToIndex(index) { - const { floor } = Math; - const { book } = this; - const viewWidth = this.br.refs.$brContainer.prop('scrollWidth') - 20; // width minus buffer - const leafWidth = this.br.thumbWidth; - let leafTop = 0; - let rightPos = 0; - let bottomPos = 0; - let rowHeight = 0; - let leafIndex = 0; - - for (let i = 0; i <= index; i++) { - if (rightPos + (leafWidth + this.br.thumbPadding) > viewWidth) { - rightPos = 0; - rowHeight = 0; - leafIndex = 0; - } - - const leafHeight = floor((book.getPageHeight(leafIndex) * this.br.thumbWidth) / book.getPageWidth(leafIndex)); - if (leafHeight > rowHeight) { rowHeight = leafHeight; } - if (leafIndex == 0) { - leafTop = bottomPos; - bottomPos += this.br.thumbPadding + rowHeight; - } - rightPos += leafWidth + this.br.thumbPadding; - leafIndex++; - } - this.br.updateFirstIndex(index); - if (this.br.refs.$brContainer.prop('scrollTop') == leafTop) { - this.br.drawLeafs(); - } else { - this.br.animating = true; - this.br.refs.$brContainer.stop(true) - .animate({ scrollTop: leafTop }, 'fast', () => { this.br.animating = false; }); - } - } + /** + * @param {BookReader} br + * @param {BookModel} bookModel + */ + constructor(br, bookModel) { + this.br = br; + this.book = bookModel; + } + + /** + * Draws the thumbnail view + * @param {number} [seekIndex] If seekIndex is defined, the view will be drawn + * with that page visible (without any animated scrolling). + * + * Creates place holder for image to load after gallery has been drawn + */ + drawLeafs(seekIndex) { + const { floor } = Math; + const { book } = this; + const viewWidth = this.br.refs.$brContainer.prop('scrollWidth') - 20; // width minus buffer + + let leafHeight; + let rightPos = 0; + let bottomPos = 0; + let maxRight = 0; + let currentRow = 0; + let leafIndex = 0; + /** @type {Array<{ leafs?: Array<{num: PageIndex, left: number}>, height?: number, top?: number }>} */ + const leafMap = []; + + // Will be set to top of requested seek index, if set + let seekTop; + + // Calculate the position of every thumbnail. $$$ cache instead of calculating on every draw + // make `leafMap` + for (const page of book.pagesIterator({ + combineConsecutiveUnviewables: true, + })) { + const leafWidth = this.br.thumbWidth; + if (rightPos + (leafWidth + this.br.thumbPadding) > viewWidth) { + currentRow++; + rightPos = 0; + leafIndex = 0; + } + + // Init current row in leafMap + if (!leafMap[currentRow]) { + leafMap[currentRow] = {}; + } + if (!leafMap[currentRow].leafs) { + leafMap[currentRow].leafs = []; + leafMap[currentRow].height = 0; + leafMap[currentRow].top = 0; + } + leafMap[currentRow].leafs[leafIndex] = { + num: page.index, + left: rightPos, + }; + + leafHeight = floor((page.height * this.br.thumbWidth) / page.width); + if (leafHeight > leafMap[currentRow].height) { + leafMap[currentRow].height = leafHeight; + } + if (leafIndex === 0) { + bottomPos += this.br.thumbPadding + leafMap[currentRow].height; + } + rightPos += leafWidth + this.br.thumbPadding; + if (rightPos > maxRight) { + maxRight = rightPos; + } + leafIndex++; + + if (page.index == seekIndex) { + seekTop = bottomPos - this.br.thumbPadding - leafMap[currentRow].height; + } + } + + // reset the bottom position based on thumbnails + this.br.refs.$brPageViewEl.height(bottomPos); + + const pageViewBuffer = + floor((this.br.refs.$brContainer.prop('scrollWidth') - maxRight) / 2) - + 14; + + // If seekTop is defined, seeking was requested and target found + if (typeof seekTop != 'undefined') { + this.br.refs.$brContainer.scrollTop(seekTop); + } + + const scrollTop = this.br.refs.$brContainer.prop('scrollTop'); + const scrollBottom = scrollTop + this.br.refs.$brContainer.height(); + + let leafTop = 0; + let leafBottom = 0; + const rowsToDisplay = []; + const imagesToDisplay = []; + + // Visible leafs with least/greatest index + let leastVisible = book.getNumLeafs() - 1; + let mostVisible = 0; + + // Determine the thumbnails in view + for (let i = 0; i < leafMap.length; i++) { + if (!leafMap[i]) { + continue; + } + leafBottom += this.br.thumbPadding + leafMap[i].height; + const topInView = leafTop >= scrollTop && leafTop <= scrollBottom; + const bottomInView = + leafBottom >= scrollTop && leafBottom <= scrollBottom; + const middleInView = leafTop <= scrollTop && leafBottom >= scrollBottom; + if (topInView || bottomInView || middleInView) { + 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; + } + leafTop = leafBottom; + } + // at this point, `rowsToDisplay` now has all the rows in view + + // create a buffer of preloaded rows before and after the visible rows + const firstRow = rowsToDisplay[0]; + const lastRow = rowsToDisplay[rowsToDisplay.length - 1]; + for (let i = 1; i < this.br.thumbRowBuffer + 1; i++) { + if (lastRow + i < leafMap.length) { + rowsToDisplay.push(lastRow + i); + } + } + for (let i = 1; i < this.br.thumbRowBuffer; i++) { + if (firstRow - i >= 0) { + rowsToDisplay.push(firstRow - i); + } + } + rowsToDisplay.sort((a, b) => a - b); + + // Create the thumbnail divs and images (lazy loaded) + for (const row of rowsToDisplay) { + if (notInArray(row, this.br.displayedRows)) { + if (!leafMap[row]) { + continue; + } + for (const { num: leaf, left: leafLeft } of leafMap[row].leafs) { + const leafWidth = this.br.thumbWidth; + const leafHeight = floor( + (book.getPageHeight(leaf) * this.br.thumbWidth) / + book.getPageWidth(leaf) + ); + const leafTop = leafMap[row].top; + let left = leafLeft + pageViewBuffer; + if ('rl' == this.br.pageProgression) { + left = viewWidth - leafWidth - left; + } + + left += this.br.thumbPadding; + imagesToDisplay.push(leaf); + + /* get thumbnail's reducer */ + const idealReduce = floor( + book.getPageWidth(leaf) / this.br.thumbWidth + ); + const nearestFactor2 = 2 * Math.round(idealReduce / 2); + const thumbReduce = nearestFactor2; + + const pageContainer = this.br._createPageContainer(leaf).update({ + dimensions: { + width: leafWidth, + height: leafHeight, + top: leafTop, + left, + }, + reduce: thumbReduce, + }); + + pageContainer.$container.data('leaf', leaf).on('mouseup', event => { + // We want to suppress the fragmentChange triggers in `updateFirstIndex` and `switchMode` + // because otherwise it repeatedly triggers listeners and we get in an infinite loop. + // We manually trigger the `fragmentChange` once at the end. + this.br.updateFirstIndex(leaf, { suppressFragmentChange: true }); + // as per request in webdev-4042, we want to switch 1-up mode while clicking on thumbnail leafs + this.br.switchMode(this.br.constMode1up, { + suppressFragmentChange: true, + }); + + // shift viewModeOrder after clicking on thumbsnail leaf + const nextModeID = this.br.viewModeOrder.shift(); + this.br.viewModeOrder.push(nextModeID); + this.br._components.navbar.updateViewModeButton( + $('.viewmode'), + 'twopg', + 'Two-page view' + ); + + this.br.trigger(EVENTS.fragmentChange); + event.stopPropagation(); + }); + + this.br.refs.$brPageViewEl.append(pageContainer.$container); + } + } + } + + // Remove thumbnails that are not to be displayed + for (const row of this.br.displayedRows) { + if (notInArray(row, rowsToDisplay)) { + for (const { num: index } of leafMap[row]?.leafs) { + if (!imagesToDisplay?.includes(index)) { + this.br.$(`.pagediv${index}`)?.remove(); + } + } + } + } + + // Update which page is considered current to make sure a visible page is the current one + const currentIndex = this.br.currentIndex(); + if (currentIndex < leastVisible) { + this.br.updateFirstIndex(leastVisible); + } else if (currentIndex > mostVisible) { + this.br.updateFirstIndex(mostVisible); + } + + // remember what rows are displayed + this.br.displayedRows = rowsToDisplay.slice(); + + // remove previous highlights + this.br + .$('.BRpagedivthumb_highlight') + .removeClass('BRpagedivthumb_highlight'); + + // highlight current page + this.br + .$('.pagediv' + this.br.currentIndex()) + .addClass('BRpagedivthumb_highlight'); + } + + /** + * Replaces placeholder image with real one + * + * @param {$lazyLoadImgPlaceholder} imgPlaceholder + */ + lazyLoadImage(imgPlaceholder) { + const leaf = $(imgPlaceholder).data('leaf'); + const reduce = $(imgPlaceholder).data('reduce'); + const $img = this.br.imageCache.image(leaf, reduce); + const $parent = $(imgPlaceholder).parent(); + /* March 16, 2021 (isa) - manually append & remove, `replaceWith` currently loses closure scope */ + $($parent).append($img); + $(imgPlaceholder).remove(); + } + + /** + * @param {'in' | 'out'} direction + */ + zoom(direction) { + const oldColumns = this.br.thumbColumns; + switch (direction) { + case 'in': + this.br.thumbColumns -= 1; + break; + case 'out': + this.br.thumbColumns += 1; + break; + default: + console.error(`Unsupported direction: ${direction}`); + } + + // Limit zoom in/out columns + this.br.thumbColumns = clamp( + this.br.thumbColumns, + this.br.options.thumbMinZoomColumns, + this.br.options.thumbMaxZoomColumns + ); + + if (this.br.thumbColumns != oldColumns) { + this.br.displayedRows = []; /* force a gallery redraw */ + this.prepare(); + } + } + + /** + * 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 + * @param {number} thumbnailColumns + */ + getThumbnailWidth(thumbnailColumns) { + const DEFAULT_THUMBNAIL_WIDTH = 100; + + const padding = (thumbnailColumns + 1) * this.br.thumbPadding; + const width = + (this.br.refs.$brPageViewEl.width() - padding) / (thumbnailColumns + 0.5); // extra 0.5 is for some space at sides + const idealThumbnailWidth = Math.floor(width); + return idealThumbnailWidth > 0 + ? idealThumbnailWidth + : DEFAULT_THUMBNAIL_WIDTH; + } + + prepare() { + this.br.refs.$brContainer.empty(); + this.br.refs.$brContainer.css({ + overflowY: 'scroll', + overflowX: 'auto', + }); + + this.br.refs.$brPageViewEl = $(""); + this.br.refs.$brContainer.append(this.br.refs.$brPageViewEl); + this.dragScrollable = + this.dragScrollable || + new DragScrollable(this.br.refs.$brContainer[0], { + preventDefault: true, + }); + + this.br.bindGestures(this.br.refs.$brContainer); + + // $$$ keep select enabled for now since disabling it breaks keyboard + // nav in FF 3.6 (https://bugs.edge.launchpad.net/bookreader/+bug/544666) + // disableSelect(this.br.$('#BRpageview')); + this.br.thumbWidth = this.getThumbnailWidth(this.br.thumbColumns); + this.br.reduce = this.book.getPageWidth(0) / this.br.thumbWidth; + this.br.displayedRows = []; + // Draw leafs with current index directly in view (no animating to the index) + this.drawLeafs(this.br.currentIndex()); + this.br.updateBrClasses(); + } + + /** + * @param {PageIndex} index + */ + jumpToIndex(index) { + const { floor } = Math; + const { book } = this; + const viewWidth = this.br.refs.$brContainer.prop('scrollWidth') - 20; // width minus buffer + const leafWidth = this.br.thumbWidth; + let leafTop = 0; + let rightPos = 0; + let bottomPos = 0; + let rowHeight = 0; + let leafIndex = 0; + + for (let i = 0; i <= index; i++) { + if (rightPos + (leafWidth + this.br.thumbPadding) > viewWidth) { + rightPos = 0; + rowHeight = 0; + leafIndex = 0; + } + + const leafHeight = floor( + (book.getPageHeight(leafIndex) * this.br.thumbWidth) / + book.getPageWidth(leafIndex) + ); + if (leafHeight > rowHeight) { + rowHeight = leafHeight; + } + if (leafIndex == 0) { + leafTop = bottomPos; + bottomPos += this.br.thumbPadding + rowHeight; + } + rightPos += leafWidth + this.br.thumbPadding; + leafIndex++; + } + this.br.updateFirstIndex(index); + if (this.br.refs.$brContainer.prop('scrollTop') == leafTop) { + this.br.drawLeafs(); + } else { + this.br.animating = true; + this.br.refs.$brContainer + .stop(true) + .animate({ scrollTop: leafTop }, 'fast', () => { + this.br.animating = false; + }); + } + } }