@internetarchive/bookreader 5.0.0-115 → 5.0.0-117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/BookReader/BookReader.css +44 -18
  2. package/BookReader/BookReader.js +1 -43
  3. package/BookReader/BookReader.js.map +1 -1
  4. package/BookReader/ia-bookreader-bundle.js +109 -67
  5. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  6. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  7. package/BookReader/plugins/plugin.autoplay.js +1 -1
  8. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  9. package/BookReader/plugins/plugin.chapters.js +2 -2
  10. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  11. package/BookReader/plugins/plugin.experiments.js +1 -1
  12. package/BookReader/plugins/plugin.experiments.js.map +1 -1
  13. package/BookReader/plugins/plugin.iframe.js +1 -1
  14. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  15. package/BookReader/plugins/plugin.iiif.js +1 -1
  16. package/BookReader/plugins/plugin.resume.js +1 -1
  17. package/BookReader/plugins/plugin.search.js +1 -1
  18. package/BookReader/plugins/plugin.search.js.map +1 -1
  19. package/BookReader/plugins/plugin.text_selection.js +63 -1
  20. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  21. package/BookReader/plugins/plugin.translate.js +65 -3
  22. package/BookReader/plugins/plugin.translate.js.map +1 -1
  23. package/BookReader/plugins/plugin.tts.js +1 -1
  24. package/BookReader/plugins/plugin.tts.js.map +1 -1
  25. package/BookReader/plugins/plugin.url.js +1 -1
  26. package/BookReader/plugins/plugin.url.js.map +1 -1
  27. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  28. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  29. package/README.md +3 -1
  30. package/jsconfig.json +2 -3
  31. package/package.json +38 -31
  32. package/src/BookReader.js +24 -20
  33. package/src/css/_TextSelection.scss +51 -18
  34. package/src/plugins/plugin.chapters.js +8 -4
  35. package/src/plugins/plugin.iframe.js +36 -37
  36. package/src/plugins/plugin.text_selection.js +40 -101
  37. package/src/plugins/search/plugin.search.js +4 -8
  38. package/src/plugins/tts/plugin.tts.js +3 -8
  39. package/src/plugins/url/UrlPlugin.js +12 -0
  40. package/src/util/TextSelectionManager.js +221 -154
  41. package/src/util/dom.js +88 -0
  42. package/src/util/generators.js +89 -0
@@ -5,6 +5,8 @@ import { applyVariables } from '../util/strings.js';
5
5
  import { Cache } from '../util/cache.js';
6
6
  import { toISO6391 } from './tts/utils.js';
7
7
  import { BookReaderTextFragment, renderHighlight, TextSelectionManager } from '../util/TextSelectionManager.js';
8
+ import { genMap, lookAroundWindow, zip } from '../util/generators.js';
9
+ import textSelectionCss from '../css/_TextSelection.scss';
8
10
  /** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
9
11
  /** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */
10
12
 
@@ -38,6 +40,13 @@ export class TextSelectionPlugin extends BookReaderPlugin {
38
40
 
39
41
  _jumpedToHighlight = false;
40
42
 
43
+ /**
44
+ * Isolated document/layout used to performantly measure OCR text-layer
45
+ * elements.
46
+ * @type {Document}
47
+ */
48
+ _measurementDocument;
49
+
41
50
  /**
42
51
  * @param {import('../BookReader.js').default} br
43
52
  */
@@ -55,6 +64,19 @@ export class TextSelectionPlugin extends BookReaderPlugin {
55
64
  init() {
56
65
  if (!this.options.enabled) return;
57
66
 
67
+ // Setup measurement iframe for OCR
68
+ const measurementIframe = document.createElement('iframe');
69
+ measurementIframe.setAttribute('aria-hidden', 'true');
70
+ measurementIframe.tabIndex = -1;
71
+ measurementIframe.style.cssText = 'position:fixed; top:-99999px; left:-99999px; width:2000px; height:4000px; border:0; visibility:hidden;';
72
+ document.body.appendChild(measurementIframe);
73
+ this._measurementDocument = measurementIframe.contentDocument;
74
+ // Injects _TextSelection.scss so measurements match the real
75
+ // rendering
76
+ const style = this._measurementDocument.createElement('style');
77
+ style.textContent = textSelectionCss;
78
+ this._measurementDocument.head.appendChild(style);
79
+
58
80
  this.br.on('pageVisible', (_, {pageContainerEl}) => {
59
81
  const textLayer = pageContainerEl.querySelector('.BRtextLayer');
60
82
  if (textLayer) {
@@ -66,8 +88,9 @@ export class TextSelectionPlugin extends BookReaderPlugin {
66
88
  this.textSelectionManager.init();
67
89
 
68
90
  // Init text fragment
69
- this.targetTextFragment = BookReaderTextFragment.fromUrl(location.search, this.br.book, this.br.firstIndex);
70
- if (this.targetTextFragment) {
91
+ const textParam = new URLSearchParams(location.search).get('text');
92
+ if (textParam) {
93
+ this.targetTextFragment = BookReaderTextFragment.fromString(textParam, this.br.book, this.br.firstIndex);
71
94
  const targetTextFragment = this.targetTextFragment;
72
95
  this.br.on('textLayerVisible', async (_, {pageContainerEl, textLayer}) => {
73
96
  const pageIndex = targetTextFragment.pageIndex;
@@ -76,7 +99,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
76
99
  const markEls = renderHighlight(textLayer, targetTextFragment, 'BRhighlight--target-text');
77
100
  // Only jump once; presumably on first page load.
78
101
  if (!this._jumpedToHighlight) {
79
- markEls[0].scrollIntoView({behavior: 'smooth', block: 'center'});
102
+ this.br.scrollIntoView(markEls[0], {behavior: 'smooth', block: 'center'});
80
103
  this._jumpedToHighlight = true;
81
104
  }
82
105
  }
@@ -92,7 +115,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
92
115
  _configurePageContainer(pageContainer) {
93
116
  // Disable if thumb mode; it's too janky
94
117
  // .page can be null for "pre-cover" region
95
- if (this.br.mode !== this.br.constModeThumb && pageContainer.page?.isViewable) {
118
+ if (this.options.enabled && this.br.mode !== this.br.constModeThumb && pageContainer.page?.isViewable) {
96
119
  this.createTextLayer(pageContainer);
97
120
  }
98
121
  return pageContainer;
@@ -202,7 +225,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
202
225
  });
203
226
 
204
227
  // Fix up paragraph positions
205
- const paragraphRects = determineRealRects(textLayer, '.BRparagraphElement');
228
+ const paragraphRects = determineRealRects(textLayer, '.BRparagraphElement', this._measurementDocument);
206
229
  let yAdded = 0;
207
230
  for (const [ocrParagraph, paragEl] of zip(ocrParagraphs, paragEls)) {
208
231
  const ocrParagBounds = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
@@ -310,7 +333,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
310
333
  paragEl.style.fontSize = `${paragWordHeight}px`;
311
334
 
312
335
  // Fix up sizes - stretch/crush words as necessary using letter spacing
313
- let wordRects = determineRealRects(paragEl, '.BRwordElement');
336
+ let wordRects = determineRealRects(paragEl, '.BRwordElement', this._measurementDocument);
314
337
  const ocrWords = $(ocrParagraph).find("WORD").toArray();
315
338
  const wordEls = paragEl.querySelectorAll('.BRwordElement');
316
339
  for (const [ocrWord, wordEl] of zip(ocrWords, wordEls)) {
@@ -331,8 +354,8 @@ export class TextSelectionPlugin extends BookReaderPlugin {
331
354
 
332
355
  // Stretch/crush lines as necessary using line spacing
333
356
  // Recompute rects after letter spacing
334
- wordRects = determineRealRects(paragEl, '.BRwordElement');
335
- const spaceRects = determineRealRects(paragEl, '.BRspace');
357
+ wordRects = determineRealRects(paragEl, '.BRwordElement', this._measurementDocument);
358
+ const spaceRects = determineRealRects(paragEl, '.BRspace', this._measurementDocument);
336
359
 
337
360
  const ocrLines = $(ocrParagraph).find("LINE[coords]").toArray();
338
361
  const lineEls = Array.from(paragEl.querySelectorAll('.BRlineElement'));
@@ -382,9 +405,11 @@ BookReader?.registerPlugin('textSelection', TextSelectionPlugin);
382
405
  /**
383
406
  * @param {HTMLElement} parentEl
384
407
  * @param {string} selector
408
+ * @param {Document} measurementDocument Isolated document to measure within
409
+ * (see TextSelectionPlugin#_measurementDocument for why).
385
410
  * @returns {Map<Element, Rect>}
386
411
  */
387
- function determineRealRects(parentEl, selector) {
412
+ function determineRealRects(parentEl, selector, measurementDocument) {
388
413
  const initals = {
389
414
  position: parentEl.style.position,
390
415
  visibility: parentEl.style.visibility,
@@ -397,21 +422,23 @@ function determineRealRects(parentEl, selector) {
397
422
  parentEl.style.top = '0';
398
423
  parentEl.style.left = '0';
399
424
  parentEl.style.transform = 'none';
400
- document.body.appendChild(parentEl);
425
+ measurementDocument.body.appendChild(parentEl);
401
426
  const rects = new Map(
402
427
  Array.from(parentEl.querySelectorAll(selector))
403
428
  .map(wordEl => {
404
429
  const origRect = wordEl.getBoundingClientRect();
405
430
  return [wordEl, new Rect(
406
- origRect.left + window.scrollX,
407
- origRect.top + window.scrollY,
431
+ origRect.left + measurementDocument.defaultView.scrollX,
432
+ origRect.top + measurementDocument.defaultView.scrollY,
408
433
  origRect.width,
409
434
  origRect.height,
410
435
  )];
411
436
  }),
412
437
  );
413
- document.body.removeChild(parentEl);
438
+ measurementDocument.body.removeChild(parentEl);
414
439
  Object.assign(parentEl.style, initals);
440
+ // Need to restore the document to the main window document
441
+ document.adoptNode(parentEl);
415
442
  return rects;
416
443
  }
417
444
 
@@ -428,94 +455,6 @@ function augmentLine(line) {
428
455
  };
429
456
  }
430
457
 
431
- /**
432
- * @template T
433
- * Get the i-th element of an iterable
434
- * @param {Iterable<T>} iterable
435
- * @param {number} index
436
- */
437
- export function genAt(iterable, index) {
438
- let i = 0;
439
- for (const x of iterable) {
440
- if (i == index) return x;
441
- i++;
442
- }
443
- return undefined;
444
- }
445
-
446
- /**
447
- * @template T
448
- * Generator version of filter
449
- * @param {Iterable<T>} iterable
450
- * @param {function(T): boolean} fn
451
- */
452
- export function* genFilter(iterable, fn) {
453
- for (const x of iterable) {
454
- if (fn(x)) yield x;
455
- }
456
- }
457
-
458
- /**
459
- * @template TFrom, TTo
460
- * Generator version of map
461
- * @param {Iterable<TFrom>} gen
462
- * @param {function(TFrom): TTo} fn
463
- * @returns {Iterable<TTo>}
464
- */
465
- export function* genMap(gen, fn) {
466
- for (const x of gen) yield fn(x);
467
- }
468
-
469
- /**
470
- * @template T
471
- * Generator that provides a sliding window of 3 elements,
472
- * prev, current, and next.
473
- * @param {Iterable<T>} gen
474
- * @returns {Iterable<[T | undefined, T, T | undefined]>}
475
- */
476
- export function* lookAroundWindow(gen) {
477
- let prev = undefined;
478
- let cur = undefined;
479
- let next = undefined;
480
- for (const x of gen) {
481
- if (typeof cur !== 'undefined') {
482
- next = x;
483
- yield [prev, cur, next];
484
- }
485
- prev = cur;
486
- cur = x;
487
- next = undefined;
488
- }
489
-
490
- if (typeof cur !== 'undefined') {
491
- yield [prev, cur, next];
492
- }
493
- }
494
-
495
- /**
496
- * @template T1, T2
497
- * Lazy zip implementation to avoid importing lodash
498
- * Expects iterators to be of the same length
499
- * @param {Iterable<T1>} gen1
500
- * @param {Iterable<T2>} gen2
501
- * @returns {Iterable<[T1, T2]>}
502
- */
503
- export function* zip(gen1, gen2) {
504
- const it1 = gen1[Symbol.iterator]();
505
- const it2 = gen2[Symbol.iterator]();
506
- while (true) {
507
- const r1 = it1.next();
508
- const r2 = it2.next();
509
- if (r1.done && r2.done) {
510
- return;
511
- }
512
- if (r1.done || r2.done) {
513
- throw new Error('zip: one of the iterators is done');
514
- }
515
- yield [r1.value, r2.value];
516
- }
517
- }
518
-
519
458
  /**
520
459
  * [left, bottom, right, top]
521
460
  * @param {Array<[number, number, number, number]>} bounds
@@ -104,6 +104,8 @@ export class SearchPlugin extends BookReaderPlugin {
104
104
  this.options.initialSearchTerm,
105
105
  { goToFirstResult: this.options.goToFirstResult, suppressFragmentChange: false },
106
106
  );
107
+ } else if (this.br.urlPlugin?.getUrlParam('focus') === 'search') {
108
+ this.searchView.toggleSidebar();
107
109
  }
108
110
  }
109
111
 
@@ -381,14 +383,8 @@ export class SearchPlugin extends BookReaderPlugin {
381
383
  const $boxes = await poll(() => $(`rect.match-index-${match.matchIndex}`), { until: result => result.length > 0 });
382
384
  if ($boxes.length) {
383
385
  $boxes.css('animation', 'none');
384
- $boxes[0].scrollIntoView({
385
- // Only vertically center the highlight if we're in 1up or in full screen. In
386
- // 2up, if we're not fullscreen, the whole body gets scrolled around to try to
387
- // center the highlight 🙄 See:
388
- // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
389
- // Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
390
- // full-width, and covers up the last line of the highlight.
391
- block: this.br.constMode1up == this.br.mode || this.br.isFullscreenActive ? 'center' : 'nearest',
386
+ this.br.scrollIntoView($boxes[0], {
387
+ block: 'center',
392
388
  inline: 'center',
393
389
  behavior: onNearbyPage ? 'smooth' : 'auto',
394
390
  });
@@ -350,14 +350,9 @@ export class TtsPlugin extends BookReaderPlugin {
350
350
  // It behaves weird if used in thumb mode
351
351
  if (this.br.constModeThumb == this.br.mode) return;
352
352
 
353
- $(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`).last()?.[0]?.scrollIntoView({
354
- // Only vertically center the highlight if we're in 1up or in full screen. In
355
- // 2up, if we're not fullscreen, the whole body gets scrolled around to try to
356
- // center the highlight 🙄 See:
357
- // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
358
- // Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
359
- // full-width, and covers up the last line of the highlight.
360
- block: this.br.constMode1up == this.br.mode || this.br.isFullscreenActive ? 'center' : 'nearest',
353
+ const highlightRect = $(`.pagediv${chunk.leafIndex} .ttsHiliteLayer rect`).last()?.[0];
354
+ this.br.scrollIntoView(highlightRect, {
355
+ block: 'center',
361
356
  inline: 'center',
362
357
  behavior: 'smooth',
363
358
  });
@@ -1,8 +1,18 @@
1
+ /**
2
+ * @typedef {Object} UrlSchemaEntry
3
+ * @property {string} name
4
+ * @property {'path' | 'query_param'} position
5
+ * @property {string} [default]
6
+ * @property {string} [deprecated_for]
7
+ * @property {boolean} [readOnly] - If true, the param is read from the URL but never written back
8
+ */
9
+
1
10
  export class UrlPlugin {
2
11
  constructor(options = {}) {
3
12
  this.bookReaderOptions = options;
4
13
 
5
14
  // the canonical order of elements is important in the path and query string
15
+ /** @type {UrlSchemaEntry[]} */
6
16
  this.urlSchema = [
7
17
  { name: 'page', position: 'path', default: 'n0' },
8
18
  { name: 'mode', position: 'path', default: '2up' },
@@ -11,6 +21,7 @@ export class UrlPlugin {
11
21
  { name: 'sort', position: 'query_param' },
12
22
  { name: 'view', position: 'query_param' },
13
23
  { name: 'admin', position: 'query_param' },
24
+ { name: 'focus', position: 'query_param', readOnly: true },
14
25
  ];
15
26
 
16
27
  this.urlState = {};
@@ -36,6 +47,7 @@ export class UrlPlugin {
36
47
  if (schema?.deprecated_for) {
37
48
  schema = this.urlSchema.find(schemaKey => schemaKey.name === schema.deprecated_for);
38
49
  }
50
+ if (schema?.readOnly) return;
39
51
  if (schema?.position == 'path') {
40
52
  pathParams[schema?.name] = urlState[key];
41
53
  } else {