Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/BookReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@ BookReader.prototype.init = function() {
plugin._bindNavigationHandlers();
}
this.setupKeyListeners();
this._setupUserActionListeners();

this.lastScroll = (new Date().getTime());
this.refs.$brContainer.on('scroll', this, function(e) {
Expand Down Expand Up @@ -821,6 +822,43 @@ BookReader.prototype.resize = function() {
this.trigger(BookReader.eventNames.resize);
};

/**
* Registers the UI wrapping BookReader (e.g. `<ia-bookreader>`), which hosts book
* UI of its own — side menus, panels — outside BookReader's own element.
*
* @param {import('@/src/ia-bookreader/ia-bookreader.js').IaBookReader} shell
*/
BookReader.prototype.initShell = function (shell) {
this.shell = shell;
// Only the shell sees interactions with both its UI and BookReader's own.
this._setupUserActionListeners(shell);
};

/**
* Binds listeners that signal the patron is actively using the book.
*
* Consumers outside BookReader (e.g. ia-book-actions) use this to keep a loan
* from expiring while it is being read. The capture phase is required: several
* control handlers return `false`, which jQuery turns into `stopPropagation()`,
* so a bubbling listener would never see clicks on the navbar.
*
* @private
* @param {HTMLElement} [target] element to listen on; prefer the outermost one
* that holds book UI, since anything outside it is invisible to these listeners.
*/
BookReader.prototype._setupUserActionListeners = function (target = this.refs.$br[0]) {
if (!target || target === this._userActionTarget) return;

if (!this._onUserAction) {
this._onUserAction = () => this.trigger(BookReader.eventNames.userAction);
}
for (const eventName of ['pointerdown', 'keydown']) {
this._userActionTarget?.removeEventListener(eventName, this._onUserAction, { capture: true });
target.addEventListener(eventName, this._onUserAction, { capture: true });
}
this._userActionTarget = target;
};

/**
* Binds keyboard and keyboard focus event listeners
*/
Expand Down
6 changes: 0 additions & 6 deletions src/BookReader/Navbar/Navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,6 @@ export class Navbar {
},
};

// custom event for auto-loan-renew in ia-book-actions
// - to know if user is actively reading
this.$nav.find('nav.BRcontrols li button').on('click', () => {
this.br.trigger(EVENTS.userAction);
});

for (const control in navigationControls) {
jIcons.filter(`.${control}`).on('click.bindNavigationHandlers', () => {
navigationControls[control]();
Expand Down
2 changes: 1 addition & 1 deletion src/ia-bookreader/ia-bookreader.js
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ export class IaBookReader extends LitElement {
_bindEventListeners() {
window.addEventListener('BookReader:PostInit', /** @param {CustomEvent} e */ (e) => {
this.bookreader = e.detail.props;
this.bookreader.shell = this;
this.bookreader.initShell(this);
this.bookReaderLoaded = true;
this.bookReaderCannotLoad = false;
this.loaded = true;
Expand Down
12 changes: 10 additions & 2 deletions src/plugins/tts/WebTTSEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ export default class WebTTSEngine extends AbstractTTSEngine {
});

navigator.mediaSession.setActionHandler('play', () => {
this.events.trigger('mediaSessionAction');
audio.play();
this.resume();
});
navigator.mediaSession.setActionHandler('pause', () => {
this.events.trigger('mediaSessionAction');
audio.pause();
this.pause();
});
Expand All @@ -70,8 +72,14 @@ export default class WebTTSEngine extends AbstractTTSEngine {
// Some devices only support the previoustrack/nexttrack (e.g. Win10), so show those.
// Android devices do support the seek actions, but we don't want to show both buttons
// and have them do the same thing.
navigator.mediaSession.setActionHandler('previoustrack', () => this.jumpBackward());
navigator.mediaSession.setActionHandler('nexttrack', () => this.jumpForward());
navigator.mediaSession.setActionHandler('previoustrack', () => {
this.events.trigger('mediaSessionAction');
this.jumpBackward();
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
this.events.trigger('mediaSessionAction');
this.jumpForward();
});
});
}

Expand Down
3 changes: 3 additions & 0 deletions src/plugins/tts/plugin.tts.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ export class TtsPlugin extends BookReaderPlugin {
renderVoicesMenu(voicesMenu);
voicesMenu.on("change", ev => this.ttsEngine.setVoice(voicesMenu.val()));
this.ttsEngine.events.on('pause resume start', () => this.updateState());
// Media session controls (lock screen, notification shade, headset buttons)
// have no DOM presence, so BookReader's own listeners cannot see them.
this.ttsEngine.events.on('mediaSessionAction', () => this.br.trigger(BookReader.eventNames.userAction));
this.ttsEngine.events.on('voiceschanged', () => renderVoicesMenu(voicesMenu));
this.br.on('translationEnabled', () => renderVoicesMenu(voicesMenu));
this.br.on('translationDisabled', () => renderVoicesMenu(voicesMenu));
Expand Down
70 changes: 70 additions & 0 deletions tests/jest/BookReader.userAction.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import BookReader from '@/src/BookReader.js';

/** @type {BookReader} */
let br;

beforeEach(() => {
document.body.innerHTML = '<div id="BookReader">';
br = new BookReader({ data: [[{ width: 123, height: 123, uri: 'https://archive.org/image0.jpg', pageNum: '1' }]] });
br.init();
});

/** @returns {jest.Mock} */
function listenForUserAction() {
const listener = jest.fn();
window.addEventListener(`BookReader:${BookReader.eventNames.userAction}`, listener);
return listener;
}

test('clicking anywhere in the BookReader signals a user action', () => {
const listener = listenForUserAction();
br.refs.$brContainer[0].dispatchEvent(new Event('pointerdown', { bubbles: true }));
expect(listener).toHaveBeenCalled();
});

test('typing anywhere in the BookReader signals a user action', () => {
const listener = listenForUserAction();
br.refs.$brContainer[0].dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }));
expect(listener).toHaveBeenCalled();
});

test('signals a user action even when a control stops propagation', () => {
const $button = br.refs.$br.find('nav.BRcontrols button').first();
expect($button.length).toBe(1);
$button.on('pointerdown', (ev) => ev.stopPropagation());

const listener = listenForUserAction();
$button[0].dispatchEvent(new Event('pointerdown', { bubbles: true }));
expect(listener).toHaveBeenCalled();
});

test('does not signal a user action for events outside the BookReader', () => {
const listener = listenForUserAction();
document.body.dispatchEvent(new Event('pointerdown', { bubbles: true }));
expect(listener).not.toHaveBeenCalled();
});

describe('when a shell wraps the BookReader', () => {
/** @type {HTMLElement} */
let shell;

beforeEach(() => {
shell = document.createElement('div');
br.refs.$br[0].replaceWith(shell);
shell.appendChild(br.refs.$br[0]);
br.initShell(shell);
});

test('signals a user action for UI outside the BookReader element', () => {
const sibling = shell.appendChild(document.createElement('nav'));
const listener = listenForUserAction();
sibling.dispatchEvent(new Event('pointerdown', { bubbles: true }));
expect(listener).toHaveBeenCalled();
});

test('signals a user action exactly once for UI inside the BookReader', () => {
const listener = listenForUserAction();
br.refs.$brContainer[0].dispatchEvent(new Event('pointerdown', { bubbles: true }));
expect(listener).toHaveBeenCalledTimes(1);
});
});
1 change: 1 addition & 0 deletions tests/jest/ia-bookreader/ia-bookreader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('<ia-bookreader>', () => {
test('Listens for Global Event @BookReader:PostInit', async () => {
const brStub = {
resize: sinon.fake(),
initShell: sinon.fake(),
currentIndex: sinon.fake(),
jumpToIndex: sinon.fake(),
options: { enableMultipleBooks: false }, // for multipleBooks
Expand Down
Loading