@internetarchive/bookreader 5.0.0-32 → 5.0.0-36

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. package/.eslintrc.js +1 -11
  2. package/BookReader/BookReader.css +1 -1
  3. package/BookReader/BookReader.js +1 -1
  4. package/BookReader/BookReader.js.map +1 -1
  5. package/BookReader/ia-bookreader-bundle.js +41 -41
  6. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  7. package/BookReader/plugins/plugin.tts.js +1 -1
  8. package/BookReader/plugins/plugin.tts.js.map +1 -1
  9. package/BookReader/plugins/plugin.url.js +1 -1
  10. package/BookReader/plugins/plugin.url.js.map +1 -1
  11. package/BookReaderDemo/IADemoBr.js +13 -0
  12. package/BookReaderDemo/demo-internetarchive.html +6 -3
  13. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  14. package/CHANGELOG.md +20 -0
  15. package/package.json +10 -10
  16. package/src/BookNavigator/book-navigator.js +43 -15
  17. package/src/BookNavigator/bookmarks/ia-bookmarks.js +26 -27
  18. package/src/BookNavigator/search/search-provider.js +2 -2
  19. package/src/BookReader/Toolbar/Toolbar.js +2 -2
  20. package/src/BookReader.js +55 -55
  21. package/src/css/_colorbox.scss +2 -2
  22. package/src/ia-bookreader/ia-bookreader.js +10 -4
  23. package/src/plugins/tts/AbstractTTSEngine.js +31 -34
  24. package/src/plugins/tts/PageChunk.js +4 -1
  25. package/src/plugins/tts/PageChunkIterator.js +8 -12
  26. package/src/plugins/tts/WebTTSEngine.js +41 -46
  27. package/src/plugins/tts/plugin.tts.js +16 -10
  28. package/src/plugins/url/UrlPlugin.js +1 -1
  29. package/tests/e2e/base.test.js +7 -4
  30. package/tests/e2e/helpers/params.js +1 -1
  31. package/tests/e2e/viewmode.test.js +30 -30
  32. package/tests/jest/plugins/url/UrlPlugin.test.js +15 -0
  33. package/tests/karma/BookNavigator/book-navigator.test.js +21 -5
@@ -258,7 +258,7 @@ export class WebTTSSound {
258
258
  }
259
259
  }
260
260
 
261
- resume() {
261
+ async resume() {
262
262
  if (!this.started) {
263
263
  this.play();
264
264
  return;
@@ -278,16 +278,15 @@ export class WebTTSSound {
278
278
  speechSynthesis.resume();
279
279
 
280
280
  if (resumeMightNotFire) {
281
- Promise.race([resumePromise, sleep(100).then(() => 'timeout')])
282
- .then(result => {
283
- if (result != 'timeout') return;
281
+ const result = await Promise.race([resumePromise, sleep(100).then(() => 'timeout')]);
284
282
 
285
- this.utterance.dispatchEvent(new CustomEvent('resume', {}));
286
- if (resumeMightNotWork) {
287
- const reloadPromise = this.reload();
288
- reloadPromise.then(() => this.play());
289
- }
290
- });
283
+ if (result != 'timeout') return;
284
+
285
+ this.utterance.dispatchEvent(new CustomEvent('resume', {}));
286
+ if (resumeMightNotWork) {
287
+ await this.reload();
288
+ this.play();
289
+ }
291
290
  }
292
291
  }
293
292
 
@@ -308,45 +307,41 @@ export class WebTTSSound {
308
307
  * We avoid this (as described here: https://bugs.chromium.org/p/chromium/issues/detail?id=679437#c15 )
309
308
  * by pausing after 14 seconds and ~instantly resuming.
310
309
  */
311
- _chromePausingBugFix() {
310
+ async _chromePausingBugFix() {
312
311
  const timeoutPromise = sleep(14000).then(() => 'timeout');
313
312
  const pausePromise = promisifyEvent(this.utterance, 'pause').then(() => 'paused');
314
313
  const endPromise = promisifyEvent(this.utterance, 'end').then(() => 'ended');
315
- return Promise.race([timeoutPromise, pausePromise, endPromise])
316
- .then(result => {
317
- if (location.toString().indexOf('_debugReadAloud=true') != -1) {
318
- console.log(`CHROME-PAUSE-HACK: ${result}`);
319
- }
320
- switch (result) {
321
- case 'ended':
322
- // audio was stopped/finished; nothing to do
323
- break;
324
- case 'paused':
325
- // audio was paused; wait for resume
326
- // Chrome won't let you resume the audio if 14s have passed 🤷‍
327
- // We could do the same as before (but resume+pause instead of pause+resume),
328
- // but that means we'd _constantly_ be running in the background. So in that
329
- // case, let's just restart the chunk
330
- Promise.race([
331
- promisifyEvent(this.utterance, 'resume'),
332
- sleep(14000).then(() => 'timeout'),
333
- ])
334
- .then(result => {
335
- result == 'timeout' ? this.reload() : this._chromePausingBugFix();
336
- });
337
- break;
338
- case 'timeout':
339
- // We hit Chrome's secret cut off time. Pause/resume
340
- // to be able to keep TTS-ing
341
- speechSynthesis.pause();
342
- sleep(25)
343
- .then(() => {
344
- speechSynthesis.resume();
345
- this._chromePausingBugFix();
346
- });
347
- break;
348
- }
349
- });
314
+ const result = await Promise.race([timeoutPromise, pausePromise, endPromise]);
315
+ if (location.toString().indexOf('_debugReadAloud=true') != -1) {
316
+ console.log(`CHROME-PAUSE-HACK: ${result}`);
317
+ }
318
+ switch (result) {
319
+ case 'ended':
320
+ // audio was stopped/finished; nothing to do
321
+ break;
322
+ case 'paused':
323
+ // audio was paused; wait for resume
324
+ // Chrome won't let you resume the audio if 14s have passed 🤷‍
325
+ // We could do the same as before (but resume+pause instead of pause+resume),
326
+ // but that means we'd _constantly_ be running in the background. So in that
327
+ // case, let's just restart the chunk
328
+ Promise.race([
329
+ promisifyEvent(this.utterance, 'resume'),
330
+ sleep(14000).then(() => 'timeout'),
331
+ ])
332
+ .then(result => {
333
+ result == 'timeout' ? this.reload() : this._chromePausingBugFix();
334
+ });
335
+ break;
336
+ case 'timeout':
337
+ // We hit Chrome's secret cut off time. Pause/resume
338
+ // to be able to keep TTS-ing
339
+ speechSynthesis.pause();
340
+ await sleep(25);
341
+ speechSynthesis.resume();
342
+ this._chromePausingBugFix();
343
+ break;
344
+ }
350
345
  }
351
346
  }
352
347
 
@@ -261,9 +261,7 @@ BookReader.prototype.ttsStop = function () {
261
261
  BookReader.prototype.ttsBeforeChunkPlay = async function(chunk) {
262
262
  await this.ttsMaybeFlipToIndex(chunk.leafIndex);
263
263
  this.ttsHighlightChunk(chunk);
264
- // This appears not to work; ttsMaybeFlipToIndex causes a scroll to the top of
265
- // the active page :/ Disabling cause the extra scroll just adds an odd jitter.
266
- // this.ttsScrollToChunk(chunk);
264
+ this.ttsScrollToChunk(chunk);
267
265
  };
268
266
 
269
267
  /**
@@ -292,10 +290,7 @@ BookReader.prototype.ttsMaybeFlipToIndex = function (leafIndex) {
292
290
  resolve();
293
291
  } else {
294
292
  this.animationFinishedCallback = resolve;
295
- const mustGoNext = leafIndex > Math.max(this.twoPage.currentIndexR, this.twoPage.currentIndexL);
296
- if (mustGoNext) this.next();
297
- else this.prev();
298
- promise.then(this.ttsMaybeFlipToIndex.bind(this, leafIndex));
293
+ this.jumpToIndex(leafIndex);
299
294
  }
300
295
  }
301
296
 
@@ -329,9 +324,20 @@ BookReader.prototype.ttsHighlightChunk = function(chunk) {
329
324
  * @param {PageChunk} chunk
330
325
  */
331
326
  BookReader.prototype.ttsScrollToChunk = function(chunk) {
332
- if (this.constMode1up != this.mode) return;
333
-
334
- $(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`)[0]?.scrollIntoView();
327
+ // It behaves weird if used in thumb mode
328
+ if (this.constModeThumb == this.mode) return;
329
+
330
+ $(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`).last()?.[0]?.scrollIntoView({
331
+ // Only vertically center the highlight if we're in 1up or in full screen. In
332
+ // 2up, if we're not fullscreen, the whole body gets scrolled around to try to
333
+ // center the highlight 🙄 See:
334
+ // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
335
+ // Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
336
+ // full-width, and covers up the last line of the highlight.
337
+ block: this.constMode1up == this.mode || this.isFullscreenActive ? 'center' : 'nearest',
338
+ inline: 'center',
339
+ behavior: 'smooth',
340
+ });
335
341
  };
336
342
 
337
343
  // ttsRemoveHilites()
@@ -140,7 +140,7 @@ export class UrlPlugin {
140
140
  const concatenatedPath = urlStrPath !== '/' ? urlStrPath : '';
141
141
  if (this.urlMode == 'history') {
142
142
  if (window.history && window.history.replaceState) {
143
- const newUrlPath = `${this.urlHistoryBasePath}${concatenatedPath}`;
143
+ const newUrlPath = `${this.urlHistoryBasePath}${concatenatedPath}`.trim().replace(/(\/+)/g, '/');
144
144
  window.history.replaceState({}, null, newUrlPath);
145
145
  }
146
146
  } else {
@@ -1,6 +1,6 @@
1
1
  import { runBaseTests } from './helpers/base';
2
2
  import BookReader from './models/BookReader';
3
- import { runDesktopSearchTests } from './helpers/desktopSearch';
3
+ // import { runDesktopSearchTests } from './helpers/desktopSearch';
4
4
  // import { runMobileSearchTests } from './helpers/mobileSearch';
5
5
  import params from './helpers/params';
6
6
 
@@ -21,10 +21,13 @@ ocaids.forEach(ocaid => {
21
21
  fixture `Base Tests for: ${ocaid}`.page `${url}`;
22
22
  runBaseTests(new BookReader());
23
23
 
24
- fixture `Desktop Search Tests for: ${ocaid}`
25
- .page `${url}`;
26
- runDesktopSearchTests(new BookReader());
27
24
 
25
+ // Todo: Re-enable when testing side panel
26
+ // fixture `Desktop Search Tests for: ${ocaid}`
27
+ // .page `${url}`;
28
+ // runDesktopSearchTests(new BookReader());
29
+
30
+ // Todo: deprecated, will remove once mmenu is removed.
28
31
  // fixture `Mobile Search Tests for: ${ocaid}`
29
32
  // .page `${url}`
30
33
  // runMobileSearchTests(new BookReader());
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  class TestParams {
3
3
  baseUrl = process.env.BASE_URL?.replace(/\/+$/, '') ?? 'http://127.0.0.1:8000'
4
- ocaids = process.env.OCAIDS?.split(',') ?? [];
4
+ ocaids = process.env.OCAIDS?.split(',') ?? null;
5
5
  /** Whether the url we're testing is a prod (or near prod) IA url, or a demos url */
6
6
  isIA = new URL(this.baseUrl).hostname.endsWith('archive.org');
7
7
 
@@ -1,36 +1,36 @@
1
- // import { Selector } from 'testcafe';
2
- // import BookReader from './models/BookReader';
3
- // import params from './helpers/params';
1
+ import { Selector } from 'testcafe';
2
+ import BookReader from './models/BookReader';
3
+ import params from './helpers/params';
4
4
 
5
- // fixture `Viewmode carousel`.page `${params.baseUrl}/BookReaderDemo/viewmode-cycle.html`;
5
+ fixture `Viewmode carousel`.page `${params.baseUrl}/BookReaderDemo/demo-internetarchive.html?ocaid=goody`;
6
6
 
7
- // test('Clicking `view mode` cycles through view modes', async t => {
8
- // const { nav } = (new BookReader());
7
+ test('Clicking `view mode` cycles through view modes', async t => {
8
+ const { nav } = (new BookReader());
9
9
 
10
- // // viewmode button only appear on mobile devices
11
- // await t.resizeWindow(400, 800);
12
- // // Flip forward one
13
- // await t.pressKey('right');
10
+ // viewmode button only appear on mobile devices
11
+ await t.resizeWindow(400, 800);
12
+ // Flip forward one
13
+ await t.pressKey('right');
14
14
 
15
- // // 2up to thumb
16
- // await t.click(nav.desktop.viewmode);
17
- // const thumbnailContainer = Selector('.BRmodeThumb');
18
- // await t.expect(thumbnailContainer.visible).ok();
19
- // const thumbImages = thumbnailContainer.find('.BRpageview img');
20
- // await t.expect(thumbImages.count).gt(0);
15
+ // 2up to thumb
16
+ await t.click(nav.desktop.viewmode);
17
+ const thumbnailContainer = Selector('.BRmodeThumb');
18
+ await t.expect(thumbnailContainer.visible).ok();
19
+ const thumbImages = thumbnailContainer.find('.BRpageview img');
20
+ await t.expect(thumbImages.count).gt(0);
21
21
 
22
- // // thumb to 1up
23
- // await t.click(nav.desktop.viewmode);
24
- // const onePageViewContainer = Selector('br-mode-1up');
25
- // await t.expect(onePageViewContainer.visible).ok();
26
- // const onePageImages = onePageViewContainer.find('.BRmode1up .BRpagecontainer');
27
- // // we usually pre-fetch the page in question & 1 before/after it
28
- // await t.expect(onePageImages.count).gte(3);
22
+ // thumb to 1up
23
+ await t.click(nav.desktop.viewmode);
24
+ const onePageViewContainer = Selector('br-mode-1up');
25
+ await t.expect(onePageViewContainer.visible).ok();
26
+ const onePageImages = onePageViewContainer.find('.BRmode1up .BRpagecontainer');
27
+ // we usually pre-fetch the page in question & 1 before/after it
28
+ await t.expect(onePageImages.count).gte(3);
29
29
 
30
- // // 1up to 2up
31
- // await t.click(nav.desktop.viewmode);
32
- // const twoPageContainer = Selector('.BRtwopageview');
33
- // await t.expect(twoPageContainer.visible).ok();
34
- // const twoPageImages = twoPageContainer.find('img.BRpageimage');
35
- // await t.expect(twoPageImages.count).gte(2);
36
- // });
30
+ // 1up to 2up
31
+ await t.click(nav.desktop.viewmode);
32
+ const twoPageContainer = Selector('.BRtwopageview');
33
+ await t.expect(twoPageContainer.visible).ok();
34
+ const twoPageImages = twoPageContainer.find('img.BRpageimage');
35
+ await t.expect(twoPageImages.count).gte(2);
36
+ });
@@ -170,6 +170,21 @@ describe('UrlPlugin tests', () => {
170
170
  const locationUrl = `${window.location.pathname}${window.location.search}`;
171
171
  expect(locationUrl).toEqual('/details/foo/page/12?q=hello&view=theater');
172
172
  });
173
+
174
+ test('strips leading slash of incoming path name for no double slash', () => {
175
+ const urlPlugin = new UrlPlugin();
176
+ urlPlugin.urlMode = 'history';
177
+
178
+ urlPlugin.urlHistoryBasePath = '/details/SubBookTest/book1/GPORFP/';
179
+ urlPlugin.urlState = {
180
+ "mode": "1up",
181
+ };
182
+
183
+ urlPlugin.setUrlParam('sort', 'title_asc');
184
+ urlPlugin.setUrlParam('mode', 'thumb');
185
+
186
+ expect(window.location.href).toEqual('http://localhost/details/SubBookTest/book1/GPORFP/mode/thumb?sort=title_asc');
187
+ });
173
188
  });
174
189
 
175
190
  });
@@ -111,11 +111,6 @@ describe('<book-navigator>', () => {
111
111
 
112
112
  it('creates an item image from metadata', async () => {
113
113
  const el = fixtureSync(container());
114
- el.item = {
115
- metadata: { identifier: 'foo' },
116
- };
117
- await elementUpdated(el);
118
-
119
114
  const itemImage = fixtureSync(el.itemImage);
120
115
  expect(itemImage).to.be.instanceOf(HTMLImageElement);
121
116
  expect(itemImage.getAttribute('class')).to.equal('cover-img');
@@ -225,6 +220,18 @@ describe('<book-navigator>', () => {
225
220
 
226
221
  describe('Controlling Menu Side Panel & Shortcuts', () => {
227
222
  describe('Side Menu Panels', () => {
223
+ it('`isWideEnoughToOpenMenu` checks if menu should be open', async () => {
224
+ const el = fixtureSync(container());
225
+ el.brWidth = 300;
226
+ await el.elementUpdated;
227
+
228
+ expect(el.isWideEnoughToOpenMenu).to.equal(false);
229
+
230
+ el.brWidth = 641;
231
+ await el.elementUpdated;
232
+
233
+ expect(el.isWideEnoughToOpenMenu).to.equal(true);
234
+ });
228
235
  describe('Control which side menu to toggle open by using: `this.updateSideMenu`', () => {
229
236
  it('Emits `@updateSideMenu` to signal which menu gets the update', async () => {
230
237
  const el = fixtureSync(container());
@@ -316,6 +323,10 @@ describe('<book-navigator>', () => {
316
323
  const el = fixtureSync(container());
317
324
  const brStub = {
318
325
  resize: sinon.fake(),
326
+ options: {},
327
+ refs: {
328
+ $brContainer: document.createElement('div')
329
+ }
319
330
  };
320
331
  el.bookreader = brStub;
321
332
  await elementUpdated(el);
@@ -342,7 +353,12 @@ describe('<book-navigator>', () => {
342
353
  const brStub = {
343
354
  animating: false,
344
355
  resize: sinon.fake(),
356
+ options: {},
357
+ refs: {
358
+ $brContainer: document.createElement('div')
359
+ }
345
360
  };
361
+
346
362
  el.bookreader = brStub;
347
363
  await elementUpdated(el);
348
364
  expect(el.brWidth).to.equal(0);