@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.
- package/BookReader/BookReader.css +44 -18
- package/BookReader/BookReader.js +1 -43
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +109 -67
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js.map +1 -1
- package/BookReader/plugins/plugin.chapters.js +2 -2
- package/BookReader/plugins/plugin.chapters.js.map +1 -1
- package/BookReader/plugins/plugin.experiments.js +1 -1
- package/BookReader/plugins/plugin.experiments.js.map +1 -1
- package/BookReader/plugins/plugin.iframe.js +1 -1
- package/BookReader/plugins/plugin.iframe.js.map +1 -1
- package/BookReader/plugins/plugin.iiif.js +1 -1
- package/BookReader/plugins/plugin.resume.js +1 -1
- package/BookReader/plugins/plugin.search.js +1 -1
- package/BookReader/plugins/plugin.search.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +63 -1
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.translate.js +65 -3
- package/BookReader/plugins/plugin.translate.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/plugins/plugin.url.js +1 -1
- package/BookReader/plugins/plugin.url.js.map +1 -1
- package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
- package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
- package/README.md +3 -1
- package/jsconfig.json +2 -3
- package/package.json +38 -31
- package/src/BookReader.js +24 -20
- package/src/css/_TextSelection.scss +51 -18
- package/src/plugins/plugin.chapters.js +8 -4
- package/src/plugins/plugin.iframe.js +36 -37
- package/src/plugins/plugin.text_selection.js +40 -101
- package/src/plugins/search/plugin.search.js +4 -8
- package/src/plugins/tts/plugin.tts.js +3 -8
- package/src/plugins/url/UrlPlugin.js +12 -0
- package/src/util/TextSelectionManager.js +221 -154
- package/src/util/dom.js +88 -0
- 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
|
-
|
|
70
|
-
if (
|
|
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]
|
|
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
|
-
|
|
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 +
|
|
407
|
-
origRect.top +
|
|
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
|
-
|
|
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]
|
|
385
|
-
|
|
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]
|
|
354
|
-
|
|
355
|
-
|
|
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 {
|