@internetarchive/bookreader 5.0.0-61 → 5.0.0-63

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.
@@ -1,7 +1,6 @@
1
1
  //@ts-check
2
- import { createSVGPageLayer } from '../BookReader/PageContainer.js';
3
- import { SelectionStartedObserver } from '../BookReader/utils/SelectionStartedObserver.js';
4
- import { isFirefox, isSafari } from '../util/browserSniffing.js';
2
+ import { createDIVPageLayer } from '../BookReader/PageContainer.js';
3
+ import { SelectionObserver } from '../BookReader/utils/SelectionObserver.js';
5
4
  import { applyVariables } from '../util/strings.js';
6
5
  /** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
7
6
  /** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */
@@ -41,25 +40,18 @@ export class Cache {
41
40
  }
42
41
 
43
42
  export class TextSelectionPlugin {
44
-
45
- constructor(options = DEFAULT_OPTIONS, optionVariables, avoidTspans = isFirefox(), pointerEventsOnParagraph = isSafari()) {
43
+ /**
44
+ * @param {'lr' | 'rl'} pageProgression In the future this should be in the ocr file
45
+ * since a book being right to left doesn't mean the ocr is right to left. But for
46
+ * now we do make that assumption.
47
+ */
48
+ constructor(options = DEFAULT_OPTIONS, optionVariables, pageProgression = 'lr') {
46
49
  this.options = options;
47
50
  this.optionVariables = optionVariables;
48
51
  /**@type {PromiseLike<JQuery<HTMLElement>|undefined>} */
49
52
  this.djvuPagesPromise = null;
50
- // Using text elements instead of tspans for words because Firefox does not allow svg tspan stretch.
51
- // Tspans are necessary on Chrome because they prevent newline character after every word when copying
52
- this.svgParagraphElement = "text";
53
- this.svgWordElement = "tspan";
54
- this.insertNewlines = avoidTspans;
55
- // Safari has a bug where `pointer-events` doesn't work on `<tspans>`. So
56
- // there we will set `pointer-events: all` on the paragraph element. We don't
57
- // do this everywhere, because it's a worse experience. Thanks Safari :/
58
- this.pointerEventsOnParagraph = pointerEventsOnParagraph;
59
- if (avoidTspans) {
60
- this.svgParagraphElement = "g";
61
- this.svgWordElement = "text";
62
- }
53
+ /** Whether the book is right-to-left */
54
+ this.rtl = pageProgression === 'rl';
63
55
 
64
56
  /** @type {Cache<{index: number, response: any}>} */
65
57
  this.pageTextCache = new Cache();
@@ -69,9 +61,27 @@ export class TextSelectionPlugin {
69
61
  * unusable. For now don't render text layer for pages with too many words.
70
62
  */
71
63
  this.maxWordRendered = 2500;
64
+
65
+ this.selectionObserver = new SelectionObserver('.BRtextLayer', this._onSelectionChange);
66
+ }
67
+
68
+ /**
69
+ * @param {'started' | 'cleared'} type
70
+ * @param {HTMLElement} target
71
+ */
72
+ _onSelectionChange = (type, target) => {
73
+ if (type === 'started') {
74
+ this.textSelectingMode(target);
75
+ } else if (type === 'cleared') {
76
+ this.defaultMode(target);
77
+ } else {
78
+ throw new Error(`Unknown type ${type}`);
79
+ }
72
80
  }
73
81
 
74
82
  init() {
83
+ this.selectionObserver.attach();
84
+
75
85
  // Only fetch the full djvu xml if the single page url isn't there
76
86
  if (this.options.singlePageDjvuXmlUrl) return;
77
87
  this.djvuPagesPromise = $.ajax({
@@ -135,53 +145,73 @@ export class TextSelectionPlugin {
135
145
 
136
146
  /**
137
147
  * Applies mouse events when in default mode
138
- * @param {SVGElement} svg
148
+ * @param {HTMLElement} textLayer
139
149
  */
140
- defaultMode(svg) {
141
- svg.classList.remove("selectingSVG");
142
- $(svg).on("mousedown.textSelectPluginHandler", (event) => {
143
- if (!$(event.target).is(".BRwordElement")) return;
144
- event.stopPropagation();
145
- svg.classList.add("selectingSVG");
146
- $(svg).one("mouseup.textSelectPluginHandler", (event) => {
147
- if (window.getSelection().toString() != "") {
148
- event.stopPropagation();
149
- $(svg).off(".textSelectPluginHandler");
150
- this.textSelectingMode(svg);
151
- }
152
- else svg.classList.remove("selectingSVG");
153
- });
150
+ defaultMode(textLayer) {
151
+ const $pageContainer = $(textLayer).closest('.BRpagecontainer');
152
+ textLayer.style.pointerEvents = "none";
153
+ $pageContainer.find("img").css("pointer-events", "auto");
154
+
155
+ $(textLayer).off(".textSelectPluginHandler");
156
+ const startedMouseDown = this.mouseIsDown;
157
+ let skipNextMouseup = this.mouseIsDown;
158
+ if (startedMouseDown) {
159
+ textLayer.style.pointerEvents = "auto";
160
+ }
161
+
162
+ // Need to stop propagation to prevent DragScrollable from
163
+ // blocking selection
164
+ $(textLayer).on("mousedown.textSelectPluginHandler", (event) => {
165
+ this.mouseIsDown = true;
166
+ if ($(event.target).is(".BRwordElement, .BRspace")) {
167
+ event.stopPropagation();
168
+ }
169
+ });
170
+
171
+ $(textLayer).on("mouseup.textSelectPluginHandler", (event) => {
172
+ this.mouseIsDown = false;
173
+ textLayer.style.pointerEvents = "none";
174
+ if (skipNextMouseup) {
175
+ skipNextMouseup = false;
176
+ event.stopPropagation();
177
+ }
154
178
  });
155
179
  }
156
180
 
157
181
  /**
158
- * Applies mouse events when in textSelecting mode
159
- * @param {SVGElement} svg
182
+ * This mode is active while there is a selection on the given textLayer
183
+ * @param {HTMLElement} textLayer
160
184
  */
161
- textSelectingMode(svg) {
162
- $(svg).on('mousedown.textSelectPluginHandler', (event) => {
163
- if (!$(event.target).is(".BRwordElement")) {
164
- if (window.getSelection().toString() != "") window.getSelection().removeAllRanges();
165
- }
185
+ textSelectingMode(textLayer) {
186
+ const $pageContainer = $(textLayer).closest('.BRpagecontainer');
187
+ // Make text layer consume all events
188
+ textLayer.style.pointerEvents = "all";
189
+ // Block img from getting long-press to save while selecting
190
+ $pageContainer.find("img").css("pointer-events", "none");
191
+
192
+ $(textLayer).off(".textSelectPluginHandler");
193
+
194
+ $(textLayer).on('mousedown.textSelectPluginHandler', (event) => {
195
+ this.mouseIsDown = true;
166
196
  event.stopPropagation();
167
197
  });
168
- $(svg).on('mouseup.textSelectPluginHandler', (event) => {
198
+
199
+ // Prevent page flip on click
200
+ $(textLayer).on('mouseup.textSelectPluginHandler', (event) => {
201
+ this.mouseIsDown = false;
169
202
  event.stopPropagation();
170
- if (window.getSelection().toString() == "") {
171
- $(svg).off(".textSelectPluginHandler");
172
- this.defaultMode(svg); }
173
203
  });
174
204
  }
175
205
 
176
206
  /**
177
- * Initializes text selection modes if there is an svg on the page
207
+ * Initializes text selection modes if there is a text layer on the page
178
208
  * @param {JQuery} $container
179
209
  */
180
210
  stopPageFlip($container) {
181
- /** @type {JQuery<SVGElement>} */
182
- const $svg = $container.find('svg.textSelectionSVG');
183
- if (!$svg.length) return;
184
- $svg.each((i, s) => this.defaultMode(s));
211
+ /** @type {JQuery<HTMLElement>} */
212
+ const $textLayer = $container.find('.BRtextLayer');
213
+ if (!$textLayer.length) return;
214
+ $textLayer.each((i, s) => this.defaultMode(s));
185
215
  this.interceptCopy($container);
186
216
  }
187
217
 
@@ -191,10 +221,11 @@ export class TextSelectionPlugin {
191
221
  async createTextLayer(pageContainer) {
192
222
  const pageIndex = pageContainer.page.index;
193
223
  const $container = pageContainer.$container;
194
- const $svgLayers = $container.find('.textSelectionSVG');
195
- if ($svgLayers.length) return;
224
+ const $textLayers = $container.find('.BRtextLayer');
225
+ if ($textLayers.length) return;
196
226
  const XMLpage = await this.getPageText(pageIndex);
197
227
  if (!XMLpage) return;
228
+ recursivelyAddCoords(XMLpage);
198
229
 
199
230
  const totalWords = $(XMLpage).find("WORD").length;
200
231
  if (totalWords > this.maxWordRendered) {
@@ -202,66 +233,174 @@ export class TextSelectionPlugin {
202
233
  return;
203
234
  }
204
235
 
205
- const svg = createSVGPageLayer(pageContainer.page, 'textSelectionSVG');
206
- $container.append(svg);
207
-
208
- $(XMLpage).find("PARAGRAPH").each((i, paragraph) => {
209
- // Adding text element for each paragraph in the page
210
- const words = $(paragraph).find("WORD");
211
- if (!words.length) return;
212
- const paragSvg = document.createElementNS("http://www.w3.org/2000/svg", this.svgParagraphElement);
213
- paragSvg.setAttribute("class", "BRparagElement");
214
- if (this.pointerEventsOnParagraph) {
215
- paragSvg.style.pointerEvents = "all";
216
- }
236
+ const textLayer = createDIVPageLayer(pageContainer.page, 'BRtextLayer');
237
+ const ratioW = parseFloat(pageContainer.$container[0].style.width) / pageContainer.page.width;
238
+ const ratioH = parseFloat(pageContainer.$container[0].style.height) / pageContainer.page.height;
239
+ textLayer.style.transform = `scale(${ratioW}, ${ratioH})`;
240
+ textLayer.setAttribute("dir", this.rtl ? "rtl" : "ltr");
241
+
242
+ const ocrParagraphs = $(XMLpage).find("PARAGRAPH[coords]").toArray();
243
+ const paragEls = ocrParagraphs.map(p => {
244
+ const el = this.renderParagraph(p);
245
+ textLayer.appendChild(el);
246
+ return el;
247
+ });
248
+
249
+ // Fix up paragraph positions
250
+ const paragraphRects = determineRealRects(textLayer, '.BRparagraphElement');
251
+ let yAdded = 0;
252
+ for (const [ocrParagraph, paragEl] of zip(ocrParagraphs, paragEls)) {
253
+ const ocrParagBounds = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
254
+ const realRect = paragraphRects.get(paragEl);
255
+ const [ocrLeft, , ocrRight, ocrTop] = ocrParagBounds;
256
+ const newStartMargin = this.rtl ? (realRect.right - ocrRight) : (ocrLeft - realRect.left);
257
+ const newTop = ocrTop - (realRect.top + yAdded);
258
+
259
+ paragEl.style[this.rtl ? 'marginRight' : 'marginLeft'] = `${newStartMargin}px`;
260
+ paragEl.style.marginTop = `${newTop}px`;
261
+ yAdded += newTop;
262
+ textLayer.appendChild(paragEl);
263
+ }
264
+ $container.append(textLayer);
265
+ this.stopPageFlip($container);
266
+ }
267
+
268
+ /**
269
+ * @param {HTMLElement} ocrParagraph
270
+ * @returns {HTMLParagraphElement}
271
+ */
272
+ renderParagraph(ocrParagraph) {
273
+ const paragEl = document.createElement('p');
274
+ paragEl.classList.add('BRparagraphElement');
275
+ const [paragLeft, paragBottom, paragRight, paragTop] = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
276
+ const wordHeightArr = [];
277
+ const lines = $(ocrParagraph).find("LINE[coords]").toArray();
278
+ if (!lines.length) return paragEl;
217
279
 
218
- const wordHeightArr = [];
219
280
 
220
- for (let i = 0; i < words.length; i++) {
221
- // Adding tspan for each word in paragraph
222
- const currWord = words[i];
223
- // eslint-disable-next-line no-unused-vars
224
- const [left, bottom, right, top] = $(currWord).attr("coords").split(',').map(parseFloat);
281
+ for (const [prevLine, line, nextLine] of lookAroundWindow(genMap(lines, augmentLine))) {
282
+ const isLastLineOfParagraph = line.ocrElement == lines[lines.length - 1];
283
+ const lineEl = document.createElement('span');
284
+ lineEl.classList.add('BRlineElement');
285
+
286
+ for (const [wordIndex, currWord] of line.words.entries()) {
287
+ const [, bottom, right, top] = $(currWord).attr("coords").split(',').map(parseFloat);
225
288
  const wordHeight = bottom - top;
226
289
  wordHeightArr.push(wordHeight);
227
290
 
228
- const wordTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
229
- wordTspan.setAttribute("class", "BRwordElement");
230
- wordTspan.setAttribute("x", left.toString());
231
- wordTspan.setAttribute("y", bottom.toString());
232
- wordTspan.setAttribute("textLength", (right - left).toString());
233
- wordTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
234
- wordTspan.textContent = currWord.textContent;
235
- paragSvg.appendChild(wordTspan);
236
-
237
- // Adding spaces after words except at the end of the paragraph
238
- // TODO: assumes left-to-right text
239
- if (i < words.length - 1) {
240
- const nextWord = words[i + 1];
241
- // eslint-disable-next-line no-unused-vars
242
- const [leftNext, bottomNext, rightNext, topNext] = $(nextWord).attr("coords").split(',').map(parseFloat);
243
- const spaceTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
244
- spaceTspan.setAttribute("class", "BRwordElement");
245
- spaceTspan.setAttribute("x", right.toString());
246
- spaceTspan.setAttribute("y", bottom.toString());
247
- if ((leftNext - right) > 0) spaceTspan.setAttribute("textLength", (leftNext - right).toString());
248
- spaceTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
249
- spaceTspan.textContent = " ";
250
- paragSvg.appendChild(spaceTspan);
291
+ if (wordIndex == 0 && prevLine?.lastWord.textContent.trim().endsWith('-')) {
292
+ // ideally prefer the next line to determine the left position,
293
+ // since the previous line could be the first line of the paragraph
294
+ // and hence have an incorrectly indented first word.
295
+ // E.g. https://archive.org/details/driitaleofdaring00bachuoft/page/360/mode/2up
296
+ const [newLeft, , , ] = $((nextLine || prevLine).firstWord).attr("coords").split(',').map(parseFloat);
297
+ $(currWord).attr("coords", `${newLeft},${bottom},${right},${top}`);
251
298
  }
252
299
 
253
- // Adds newline at the end of paragraph on Firefox
254
- if ((i == words.length - 1 && (this.insertNewlines))) {
255
- paragSvg.appendChild(document.createTextNode("\n"));
300
+ const wordEl = document.createElement('span');
301
+ wordEl.setAttribute("class", "BRwordElement");
302
+ wordEl.textContent = currWord.textContent.trim();
303
+
304
+ if (wordIndex > 0) {
305
+ const space = document.createElement('span');
306
+ space.classList.add('BRspace');
307
+ space.textContent = ' ';
308
+ lineEl.append(space);
309
+
310
+ // Edge ignores empty elements (like BRspace), so add another
311
+ // space to ensure Edge's ReadAloud works correctly.
312
+ lineEl.appendChild(document.createTextNode(' '));
256
313
  }
314
+
315
+ lineEl.appendChild(wordEl);
257
316
  }
258
317
 
259
- wordHeightArr.sort();
260
- const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)];
261
- paragSvg.setAttribute("font-size", paragWordHeight.toString());
262
- svg.appendChild(paragSvg);
263
- });
264
- this.stopPageFlip($container);
318
+ const hasHyphen = line.lastWord.textContent.trim().endsWith('-');
319
+ const lastWordEl = lineEl.children[lineEl.children.length - 1];
320
+ if (hasHyphen && !isLastLineOfParagraph) {
321
+ lastWordEl.textContent = lastWordEl.textContent.trim().slice(0, -1);
322
+ lastWordEl.classList.add('BRwordElement--hyphen');
323
+ }
324
+
325
+ paragEl.appendChild(lineEl);
326
+ if (!isLastLineOfParagraph && !hasHyphen) {
327
+ // Edge does not correctly have spaces between the lines.
328
+ paragEl.appendChild(document.createTextNode(' '));
329
+ }
330
+ }
331
+
332
+ wordHeightArr.sort((a, b) => a - b);
333
+ const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)] + 4;
334
+ paragEl.style.left = `${paragLeft}px`;
335
+ paragEl.style.top = `${paragTop}px`;
336
+ paragEl.style.width = `${paragRight - paragLeft}px`;
337
+ paragEl.style.height = `${paragBottom - paragTop}px`;
338
+ paragEl.style.fontSize = `${paragWordHeight}px`;
339
+
340
+ // Fix up sizes - stretch/crush words as necessary using letter spacing
341
+ let wordRects = determineRealRects(paragEl, '.BRwordElement');
342
+ const ocrWords = $(ocrParagraph).find("WORD").toArray();
343
+ const wordEls = paragEl.querySelectorAll('.BRwordElement');
344
+ for (const [ocrWord, wordEl] of zip(ocrWords, wordEls)) {
345
+ const realRect = wordRects.get(wordEl);
346
+ const [left, , right ] = $(ocrWord).attr("coords").split(',').map(parseFloat);
347
+ let ocrWidth = right - left;
348
+ // Some books (eg theworksofplato01platiala) have a space _inside_ the <WORD>
349
+ // element. That makes it impossible to determine the correct positining
350
+ // of everything, but to avoid the BRspace's being width 0, which makes selection
351
+ // janky on Chrome Android, assume the space is the same width as one of the
352
+ // letters.
353
+ if (ocrWord.textContent.endsWith(' ')) {
354
+ ocrWidth = ocrWidth * (ocrWord.textContent.length - 1) / ocrWord.textContent.length;
355
+ }
356
+ const diff = ocrWidth - realRect.width;
357
+ wordEl.style.letterSpacing = `${diff / (ocrWord.textContent.length - 1)}px`;
358
+ }
359
+
360
+ // Stretch/crush lines as necessary using line spacing
361
+ // Recompute rects after letter spacing
362
+ wordRects = determineRealRects(paragEl, '.BRwordElement');
363
+ const spaceRects = determineRealRects(paragEl, '.BRspace');
364
+
365
+ const ocrLines = $(ocrParagraph).find("LINE[coords]").toArray();
366
+ const lineEls = Array.from(paragEl.querySelectorAll('.BRlineElement'));
367
+
368
+ let ySoFar = paragTop;
369
+ for (const [ocrLine, lineEl] of zip(ocrLines, lineEls)) {
370
+ // shift words using marginLeft to align with the correct x position
371
+ const words = $(ocrLine).find("WORD").toArray();
372
+ // const ocrLineLeft = Math.min(...words.map(w => parseFloat($(w).attr("coords").split(',')[0])));
373
+ let xSoFar = this.rtl ? paragRight : paragLeft;
374
+ for (const [ocrWord, wordEl] of zip(words, lineEl.querySelectorAll('.BRwordElement'))) {
375
+ // start of line, need to compute the offset relative to the OCR words
376
+ const wordRect = wordRects.get(wordEl);
377
+ const [ocrLeft, , ocrRight ] = $(ocrWord).attr("coords").split(',').map(parseFloat);
378
+ const diff = (this.rtl ? -(ocrRight - xSoFar) : ocrLeft - xSoFar);
379
+
380
+ if (wordEl.previousElementSibling) {
381
+ const space = wordEl.previousElementSibling;
382
+ space.style.letterSpacing = `${diff - spaceRects.get(space).width}px`;
383
+ } else {
384
+ wordEl.style[this.rtl ? 'paddingRight' : 'paddingLeft'] = `${diff}px`;
385
+ }
386
+ if (this.rtl) xSoFar -= diff + wordRect.width;
387
+ else xSoFar += diff + wordRect.width;
388
+ }
389
+ // And also fix y position
390
+ const ocrLineTop = Math.min(...words.map(w => parseFloat($(w).attr("coords").split(',')[3])));
391
+ const diff = ocrLineTop - ySoFar;
392
+ if (lineEl.previousElementSibling) {
393
+ lineEl.previousElementSibling.style.lineHeight = `${diff}px`;
394
+ ySoFar += diff;
395
+ }
396
+ }
397
+
398
+ // The last line will have a line height subtracting from the paragraph height
399
+ lineEls[lineEls.length - 1].style.lineHeight = `${paragBottom - ySoFar}px`;
400
+
401
+ // Edge does not include a newline for some reason when copying/pasting the <p> els
402
+ paragEl.appendChild(document.createElement('br'));
403
+ return paragEl;
265
404
  }
266
405
  }
267
406
 
@@ -269,23 +408,22 @@ export class BookreaderWithTextSelection extends BookReader {
269
408
  init() {
270
409
  const options = Object.assign({}, DEFAULT_OPTIONS, this.options.plugins.textSelection);
271
410
  if (options.enabled) {
272
- this.textSelectionPlugin = new TextSelectionPlugin(options, this.options.vars);
411
+ this.textSelectionPlugin = new TextSelectionPlugin(options, this.options.vars, this.pageProgression);
273
412
  // Write this back; this way the plugin is the source of truth, and BR just
274
413
  // contains a reference to it.
275
414
  this.options.plugins.textSelection = options;
276
415
  this.textSelectionPlugin.init();
277
416
 
278
- // Track how often selection is used
279
- const sso = new SelectionStartedObserver('.textSelectionSVG', () => {
280
- // Don't assume the order of the plugins ; the analytics plugin could
281
- // have been added later. But at this point we should know for certain.
282
- if (!this.archiveAnalyticsSendEvent) {
283
- sso.detach();
284
- } else {
285
- this.archiveAnalyticsSendEvent('BookReader', 'SelectStart');
417
+ new SelectionObserver('.BRtextLayer', (selectEvent) => {
418
+ // Track how often selection is used
419
+ if (selectEvent == 'started') {
420
+ this.archiveAnalyticsSendEvent?.('BookReader', 'SelectStart');
421
+
422
+ // Set a class on the page to avoid hiding it when zooming/etc
423
+ this.refs.$br.find('.BRpagecontainer--hasSelection').removeClass('BRpagecontainer--hasSelection');
424
+ $(window.getSelection().anchorNode).closest('.BRpagecontainer').addClass('BRpagecontainer--hasSelection');
286
425
  }
287
- });
288
- sso.attach();
426
+ }).attach();
289
427
  }
290
428
 
291
429
  super.init();
@@ -306,3 +444,190 @@ export class BookreaderWithTextSelection extends BookReader {
306
444
  }
307
445
  window.BookReader = BookreaderWithTextSelection;
308
446
  export default BookreaderWithTextSelection;
447
+
448
+
449
+ /**
450
+ * @param {HTMLElement} parentEl
451
+ * @param {string} selector
452
+ * @returns {Map<Element, Rect>}
453
+ */
454
+ function determineRealRects(parentEl, selector) {
455
+ const initals = {
456
+ position: parentEl.style.position,
457
+ visibility: parentEl.style.visibility,
458
+ top: parentEl.style.top,
459
+ left: parentEl.style.left,
460
+ transform: parentEl.style.transform,
461
+ };
462
+ parentEl.style.position = 'absolute';
463
+ parentEl.style.visibility = 'hidden';
464
+ parentEl.style.top = '0';
465
+ parentEl.style.left = '0';
466
+ parentEl.style.transform = 'none';
467
+ document.body.appendChild(parentEl);
468
+ const rects = new Map(
469
+ Array.from(parentEl.querySelectorAll(selector))
470
+ .map(wordEl => {
471
+ const origRect = wordEl.getBoundingClientRect();
472
+ return [wordEl, new Rect(
473
+ origRect.left + window.scrollX,
474
+ origRect.top + window.scrollY,
475
+ origRect.width,
476
+ origRect.height,
477
+ )];
478
+ })
479
+ );
480
+ document.body.removeChild(parentEl);
481
+ Object.assign(parentEl.style, initals);
482
+ return rects;
483
+ }
484
+
485
+ /**
486
+ * @param {HTMLElement} line
487
+ */
488
+ function augmentLine(line) {
489
+ const words = $(line).find("WORD").toArray();
490
+ return {
491
+ ocrElement: line,
492
+ words,
493
+ firstWord: words[0],
494
+ lastWord: words[words.length - 1],
495
+ };
496
+ }
497
+
498
+ /**
499
+ * @template TFrom, TTo
500
+ * Generator version of map
501
+ * @param {Iterable<TFrom>} gen
502
+ * @param {function(TFrom): TTo} fn
503
+ * @returns {Iterable<TTo>}
504
+ */
505
+ export function* genMap(gen, fn) {
506
+ for (const x of gen) yield fn(x);
507
+ }
508
+
509
+ /**
510
+ * @template T
511
+ * Generator that provides a sliding window of 3 elements,
512
+ * prev, current, and next.
513
+ * @param {Iterable<T>} gen
514
+ * @returns {Iterable<[T | undefined, T, T | undefined]>}
515
+ */
516
+ export function* lookAroundWindow(gen) {
517
+ let prev = undefined;
518
+ let cur = undefined;
519
+ let next = undefined;
520
+ for (const x of gen) {
521
+ if (typeof cur !== 'undefined') {
522
+ next = x;
523
+ yield [prev, cur, next];
524
+ }
525
+ prev = cur;
526
+ cur = x;
527
+ next = undefined;
528
+ }
529
+
530
+ if (typeof cur !== 'undefined') {
531
+ yield [prev, cur, next];
532
+ }
533
+ }
534
+
535
+ /**
536
+ * @template T1, T2
537
+ * Lazy zip implementation to avoid importing lodash
538
+ * Expects iterators to be of the same length
539
+ * @param {Iterable<T1>} gen1
540
+ * @param {Iterable<T2>} gen2
541
+ * @returns {Iterable<[T1, T2]>}
542
+ */
543
+ export function* zip(gen1, gen2) {
544
+ const it1 = gen1[Symbol.iterator]();
545
+ const it2 = gen2[Symbol.iterator]();
546
+ while (true) {
547
+ const r1 = it1.next();
548
+ const r2 = it2.next();
549
+ if (r1.done && r2.done) {
550
+ return;
551
+ }
552
+ if (r1.done || r2.done) {
553
+ throw new Error('zip: one of the iterators is done');
554
+ }
555
+ yield [r1.value, r2.value];
556
+ }
557
+ }
558
+
559
+ /**
560
+ * [left, bottom, right, top]
561
+ * @param {Array<[number, number, number, number]>} bounds
562
+ * @returns {[number, number, number, number]}
563
+ */
564
+ function determineBounds(bounds) {
565
+ let leftMost = Infinity;
566
+ let bottomMost = -Infinity;
567
+ let rightMost = -Infinity;
568
+ let topMost = Infinity;
569
+
570
+ for (const [left, bottom, right, top] of bounds) {
571
+ leftMost = Math.min(leftMost, left);
572
+ bottomMost = Math.max(bottomMost, bottom);
573
+ rightMost = Math.max(rightMost, right);
574
+ topMost = Math.min(topMost, top);
575
+ }
576
+
577
+ return [leftMost, bottomMost, rightMost, topMost];
578
+ }
579
+
580
+ /**
581
+ * Recursively traverses the XML tree and adds coords
582
+ * which are the bounding box of all child coords
583
+ * @param {Element} xmlEl
584
+ */
585
+ function recursivelyAddCoords(xmlEl) {
586
+ if ($(xmlEl).attr('coords') || !xmlEl.children) {
587
+ return;
588
+ }
589
+
590
+ const children = $(xmlEl).children().toArray();
591
+ if (children.length === 0) {
592
+ return;
593
+ }
594
+
595
+ for (const child of children) {
596
+ recursivelyAddCoords(child);
597
+ }
598
+
599
+ const childCoords = [];
600
+
601
+ for (const child of children) {
602
+ if (!$(child).attr('coords')) continue;
603
+ childCoords.push($(child).attr('coords').split(',').map(parseFloat));
604
+ }
605
+
606
+ const boundingCoords = determineBounds(childCoords);
607
+ if (Math.abs(boundingCoords[0]) != Infinity) {
608
+ $(xmlEl).attr('coords', boundingCoords.join(','));
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Basically a polyfill for the native DOMRect class
614
+ */
615
+ class Rect {
616
+ /**
617
+ * @param {number} x
618
+ * @param {number} y
619
+ * @param {number} width
620
+ * @param {number} height
621
+ */
622
+ constructor(x, y, width, height) {
623
+ this.x = x;
624
+ this.y = y;
625
+ this.width = width;
626
+ this.height = height;
627
+ }
628
+
629
+ get right() { return this.x + this.width; }
630
+ get bottom() { return this.y + this.height; }
631
+ get top() { return this.y; }
632
+ get left() { return this.x; }
633
+ }
@@ -383,14 +383,6 @@ class SearchView {
383
383
  }
384
384
  }
385
385
 
386
- /**
387
- * @param {Event} e
388
- */
389
- handleNavToggledCallback(e) {
390
- const is_visible = this.br.navigationIsVisible();
391
- this.togglePinsFor(is_visible);
392
- }
393
-
394
386
  handleSearchStarted() {
395
387
  this.emptyMatches();
396
388
  this.br.removeSearchHilites();
@@ -425,7 +417,6 @@ class SearchView {
425
417
 
426
418
  window.addEventListener(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this));
427
419
  $(document).on(`${namespace}SearchCallback`, this.handleSearchCallback.bind(this))
428
- .on(`${namespace}navToggled`, this.handleNavToggledCallback.bind(this))
429
420
  .on(`${namespace}SearchStarted`, this.handleSearchStarted.bind(this))
430
421
  .on(`${namespace}SearchCallbackBookNotIndexed`, this.handleSearchCallbackBookNotIndexed.bind(this))
431
422
  .on(`${namespace}SearchCallbackEmpty`, this.handleSearchCallbackEmpty.bind(this))