@internetarchive/bookreader 5.0.0-9-multiple-files → 5.0.0-90

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 (333) hide show
  1. package/.eslintrc.js +21 -19
  2. package/.github/workflows/node.js.yml +81 -7
  3. package/.github/workflows/npm-publish.yml +6 -20
  4. package/.testcaferc.js +10 -0
  5. package/BookReader/BookReader.css +505 -1442
  6. package/BookReader/BookReader.js +2 -21564
  7. package/BookReader/BookReader.js.LICENSE.txt +20 -20
  8. package/BookReader/BookReader.js.map +1 -1
  9. package/BookReader/ia-bookreader-bundle.js +1782 -0
  10. package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +7 -0
  11. package/BookReader/ia-bookreader-bundle.js.map +1 -0
  12. package/BookReader/icons/1up.svg +1 -12
  13. package/BookReader/icons/2up.svg +1 -15
  14. package/BookReader/icons/advance.svg +3 -26
  15. package/BookReader/icons/chevron-right.svg +1 -1
  16. package/BookReader/icons/close-circle-dark.svg +1 -0
  17. package/BookReader/icons/close-circle.svg +1 -1
  18. package/BookReader/icons/fullscreen.svg +1 -17
  19. package/BookReader/icons/fullscreen_exit.svg +1 -17
  20. package/BookReader/icons/hamburger.svg +1 -15
  21. package/BookReader/icons/left-arrow.svg +1 -12
  22. package/BookReader/icons/magnify-minus.svg +1 -16
  23. package/BookReader/icons/magnify-plus.svg +1 -17
  24. package/BookReader/icons/magnify.svg +1 -15
  25. package/BookReader/icons/pause.svg +1 -23
  26. package/BookReader/icons/play.svg +1 -22
  27. package/BookReader/icons/playback-speed.svg +1 -34
  28. package/BookReader/icons/read-aloud.svg +1 -22
  29. package/BookReader/icons/review.svg +3 -22
  30. package/BookReader/icons/thumbnails.svg +1 -17
  31. package/BookReader/icons/voice.svg +1 -0
  32. package/BookReader/icons/volume-full.svg +1 -22
  33. package/BookReader/images/BRicons.svg +5 -94
  34. package/BookReader/images/books_graphic.svg +1 -177
  35. package/BookReader/images/icon_book.svg +1 -12
  36. package/BookReader/images/icon_bookmark.svg +1 -12
  37. package/BookReader/images/icon_gear.svg +1 -14
  38. package/BookReader/images/icon_hamburger.svg +1 -20
  39. package/BookReader/images/icon_home.svg +1 -21
  40. package/BookReader/images/icon_info.svg +1 -11
  41. package/BookReader/images/icon_one_page.svg +1 -8
  42. package/BookReader/images/icon_pause.svg +1 -1
  43. package/BookReader/images/icon_play.svg +1 -1
  44. package/BookReader/images/icon_playback-rate.svg +1 -15
  45. package/BookReader/images/icon_search_button.svg +1 -8
  46. package/BookReader/images/icon_share.svg +1 -9
  47. package/BookReader/images/icon_skip-ahead.svg +1 -6
  48. package/BookReader/images/icon_skip-back.svg +2 -13
  49. package/BookReader/images/icon_speaker.svg +1 -18
  50. package/BookReader/images/icon_speaker_open.svg +1 -10
  51. package/BookReader/images/icon_thumbnails.svg +1 -12
  52. package/BookReader/images/icon_toc.svg +1 -5
  53. package/BookReader/images/icon_two_pages.svg +1 -9
  54. package/BookReader/images/marker_chap-off.svg +1 -11
  55. package/BookReader/images/marker_chap-on.svg +1 -11
  56. package/BookReader/images/marker_srch-on.svg +1 -11
  57. package/BookReader/images/unviewable_page.png +0 -0
  58. package/BookReader/jquery-3.js +2 -0
  59. package/BookReader/jquery-3.js.LICENSE.txt +24 -0
  60. package/BookReader/plugins/plugin.archive_analytics.js +1 -172
  61. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  62. package/BookReader/plugins/plugin.autoplay.js +1 -165
  63. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  64. package/BookReader/plugins/plugin.chapters.js +22 -301
  65. package/BookReader/plugins/plugin.chapters.js.LICENSE.txt +1 -0
  66. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  67. package/BookReader/plugins/plugin.iframe.js +1 -74
  68. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  69. package/BookReader/plugins/plugin.iiif.js +2 -0
  70. package/BookReader/plugins/plugin.iiif.js.map +1 -0
  71. package/BookReader/plugins/plugin.resume.js +1 -368
  72. package/BookReader/plugins/plugin.resume.js.map +1 -1
  73. package/BookReader/plugins/plugin.search.js +2 -1420
  74. package/BookReader/plugins/plugin.search.js.LICENSE.txt +1 -0
  75. package/BookReader/plugins/plugin.search.js.map +1 -1
  76. package/BookReader/plugins/plugin.text_selection.js +2 -1080
  77. package/BookReader/plugins/plugin.text_selection.js.LICENSE.txt +1 -0
  78. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  79. package/BookReader/plugins/plugin.tts.js +2 -9193
  80. package/BookReader/plugins/plugin.tts.js.LICENSE.txt +2 -0
  81. package/BookReader/plugins/plugin.tts.js.map +1 -1
  82. package/BookReader/plugins/plugin.url.js +1 -269
  83. package/BookReader/plugins/plugin.url.js.map +1 -1
  84. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -379
  85. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  86. package/BookReader/webcomponents-bundle.js +3 -0
  87. package/BookReader/webcomponents-bundle.js.LICENSE.txt +9 -0
  88. package/BookReader/webcomponents-bundle.js.map +1 -0
  89. package/BookReaderDemo/BookReaderDemo.css +18 -19
  90. package/BookReaderDemo/BookReaderJSAdvanced.js +0 -3
  91. package/BookReaderDemo/BookReaderJSSimple.js +1 -0
  92. package/BookReaderDemo/IADemoBr.js +144 -0
  93. package/BookReaderDemo/demo-advanced.html +2 -2
  94. package/BookReaderDemo/demo-embed-iframe-src.html +2 -1
  95. package/BookReaderDemo/demo-fullscreen-mobile.html +3 -5
  96. package/BookReaderDemo/demo-fullscreen.html +2 -4
  97. package/BookReaderDemo/demo-iiif.html +99 -12
  98. package/BookReaderDemo/demo-internetarchive.html +214 -18
  99. package/BookReaderDemo/demo-multiple.html +2 -1
  100. package/BookReaderDemo/demo-preview-pages.html +526 -525
  101. package/BookReaderDemo/demo-simple.html +2 -1
  102. package/BookReaderDemo/demo-vendor-fullscreen.html +2 -4
  103. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  104. package/BookReaderDemo/immersion-1up.html +2 -2
  105. package/BookReaderDemo/immersion-mode.html +2 -4
  106. package/BookReaderDemo/toggle_controls.html +3 -2
  107. package/BookReaderDemo/view_mode.html +2 -1
  108. package/BookReaderDemo/viewmode-cycle.html +2 -3
  109. package/CHANGELOG.md +595 -33
  110. package/README.md +14 -1
  111. package/babel.config.js +20 -0
  112. package/codecov.yml +6 -0
  113. package/index.html +5 -2
  114. package/jsconfig.json +19 -0
  115. package/netlify.toml +9 -0
  116. package/package.json +70 -62
  117. package/renovate.json +52 -0
  118. package/scripts/preversion.js +0 -1
  119. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  120. package/src/BookNavigator/assets/button-base.js +10 -2
  121. package/src/BookNavigator/assets/ia-logo.js +17 -0
  122. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  123. package/src/BookNavigator/assets/icon_close.js +1 -1
  124. package/src/BookNavigator/book-navigator.js +590 -0
  125. package/src/BookNavigator/bookmarks/bookmark-button.js +3 -2
  126. package/src/BookNavigator/bookmarks/bookmark-edit.js +3 -4
  127. package/src/BookNavigator/bookmarks/bookmarks-list.js +2 -3
  128. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +4 -9
  129. package/src/BookNavigator/bookmarks/bookmarks-provider.js +27 -17
  130. package/src/BookNavigator/bookmarks/ia-bookmarks.js +116 -67
  131. package/src/BookNavigator/delete-modal-actions.js +1 -1
  132. package/src/BookNavigator/downloads/downloads-provider.js +36 -21
  133. package/src/BookNavigator/downloads/downloads.js +29 -25
  134. package/src/BookNavigator/search/search-provider.js +80 -28
  135. package/src/BookNavigator/search/search-results.js +29 -26
  136. package/src/BookNavigator/sharing.js +27 -0
  137. package/src/BookNavigator/viewable-files.js +95 -0
  138. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +13 -12
  139. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +7 -7
  140. package/src/BookReader/BookModel.js +76 -41
  141. package/src/BookReader/DragScrollable.js +233 -0
  142. package/src/BookReader/ImageCache.js +48 -15
  143. package/src/BookReader/Mode1Up.js +56 -351
  144. package/src/BookReader/Mode1UpLit.js +388 -0
  145. package/src/BookReader/Mode2Up.js +73 -1318
  146. package/src/BookReader/Mode2UpLit.js +777 -0
  147. package/src/BookReader/ModeCoordinateSpace.js +29 -0
  148. package/src/BookReader/ModeSmoothZoom.js +312 -0
  149. package/src/BookReader/ModeThumb.js +19 -13
  150. package/src/BookReader/Navbar/Navbar.js +70 -54
  151. package/src/BookReader/PageContainer.js +116 -22
  152. package/src/BookReader/ReduceSet.js +3 -3
  153. package/src/BookReader/Toolbar/Toolbar.js +14 -41
  154. package/src/BookReader/events.js +2 -3
  155. package/src/BookReader/options.js +73 -15
  156. package/src/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  157. package/src/BookReader/utils/ScrollClassAdder.js +31 -0
  158. package/src/BookReader/utils/SelectionObserver.js +45 -0
  159. package/src/BookReader/utils/classes.js +1 -1
  160. package/src/BookReader/utils.js +128 -13
  161. package/src/BookReader.js +562 -1078
  162. package/src/BookReaderPlugin.js +44 -0
  163. package/src/assets/icons/close-circle-dark.svg +1 -0
  164. package/src/assets/icons/magnify-minus.svg +3 -7
  165. package/src/assets/icons/magnify-plus.svg +3 -7
  166. package/src/assets/icons/voice.svg +1 -0
  167. package/src/assets/images/unviewable_page.png +0 -0
  168. package/src/css/BookReader.scss +1 -17
  169. package/src/css/_BRBookmarks.scss +1 -1
  170. package/src/css/_BRComponent.scss +1 -1
  171. package/src/css/_BRicon.scss +8 -2
  172. package/src/css/_BRmain.scss +33 -27
  173. package/src/css/_BRnav.scss +12 -42
  174. package/src/css/_BRpages.scss +170 -42
  175. package/src/css/_BRsearch.scss +68 -230
  176. package/src/css/_BRtoolbar.scss +5 -5
  177. package/src/css/_TextSelection.scss +87 -27
  178. package/src/css/_colorbox.scss +2 -2
  179. package/src/css/_controls.scss +24 -7
  180. package/src/css/_icons.scss +7 -1
  181. package/src/ia-bookreader/ia-bookreader.js +224 -0
  182. package/src/plugins/plugin.archive_analytics.js +84 -78
  183. package/src/plugins/plugin.autoplay.js +99 -104
  184. package/src/plugins/plugin.chapters.js +237 -191
  185. package/src/plugins/plugin.iframe.js +1 -1
  186. package/src/plugins/plugin.iiif.js +141 -0
  187. package/src/plugins/plugin.resume.js +53 -50
  188. package/src/plugins/plugin.text_selection.js +503 -175
  189. package/src/plugins/plugin.vendor-fullscreen.js +7 -7
  190. package/src/plugins/search/plugin.search.js +183 -121
  191. package/src/plugins/search/utils.js +43 -0
  192. package/src/plugins/search/view.js +67 -202
  193. package/src/plugins/tts/AbstractTTSEngine.js +75 -45
  194. package/src/plugins/tts/FestivalTTSEngine.js +21 -31
  195. package/src/plugins/tts/PageChunk.js +16 -23
  196. package/src/plugins/tts/PageChunkIterator.js +11 -17
  197. package/src/plugins/tts/WebTTSEngine.js +88 -72
  198. package/src/plugins/tts/plugin.tts.js +310 -350
  199. package/src/plugins/tts/utils.js +16 -26
  200. package/src/plugins/url/UrlPlugin.js +191 -0
  201. package/src/plugins/{plugin.url.js → url/plugin.url.js} +47 -18
  202. package/src/util/browserSniffing.js +22 -0
  203. package/src/util/docCookies.js +21 -2
  204. package/src/util/strings.js +1 -0
  205. package/tests/e2e/README.md +37 -0
  206. package/tests/e2e/autoplay.test.js +9 -6
  207. package/tests/e2e/base.test.js +8 -16
  208. package/tests/e2e/helpers/base.js +55 -50
  209. package/tests/e2e/helpers/debug.js +1 -1
  210. package/tests/e2e/helpers/mockSearch.js +19 -22
  211. package/tests/e2e/helpers/params.js +17 -0
  212. package/tests/e2e/helpers/rightToLeft.js +8 -14
  213. package/tests/e2e/helpers/search.js +73 -0
  214. package/tests/e2e/models/Navigation.js +20 -37
  215. package/tests/e2e/rightToLeft.test.js +4 -5
  216. package/tests/e2e/viewmode.test.js +40 -33
  217. package/tests/jest/BookNavigator/book-navigator.test.js +661 -0
  218. package/tests/jest/BookNavigator/bookmarks/bookmark-button.test.js +43 -0
  219. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-edit.test.js +25 -26
  220. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmarks-list.test.js +41 -42
  221. package/tests/jest/BookNavigator/bookmarks/ia-bookmarks.test.js +45 -0
  222. package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +67 -0
  223. package/tests/jest/BookNavigator/downloads/downloads.test.js +53 -0
  224. package/tests/jest/BookNavigator/search/search-provider.test.js +167 -0
  225. package/tests/{karma/BookNavigator → jest/BookNavigator/search}/search-results.test.js +109 -60
  226. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +49 -0
  227. package/tests/jest/BookNavigator/viewable-files/viewable-files-provider.test.js +80 -0
  228. package/tests/jest/BookNavigator/visual-adjustments.test.js +200 -0
  229. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +74 -14
  230. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +193 -0
  231. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +4 -4
  232. package/tests/jest/BookReader/Mode1UpLit.test.js +73 -0
  233. package/tests/jest/BookReader/Mode2Up.test.js +98 -0
  234. package/tests/jest/BookReader/Mode2UpLit.test.js +190 -0
  235. package/tests/jest/BookReader/ModeCoordinateSpace.test.js +16 -0
  236. package/tests/jest/BookReader/ModeSmoothZoom.test.js +218 -0
  237. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  238. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +42 -29
  239. package/tests/jest/BookReader/PageContainer.test.js +238 -0
  240. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +1 -1
  241. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +3 -3
  242. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  243. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  244. package/tests/jest/BookReader/utils/SelectionObserver.test.js +57 -0
  245. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +1 -1
  246. package/tests/jest/BookReader/utils.test.js +250 -0
  247. package/tests/jest/BookReader.keyboard.test.js +190 -0
  248. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +10 -2
  249. package/tests/{BookReader.test.js → jest/BookReader.test.js} +43 -53
  250. package/tests/jest/plugins/plugin.archive_analytics.test.js +20 -0
  251. package/tests/jest/plugins/plugin.autoplay.test.js +35 -0
  252. package/tests/jest/plugins/plugin.chapters.test.js +195 -0
  253. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +4 -4
  254. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +22 -35
  255. package/tests/jest/plugins/plugin.text_selection.test.js +316 -0
  256. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +2 -2
  257. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +26 -47
  258. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +42 -9
  259. package/tests/jest/plugins/search/utils.js +25 -0
  260. package/tests/jest/plugins/search/utils.test.js +29 -0
  261. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +30 -10
  262. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +4 -4
  263. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +1 -1
  264. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +3 -3
  265. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +47 -1
  266. package/tests/{plugins → jest/plugins}/tts/utils.test.js +1 -60
  267. package/tests/jest/plugins/url/UrlPlugin.test.js +198 -0
  268. package/tests/{plugins → jest/plugins/url}/plugin.url.test.js +57 -18
  269. package/tests/jest/setup.js +3 -0
  270. package/tests/{util → jest/util}/browserSniffing.test.js +1 -1
  271. package/tests/jest/util/docCookies.test.js +24 -0
  272. package/tests/{util → jest/util}/strings.test.js +1 -1
  273. package/tests/{utils.js → jest/utils.js} +38 -0
  274. package/webpack.config.js +16 -10
  275. package/.babelrc +0 -12
  276. package/.dependabot/config.yml +0 -6
  277. package/.testcaferc.json +0 -5
  278. package/BookReader/bookreader-component-bundle.js +0 -14330
  279. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +0 -38
  280. package/BookReader/bookreader-component-bundle.js.map +0 -1
  281. package/BookReader/icons/sort-ascending.svg +0 -1
  282. package/BookReader/icons/sort-descending.svg +0 -1
  283. package/BookReader/jquery-1.10.1.js +0 -108
  284. package/BookReader/jquery-1.10.1.js.LICENSE.txt +0 -24
  285. package/BookReader/plugins/plugin.menu_toggle.js +0 -369
  286. package/BookReader/plugins/plugin.menu_toggle.js.map +0 -1
  287. package/BookReader/plugins/plugin.mobile_nav.js +0 -335
  288. package/BookReader/plugins/plugin.mobile_nav.js.map +0 -1
  289. package/BookReaderDemo/BookReaderJSAutoplay.js +0 -56
  290. package/BookReaderDemo/IIIFBookReader.js +0 -207
  291. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
  292. package/BookReaderDemo/demo-autoplay.html +0 -38
  293. package/BookReaderDemo/demo-iiif.js +0 -26
  294. package/BookReaderDemo/demo-plugin-menu-toggle.html +0 -34
  295. package/karma.conf.js +0 -23
  296. package/src/BookNavigator/BookModel.js +0 -14
  297. package/src/BookNavigator/BookNavigator.js +0 -452
  298. package/src/BookNavigator/assets/book-loader.js +0 -27
  299. package/src/BookNavigator/assets/icon_sort_ascending.js +0 -5
  300. package/src/BookNavigator/assets/icon_sort_descending.js +0 -5
  301. package/src/BookNavigator/br-fullscreen-mgr.js +0 -83
  302. package/src/BookNavigator/search/a-search-result.js +0 -55
  303. package/src/BookNavigator/volumes/volumes-provider.js +0 -108
  304. package/src/BookNavigator/volumes/volumes.js +0 -162
  305. package/src/BookReader/DebugConsole.js +0 -54
  306. package/src/BookReaderComponent/BookReaderComponent.js +0 -112
  307. package/src/ItemNavigator/ItemNavigator.js +0 -372
  308. package/src/ItemNavigator/providers/sharing.js +0 -29
  309. package/src/assets/icons/sort-ascending.svg +0 -1
  310. package/src/assets/icons/sort-descending.svg +0 -1
  311. package/src/css/_MobileNav.scss +0 -194
  312. package/src/dragscrollable-br.js +0 -261
  313. package/src/plugins/menu_toggle/plugin.menu_toggle.js +0 -324
  314. package/src/plugins/plugin.mobile_nav.js +0 -287
  315. package/tests/BookReader/BookReaderPublicFunctions.test.js +0 -171
  316. package/tests/BookReader/DebugConsole.test.js +0 -25
  317. package/tests/BookReader/Mode1Up.test.js +0 -164
  318. package/tests/BookReader/Mode2Up.test.js +0 -247
  319. package/tests/BookReader/PageContainer.test.js +0 -115
  320. package/tests/BookReader/utils.test.js +0 -109
  321. package/tests/e2e/helpers/desktopSearch.js +0 -72
  322. package/tests/e2e/helpers/mobileSearch.js +0 -85
  323. package/tests/e2e/ia-production/ia-prod-base.js +0 -17
  324. package/tests/karma/BookNavigator/book-navigator.test.js +0 -132
  325. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -201
  326. package/tests/karma/BookNavigator/volumes.test.js +0 -133
  327. package/tests/plugins/menu_toggle/plugin.menu_toggle.test.js +0 -68
  328. package/tests/plugins/plugin.archive_analytics.test.js +0 -23
  329. package/tests/plugins/plugin.autoplay.test.js +0 -52
  330. package/tests/plugins/plugin.chapters.test.js +0 -130
  331. package/tests/plugins/plugin.mobile_nav.test.js +0 -66
  332. package/tests/plugins/plugin.text_selection.test.js +0 -203
  333. package/tests/util/docCookies.test.js +0 -15
@@ -1,19 +1,13 @@
1
1
  //@ts-check
2
- import { isFirefox, isSafari } from '../util/browserSniffing.js';
2
+ import { createDIVPageLayer } from '../BookReader/PageContainer.js';
3
+ import { SelectionObserver } from '../BookReader/utils/SelectionObserver.js';
4
+ import { BookReaderPlugin } from '../BookReaderPlugin.js';
3
5
  import { applyVariables } from '../util/strings.js';
4
6
  /** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
7
+ /** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */
5
8
 
6
9
  const BookReader = /** @type {typeof import('../BookReader').default} */(window.BookReader);
7
10
 
8
- export const DEFAULT_OPTIONS = {
9
- enabled: true,
10
- /** @type {StringWithVars} The URL to fetch the entire DJVU xml. Supports options.vars */
11
- fullDjvuXmlUrl: null,
12
- /** @type {StringWithVars} The URL to fetch a single page of the DJVU xml. Supports options.vars. Also has {{pageIndex}} */
13
- singlePageDjvuXmlUrl: null,
14
- };
15
- /** @typedef {typeof DEFAULT_OPTIONS} TextSelectionPluginOptions */
16
-
17
11
  /**
18
12
  * @template T
19
13
  */
@@ -35,45 +29,101 @@ export class Cache {
35
29
  }
36
30
  }
37
31
 
38
- export class TextSelectionPlugin {
39
-
40
- constructor(options = DEFAULT_OPTIONS, optionVariables, avoidTspans = isFirefox(), pointerEventsOnParagraph = isSafari()) {
41
- this.options = options;
42
- this.optionVariables = optionVariables;
43
- /**@type {PromiseLike<JQuery<HTMLElement>|undefined>} */
44
- this.djvuPagesPromise = null;
45
- // Using text elements instead of tspans for words because Firefox does not allow svg tspan stretch.
46
- // Tspans are necessary on Chrome because they prevent newline character after every word when copying
47
- this.svgParagraphElement = "text";
48
- this.svgWordElement = "tspan";
49
- this.insertNewlines = avoidTspans
50
- // Safari has a bug where `pointer-events` doesn't work on `<tspans>`. So
51
- // there we will set `pointer-events: all` on the paragraph element. We don't
52
- // do this everywhere, because it's a worse experience. Thanks Safari :/
53
- this.pointerEventsOnParagraph = pointerEventsOnParagraph;
54
- if (avoidTspans) {
55
- this.svgParagraphElement = "g";
56
- this.svgWordElement = "text";
57
- }
32
+ export class TextSelectionPlugin extends BookReaderPlugin {
33
+ options = {
34
+ enabled: true,
35
+ /** @type {StringWithVars} The URL to fetch the entire DJVU xml. Supports options.vars */
36
+ fullDjvuXmlUrl: null,
37
+ /** @type {StringWithVars} The URL to fetch a single page of the DJVU xml. Supports options.vars. Also has {{pageIndex}} */
38
+ singlePageDjvuXmlUrl: null,
39
+ /** Whether to fetch the XML as a jsonp */
40
+ jsonp: false,
41
+ }
58
42
 
59
- /** @type {Cache<{index: number, response: any}>} */
60
- this.pageTextCache = new Cache();
43
+ /**@type {PromiseLike<JQuery<HTMLElement>|undefined>} */
44
+ djvuPagesPromise = null;
61
45
 
62
- /**
63
- * Sometimes there are too many words on a page, and the browser becomes near
64
- * unusable. For now don't render text layer for pages with too many words.
65
- */
66
- this.maxWordRendered = 2500;
46
+ /** @type {Cache<{index: number, response: any}>} */
47
+ pageTextCache = new Cache();
48
+
49
+ /**
50
+ * Sometimes there are too many words on a page, and the browser becomes near
51
+ * unusable. For now don't render text layer for pages with too many words.
52
+ */
53
+ maxWordRendered = 2500;
54
+
55
+ /**
56
+ * @param {import('../BookReader.js').default} br
57
+ */
58
+ constructor(br) {
59
+ super(br);
60
+ // In the future this should be in the ocr file
61
+ // since a book being right to left doesn't mean the ocr is right to left. But for
62
+ // now we do make that assumption.
63
+ /** Whether the book is right-to-left */
64
+ this.rtl = this.br.pageProgression === 'rl';
65
+ this.selectionObserver = new SelectionObserver('.BRtextLayer', this._onSelectionChange);
67
66
  }
68
67
 
68
+ /** @override */
69
69
  init() {
70
+ if (!this.options.enabled) return;
71
+
72
+ this.loadData();
73
+
74
+ this.selectionObserver.attach();
75
+ new SelectionObserver('.BRtextLayer', (selectEvent) => {
76
+ // Track how often selection is used
77
+ if (selectEvent == 'started') {
78
+ this.br._plugins.archiveAnalytics?.sendEvent('BookReader', 'SelectStart');
79
+
80
+ // Set a class on the page to avoid hiding it when zooming/etc
81
+ this.br.refs.$br.find('.BRpagecontainer--hasSelection').removeClass('BRpagecontainer--hasSelection');
82
+ $(window.getSelection().anchorNode).closest('.BRpagecontainer').addClass('BRpagecontainer--hasSelection');
83
+ }
84
+ }).attach();
85
+ }
86
+
87
+ /**
88
+ * @override
89
+ * @param {PageContainer} pageContainer
90
+ * @returns {PageContainer}
91
+ */
92
+ _configurePageContainer(pageContainer) {
93
+ // Disable if thumb mode; it's too janky
94
+ // .page can be null for "pre-cover" region
95
+ if (this.br.mode !== this.br.constModeThumb && pageContainer.page) {
96
+ this.createTextLayer(pageContainer);
97
+ }
98
+ return pageContainer;
99
+ }
100
+
101
+ /**
102
+ * @param {'started' | 'cleared'} type
103
+ * @param {HTMLElement} target
104
+ */
105
+ _onSelectionChange = (type, target) => {
106
+ if (type === 'started') {
107
+ this.textSelectingMode(target);
108
+ } else if (type === 'cleared') {
109
+ this.defaultMode(target);
110
+ } else {
111
+ throw new Error(`Unknown type ${type}`);
112
+ }
113
+ }
114
+
115
+ loadData() {
70
116
  // Only fetch the full djvu xml if the single page url isn't there
71
117
  if (this.options.singlePageDjvuXmlUrl) return;
72
118
  this.djvuPagesPromise = $.ajax({
73
119
  type: "GET",
74
- url: applyVariables(this.options.fullDjvuXmlUrl, this.optionVariables),
75
- dataType: "html",
76
- error: (e) => undefined
120
+ url: applyVariables(this.options.fullDjvuXmlUrl, this.br.options.vars),
121
+ dataType: this.options.jsonp ? "jsonp" : "html",
122
+ cache: true,
123
+ xhrFields: {
124
+ withCredentials: this.br.protected,
125
+ },
126
+ error: (e) => undefined,
77
127
  }).then((res) => {
78
128
  try {
79
129
  const xmlMap = $.parseXML(res);
@@ -94,21 +144,24 @@ export class TextSelectionPlugin {
94
144
  if (cachedEntry) {
95
145
  return cachedEntry.response;
96
146
  }
97
- return $.ajax({
147
+ const res = await $.ajax({
98
148
  type: "GET",
99
- url: applyVariables(this.options.singlePageDjvuXmlUrl, this.optionVariables, { pageIndex: index }),
100
- dataType: "html",
149
+ url: applyVariables(this.options.singlePageDjvuXmlUrl, this.br.options.vars, { pageIndex: index }),
150
+ dataType: this.options.jsonp ? "jsonp" : "html",
151
+ cache: true,
152
+ xhrFields: {
153
+ withCredentials: this.br.protected,
154
+ },
101
155
  error: (e) => undefined,
102
- }).then((res) => {
103
- try {
104
- const xmlDoc = $.parseXML(res);
105
- const result = xmlDoc && $(xmlDoc).find("OBJECT")[0];
106
- this.pageTextCache.add({ index, response: result });
107
- return result;
108
- } catch (e) {
109
- return undefined;
110
- }
111
156
  });
157
+ try {
158
+ const xmlDoc = $.parseXML(res);
159
+ const result = xmlDoc && $(xmlDoc).find("OBJECT")[0];
160
+ this.pageTextCache.add({ index, response: result });
161
+ return result;
162
+ } catch (e) {
163
+ return undefined;
164
+ }
112
165
  } else {
113
166
  const XMLpagesArr = await this.djvuPagesPromise;
114
167
  if (XMLpagesArr) return XMLpagesArr[index];
@@ -129,67 +182,87 @@ export class TextSelectionPlugin {
129
182
 
130
183
  /**
131
184
  * Applies mouse events when in default mode
132
- * @param {SVGElement} svg
185
+ * @param {HTMLElement} textLayer
133
186
  */
134
- defaultMode(svg) {
135
- svg.classList.remove("selectingSVG");
136
- $(svg).on("mousedown.textSelectPluginHandler", (event) => {
137
- if (!$(event.target).is(".BRwordElement")) return;
138
- event.stopPropagation();
139
- svg.classList.add("selectingSVG");
140
- $(svg).one("mouseup.textSelectPluginHandler", (event) => {
141
- if (window.getSelection().toString() != "") {
142
- event.stopPropagation();
143
- $(svg).off(".textSelectPluginHandler");
144
- this.textSelectingMode(svg);
145
- }
146
- else svg.classList.remove("selectingSVG");
147
- })
148
- })
187
+ defaultMode(textLayer) {
188
+ const $pageContainer = $(textLayer).closest('.BRpagecontainer');
189
+ textLayer.style.pointerEvents = "none";
190
+ $pageContainer.find("img").css("pointer-events", "auto");
191
+
192
+ $(textLayer).off(".textSelectPluginHandler");
193
+ const startedMouseDown = this.mouseIsDown;
194
+ let skipNextMouseup = this.mouseIsDown;
195
+ if (startedMouseDown) {
196
+ textLayer.style.pointerEvents = "auto";
197
+ }
198
+
199
+ // Need to stop propagation to prevent DragScrollable from
200
+ // blocking selection
201
+ $(textLayer).on("mousedown.textSelectPluginHandler", (event) => {
202
+ this.mouseIsDown = true;
203
+ if ($(event.target).is(".BRwordElement, .BRspace")) {
204
+ event.stopPropagation();
205
+ }
206
+ });
207
+
208
+ $(textLayer).on("mouseup.textSelectPluginHandler", (event) => {
209
+ this.mouseIsDown = false;
210
+ textLayer.style.pointerEvents = "none";
211
+ if (skipNextMouseup) {
212
+ skipNextMouseup = false;
213
+ event.stopPropagation();
214
+ }
215
+ });
149
216
  }
150
217
 
151
218
  /**
152
- * Applies mouse events when in textSelecting mode
153
- * @param {SVGElement} svg
219
+ * This mode is active while there is a selection on the given textLayer
220
+ * @param {HTMLElement} textLayer
154
221
  */
155
- textSelectingMode(svg) {
156
- $(svg).on('mousedown.textSelectPluginHandler', (event) => {
157
- if (!$(event.target).is(".BRwordElement")) {
158
- if (window.getSelection().toString() != "") window.getSelection().removeAllRanges();
159
- }
222
+ textSelectingMode(textLayer) {
223
+ const $pageContainer = $(textLayer).closest('.BRpagecontainer');
224
+ // Make text layer consume all events
225
+ textLayer.style.pointerEvents = "all";
226
+ // Block img from getting long-press to save while selecting
227
+ $pageContainer.find("img").css("pointer-events", "none");
228
+
229
+ $(textLayer).off(".textSelectPluginHandler");
230
+
231
+ $(textLayer).on('mousedown.textSelectPluginHandler', (event) => {
232
+ this.mouseIsDown = true;
160
233
  event.stopPropagation();
161
- })
162
- $(svg).on('mouseup.textSelectPluginHandler', (event) => {
234
+ });
235
+
236
+ // Prevent page flip on click
237
+ $(textLayer).on('mouseup.textSelectPluginHandler', (event) => {
238
+ this.mouseIsDown = false;
163
239
  event.stopPropagation();
164
- if (window.getSelection().toString() == "") {
165
- $(svg).off(".textSelectPluginHandler");
166
- this.defaultMode(svg); }
167
- })
240
+ });
168
241
  }
169
242
 
170
243
  /**
171
- * Initializes text selection modes if there is an svg on the page
244
+ * Initializes text selection modes if there is a text layer on the page
172
245
  * @param {JQuery} $container
173
246
  */
174
247
  stopPageFlip($container) {
175
- /** @type {JQuery<SVGElement>} */
176
- const $svg = $container.find('svg.textSelectionSVG');
177
- if (!$svg.length) return;
178
- $svg.each((i, s) => this.defaultMode(s))
248
+ /** @type {JQuery<HTMLElement>} */
249
+ const $textLayer = $container.find('.BRtextLayer');
250
+ if (!$textLayer.length) return;
251
+ $textLayer.each((i, s) => this.defaultMode(s));
179
252
  this.interceptCopy($container);
180
253
  }
181
254
 
182
255
  /**
183
- * @param {number} pageIndex
184
- * @param {JQuery} $container
256
+ * @param {PageContainer} pageContainer
185
257
  */
186
- async createTextLayer(pageIndex, $container) {
187
- const $svgLayers = $container.find('.textSelectionSVG');
188
- if ($svgLayers.length) return;
258
+ async createTextLayer(pageContainer) {
259
+ const pageIndex = pageContainer.page.index;
260
+ const $container = pageContainer.$container;
261
+ const $textLayers = $container.find('.BRtextLayer');
262
+ if ($textLayers.length) return;
189
263
  const XMLpage = await this.getPageText(pageIndex);
190
264
  if (!XMLpage) return;
191
- const XMLwidth = $(XMLpage).attr("width");
192
- const XMLheight = $(XMLpage).attr("height");
265
+ recursivelyAddCoords(XMLpage);
193
266
 
194
267
  const totalWords = $(XMLpage).find("WORD").length;
195
268
  if (totalWords > this.maxWordRendered) {
@@ -197,107 +270,362 @@ export class TextSelectionPlugin {
197
270
  return;
198
271
  }
199
272
 
200
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
201
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
202
- svg.setAttribute("viewBox", `0 0 ${XMLwidth} ${XMLheight}`);
203
- $container.append(svg);
204
- svg.setAttribute('class', 'textSelectionSVG');
205
- svg.setAttribute('preserveAspectRatio', 'none');
206
- $(svg).css({
207
- "width": "100%",
208
- "position": "absolute",
209
- "height": "100%",
210
- "top": "0",
211
- "left": "0",
273
+ const textLayer = createDIVPageLayer(pageContainer.page, 'BRtextLayer');
274
+ const ratioW = parseFloat(pageContainer.$container[0].style.width) / pageContainer.page.width;
275
+ const ratioH = parseFloat(pageContainer.$container[0].style.height) / pageContainer.page.height;
276
+ textLayer.style.transform = `scale(${ratioW}, ${ratioH})`;
277
+ textLayer.setAttribute("dir", this.rtl ? "rtl" : "ltr");
278
+
279
+ const ocrParagraphs = $(XMLpage).find("PARAGRAPH[coords]").toArray();
280
+ const paragEls = ocrParagraphs.map(p => {
281
+ const el = this.renderParagraph(p);
282
+ textLayer.appendChild(el);
283
+ return el;
212
284
  });
213
285
 
214
- $(XMLpage).find("PARAGRAPH").each((i, paragraph) => {
215
- // Adding text element for each paragraph in the page
216
- const words = $(paragraph).find("WORD");
217
- if (!words.length) return;
218
- const paragSvg = document.createElementNS("http://www.w3.org/2000/svg", this.svgParagraphElement);
219
- paragSvg.setAttribute("class", "BRparagElement");
220
- if (this.pointerEventsOnParagraph) {
221
- paragSvg.style.pointerEvents = "all";
222
- }
286
+ // Fix up paragraph positions
287
+ const paragraphRects = determineRealRects(textLayer, '.BRparagraphElement');
288
+ let yAdded = 0;
289
+ for (const [ocrParagraph, paragEl] of zip(ocrParagraphs, paragEls)) {
290
+ const ocrParagBounds = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
291
+ const realRect = paragraphRects.get(paragEl);
292
+ const [ocrLeft, , ocrRight, ocrTop] = ocrParagBounds;
293
+ const newStartMargin = this.rtl ? (realRect.right - ocrRight) : (ocrLeft - realRect.left);
294
+ const newTop = ocrTop - (realRect.top + yAdded);
223
295
 
224
- const wordHeightArr = [];
296
+ paragEl.style[this.rtl ? 'marginRight' : 'marginLeft'] = `${newStartMargin}px`;
297
+ paragEl.style.marginTop = `${newTop}px`;
298
+ yAdded += newTop;
299
+ textLayer.appendChild(paragEl);
300
+ }
301
+ $container.append(textLayer);
302
+ this.stopPageFlip($container);
303
+ }
304
+
305
+ /**
306
+ * @param {HTMLElement} ocrParagraph
307
+ * @returns {HTMLParagraphElement}
308
+ */
309
+ renderParagraph(ocrParagraph) {
310
+ const paragEl = document.createElement('p');
311
+ paragEl.classList.add('BRparagraphElement');
312
+ const [paragLeft, paragBottom, paragRight, paragTop] = $(ocrParagraph).attr("coords").split(",").map(parseFloat);
313
+ const wordHeightArr = [];
314
+ const lines = $(ocrParagraph).find("LINE[coords]").toArray();
315
+ if (!lines.length) return paragEl;
225
316
 
226
- for (let i = 0; i < words.length; i++) {
227
- // Adding tspan for each word in paragraph
228
- const currWord = words[i];
229
- // eslint-disable-next-line no-unused-vars
230
- const [left, bottom, right, top] = $(currWord).attr("coords").split(',').map(parseFloat);
317
+
318
+ for (const [prevLine, line, nextLine] of lookAroundWindow(genMap(lines, augmentLine))) {
319
+ const isLastLineOfParagraph = line.ocrElement == lines[lines.length - 1];
320
+ const lineEl = document.createElement('span');
321
+ lineEl.classList.add('BRlineElement');
322
+
323
+ for (const [wordIndex, currWord] of line.words.entries()) {
324
+ const [, bottom, right, top] = $(currWord).attr("coords").split(',').map(parseFloat);
231
325
  const wordHeight = bottom - top;
232
326
  wordHeightArr.push(wordHeight);
233
327
 
234
- const wordTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
235
- wordTspan.setAttribute("class", "BRwordElement");
236
- wordTspan.setAttribute("x", left.toString());
237
- wordTspan.setAttribute("y", bottom.toString());
238
- wordTspan.setAttribute("textLength", (right - left).toString());
239
- wordTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
240
- wordTspan.textContent = currWord.textContent;
241
- paragSvg.appendChild(wordTspan);
242
-
243
- // Adding spaces after words except at the end of the paragraph
244
- // TODO: assumes left-to-right text
245
- if (i < words.length - 1) {
246
- const nextWord = words[i + 1];
247
- // eslint-disable-next-line no-unused-vars
248
- const [leftNext, bottomNext, rightNext, topNext] = $(nextWord).attr("coords").split(',').map(parseFloat);
249
- const spaceTspan = document.createElementNS("http://www.w3.org/2000/svg", this.svgWordElement);
250
- spaceTspan.setAttribute("class", "BRwordElement");
251
- spaceTspan.setAttribute("x", right.toString());
252
- spaceTspan.setAttribute("y", bottom.toString());
253
- if ((leftNext - right) > 0) spaceTspan.setAttribute("textLength", (leftNext - right).toString());
254
- spaceTspan.setAttribute("lengthAdjust", "spacingAndGlyphs");
255
- spaceTspan.textContent = " ";
256
- paragSvg.appendChild(spaceTspan);
328
+ if (wordIndex == 0 && prevLine?.lastWord.textContent.trim().endsWith('-')) {
329
+ // ideally prefer the next line to determine the left position,
330
+ // since the previous line could be the first line of the paragraph
331
+ // and hence have an incorrectly indented first word.
332
+ // E.g. https://archive.org/details/driitaleofdaring00bachuoft/page/360/mode/2up
333
+ const [newLeft, , , ] = $((nextLine || prevLine).firstWord).attr("coords").split(',').map(parseFloat);
334
+ $(currWord).attr("coords", `${newLeft},${bottom},${right},${top}`);
257
335
  }
258
336
 
259
- // Adds newline at the end of paragraph on Firefox
260
- if ((i == words.length - 1 && (this.insertNewlines))) {
261
- paragSvg.appendChild(document.createTextNode("\n"));
337
+ const wordEl = document.createElement('span');
338
+ wordEl.setAttribute("class", "BRwordElement");
339
+ wordEl.textContent = currWord.textContent.trim();
340
+
341
+ if (wordIndex > 0) {
342
+ const space = document.createElement('span');
343
+ space.classList.add('BRspace');
344
+ space.textContent = ' ';
345
+ lineEl.append(space);
346
+
347
+ // Edge ignores empty elements (like BRspace), so add another
348
+ // space to ensure Edge's ReadAloud works correctly.
349
+ lineEl.appendChild(document.createTextNode(' '));
262
350
  }
351
+
352
+ lineEl.appendChild(wordEl);
263
353
  }
264
354
 
265
- wordHeightArr.sort();
266
- const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)];
267
- paragSvg.setAttribute("font-size", paragWordHeight.toString());
268
- svg.appendChild(paragSvg);
269
- })
270
- this.stopPageFlip($container);
355
+ const hasHyphen = line.lastWord.textContent.trim().endsWith('-');
356
+ const lastWordEl = lineEl.children[lineEl.children.length - 1];
357
+ if (hasHyphen && !isLastLineOfParagraph) {
358
+ lastWordEl.textContent = lastWordEl.textContent.trim().slice(0, -1);
359
+ lastWordEl.classList.add('BRwordElement--hyphen');
360
+ }
361
+
362
+ paragEl.appendChild(lineEl);
363
+ if (!isLastLineOfParagraph && !hasHyphen) {
364
+ // Edge does not correctly have spaces between the lines.
365
+ paragEl.appendChild(document.createTextNode(' '));
366
+ }
367
+ }
368
+
369
+ wordHeightArr.sort((a, b) => a - b);
370
+ const paragWordHeight = wordHeightArr[Math.floor(wordHeightArr.length * 0.85)] + 4;
371
+ paragEl.style.left = `${paragLeft}px`;
372
+ paragEl.style.top = `${paragTop}px`;
373
+ paragEl.style.width = `${paragRight - paragLeft}px`;
374
+ paragEl.style.height = `${paragBottom - paragTop}px`;
375
+ paragEl.style.fontSize = `${paragWordHeight}px`;
376
+
377
+ // Fix up sizes - stretch/crush words as necessary using letter spacing
378
+ let wordRects = determineRealRects(paragEl, '.BRwordElement');
379
+ const ocrWords = $(ocrParagraph).find("WORD").toArray();
380
+ const wordEls = paragEl.querySelectorAll('.BRwordElement');
381
+ for (const [ocrWord, wordEl] of zip(ocrWords, wordEls)) {
382
+ const realRect = wordRects.get(wordEl);
383
+ const [left, , right ] = $(ocrWord).attr("coords").split(',').map(parseFloat);
384
+ let ocrWidth = right - left;
385
+ // Some books (eg theworksofplato01platiala) have a space _inside_ the <WORD>
386
+ // element. That makes it impossible to determine the correct positining
387
+ // of everything, but to avoid the BRspace's being width 0, which makes selection
388
+ // janky on Chrome Android, assume the space is the same width as one of the
389
+ // letters.
390
+ if (ocrWord.textContent.endsWith(' ')) {
391
+ ocrWidth = ocrWidth * (ocrWord.textContent.length - 1) / ocrWord.textContent.length;
392
+ }
393
+ const diff = ocrWidth - realRect.width;
394
+ wordEl.style.letterSpacing = `${diff / (ocrWord.textContent.length - 1)}px`;
395
+ }
396
+
397
+ // Stretch/crush lines as necessary using line spacing
398
+ // Recompute rects after letter spacing
399
+ wordRects = determineRealRects(paragEl, '.BRwordElement');
400
+ const spaceRects = determineRealRects(paragEl, '.BRspace');
401
+
402
+ const ocrLines = $(ocrParagraph).find("LINE[coords]").toArray();
403
+ const lineEls = Array.from(paragEl.querySelectorAll('.BRlineElement'));
404
+
405
+ let ySoFar = paragTop;
406
+ for (const [ocrLine, lineEl] of zip(ocrLines, lineEls)) {
407
+ // shift words using marginLeft to align with the correct x position
408
+ const words = $(ocrLine).find("WORD").toArray();
409
+ // const ocrLineLeft = Math.min(...words.map(w => parseFloat($(w).attr("coords").split(',')[0])));
410
+ let xSoFar = this.rtl ? paragRight : paragLeft;
411
+ for (const [ocrWord, wordEl] of zip(words, lineEl.querySelectorAll('.BRwordElement'))) {
412
+ // start of line, need to compute the offset relative to the OCR words
413
+ const wordRect = wordRects.get(wordEl);
414
+ const [ocrLeft, , ocrRight ] = $(ocrWord).attr("coords").split(',').map(parseFloat);
415
+ const diff = (this.rtl ? -(ocrRight - xSoFar) : ocrLeft - xSoFar);
416
+
417
+ if (wordEl.previousElementSibling) {
418
+ const space = wordEl.previousElementSibling;
419
+ space.style.letterSpacing = `${diff - spaceRects.get(space).width}px`;
420
+ } else {
421
+ wordEl.style[this.rtl ? 'paddingRight' : 'paddingLeft'] = `${diff}px`;
422
+ }
423
+ if (this.rtl) xSoFar -= diff + wordRect.width;
424
+ else xSoFar += diff + wordRect.width;
425
+ }
426
+ // And also fix y position
427
+ const ocrLineTop = Math.min(...words.map(w => parseFloat($(w).attr("coords").split(',')[3])));
428
+ const diff = ocrLineTop - ySoFar;
429
+ if (lineEl.previousElementSibling) {
430
+ lineEl.previousElementSibling.style.lineHeight = `${diff}px`;
431
+ ySoFar += diff;
432
+ }
433
+ }
434
+
435
+ // The last line will have a line height subtracting from the paragraph height
436
+ lineEls[lineEls.length - 1].style.lineHeight = `${paragBottom - ySoFar}px`;
437
+
438
+ // Edge does not include a newline for some reason when copying/pasting the <p> els
439
+ paragEl.appendChild(document.createElement('br'));
440
+ return paragEl;
271
441
  }
272
442
  }
273
443
 
274
- export class BookreaderWithTextSelection extends BookReader {
275
- init() {
276
- const options = Object.assign({}, DEFAULT_OPTIONS, this.options.plugins.textSelection);
277
- if (options.enabled) {
278
- this.textSelectionPlugin = new TextSelectionPlugin(options, this.options.vars);
279
- // Write this back; this way the plugin is the source of truth, and BR just
280
- // contains a reference to it.
281
- this.options.plugins.textSelection = options;
282
- this.textSelectionPlugin.init();
444
+ BookReader?.registerPlugin('textSelection', TextSelectionPlugin);
445
+
446
+
447
+ /**
448
+ * @param {HTMLElement} parentEl
449
+ * @param {string} selector
450
+ * @returns {Map<Element, Rect>}
451
+ */
452
+ function determineRealRects(parentEl, selector) {
453
+ const initals = {
454
+ position: parentEl.style.position,
455
+ visibility: parentEl.style.visibility,
456
+ top: parentEl.style.top,
457
+ left: parentEl.style.left,
458
+ transform: parentEl.style.transform,
459
+ };
460
+ parentEl.style.position = 'absolute';
461
+ parentEl.style.visibility = 'hidden';
462
+ parentEl.style.top = '0';
463
+ parentEl.style.left = '0';
464
+ parentEl.style.transform = 'none';
465
+ document.body.appendChild(parentEl);
466
+ const rects = new Map(
467
+ Array.from(parentEl.querySelectorAll(selector))
468
+ .map(wordEl => {
469
+ const origRect = wordEl.getBoundingClientRect();
470
+ return [wordEl, new Rect(
471
+ origRect.left + window.scrollX,
472
+ origRect.top + window.scrollY,
473
+ origRect.width,
474
+ origRect.height,
475
+ )];
476
+ }),
477
+ );
478
+ document.body.removeChild(parentEl);
479
+ Object.assign(parentEl.style, initals);
480
+ return rects;
481
+ }
482
+
483
+ /**
484
+ * @param {HTMLElement} line
485
+ */
486
+ function augmentLine(line) {
487
+ const words = $(line).find("WORD").toArray();
488
+ return {
489
+ ocrElement: line,
490
+ words,
491
+ firstWord: words[0],
492
+ lastWord: words[words.length - 1],
493
+ };
494
+ }
495
+
496
+ /**
497
+ * @template TFrom, TTo
498
+ * Generator version of map
499
+ * @param {Iterable<TFrom>} gen
500
+ * @param {function(TFrom): TTo} fn
501
+ * @returns {Iterable<TTo>}
502
+ */
503
+ export function* genMap(gen, fn) {
504
+ for (const x of gen) yield fn(x);
505
+ }
506
+
507
+ /**
508
+ * @template T
509
+ * Generator that provides a sliding window of 3 elements,
510
+ * prev, current, and next.
511
+ * @param {Iterable<T>} gen
512
+ * @returns {Iterable<[T | undefined, T, T | undefined]>}
513
+ */
514
+ export function* lookAroundWindow(gen) {
515
+ let prev = undefined;
516
+ let cur = undefined;
517
+ let next = undefined;
518
+ for (const x of gen) {
519
+ if (typeof cur !== 'undefined') {
520
+ next = x;
521
+ yield [prev, cur, next];
283
522
  }
284
- super.init();
523
+ prev = cur;
524
+ cur = x;
525
+ next = undefined;
526
+ }
527
+
528
+ if (typeof cur !== 'undefined') {
529
+ yield [prev, cur, next];
285
530
  }
531
+ }
286
532
 
533
+ /**
534
+ * @template T1, T2
535
+ * Lazy zip implementation to avoid importing lodash
536
+ * Expects iterators to be of the same length
537
+ * @param {Iterable<T1>} gen1
538
+ * @param {Iterable<T2>} gen2
539
+ * @returns {Iterable<[T1, T2]>}
540
+ */
541
+ export function* zip(gen1, gen2) {
542
+ const it1 = gen1[Symbol.iterator]();
543
+ const it2 = gen2[Symbol.iterator]();
544
+ while (true) {
545
+ const r1 = it1.next();
546
+ const r2 = it2.next();
547
+ if (r1.done && r2.done) {
548
+ return;
549
+ }
550
+ if (r1.done || r2.done) {
551
+ throw new Error('zip: one of the iterators is done');
552
+ }
553
+ yield [r1.value, r2.value];
554
+ }
555
+ }
556
+
557
+ /**
558
+ * [left, bottom, right, top]
559
+ * @param {Array<[number, number, number, number]>} bounds
560
+ * @returns {[number, number, number, number]}
561
+ */
562
+ function determineBounds(bounds) {
563
+ let leftMost = Infinity;
564
+ let bottomMost = -Infinity;
565
+ let rightMost = -Infinity;
566
+ let topMost = Infinity;
567
+
568
+ for (const [left, bottom, right, top] of bounds) {
569
+ leftMost = Math.min(leftMost, left);
570
+ bottomMost = Math.max(bottomMost, bottom);
571
+ rightMost = Math.max(rightMost, right);
572
+ topMost = Math.min(topMost, top);
573
+ }
574
+
575
+ return [leftMost, bottomMost, rightMost, topMost];
576
+ }
577
+
578
+ /**
579
+ * Recursively traverses the XML tree and adds coords
580
+ * which are the bounding box of all child coords
581
+ * @param {Element} xmlEl
582
+ */
583
+ function recursivelyAddCoords(xmlEl) {
584
+ if ($(xmlEl).attr('coords') || !xmlEl.children) {
585
+ return;
586
+ }
587
+
588
+ const children = $(xmlEl).children().toArray();
589
+ if (children.length === 0) {
590
+ return;
591
+ }
592
+
593
+ for (const child of children) {
594
+ recursivelyAddCoords(child);
595
+ }
596
+
597
+ const childCoords = [];
598
+
599
+ for (const child of children) {
600
+ if (!$(child).attr('coords')) continue;
601
+ childCoords.push($(child).attr('coords').split(',').map(parseFloat));
602
+ }
603
+
604
+ const boundingCoords = determineBounds(childCoords);
605
+ if (Math.abs(boundingCoords[0]) != Infinity) {
606
+ $(xmlEl).attr('coords', boundingCoords.join(','));
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Basically a polyfill for the native DOMRect class
612
+ */
613
+ class Rect {
287
614
  /**
288
- * @param {number} index
615
+ * @param {number} x
616
+ * @param {number} y
617
+ * @param {number} width
618
+ * @param {number} height
289
619
  */
290
- _createPageContainer(index) {
291
- const pageContainer = super._createPageContainer(index);
292
- // Disable if thumb mode; it's too janky
293
- // index can be -1 for "pre-cover" region
294
- // Added checking of lastPageIndex to avoid loop around index value
295
- const lastPageIndex = this.getNumLeafs() - 1;
296
- if (this.mode !== this.constModeThumb && (index >= 0 && index <= lastPageIndex)) {
297
- this.textSelectionPlugin?.createTextLayer(index, pageContainer.$container);
298
- }
299
- return pageContainer;
620
+ constructor(x, y, width, height) {
621
+ this.x = x;
622
+ this.y = y;
623
+ this.width = width;
624
+ this.height = height;
300
625
  }
626
+
627
+ get right() { return this.x + this.width; }
628
+ get bottom() { return this.y + this.height; }
629
+ get top() { return this.y; }
630
+ get left() { return this.x; }
301
631
  }
302
- window.BookReader = BookreaderWithTextSelection;
303
- export default BookreaderWithTextSelection;