@internetarchive/bookreader 5.0.0-60 → 5.0.0-62

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,6 +1,6 @@
1
1
  //@ts-check
2
- import { createSVGPageLayer } from '../BookReader/PageContainer.js';
3
- import { isFirefox, isSafari } from '../util/browserSniffing.js';
2
+ import { createDIVPageLayer } from '../BookReader/PageContainer.js';
3
+ import { SelectionObserver } from '../BookReader/utils/SelectionObserver.js';
4
4
  import { applyVariables } from '../util/strings.js';
5
5
  /** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
6
6
  /** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */
@@ -40,25 +40,18 @@ export class Cache {
40
40
  }
41
41
 
42
42
  export class TextSelectionPlugin {
43
-
44
- 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') {
45
49
  this.options = options;
46
50
  this.optionVariables = optionVariables;
47
51
  /**@type {PromiseLike<JQuery<HTMLElement>|undefined>} */
48
52
  this.djvuPagesPromise = null;
49
- // Using text elements instead of tspans for words because Firefox does not allow svg tspan stretch.
50
- // Tspans are necessary on Chrome because they prevent newline character after every word when copying
51
- this.svgParagraphElement = "text";
52
- this.svgWordElement = "tspan";
53
- this.insertNewlines = avoidTspans;
54
- // Safari has a bug where `pointer-events` doesn't work on `<tspans>`. So
55
- // there we will set `pointer-events: all` on the paragraph element. We don't
56
- // do this everywhere, because it's a worse experience. Thanks Safari :/
57
- this.pointerEventsOnParagraph = pointerEventsOnParagraph;
58
- if (avoidTspans) {
59
- this.svgParagraphElement = "g";
60
- this.svgWordElement = "text";
61
- }
53
+ /** Whether the book is right-to-left */
54
+ this.rtl = pageProgression === 'rl';
62
55
 
63
56
  /** @type {Cache<{index: number, response: any}>} */
64
57
  this.pageTextCache = new Cache();
@@ -68,9 +61,27 @@ export class TextSelectionPlugin {
68
61
  * unusable. For now don't render text layer for pages with too many words.
69
62
  */
70
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
+ }
71
80
  }
72
81
 
73
82
  init() {
83
+ this.selectionObserver.attach();
84
+
74
85
  // Only fetch the full djvu xml if the single page url isn't there
75
86
  if (this.options.singlePageDjvuXmlUrl) return;
76
87
  this.djvuPagesPromise = $.ajax({
@@ -134,53 +145,73 @@ export class TextSelectionPlugin {
134
145
 
135
146
  /**
136
147
  * Applies mouse events when in default mode
137
- * @param {SVGElement} svg
148
+ * @param {HTMLElement} textLayer
138
149
  */
139
- defaultMode(svg) {
140
- svg.classList.remove("selectingSVG");
141
- $(svg).on("mousedown.textSelectPluginHandler", (event) => {
142
- if (!$(event.target).is(".BRwordElement")) return;
143
- event.stopPropagation();
144
- svg.classList.add("selectingSVG");
145
- $(svg).one("mouseup.textSelectPluginHandler", (event) => {
146
- if (window.getSelection().toString() != "") {
147
- event.stopPropagation();
148
- $(svg).off(".textSelectPluginHandler");
149
- this.textSelectingMode(svg);
150
- }
151
- else svg.classList.remove("selectingSVG");
152
- });
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
+ }
153
178
  });
154
179
  }
155
180
 
156
181
  /**
157
- * Applies mouse events when in textSelecting mode
158
- * @param {SVGElement} svg
182
+ * This mode is active while there is a selection on the given textLayer
183
+ * @param {HTMLElement} textLayer
159
184
  */
160
- textSelectingMode(svg) {
161
- $(svg).on('mousedown.textSelectPluginHandler', (event) => {
162
- if (!$(event.target).is(".BRwordElement")) {
163
- if (window.getSelection().toString() != "") window.getSelection().removeAllRanges();
164
- }
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;
165
196
  event.stopPropagation();
166
197
  });
167
- $(svg).on('mouseup.textSelectPluginHandler', (event) => {
198
+
199
+ // Prevent page flip on click
200
+ $(textLayer).on('mouseup.textSelectPluginHandler', (event) => {
201
+ this.mouseIsDown = false;
168
202
  event.stopPropagation();
169
- if (window.getSelection().toString() == "") {
170
- $(svg).off(".textSelectPluginHandler");
171
- this.defaultMode(svg); }
172
203
  });
173
204
  }
174
205
 
175
206
  /**
176
- * 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
177
208
  * @param {JQuery} $container
178
209
  */
179
210
  stopPageFlip($container) {
180
- /** @type {JQuery<SVGElement>} */
181
- const $svg = $container.find('svg.textSelectionSVG');
182
- if (!$svg.length) return;
183
- $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));
184
215
  this.interceptCopy($container);
185
216
  }
186
217
 
@@ -190,10 +221,11 @@ export class TextSelectionPlugin {
190
221
  async createTextLayer(pageContainer) {
191
222
  const pageIndex = pageContainer.page.index;
192
223
  const $container = pageContainer.$container;
193
- const $svgLayers = $container.find('.textSelectionSVG');
194
- if ($svgLayers.length) return;
224
+ const $textLayers = $container.find('.BRtextLayer');
225
+ if ($textLayers.length) return;
195
226
  const XMLpage = await this.getPageText(pageIndex);
196
227
  if (!XMLpage) return;
228
+ recursivelyAddCoords(XMLpage);
197
229
 
198
230
  const totalWords = $(XMLpage).find("WORD").length;
199
231
  if (totalWords > this.maxWordRendered) {
@@ -201,66 +233,174 @@ export class TextSelectionPlugin {
201
233
  return;
202
234
  }
203
235
 
204
- const svg = createSVGPageLayer(pageContainer.page, 'textSelectionSVG');
205
- $container.append(svg);
206
-
207
- $(XMLpage).find("PARAGRAPH").each((i, paragraph) => {
208
- // Adding text element for each paragraph in the page
209
- const words = $(paragraph).find("WORD");
210
- if (!words.length) return;
211
- const paragSvg = document.createElementNS("http://www.w3.org/2000/svg", this.svgParagraphElement);
212
- paragSvg.setAttribute("class", "BRparagElement");
213
- if (this.pointerEventsOnParagraph) {
214
- paragSvg.style.pointerEvents = "all";
215
- }
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;
216
279
 
217
- const wordHeightArr = [];
218
280
 
219
- for (let i = 0; i < words.length; i++) {
220
- // Adding tspan for each word in paragraph
221
- const currWord = words[i];
222
- // eslint-disable-next-line no-unused-vars
223
- 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);
224
288
  const wordHeight = bottom - top;
225
289
  wordHeightArr.push(wordHeight);
226
290
 
227
- const wordTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
228
- wordTspan.setAttribute("class", "BRwordElement");
229
- wordTspan.setAttribute("x", left.toString());
230
- wordTspan.setAttribute("y", bottom.toString());
231
- wordTspan.setAttribute("textLength", (right - left).toString());
232
- wordTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
233
- wordTspan.textContent = currWord.textContent;
234
- paragSvg.appendChild(wordTspan);
235
-
236
- // Adding spaces after words except at the end of the paragraph
237
- // TODO: assumes left-to-right text
238
- if (i < words.length - 1) {
239
- const nextWord = words[i + 1];
240
- // eslint-disable-next-line no-unused-vars
241
- const [leftNext, bottomNext, rightNext, topNext] = $(nextWord).attr("coords").split(',').map(parseFloat);
242
- const spaceTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
243
- spaceTspan.setAttribute("class", "BRwordElement");
244
- spaceTspan.setAttribute("x", right.toString());
245
- spaceTspan.setAttribute("y", bottom.toString());
246
- if ((leftNext - right) > 0) spaceTspan.setAttribute("textLength", (leftNext - right).toString());
247
- spaceTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
248
- spaceTspan.textContent = " ";
249
- 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}`);
250
298
  }
251
299
 
252
- // Adds newline at the end of paragraph on Firefox
253
- if ((i == words.length - 1 && (this.insertNewlines))) {
254
- 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(' '));
255
313
  }
314
+
315
+ lineEl.appendChild(wordEl);
256
316
  }
257
317
 
258
- wordHeightArr.sort();
259
- const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)];
260
- paragSvg.setAttribute("font-size", paragWordHeight.toString());
261
- svg.appendChild(paragSvg);
262
- });
263
- 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;
264
404
  }
265
405
  }
266
406
 
@@ -268,12 +408,24 @@ export class BookreaderWithTextSelection extends BookReader {
268
408
  init() {
269
409
  const options = Object.assign({}, DEFAULT_OPTIONS, this.options.plugins.textSelection);
270
410
  if (options.enabled) {
271
- this.textSelectionPlugin = new TextSelectionPlugin(options, this.options.vars);
411
+ this.textSelectionPlugin = new TextSelectionPlugin(options, this.options.vars, this.pageProgression);
272
412
  // Write this back; this way the plugin is the source of truth, and BR just
273
413
  // contains a reference to it.
274
414
  this.options.plugins.textSelection = options;
275
415
  this.textSelectionPlugin.init();
416
+
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');
425
+ }
426
+ }).attach();
276
427
  }
428
+
277
429
  super.init();
278
430
  }
279
431
 
@@ -292,3 +444,190 @@ export class BookreaderWithTextSelection extends BookReader {
292
444
  }
293
445
  window.BookReader = BookreaderWithTextSelection;
294
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
+ }
@@ -418,7 +418,9 @@ BookReader.prototype._searchPluginGoToResult = async function (matchIndex) {
418
418
 
419
419
  // Trigger an update of book
420
420
  this._modes.mode1Up.mode1UpLit.updatePages();
421
- await this._modes.mode1Up.mode1UpLit.updateComplete;
421
+ if (this.activeMode == this._modes.mode1Up) {
422
+ await this._modes.mode1Up.mode1UpLit.updateComplete;
423
+ }
422
424
  }
423
425
  /* this updates the URL */
424
426
  if (!this._isIndexDisplayed(pageIndex)) {
@@ -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))