diff --git a/src/BookReader.js b/src/BookReader.js index 0251c23fc..36e475bdd 100644 --- a/src/BookReader.js +++ b/src/BookReader.js @@ -68,6 +68,8 @@ BookReader.constModeThumb = 3; BookReader.PLUGINS = { /** @type {typeof import('./plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin | null}*/ archiveAnalytics: null, + /** @type {typeof import('./plugins/plugin.autoplay.js').AutoplayPlugin | null}*/ + autoplay: null, /** @type {typeof import('./plugins/plugin.text_selection.js').TextSelectionPlugin | null}*/ textSelection: null, }; @@ -168,11 +170,7 @@ BookReader.prototype.setup = function(options) { this.displayedIndices = []; this.animating = false; - this.flipSpeed = typeof options.flipSpeed === 'number' ? options.flipSpeed : { - 'fast': 200, - 'slow': 600, - }[options.flipSpeed] || 400; - this.flipDelay = options.flipDelay; + this.flipSpeed = utils.parseAnimationSpeed(options.flipSpeed) || 400; /** * Represents the first displayed index @@ -261,6 +259,7 @@ BookReader.prototype.setup = function(options) { // Construct the usual suspects first to get type hints this._plugins = { archiveAnalytics: BookReader.PLUGINS.archiveAnalytics ? new BookReader.PLUGINS.archiveAnalytics(this) : null, + autoplay: BookReader.PLUGINS.autoplay ? new BookReader.PLUGINS.autoplay(this) : null, textSelection: BookReader.PLUGINS.textSelection ? new BookReader.PLUGINS.textSelection(this) : null, }; @@ -1343,10 +1342,19 @@ BookReader.prototype.leftmost = function() { } }; -BookReader.prototype.next = function({triggerStop = true} = {}) { +/** + * @param {object} options + * @param {boolean} [options.triggerStop = true] + * @param {number | 'fast' | 'slow'} [options.flipSpeed] + */ +BookReader.prototype.next = function({ + triggerStop = true, + flipSpeed = null, +} = {}) { if (this.constMode2up == this.mode) { if (triggerStop) this.trigger(BookReader.eventNames.stop); - this._modes.mode2Up.mode2UpLit.flipAnimation('next'); + flipSpeed = utils.parseAnimationSpeed(flipSpeed) || this.flipSpeed; + this._modes.mode2Up.mode2UpLit.flipAnimation('next', {flipSpeed}); } else { if (this.firstIndex < this.book.getNumLeafs() - 1) { this.jumpToIndex(this.firstIndex + 1); @@ -1354,13 +1362,22 @@ BookReader.prototype.next = function({triggerStop = true} = {}) { } }; -BookReader.prototype.prev = function({triggerStop = true} = {}) { +/** + * @param {object} options + * @param {boolean} [options.triggerStop = true] + * @param {number | 'fast' | 'slow'} [options.flipSpeed] + */ +BookReader.prototype.prev = function({ + triggerStop = true, + flipSpeed = null, +} = {}) { 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'); + flipSpeed = utils.parseAnimationSpeed(flipSpeed) || this.flipSpeed; + this._modes.mode2Up.mode2UpLit.flipAnimation('prev', {flipSpeed}); } else { if (this.firstIndex >= 1) { this.jumpToIndex(this.firstIndex - 1); @@ -1545,6 +1562,11 @@ BookReader.prototype.bindNavigationHandlers = function() { self.$('.BRnavCntl').animate({opacity:.75},250); } }); + + // Call _bindNavigationHandlers on the plugins + for (const plugin of Object.values(this._plugins)) { + plugin._bindNavigationHandlers(); + } }; /**************************/ diff --git a/src/BookReader/Mode2UpLit.js b/src/BookReader/Mode2UpLit.js index e542a4f20..dc626d25f 100644 --- a/src/BookReader/Mode2UpLit.js +++ b/src/BookReader/Mode2UpLit.js @@ -493,13 +493,14 @@ export class Mode2UpLit extends LitElement { /** * @param {'left' | 'right' | 'next' | 'prev' | PageIndex | PageModel | {left: PageModel | null, right: PageModel | null}} nextSpread */ - async flipAnimation(nextSpread, { animate = true } = {}) { + async flipAnimation(nextSpread, { animate = true, flipSpeed = this.flipSpeed } = {}) { const curSpread = (this.pageLeft || this.pageRight)?.spread; if (!curSpread) { // Nothings been actually rendered yet! Will be corrected during initFirstRender return; } + flipSpeed = flipSpeed || this.flipSpeed; // Handle null nextSpread = this.parseNextSpread(nextSpread); if (this.activeFlip || !nextSpread) return; @@ -559,7 +560,7 @@ export class Mode2UpLit extends LitElement { /** @type {KeyframeAnimationOptions} */ const animationStyle = { - duration: this.flipSpeed + this.activeFlip.pagesFlippingCount, + duration: flipSpeed + this.activeFlip.pagesFlippingCount, easing: 'ease-in', fill: 'none', }; diff --git a/src/BookReader/options.js b/src/BookReader/options.js index 635d6c62c..7515bcd1d 100644 --- a/src/BookReader/options.js +++ b/src/BookReader/options.js @@ -143,6 +143,8 @@ export const DEFAULT_OPTIONS = { plugins: { /** @type {import('../plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin['options']}*/ archiveAnalytics: null, + /** @type {import('../plugins/plugin.autoplay.js').AutoplayPlugin['options']}*/ + autoplay: null, /** @type {import('../plugins/plugin.text_selection.js').TextSelectionPlugin['options']} */ textSelection: null, }, diff --git a/src/BookReader/utils.js b/src/BookReader/utils.js index 4306340c6..f5fd0aa7e 100644 --- a/src/BookReader/utils.js +++ b/src/BookReader/utils.js @@ -288,3 +288,13 @@ export function promisifyEvent(target, eventType) { export function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string } + +/** + * @param {number | 'fast' | 'slow' | string} speed + * Parsing of the jquery animation speed; see https://api.jquery.com/animate/ + */ +export function parseAnimationSpeed(speed) { + if (speed === 'slow') return 600; + if (speed === 'fast') return 200; + return parseInt(speed, 10); +} diff --git a/src/BookReaderPlugin.js b/src/BookReaderPlugin.js index 209f0cd7d..df1937543 100644 --- a/src/BookReaderPlugin.js +++ b/src/BookReaderPlugin.js @@ -33,4 +33,7 @@ export class BookReaderPlugin { */ _configurePageContainer(pageContainer) { } + + /** @abstract @protected */ + _bindNavigationHandlers() {} } diff --git a/src/plugins/plugin.autoplay.js b/src/plugins/plugin.autoplay.js index 4736c825b..e6f697133 100644 --- a/src/plugins/plugin.autoplay.js +++ b/src/plugins/plugin.autoplay.js @@ -1,128 +1,124 @@ -/*global BookReader */ +// @ts-check +import { EVENTS } from "../BookReader/events"; +import { parseAnimationSpeed } from "../BookReader/utils"; +import { BookReaderPlugin } from "../BookReaderPlugin"; /** * Plugin which adds an autoplay feature. Useful for kiosk situations. */ -jQuery.extend(BookReader.defaultOptions, { - enableAutoPlayPlugin: true, -}); - -/** - * @override BookReader.setup - */ -BookReader.prototype.setup = (function(super_) { - return function (options) { - super_.call(this, options); - - this.autoTimer = null; - this.flipDelay = 5000; - }; -})(BookReader.prototype.setup); +export class AutoplayPlugin extends BookReaderPlugin { + options = { + enabled: true, + /** + * @type {number | 'fast' | 'slow'} + * How quickly the flip animation should run. + **/ + flipSpeed: 1500, + /** How long to pause on each page between flips */ + flipDelay: 5000, + /** Allow controlling the autoflip/speed/delay from the url */ + urlParams: true, + } -/** - * @override BookReader.init - */ -BookReader.prototype.init = (function(super_) { - return function (options) { - super_.call(this, options); + timer = null; - if (!this.options.enableAutoPlayPlugin) return; + /** @override */ + init() { + if (!this.options.enabled) return; - this.bind(BookReader.eventNames.stop, () => this.autoStop()); + this.br.bind(EVENTS.stop, () => this.stop()); - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.get('autoflip') === '1') { - this.autoToggle(); + if (this.options.urlParams) { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('flipSpeed')) { + this.options.flipSpeed = parseAnimationSpeed(urlParams.get('flipSpeed')) || this.options.flipSpeed; + } + if (urlParams.get('flipDelay')) { + this.options.flipDelay = parseAnimationSpeed(urlParams.get('flipDelay')) || this.options.flipDelay; + } + if (urlParams.get('autoflip') === '1') { + this.toggle(); + } } - }; -})(BookReader.prototype.init); - -/** - * @override BookReader.bindNavigationHandlers - */ -BookReader.prototype.bindNavigationHandlers = (function(super_) { - return function() { - super_.call(this); + } - if (!this.options.enableAutoPlayPlugin) return; + /** @override */ + _bindNavigationHandlers() { + if (!this.options.enabled) return; - const jIcons = this.$('.BRicon'); + const jIcons = this.br.$('.BRicon'); - jIcons.filter('.play').click(() => { - this.autoToggle(); + jIcons.filter('.play').on('click', () => { + this.toggle(); return false; }); - jIcons.filter('.pause').click(() => { - this.autoToggle(); + jIcons.filter('.pause').on('click', () => { + this.toggle(); return false; }); - }; -})(BookReader.prototype.bindNavigationHandlers); - -/** - * Starts autoplay mode - * @param {object} overrides - * @param {number} overrides.flipSpeed - * @param {number} overrides.flipDelay - */ -BookReader.prototype.autoToggle = function(overrides) { - if (!this.options.enableAutoPlayPlugin) return; - - const options = $.extend({ - flipSpeed: this.flipSpeed, - flipDelay: this.flipDelay, - }, overrides); - - this.flipSpeed = typeof options.flipSpeed === "number" ? options.flipSpeed : this.flipSpeed; - this.flipDelay = typeof options.flipDelay === "number" ? options.flipDelay : this.flipDelay; - this.trigger(BookReader.eventNames.stop); - - let bComingFrom1up = false; - if (this.constMode2up != this.mode) { - bComingFrom1up = true; - this.switchMode(this.constMode2up); } - if (null == this.autoTimer) { - // $$$ Draw events currently cause layout problems when they occur during animation. - // There is a specific problem when changing from 1-up immediately to autoplay in RTL so - // we workaround for now by not triggering immediate animation in that case. - // See https://bugs.launchpad.net/gnubook/+bug/328327 - if (('rl' == this.pageProgression) && bComingFrom1up) { - // don't flip immediately -- wait until timer fires - } else { - // flip immediately - this.next({ triggerStop: false }); + /** + * Starts autoplay mode + * @param {object} overrides + * @param {number} overrides.flipSpeed + * @param {number} overrides.flipDelay + */ + toggle(overrides = null) { + if (!this.options.enabled) return; + + Object.assign(this.options, overrides); + this.br.trigger(EVENTS.stop); + + let bComingFrom1up = false; + if (this.br.constMode2up != this.br.mode) { + bComingFrom1up = true; + this.br.switchMode(this.br.constMode2up); } - this.$('.play').hide(); - this.$('.pause').show(); - this.autoTimer = setInterval(() => { - if (this.animating) return; - - if (Math.max(this.twoPage.currentIndexL, this.twoPage.currentIndexR) >= this.book.getNumLeafs() - 1) { - this.prev({ triggerStop: false }); // $$$ really what we want? + if (null == this.timer) { + // $$$ Draw events currently cause layout problems when they occur during animation. + // There is a specific problem when changing from 1-up immediately to autoplay in RTL so + // we workaround for now by not triggering immediate animation in that case. + // See https://bugs.launchpad.net/gnubook/+bug/328327 + if (('rl' == this.br.pageProgression) && bComingFrom1up) { + // don't flip immediately -- wait until timer fires } else { - this.next({ triggerStop: false }); + // flip immediately + this.br.next({ triggerStop: false, flipSpeed: this.options.flipSpeed }); } - }, this.flipDelay); - } else { - this.autoStop(); + + this.br.$('.play').hide(); + this.br.$('.pause').show(); + this.timer = setInterval(() => { + if (this.br.animating) return; + + if (Math.max(this.br.twoPage.currentIndexL, this.br.twoPage.currentIndexR) >= this.br.book.getNumLeafs() - 1) { + this.br.prev({ triggerStop: false, flipSpeed: this.options.flipSpeed }); // $$$ really what we want? + } else { + this.br.next({ triggerStop: false, flipSpeed: this.options.flipSpeed }); + } + }, parseAnimationSpeed(this.options.flipDelay)); + } else { + this.stop(); + } } -}; -/** - * Stop autoplay mode, allowing animations to finish - */ -BookReader.prototype.autoStop = function() { - if (!this.options.enableAutoPlayPlugin) return; - - if (null != this.autoTimer) { - clearInterval(this.autoTimer); - this.flipSpeed = 'fast'; - this.$('.pause').hide(); - this.$('.play').show(); - this.autoTimer = null; + /** + * Stop autoplay mode, allowing animations to finish + */ + stop() { + if (!this.options.enabled) return; + + if (null != this.timer) { + clearInterval(this.timer); + this.br.$('.pause').hide(); + this.br.$('.play').show(); + this.timer = null; + } } -}; +} + +const BookReader = /** @type {typeof import('../BookReader').default} */(window.BookReader); +BookReader?.registerPlugin('autoplay', AutoplayPlugin); diff --git a/src/plugins/tts/plugin.tts.js b/src/plugins/tts/plugin.tts.js index 9e31d0f45..7de17d750 100644 --- a/src/plugins/tts/plugin.tts.js +++ b/src/plugins/tts/plugin.tts.js @@ -206,7 +206,7 @@ BookReader.prototype.initNavbar = (function (super_) { // ttsToggle() //______________________________________________________________________________ BookReader.prototype.ttsToggle = function () { - if (this.autoStop) this.autoStop(); + this._plugins.autoplay?.stop(); if (this.ttsEngine.playing) { this.ttsStop(); } else { diff --git a/src/plugins/url/plugin.url.js b/src/plugins/url/plugin.url.js index 31aa632b3..2e5cf424a 100644 --- a/src/plugins/url/plugin.url.js +++ b/src/plugins/url/plugin.url.js @@ -106,7 +106,7 @@ BookReader.prototype.urlStartLocationPolling = function() { this.trigger(BookReader.eventNames.stop); if (this.animating) { // Queue change if animating - if (this.autoStop) this.autoStop(); + this._plugins.autoplay?.stop(); this.animationFinishedCallback = updateParams; } else { // update immediately diff --git a/tests/e2e/autoplay.test.js b/tests/e2e/autoplay.test.js index 6f4583b2d..60b4ab528 100644 --- a/tests/e2e/autoplay.test.js +++ b/tests/e2e/autoplay.test.js @@ -2,12 +2,15 @@ import { ClientFunction } from 'testcafe'; import params from './helpers/params'; const getLocationHref = ClientFunction(() => window.location.href.toString()); -const FLIP_SPEED = 2000; -const FIRST_PAGE_DELAY = 6000; +const FLIP_SPEED = 200; +const FLIP_DELAY = 500; -fixture `Autoplay plugin`.page `${params.baseUrl}/BookReaderDemo/demo-internetarchive.html?ocaid=goody&autoflip=1`; +fixture `Autoplay plugin`.page `${params.baseUrl}/BookReaderDemo/demo-internetarchive.html?ocaid=goody&autoflip=1&flipSpeed=${FLIP_SPEED}&flipDelay=${FLIP_DELAY}`; test('page auto-advances after allotted flip speed and delay', async t => { - await t.wait(2 * FLIP_SPEED + FIRST_PAGE_DELAY); - await t.expect(getLocationHref()).match(/page\/n3/); + // Flips from cover, to #page/n1 to #page/n3, etc + await t.expect(getLocationHref()).notMatch(/page\/n\d+/); + await t.wait(2 * (FLIP_SPEED + FLIP_DELAY) + 500); + // Don't check for a specific page; initial load time can vary + await t.expect(getLocationHref()).match(/page\/n\d+/); }); diff --git a/tests/jest/BookReader/utils.test.js b/tests/jest/BookReader/utils.test.js index 9f6b86d20..0735d9fb1 100644 --- a/tests/jest/BookReader/utils.test.js +++ b/tests/jest/BookReader/utils.test.js @@ -10,6 +10,7 @@ import { escapeRegExp, getActiveElement, isInputActive, + parseAnimationSpeed, poll, polyfillCustomEvent, PolyfilledCustomEvent, @@ -227,3 +228,23 @@ describe('escapeRegex', () => { expect(escapeRegExp('https://example.com')).toBe('https://example\\.com'); }); }); + +describe('parseAnimationSpeed', () => { + test('Parses numbers', () => { + expect(parseAnimationSpeed(100)).toBe(100); + expect(parseAnimationSpeed(0)).toBe(0); + expect(parseAnimationSpeed(1000)).toBe(1000); + }); + + test('Parses strings', () => { + expect(parseAnimationSpeed('slow')).toBe(600); + expect(parseAnimationSpeed('fast')).toBe(200); + expect(parseAnimationSpeed('100')).toBe(100); + }); + + test('Handles invalid input', () => { + expect(parseAnimationSpeed('foo')).toBeFalsy(); + expect(parseAnimationSpeed('')).toBeFalsy(); + expect(parseAnimationSpeed(null)).toBeFalsy(); + }); +}); diff --git a/tests/jest/plugins/plugin.autoplay.test.js b/tests/jest/plugins/plugin.autoplay.test.js index c861bba8c..86f71db18 100644 --- a/tests/jest/plugins/plugin.autoplay.test.js +++ b/tests/jest/plugins/plugin.autoplay.test.js @@ -2,12 +2,11 @@ import BookReader from '@/src/BookReader.js'; import '@/src/plugins/plugin.autoplay.js'; - +/** @type {BookReader} */ let br; beforeEach(() => { document.body.innerHTML = '