@internetarchive/bookreader 5.0.0-18

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 (428) hide show
  1. package/.eslintrc.js +58 -0
  2. package/.gitattributes +2 -0
  3. package/.github/ISSUE_TEMPLATE/bug.md +32 -0
  4. package/.github/ISSUE_TEMPLATE/feature-request.md +30 -0
  5. package/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +15 -0
  6. package/.github/dependabot.yml +8 -0
  7. package/.github/workflows/node.js.yml +37 -0
  8. package/.github/workflows/npm-publish.yml +47 -0
  9. package/.testcaferc.json +5 -0
  10. package/BookReader/BookReader.css +2983 -0
  11. package/BookReader/BookReader.js +3 -0
  12. package/BookReader/BookReader.js.LICENSE.txt +117 -0
  13. package/BookReader/BookReader.js.map +1 -0
  14. package/BookReader/bookreader-component-bundle.js +1436 -0
  15. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +27 -0
  16. package/BookReader/bookreader-component-bundle.js.map +1 -0
  17. package/BookReader/icons/1up.svg +1 -0
  18. package/BookReader/icons/2up.svg +1 -0
  19. package/BookReader/icons/advance.svg +3 -0
  20. package/BookReader/icons/chevron-right.svg +1 -0
  21. package/BookReader/icons/close-circle-dark.svg +1 -0
  22. package/BookReader/icons/close-circle.svg +1 -0
  23. package/BookReader/icons/fullscreen.svg +1 -0
  24. package/BookReader/icons/fullscreen_exit.svg +1 -0
  25. package/BookReader/icons/hamburger.svg +1 -0
  26. package/BookReader/icons/left-arrow.svg +1 -0
  27. package/BookReader/icons/magnify-minus.svg +1 -0
  28. package/BookReader/icons/magnify-plus.svg +1 -0
  29. package/BookReader/icons/magnify.svg +1 -0
  30. package/BookReader/icons/pause.svg +1 -0
  31. package/BookReader/icons/play.svg +1 -0
  32. package/BookReader/icons/playback-speed.svg +1 -0
  33. package/BookReader/icons/read-aloud.svg +1 -0
  34. package/BookReader/icons/review.svg +3 -0
  35. package/BookReader/icons/thumbnails.svg +1 -0
  36. package/BookReader/icons/volume-full.svg +1 -0
  37. package/BookReader/images/BRicons.png +0 -0
  38. package/BookReader/images/BRicons.svg +5 -0
  39. package/BookReader/images/BRicons_ia.png +0 -0
  40. package/BookReader/images/back_pages.png +0 -0
  41. package/BookReader/images/book_bottom_icon.png +0 -0
  42. package/BookReader/images/book_down_icon.png +0 -0
  43. package/BookReader/images/book_left_icon.png +0 -0
  44. package/BookReader/images/book_leftmost_icon.png +0 -0
  45. package/BookReader/images/book_right_icon.png +0 -0
  46. package/BookReader/images/book_rightmost_icon.png +0 -0
  47. package/BookReader/images/book_top_icon.png +0 -0
  48. package/BookReader/images/book_up_icon.png +0 -0
  49. package/BookReader/images/books_graphic.svg +1 -0
  50. package/BookReader/images/booksplit.png +0 -0
  51. package/BookReader/images/control_pause_icon.png +0 -0
  52. package/BookReader/images/control_play_icon.png +0 -0
  53. package/BookReader/images/embed_icon.png +0 -0
  54. package/BookReader/images/icon-home-ia.png +0 -0
  55. package/BookReader/images/icon_OL-logo-xs.png +0 -0
  56. package/BookReader/images/icon_alert-xs.png +0 -0
  57. package/BookReader/images/icon_book.svg +1 -0
  58. package/BookReader/images/icon_bookmark.svg +1 -0
  59. package/BookReader/images/icon_close-pop.png +0 -0
  60. package/BookReader/images/icon_download.png +0 -0
  61. package/BookReader/images/icon_gear.svg +1 -0
  62. package/BookReader/images/icon_hamburger.svg +1 -0
  63. package/BookReader/images/icon_home.png +0 -0
  64. package/BookReader/images/icon_home.svg +1 -0
  65. package/BookReader/images/icon_home_ia.png +0 -0
  66. package/BookReader/images/icon_indicator.png +0 -0
  67. package/BookReader/images/icon_info.svg +1 -0
  68. package/BookReader/images/icon_one_page.svg +1 -0
  69. package/BookReader/images/icon_pause.svg +1 -0
  70. package/BookReader/images/icon_play.svg +1 -0
  71. package/BookReader/images/icon_playback-rate.svg +1 -0
  72. package/BookReader/images/icon_return.png +0 -0
  73. package/BookReader/images/icon_search_button.svg +1 -0
  74. package/BookReader/images/icon_share.svg +1 -0
  75. package/BookReader/images/icon_skip-ahead.svg +1 -0
  76. package/BookReader/images/icon_skip-back.svg +2 -0
  77. package/BookReader/images/icon_speaker.svg +1 -0
  78. package/BookReader/images/icon_speaker_open.svg +1 -0
  79. package/BookReader/images/icon_thumbnails.svg +1 -0
  80. package/BookReader/images/icon_toc.svg +1 -0
  81. package/BookReader/images/icon_two_pages.svg +1 -0
  82. package/BookReader/images/icon_zoomer.png +0 -0
  83. package/BookReader/images/loading.gif +0 -0
  84. package/BookReader/images/logo_icon.png +0 -0
  85. package/BookReader/images/marker_chap-off.png +0 -0
  86. package/BookReader/images/marker_chap-off.svg +1 -0
  87. package/BookReader/images/marker_chap-off_ia.png +0 -0
  88. package/BookReader/images/marker_chap-on.png +0 -0
  89. package/BookReader/images/marker_chap-on.svg +1 -0
  90. package/BookReader/images/marker_srch-on.svg +1 -0
  91. package/BookReader/images/marker_srchchap-off.png +0 -0
  92. package/BookReader/images/marker_srchchap-on.png +0 -0
  93. package/BookReader/images/nav_control-dn.png +0 -0
  94. package/BookReader/images/nav_control-dn_ia.png +0 -0
  95. package/BookReader/images/nav_control-up.png +0 -0
  96. package/BookReader/images/nav_control-up_ia.png +0 -0
  97. package/BookReader/images/nav_control.png +0 -0
  98. package/BookReader/images/one_page_mode_icon.png +0 -0
  99. package/BookReader/images/paper-badge.png +0 -0
  100. package/BookReader/images/print_icon.png +0 -0
  101. package/BookReader/images/progressbar.gif +0 -0
  102. package/BookReader/images/right_edges.png +0 -0
  103. package/BookReader/images/slider.png +0 -0
  104. package/BookReader/images/slider_ia.png +0 -0
  105. package/BookReader/images/thumbnail_mode_icon.png +0 -0
  106. package/BookReader/images/transparent.png +0 -0
  107. package/BookReader/images/two_page_mode_icon.png +0 -0
  108. package/BookReader/images/zoom_in_icon.png +0 -0
  109. package/BookReader/images/zoom_out_icon.png +0 -0
  110. package/BookReader/jquery-1.10.1.js +2 -0
  111. package/BookReader/jquery-1.10.1.js.LICENSE.txt +24 -0
  112. package/BookReader/plugins/plugin.archive_analytics.js +2 -0
  113. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -0
  114. package/BookReader/plugins/plugin.autoplay.js +2 -0
  115. package/BookReader/plugins/plugin.autoplay.js.map +1 -0
  116. package/BookReader/plugins/plugin.chapters.js +2 -0
  117. package/BookReader/plugins/plugin.chapters.js.map +1 -0
  118. package/BookReader/plugins/plugin.iframe.js +2 -0
  119. package/BookReader/plugins/plugin.iframe.js.map +1 -0
  120. package/BookReader/plugins/plugin.mobile_nav.js +2 -0
  121. package/BookReader/plugins/plugin.mobile_nav.js.map +1 -0
  122. package/BookReader/plugins/plugin.resume.js +2 -0
  123. package/BookReader/plugins/plugin.resume.js.map +1 -0
  124. package/BookReader/plugins/plugin.search.js +2 -0
  125. package/BookReader/plugins/plugin.search.js.map +1 -0
  126. package/BookReader/plugins/plugin.text_selection.js +2 -0
  127. package/BookReader/plugins/plugin.text_selection.js.map +1 -0
  128. package/BookReader/plugins/plugin.tts.js +3 -0
  129. package/BookReader/plugins/plugin.tts.js.LICENSE.txt +27 -0
  130. package/BookReader/plugins/plugin.tts.js.map +1 -0
  131. package/BookReader/plugins/plugin.url.js +2 -0
  132. package/BookReader/plugins/plugin.url.js.map +1 -0
  133. package/BookReader/plugins/plugin.vendor-fullscreen.js +2 -0
  134. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -0
  135. package/BookReaderDemo/BookReaderDemo.css +41 -0
  136. package/BookReaderDemo/BookReaderJSAdvanced.js +115 -0
  137. package/BookReaderDemo/BookReaderJSAutoplay.js +56 -0
  138. package/BookReaderDemo/BookReaderJSSimple.js +55 -0
  139. package/BookReaderDemo/IIIFBookReader.js +207 -0
  140. package/BookReaderDemo/assets/v5/Bookreader-logo-cool-grad.svg +1 -0
  141. package/BookReaderDemo/assets/v5/Bookreader-logo-flat.svg +1 -0
  142. package/BookReaderDemo/assets/v5/Bookreader-logo-hex-cool-grad.png +0 -0
  143. package/BookReaderDemo/assets/v5/Bookreader-logo-hex-flat.png +0 -0
  144. package/BookReaderDemo/assets/v5/Bookreader-logo-lines.png +0 -0
  145. package/BookReaderDemo/assets/v5/Bookreader-logo-lines.svg +1 -0
  146. package/BookReaderDemo/assets/v5/Bookreader-logo-warm.svg +1 -0
  147. package/BookReaderDemo/assets/v5/bookreader-logo-renders@1x.png +0 -0
  148. package/BookReaderDemo/assets/v5/bookreader-logo-renders@2x.png +0 -0
  149. package/BookReaderDemo/assets/v5/bookreader-v5-screenshot.png +0 -0
  150. package/BookReaderDemo/bookreader-template-bundle.js +7178 -0
  151. package/BookReaderDemo/demo-advanced.html +32 -0
  152. package/BookReaderDemo/demo-autoplay.html +38 -0
  153. package/BookReaderDemo/demo-embed-iframe-src.html +84 -0
  154. package/BookReaderDemo/demo-embed.html +26 -0
  155. package/BookReaderDemo/demo-fullscreen-mobile.html +36 -0
  156. package/BookReaderDemo/demo-fullscreen.html +33 -0
  157. package/BookReaderDemo/demo-iiif.html +34 -0
  158. package/BookReaderDemo/demo-iiif.js +26 -0
  159. package/BookReaderDemo/demo-internetarchive.html +74 -0
  160. package/BookReaderDemo/demo-multiple.html +43 -0
  161. package/BookReaderDemo/demo-preview-pages.html +1092 -0
  162. package/BookReaderDemo/demo-simple.html +34 -0
  163. package/BookReaderDemo/demo-vendor-fullscreen.html +36 -0
  164. package/BookReaderDemo/immersion-1up.html +64 -0
  165. package/BookReaderDemo/immersion-mode.html +35 -0
  166. package/BookReaderDemo/toggle_controls.html +53 -0
  167. package/BookReaderDemo/view_mode.html +39 -0
  168. package/BookReaderDemo/viewmode-cycle.html +41 -0
  169. package/CHANGELOG.md +540 -0
  170. package/CONTRIBUTING.md +7 -0
  171. package/LICENSE +661 -0
  172. package/README.md +205 -0
  173. package/babel.config.js +18 -0
  174. package/codecov.yml +17 -0
  175. package/index.html +31 -0
  176. package/jsconfig.json +14 -0
  177. package/karma.conf.js +23 -0
  178. package/package.json +129 -0
  179. package/screenshot.png +0 -0
  180. package/scripts/postversion.js +10 -0
  181. package/scripts/preversion.js +14 -0
  182. package/scripts/version.js +26 -0
  183. package/src/BookNavigator/BookModel.js +14 -0
  184. package/src/BookNavigator/BookNavigator.js +468 -0
  185. package/src/BookNavigator/assets/book-loader.js +27 -0
  186. package/src/BookNavigator/assets/bookmark-colors.js +15 -0
  187. package/src/BookNavigator/assets/button-base.js +61 -0
  188. package/src/BookNavigator/assets/icon_checkmark.js +6 -0
  189. package/src/BookNavigator/assets/icon_close.js +3 -0
  190. package/src/BookNavigator/assets/icon_sort_asc.js +5 -0
  191. package/src/BookNavigator/assets/icon_sort_desc.js +5 -0
  192. package/src/BookNavigator/assets/icon_sort_neutral.js +5 -0
  193. package/src/BookNavigator/assets/icon_volumes.js +11 -0
  194. package/src/BookNavigator/bookmarks/bookmark-button.js +64 -0
  195. package/src/BookNavigator/bookmarks/bookmark-edit.js +215 -0
  196. package/src/BookNavigator/bookmarks/bookmarks-list.js +285 -0
  197. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +28 -0
  198. package/src/BookNavigator/bookmarks/bookmarks-provider.js +53 -0
  199. package/src/BookNavigator/bookmarks/ia-bookmarks.js +500 -0
  200. package/src/BookNavigator/br-fullscreen-mgr.js +83 -0
  201. package/src/BookNavigator/delete-modal-actions.js +49 -0
  202. package/src/BookNavigator/downloads/downloads-provider.js +76 -0
  203. package/src/BookNavigator/downloads/downloads.js +138 -0
  204. package/src/BookNavigator/search/a-search-result.js +55 -0
  205. package/src/BookNavigator/search/search-provider.js +180 -0
  206. package/src/BookNavigator/search/search-results.js +360 -0
  207. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +93 -0
  208. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +280 -0
  209. package/src/BookNavigator/volumes/volumes-provider.js +83 -0
  210. package/src/BookNavigator/volumes/volumes.js +178 -0
  211. package/src/BookReader/BookModel.js +518 -0
  212. package/src/BookReader/DebugConsole.js +54 -0
  213. package/src/BookReader/ImageCache.js +116 -0
  214. package/src/BookReader/Mode1Up.js +90 -0
  215. package/src/BookReader/Mode1UpLit.js +434 -0
  216. package/src/BookReader/Mode2Up.js +1372 -0
  217. package/src/BookReader/ModeSmoothZoom.js +177 -0
  218. package/src/BookReader/ModeThumb.js +336 -0
  219. package/src/BookReader/Navbar/Navbar.js +339 -0
  220. package/src/BookReader/PageContainer.js +120 -0
  221. package/src/BookReader/ReduceSet.js +26 -0
  222. package/src/BookReader/Toolbar/Toolbar.js +384 -0
  223. package/src/BookReader/events.js +20 -0
  224. package/src/BookReader/options.js +320 -0
  225. package/src/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  226. package/src/BookReader/utils/classes.js +36 -0
  227. package/src/BookReader/utils.js +240 -0
  228. package/src/BookReader.js +2546 -0
  229. package/src/BookReaderComponent/BookReaderComponent.js +112 -0
  230. package/src/ItemNavigator/ItemNavigator.js +376 -0
  231. package/src/ItemNavigator/providers/sharing.js +33 -0
  232. package/src/assets/icons/1up.svg +12 -0
  233. package/src/assets/icons/2up.svg +15 -0
  234. package/src/assets/icons/advance.svg +26 -0
  235. package/src/assets/icons/chevron-right.svg +1 -0
  236. package/src/assets/icons/close-circle-dark.svg +1 -0
  237. package/src/assets/icons/close-circle.svg +1 -0
  238. package/src/assets/icons/fullscreen.svg +17 -0
  239. package/src/assets/icons/fullscreen_exit.svg +17 -0
  240. package/src/assets/icons/hamburger.svg +15 -0
  241. package/src/assets/icons/left-arrow.svg +12 -0
  242. package/src/assets/icons/magnify-minus.svg +16 -0
  243. package/src/assets/icons/magnify-plus.svg +17 -0
  244. package/src/assets/icons/magnify.svg +15 -0
  245. package/src/assets/icons/pause.svg +23 -0
  246. package/src/assets/icons/play.svg +22 -0
  247. package/src/assets/icons/playback-speed.svg +34 -0
  248. package/src/assets/icons/read-aloud.svg +22 -0
  249. package/src/assets/icons/review.svg +22 -0
  250. package/src/assets/icons/thumbnails.svg +17 -0
  251. package/src/assets/icons/volume-full.svg +22 -0
  252. package/src/assets/images/BRicons.png +0 -0
  253. package/src/assets/images/BRicons.svg +94 -0
  254. package/src/assets/images/BRicons_ia.png +0 -0
  255. package/src/assets/images/back_pages.png +0 -0
  256. package/src/assets/images/book_bottom_icon.png +0 -0
  257. package/src/assets/images/book_down_icon.png +0 -0
  258. package/src/assets/images/book_left_icon.png +0 -0
  259. package/src/assets/images/book_leftmost_icon.png +0 -0
  260. package/src/assets/images/book_right_icon.png +0 -0
  261. package/src/assets/images/book_rightmost_icon.png +0 -0
  262. package/src/assets/images/book_top_icon.png +0 -0
  263. package/src/assets/images/book_up_icon.png +0 -0
  264. package/src/assets/images/books_graphic.svg +177 -0
  265. package/src/assets/images/booksplit.png +0 -0
  266. package/src/assets/images/control_pause_icon.png +0 -0
  267. package/src/assets/images/control_play_icon.png +0 -0
  268. package/src/assets/images/embed_icon.png +0 -0
  269. package/src/assets/images/icon-home-ia.png +0 -0
  270. package/src/assets/images/icon_OL-logo-xs.png +0 -0
  271. package/src/assets/images/icon_alert-xs.png +0 -0
  272. package/src/assets/images/icon_book.svg +12 -0
  273. package/src/assets/images/icon_bookmark.svg +12 -0
  274. package/src/assets/images/icon_close-pop.png +0 -0
  275. package/src/assets/images/icon_download.png +0 -0
  276. package/src/assets/images/icon_gear.svg +14 -0
  277. package/src/assets/images/icon_hamburger.svg +20 -0
  278. package/src/assets/images/icon_home.png +0 -0
  279. package/src/assets/images/icon_home.svg +21 -0
  280. package/src/assets/images/icon_home_ia.png +0 -0
  281. package/src/assets/images/icon_indicator.png +0 -0
  282. package/src/assets/images/icon_info.svg +11 -0
  283. package/src/assets/images/icon_one_page.svg +8 -0
  284. package/src/assets/images/icon_pause.svg +1 -0
  285. package/src/assets/images/icon_play.svg +1 -0
  286. package/src/assets/images/icon_playback-rate.svg +15 -0
  287. package/src/assets/images/icon_return.png +0 -0
  288. package/src/assets/images/icon_search_button.svg +8 -0
  289. package/src/assets/images/icon_share.svg +9 -0
  290. package/src/assets/images/icon_skip-ahead.svg +6 -0
  291. package/src/assets/images/icon_skip-back.svg +13 -0
  292. package/src/assets/images/icon_speaker.svg +18 -0
  293. package/src/assets/images/icon_speaker_open.svg +10 -0
  294. package/src/assets/images/icon_thumbnails.svg +12 -0
  295. package/src/assets/images/icon_toc.svg +5 -0
  296. package/src/assets/images/icon_two_pages.svg +9 -0
  297. package/src/assets/images/icon_zoomer.png +0 -0
  298. package/src/assets/images/loading.gif +0 -0
  299. package/src/assets/images/logo_icon.png +0 -0
  300. package/src/assets/images/marker_chap-off.png +0 -0
  301. package/src/assets/images/marker_chap-off.svg +11 -0
  302. package/src/assets/images/marker_chap-off_ia.png +0 -0
  303. package/src/assets/images/marker_chap-on.png +0 -0
  304. package/src/assets/images/marker_chap-on.svg +11 -0
  305. package/src/assets/images/marker_srch-on.svg +11 -0
  306. package/src/assets/images/marker_srchchap-off.png +0 -0
  307. package/src/assets/images/marker_srchchap-on.png +0 -0
  308. package/src/assets/images/nav_control-dn.png +0 -0
  309. package/src/assets/images/nav_control-dn_ia.png +0 -0
  310. package/src/assets/images/nav_control-up.png +0 -0
  311. package/src/assets/images/nav_control-up_ia.png +0 -0
  312. package/src/assets/images/nav_control.png +0 -0
  313. package/src/assets/images/one_page_mode_icon.png +0 -0
  314. package/src/assets/images/paper-badge.png +0 -0
  315. package/src/assets/images/print_icon.png +0 -0
  316. package/src/assets/images/progressbar.gif +0 -0
  317. package/src/assets/images/right_edges.png +0 -0
  318. package/src/assets/images/slider.png +0 -0
  319. package/src/assets/images/slider_ia.png +0 -0
  320. package/src/assets/images/thumbnail_mode_icon.png +0 -0
  321. package/src/assets/images/transparent.png +0 -0
  322. package/src/assets/images/two_page_mode_icon.png +0 -0
  323. package/src/assets/images/zoom_in_icon.png +0 -0
  324. package/src/assets/images/zoom_out_icon.png +0 -0
  325. package/src/css/BookReader.scss +89 -0
  326. package/src/css/_BRBookmarks.scss +29 -0
  327. package/src/css/_BRComponent.scss +13 -0
  328. package/src/css/_BRfloat.scss +197 -0
  329. package/src/css/_BRicon.scss +48 -0
  330. package/src/css/_BRmain.scss +251 -0
  331. package/src/css/_BRnav.scss +382 -0
  332. package/src/css/_BRpages.scss +139 -0
  333. package/src/css/_BRsearch.scss +226 -0
  334. package/src/css/_BRtoolbar.scss +84 -0
  335. package/src/css/_BRvendor.scss +5 -0
  336. package/src/css/_MobileNav.scss +194 -0
  337. package/src/css/_TextSelection.scss +32 -0
  338. package/src/css/_colorbox.scss +52 -0
  339. package/src/css/_controls.scss +244 -0
  340. package/src/css/_icons.scss +121 -0
  341. package/src/dragscrollable-br.js +261 -0
  342. package/src/jquery-wrapper.js +4 -0
  343. package/src/plugins/plugin.archive_analytics.js +86 -0
  344. package/src/plugins/plugin.autoplay.js +129 -0
  345. package/src/plugins/plugin.chapters.js +251 -0
  346. package/src/plugins/plugin.iframe.js +48 -0
  347. package/src/plugins/plugin.mobile_nav.js +287 -0
  348. package/src/plugins/plugin.resume.js +68 -0
  349. package/src/plugins/plugin.text_selection.js +291 -0
  350. package/src/plugins/plugin.url.js +198 -0
  351. package/src/plugins/plugin.vendor-fullscreen.js +247 -0
  352. package/src/plugins/search/plugin.search.js +439 -0
  353. package/src/plugins/search/view.js +440 -0
  354. package/src/plugins/tts/AbstractTTSEngine.js +242 -0
  355. package/src/plugins/tts/FestivalTTSEngine.js +169 -0
  356. package/src/plugins/tts/PageChunk.js +107 -0
  357. package/src/plugins/tts/PageChunkIterator.js +163 -0
  358. package/src/plugins/tts/WebTTSEngine.js +352 -0
  359. package/src/plugins/tts/plugin.tts.js +335 -0
  360. package/src/plugins/tts/tooltip_dict.js +15 -0
  361. package/src/plugins/tts/utils.js +91 -0
  362. package/src/util/browserSniffing.js +30 -0
  363. package/src/util/debouncer.js +26 -0
  364. package/src/util/docCookies.js +67 -0
  365. package/src/util/strings.js +34 -0
  366. package/tests/BookReader/BookModel.test.js +312 -0
  367. package/tests/BookReader/BookReaderPublicFunctions.test.js +164 -0
  368. package/tests/BookReader/DebugConsole.test.js +25 -0
  369. package/tests/BookReader/ImageCache.test.js +150 -0
  370. package/tests/BookReader/Mode1UpLit.test.js +87 -0
  371. package/tests/BookReader/Mode2Up.test.js +245 -0
  372. package/tests/BookReader/ModeSmoothZoom.test.js +149 -0
  373. package/tests/BookReader/Navbar/Navbar.test.js +169 -0
  374. package/tests/BookReader/PageContainer.test.js +187 -0
  375. package/tests/BookReader/ReduceSet.test.js +38 -0
  376. package/tests/BookReader/Toolbar/Toolbar.test.js +26 -0
  377. package/tests/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  378. package/tests/BookReader/utils/classes.test.js +88 -0
  379. package/tests/BookReader/utils.test.js +136 -0
  380. package/tests/BookReader.options.test.js +39 -0
  381. package/tests/BookReader.test.js +301 -0
  382. package/tests/e2e/README.md +75 -0
  383. package/tests/e2e/autoplay.test.js +13 -0
  384. package/tests/e2e/base.test.js +35 -0
  385. package/tests/e2e/helpers/base.js +263 -0
  386. package/tests/e2e/helpers/debug.js +13 -0
  387. package/tests/e2e/helpers/desktopSearch.js +72 -0
  388. package/tests/e2e/helpers/mobileSearch.js +85 -0
  389. package/tests/e2e/helpers/mockSearch.js +93 -0
  390. package/tests/e2e/helpers/rightToLeft.js +29 -0
  391. package/tests/e2e/ia-production/ia-prod-base.js +17 -0
  392. package/tests/e2e/models/BookReader.js +11 -0
  393. package/tests/e2e/models/Navigation.js +56 -0
  394. package/tests/e2e/rightToLeft.test.js +20 -0
  395. package/tests/e2e/viewmode.test.js +37 -0
  396. package/tests/karma/BookNavigator/book-navigator.test.js +180 -0
  397. package/tests/karma/BookNavigator/bookmarks/bookmark-edit.test.js +133 -0
  398. package/tests/karma/BookNavigator/bookmarks/bookmarks-list.test.js +222 -0
  399. package/tests/karma/BookNavigator/downloads/downloads-provider.test.js +64 -0
  400. package/tests/karma/BookNavigator/downloads/downloads.test.js +54 -0
  401. package/tests/karma/BookNavigator/search/search-provider.test.js +23 -0
  402. package/tests/karma/BookNavigator/search/search-results.test.js +240 -0
  403. package/tests/karma/BookNavigator/sharing/sharing-provider.test.js +40 -0
  404. package/tests/karma/BookNavigator/visual-adjustments.test.js +201 -0
  405. package/tests/karma/BookNavigator/volumes/volumes-provider.test.js +160 -0
  406. package/tests/karma/BookNavigator/volumes/volumes.test.js +98 -0
  407. package/tests/plugins/plugin.archive_analytics.test.js +23 -0
  408. package/tests/plugins/plugin.autoplay.test.js +52 -0
  409. package/tests/plugins/plugin.chapters.test.js +130 -0
  410. package/tests/plugins/plugin.iframe.test.js +42 -0
  411. package/tests/plugins/plugin.mobile_nav.test.js +66 -0
  412. package/tests/plugins/plugin.resume.test.js +98 -0
  413. package/tests/plugins/plugin.text_selection.test.js +193 -0
  414. package/tests/plugins/plugin.url.test.js +129 -0
  415. package/tests/plugins/plugin.vendor-fullscreen.test.js +65 -0
  416. package/tests/plugins/search/plugin.search.test.js +173 -0
  417. package/tests/plugins/search/plugin.search.view.test.js +106 -0
  418. package/tests/plugins/tts/AbstractTTSEngine.test.js +153 -0
  419. package/tests/plugins/tts/FestivalTTSEngine.test.js +59 -0
  420. package/tests/plugins/tts/PageChunk.test.js +57 -0
  421. package/tests/plugins/tts/PageChunkIterator.test.js +179 -0
  422. package/tests/plugins/tts/WebTTSEngine.test.js +126 -0
  423. package/tests/plugins/tts/utils.test.js +133 -0
  424. package/tests/util/browserSniffing.test.js +56 -0
  425. package/tests/util/docCookies.test.js +15 -0
  426. package/tests/util/strings.test.js +63 -0
  427. package/tests/utils.js +80 -0
  428. package/webpack.config.js +85 -0
@@ -0,0 +1,440 @@
1
+ class SearchView {
2
+ /**
3
+ * @param {object} params
4
+ * @param {object} params.br The BookReader instance
5
+ * @param {function} params.cancelSearch callback when a user wants to cancel search
6
+ *
7
+ * @event BookReader:SearchResultsCleared - when the search results nav gets cleared
8
+ * @event BookReader:ToggleSearchMenu - when search results menu should toggle
9
+ */
10
+ constructor({ br, searchCancelledCallback = () => {} }) {
11
+ this.br = br;
12
+
13
+ // Search results are returned as a text blob with the hits wrapped in
14
+ // triple mustaches. Hits occasionally include text beyond the search
15
+ // term, so everything within the staches is captured and wrapped.
16
+ this.matcher = new RegExp('{{{(.+?)}}}', 'g');
17
+ this.matches = [];
18
+ this.cacheDOMElements();
19
+ this.bindEvents();
20
+ this.cancelSearch = searchCancelledCallback;
21
+ }
22
+
23
+ cacheDOMElements() {
24
+ this.dom = {};
25
+ // Search input within the top toolbar. Will be removed once the mobile menu is replaced.
26
+ this.dom.toolbarSearch = this.buildToolbarSearch();
27
+ }
28
+
29
+ /**
30
+ * @param {string} query
31
+ */
32
+ setQuery(query) {
33
+ this.br.$('[name="query"]').val(query);
34
+ }
35
+
36
+ emptyMatches() {
37
+ this.matches = [];
38
+ }
39
+
40
+ removeResultPins() {
41
+ this.br.$('.BRnavpos .BRsearch').remove();
42
+ }
43
+
44
+ clearSearchFieldAndResults(dispatchEventWhenComplete = true) {
45
+ this.br.removeSearchResults();
46
+ this.removeResultPins();
47
+ this.emptyMatches();
48
+ this.setQuery('');
49
+ this.teardownSearchNavigation();
50
+ if (dispatchEventWhenComplete) {
51
+ this.br.trigger('SearchResultsCleared');
52
+ }
53
+ }
54
+
55
+ toggleSidebar() {
56
+ this.br.trigger('ToggleSearchMenu');
57
+ }
58
+
59
+ renderSearchNavigation() {
60
+ const selector = 'BRsearch-navigation';
61
+ $('.BRnav').before(`
62
+ <div class="${selector}">
63
+ <button class="toggle-sidebar">
64
+ <h4>
65
+ <span class="icon icon-search"></span> Results
66
+ </h4>
67
+ </button>
68
+ <div class="pagination">
69
+ <button class="prev" title="Previous result"><span class="icon icon-chevron hflip"></span></button>
70
+ <span data-id="resultsCount">${this.resultsPosition()}</span>
71
+ <button class="next" title="Next result"><span class="icon icon-chevron"></button>
72
+ </div>
73
+ <button class="clear" title="Clear search results">
74
+ <span class="icon icon-close"></span>
75
+ </button>
76
+ </div>
77
+ `);
78
+ this.dom.searchNavigation = $(`.${selector}`);
79
+ }
80
+
81
+ resultsPosition() {
82
+ let positionMessage = `${this.matches.length} result${this.matches.length === 1 ? '' : 's'}`;
83
+ if (~this.currentMatchIndex) {
84
+ positionMessage = `${this.currentMatchIndex + 1} / ${this.matches.length}`;
85
+ }
86
+ return positionMessage;
87
+ }
88
+
89
+ bindSearchNavigationEvents() {
90
+ if (!this.dom.searchNavigation) { return; }
91
+ const namespace = 'searchNavigation';
92
+
93
+ this.dom.searchNavigation
94
+ .on(`click.${namespace}`, '.clear', this.clearSearchFieldAndResults.bind(this))
95
+ .on(`click.${namespace}`, '.prev', this.showPrevResult.bind(this))
96
+ .on(`click.${namespace}`, '.next', this.showNextResult.bind(this))
97
+ .on(`click.${namespace}`, '.toggle-sidebar', this.toggleSidebar.bind(this))
98
+ .on(`click.${namespace}`, false);
99
+ }
100
+
101
+ showPrevResult() {
102
+ if (this.currentMatchIndex === 0) { return; }
103
+ if (this.br.mode === this.br.constModeThumb) { this.br.switchMode(this.br.constMode1up); }
104
+ if (!~this.currentMatchIndex) {
105
+ this.currentMatchIndex = this.getClosestMatchIndex((start, end, comparator) => end[0] > comparator) + 1;
106
+ }
107
+ this.br.$('.BRnavline .BRsearch').eq(--this.currentMatchIndex).click();
108
+ this.updateResultsPosition();
109
+ this.updateSearchNavigationButtons();
110
+ }
111
+
112
+ showNextResult() {
113
+ if (this.currentMatchIndex + 1 === this.matches.length) { return; }
114
+ if (this.br.mode === this.br.constModeThumb) { this.br.switchMode(this.br.constMode1up); }
115
+ if (!~this.currentMatchIndex) {
116
+ this.currentMatchIndex = this.getClosestMatchIndex((start, end, comparator) => start[start.length - 1] > comparator) - 1;
117
+ }
118
+ this.br.$('.BRnavline .BRsearch').eq(++this.currentMatchIndex).click();
119
+ this.updateResultsPosition();
120
+ this.updateSearchNavigationButtons();
121
+ }
122
+
123
+ /**
124
+ * Obtains closest match based on the logical comparison function passed in.
125
+ * When the comparison function returns true, the starting (left) half of the
126
+ * matches array is used in the binary split, else the ending (right) half is
127
+ * used. A recursive call is made to perform the same split and comparison
128
+ * on the winning half of the matches. This is traditionally known as binary
129
+ * search (https://en.wikipedia.org/wiki/Binary_search_algorithm), and in
130
+ * most cases (medium to large search result arrays) should outperform
131
+ * traversing the array from start to finish. In the case of small arrays,
132
+ * the speed difference is negligible.
133
+ *
134
+ * @param {function} comparisonFn
135
+ * @return {number} matchIndex
136
+ */
137
+ getClosestMatchIndex(comparisonFn) {
138
+ const matchPages = this.matches.map((m) => m.par[0].page);
139
+ const currentPage = this.br.currentIndex() + 1;
140
+ const closestTo = (pool, comparator) => {
141
+ if (pool.length === 1) { return pool[0]; }
142
+ const start = pool.slice(0, pool.length / 2);
143
+ const end = pool.slice(pool.length / 2);
144
+ return closestTo((comparisonFn(start, end, comparator) ? start : end), comparator);
145
+ };
146
+
147
+ const closestPage = closestTo(matchPages, currentPage);
148
+ return this.matches.indexOf(this.matches.find((m) => m.par[0].page === closestPage));
149
+ }
150
+
151
+ updateResultsPosition() {
152
+ this.dom.searchNavigation.find('[data-id=resultsCount]').text(this.resultsPosition());
153
+ }
154
+
155
+ updateSearchNavigationButtons() {
156
+ this.dom.searchNavigation.find('.prev').attr('disabled', !this.currentMatchIndex);
157
+ this.dom.searchNavigation.find('.next').attr('disabled', this.currentMatchIndex + 1 === this.matches.length);
158
+ }
159
+
160
+ teardownSearchNavigation() {
161
+ if (!this.dom.searchNavigation) {
162
+ this.dom.searchNavigation = $('.BRsearch-navigation');
163
+ }
164
+ if (!this.dom.searchNavigation.length) { return; }
165
+
166
+ this.dom.searchNavigation.off('.searchNavigation').remove();
167
+ this.dom.searchNavigation = null;
168
+ this.br.resize();
169
+ }
170
+
171
+ setCurrentMatchIndex() {
172
+ let matchingSearchResult;
173
+ if (this.br.mode === this.br.constModeThumb) {
174
+ this.currentMatchIndex = -1;
175
+ return;
176
+ }
177
+ if (this.br.mode === this.br.constMode2up) {
178
+ matchingSearchResult = this.find2upMatchingSearchResult();
179
+ }
180
+ else {
181
+ matchingSearchResult = this.find1upMatchingSearchResult();
182
+ }
183
+ this.currentMatchIndex = this.matches.indexOf(matchingSearchResult);
184
+ }
185
+
186
+ find1upMatchingSearchResult() {
187
+ return this.matches.find((m) => this.br.currentIndex() === m.par[0].page - 1);
188
+ }
189
+
190
+ find2upMatchingSearchResult() {
191
+ return this.matches.find((m) => this.br._isIndexDisplayed(m.par[0].page - 1));
192
+ }
193
+
194
+ updateSearchNavigation() {
195
+ if (!this.matches.length) { return; }
196
+
197
+ this.setCurrentMatchIndex();
198
+ this.updateResultsPosition();
199
+ this.updateSearchNavigationButtons();
200
+ }
201
+
202
+ /**
203
+ * @param {boolean} bool
204
+ */
205
+ togglePinsFor(bool) {
206
+ const pinsVisibleState = bool ? 'visible' : 'hidden';
207
+ this.br.refs.$BRfooter.find('.BRsearch').css({ visibility: pinsVisibleState });
208
+ }
209
+
210
+ buildToolbarSearch() {
211
+ const toolbarSearch = document.createElement('span');
212
+ toolbarSearch.classList.add('BRtoolbarSection', 'BRtoolbarSectionSearch');
213
+ toolbarSearch.innerHTML = `
214
+ <form class="BRbooksearch desktop">
215
+ <input type="search" name="query" class="BRsearchInput" value="" placeholder="Search inside"/>
216
+ <button type="submit" class="BRsearchSubmit">
217
+ <img src="${this.br.imagesBaseURL}icon_search_button.svg" />
218
+ </button>
219
+ </form>
220
+ `;
221
+ return toolbarSearch;
222
+ }
223
+
224
+ /**
225
+ * @param {array} matches
226
+ */
227
+ renderPins(matches) {
228
+ matches.forEach((match) => {
229
+ const queryString = match.text;
230
+ const pageIndex = this.br.leafNumToIndex(match.par[0].page);
231
+ const pageNumber = this.br.getPageNum(pageIndex);
232
+ const uiStringSearch = "Search result"; // i18n
233
+ const uiStringPage = "Page"; // i18n
234
+
235
+ const percentThrough = this.br.constructor.util.cssPercentage(pageIndex, this.br.getNumLeafs() - 1);
236
+
237
+ const queryStringWithB = queryString.replace(this.matcher, '<b>$1</b>');
238
+
239
+ let queryStringWithBTruncated = '';
240
+
241
+ if (queryString.length > 100) {
242
+ queryStringWithBTruncated = queryString
243
+ .replace(/^(.{100}[^\s]*).*/, "$1")
244
+ .replace(this.matcher, '<b>$1</b>')
245
+ + '...';
246
+ }
247
+
248
+ // draw marker
249
+ $('<div>')
250
+ .addClass('BRsearch')
251
+ .css({
252
+ left: percentThrough,
253
+ })
254
+ .attr('title', uiStringSearch)
255
+ .append(`
256
+ <div class="BRquery">
257
+ <div>${queryStringWithBTruncated || queryStringWithB}</div>
258
+ <div>${uiStringPage} ${pageNumber}</div>
259
+ </div>
260
+ `)
261
+ .data({ pageIndex })
262
+ .appendTo(this.br.$('.BRnavline'))
263
+ .hover(
264
+ (event) => {
265
+ // remove from other markers then turn on just for this
266
+ // XXX should be done when nav slider moves
267
+ const marker = event.currentTarget;
268
+ const tooltip = marker.querySelector('.BRquery');
269
+ const tooltipOffset = tooltip.getBoundingClientRect();
270
+ const targetOffset = marker.getBoundingClientRect();
271
+ const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
272
+ if (tooltipOffset.x - boxSizeAdjust < 0) {
273
+ tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
274
+ }
275
+ $('.BRsearch,.BRchapter').removeClass('front');
276
+ $(event.target).addClass('front');
277
+ },
278
+ (event) => $(event.target).removeClass('front'))
279
+ .click(function (event) {
280
+ // closures are nested and deep, using an arrow function breaks references.
281
+ // Todo: update to arrow function & clean up closures
282
+ // to remove `bind` dependency
283
+ this.br._searchPluginGoToResult(+$(event.target).data('pageIndex'));
284
+ }.bind(this));
285
+ });
286
+ }
287
+
288
+ /**
289
+ * @param {boolean} bool
290
+ */
291
+ toggleSearchPending(bool) {
292
+ if (bool) {
293
+ this.br.showProgressPopup("Search results will appear below...", () => this.progressPopupClosed());
294
+ }
295
+ else {
296
+ this.br.removeProgressPopup();
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Primary callback when user cancels search popup
302
+ */
303
+ progressPopupClosed() {
304
+ this.toggleSearchPending();
305
+ this.cancelSearch();
306
+ }
307
+
308
+ renderErrorModal(textIsProcessing = false) {
309
+ const errorDetails = `${!textIsProcessing ? 'The text may still be processing. ' : ''}Please try again.`;
310
+ this.renderModalMessage(`
311
+ Sorry, there was an error with your search.
312
+ <br />
313
+ ${errorDetails}
314
+ `);
315
+ this.delayModalRemovalFor(4000);
316
+ }
317
+
318
+ renderBookNotIndexedModal() {
319
+ this.renderModalMessage(`
320
+ <p>
321
+ This book hasn't been indexed for searching yet.
322
+ We've just started indexing it, so search should be available soon.
323
+ <br />
324
+ Please try again later. Thanks!
325
+ </p>
326
+ `);
327
+ this.delayModalRemovalFor(5000);
328
+ }
329
+
330
+ renderResultsEmptyModal() {
331
+ this.renderModalMessage('No matches were found.');
332
+ this.delayModalRemovalFor(2000);
333
+ }
334
+
335
+ /**
336
+ * @param {string} messageHTML The innerHTML string used to popupate the modal contents
337
+ */
338
+ renderModalMessage(messageHTML) {
339
+ const modal = document.createElement('div');
340
+ modal.classList.add('BRprogresspopup', 'search_modal');
341
+ modal.innerHTML = messageHTML;
342
+ document.querySelector(this.br.el).append(modal);
343
+ }
344
+
345
+ /**
346
+ * @param {number} timeoutMS
347
+ */
348
+ delayModalRemovalFor(timeoutMS) {
349
+ setTimeout(this.br.removeProgressPopup.bind(this.br), timeoutMS);
350
+ }
351
+
352
+ /**
353
+ * @param {Event} e
354
+ */
355
+ submitHandler(e) {
356
+ e.preventDefault();
357
+ const query = e.target.querySelector('[name="query"]').value;
358
+ if (!query.length) { return false; }
359
+ this.br.search(query);
360
+ this.emptyMatches();
361
+ this.toggleSearchPending(true);
362
+ return false;
363
+ }
364
+
365
+ /**
366
+ * @param {Event} e
367
+ * @param {object} properties
368
+ * @param {object} properties.results
369
+ * @param {object} properties.options
370
+ */
371
+ handleSearchCallback(e, { results, options }) {
372
+ this.matches = results.matches;
373
+ this.setCurrentMatchIndex();
374
+ this.teardownSearchNavigation();
375
+ this.renderSearchNavigation();
376
+ this.bindSearchNavigationEvents();
377
+ this.renderPins(results.matches);
378
+ this.toggleSearchPending(false);
379
+ if (options.goToFirstResult) {
380
+ $(document).one('BookReader:pageChanged', () => {
381
+ this.br.resize();
382
+ });
383
+ } else {
384
+ this.br.resize();
385
+ }
386
+ }
387
+
388
+ /**
389
+ * @param {Event} e
390
+ */
391
+ handleNavToggledCallback(e) {
392
+ const is_visible = this.br.navigationIsVisible();
393
+ this.togglePinsFor(is_visible);
394
+ }
395
+
396
+ handleSearchStarted() {
397
+ this.emptyMatches();
398
+ this.br.removeSearchHilites();
399
+ this.removeResultPins();
400
+ this.toggleSearchPending(true);
401
+ this.teardownSearchNavigation();
402
+ this.setQuery(this.br.searchTerm);
403
+ }
404
+
405
+ /**
406
+ * Event listener for: `BookReader:SearchCallbackError`
407
+ * @param {CustomEvent} event
408
+ */
409
+ handleSearchCallbackError(event = {}) {
410
+ this.toggleSearchPending(false);
411
+ const isIndexed = event?.detail?.props?.results?.indexed;
412
+ this.renderErrorModal(isIndexed);
413
+ }
414
+
415
+ handleSearchCallbackBookNotIndexed() {
416
+ this.toggleSearchPending(false);
417
+ this.renderBookNotIndexedModal();
418
+ }
419
+
420
+ handleSearchCallbackEmpty() {
421
+ this.toggleSearchPending(false);
422
+ this.renderResultsEmptyModal();
423
+ }
424
+
425
+ bindEvents() {
426
+ const namespace = 'BookReader:';
427
+
428
+ window.addEventListener(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this));
429
+ $(document).on(`${namespace}SearchCallback`, this.handleSearchCallback.bind(this))
430
+ .on(`${namespace}navToggled`, this.handleNavToggledCallback.bind(this))
431
+ .on(`${namespace}SearchStarted`, this.handleSearchStarted.bind(this))
432
+ .on(`${namespace}SearchCallbackBookNotIndexed`, this.handleSearchCallbackBookNotIndexed.bind(this))
433
+ .on(`${namespace}SearchCallbackEmpty`, this.handleSearchCallbackEmpty.bind(this))
434
+ .on(`${namespace}pageChanged`, this.updateSearchNavigation.bind(this));
435
+
436
+ this.dom.toolbarSearch.querySelector('form').addEventListener('submit', this.submitHandler.bind(this));
437
+ }
438
+ }
439
+
440
+ export default SearchView;
@@ -0,0 +1,242 @@
1
+ import PageChunkIterator from './PageChunkIterator.js';
2
+ /** @typedef {import('./utils.js').ISO6391} ISO6391 */
3
+ /** @typedef {import('./PageChunk.js')} PageChunk */
4
+
5
+ /**
6
+ * @export
7
+ * @typedef {Object} TTSEngineOptions
8
+ * @property {String} server
9
+ * @property {String} bookPath
10
+ * @property {ISO6391} bookLanguage
11
+ * @property {Function} onLoadingStart
12
+ * @property {Function} onLoadingComplete
13
+ * @property {Function} onDone called when the entire book is done
14
+ * @property {function(PageChunk): PromiseLike} beforeChunkPlay will delay the playing of the chunk
15
+ * @property {function(PageChunk): void} afterChunkPlay fires once a chunk has fully finished
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} AbstractTTSSound
20
+ * @property {PageChunk} chunk
21
+ * @property {boolean} loaded
22
+ * @property {number} rate
23
+ * @property {SpeechSynthesisVoice} voice
24
+ * @property {(callback: Function) => void} load
25
+ * @property {() => PromiseLike} play
26
+ * @property {() => Promise} stop
27
+ * @property {() => void} pause
28
+ * @property {() => void} resume
29
+ * @property {() => void} finish force the sound to 'finish'
30
+ * @property {number => void} setPlaybackRate
31
+ **/
32
+
33
+ /** Handling bookreader's text-to-speech */
34
+ export default class AbstractTTSEngine {
35
+ /**
36
+ * @protected
37
+ * @param {TTSEngineOptions} options
38
+ */
39
+ constructor(options) {
40
+ this.playing = false;
41
+ this.paused = false;
42
+ this.opts = options;
43
+ /** @type {PageChunkIterator} */
44
+ this._chunkIterator = null;
45
+ /** @type {AbstractTTSSound} */
46
+ this.activeSound = null;
47
+ this.playbackRate = 1;
48
+ /** Events we can bind to */
49
+ this.events = $({});
50
+ /** @type {SpeechSynthesisVoice} */
51
+ this.voice = null;
52
+ // Listen for voice changes (fired by subclasses)
53
+ this.events.on('voiceschanged', () => {
54
+ this.voice = AbstractTTSEngine.getBestBookVoice(this.getVoices(), this.opts.bookLanguage);
55
+ });
56
+ this.events.trigger('voiceschanged');
57
+ }
58
+
59
+ /**
60
+ * @abstract
61
+ * @return {boolean}
62
+ */
63
+ static isSupported() { throw new Error("Unimplemented abstract class"); }
64
+
65
+ /**
66
+ * @abstract
67
+ * @return {SpeechSynthesisVoice[]}
68
+ */
69
+ getVoices() { throw new Error("Unimplemented abstract class"); }
70
+
71
+ /** @abstract */
72
+ init() { return null; }
73
+
74
+ /**
75
+ * @param {number} leafIndex
76
+ * @param {number} numLeafs total number of leafs in the current book
77
+ */
78
+ start(leafIndex, numLeafs) {
79
+ this.playing = true;
80
+ this.opts.onLoadingStart();
81
+
82
+ this._chunkIterator = new PageChunkIterator(numLeafs, leafIndex, {
83
+ server: this.opts.server,
84
+ bookPath: this.opts.bookPath,
85
+ pageBufferSize: 5,
86
+ });
87
+
88
+ this.step();
89
+ this.events.trigger('start');
90
+ }
91
+
92
+ stop() {
93
+ if (this.activeSound) this.activeSound.stop();
94
+ this.playing = false;
95
+ this._chunkIterator = null;
96
+ this.activeSound = null;
97
+ this.events.trigger('stop');
98
+ }
99
+
100
+ /** @public */
101
+ pause() {
102
+ const fireEvent = !this.paused && this.activeSound;
103
+ this.paused = true;
104
+ if (this.activeSound) this.activeSound.pause();
105
+ if (fireEvent) this.events.trigger('pause');
106
+ }
107
+
108
+ /** @public */
109
+ resume() {
110
+ const fireEvent = this.paused && this.activeSound;
111
+ this.paused = false;
112
+ if (this.activeSound) this.activeSound.resume();
113
+ if (fireEvent) this.events.trigger('resume');
114
+ }
115
+
116
+ togglePlayPause() {
117
+ if (this.paused) this.resume();
118
+ else this.pause();
119
+ }
120
+
121
+ /** @public */
122
+ jumpForward() {
123
+ if (this.activeSound) this.activeSound.finish();
124
+ }
125
+
126
+ /** @public */
127
+ jumpBackward() {
128
+ Promise.all([
129
+ this.activeSound.stop(),
130
+ this._chunkIterator.decrement()
131
+ .then(() => this._chunkIterator.decrement())
132
+ ])
133
+ .then(() => this.step());
134
+ }
135
+
136
+ /** @param {number} newRate */
137
+ setPlaybackRate(newRate) {
138
+ this.playbackRate = newRate;
139
+ if (this.activeSound) this.activeSound.setPlaybackRate(newRate);
140
+ }
141
+
142
+ /** @private */
143
+ step() {
144
+ this._chunkIterator.next()
145
+ .then(chunk => {
146
+ if (chunk == PageChunkIterator.AT_END) {
147
+ this.stop();
148
+ this.opts.onDone();
149
+ return;
150
+ }
151
+
152
+ this.opts.onLoadingStart();
153
+ const sound = this.createSound(chunk);
154
+ sound.chunk = chunk;
155
+ sound.rate = this.playbackRate;
156
+ sound.voice = this.voice;
157
+ sound.load(() => this.opts.onLoadingComplete());
158
+
159
+ this.opts.onLoadingComplete();
160
+ return this.opts.beforeChunkPlay(chunk).then(() => sound);
161
+ })
162
+ .then(sound => {
163
+ if (!this.playing) return;
164
+
165
+ const playPromise = this.playSound(sound)
166
+ .then(() => this.opts.afterChunkPlay(sound.chunk));
167
+ if (this.paused) this.pause();
168
+ return playPromise;
169
+ })
170
+ .then(() => {
171
+ if (this.playing) return this.step();
172
+ });
173
+ }
174
+
175
+ /**
176
+ * @abstract
177
+ * @param {PageChunk} chunk
178
+ * @return {AbstractTTSSound}
179
+ */
180
+ createSound(chunk) { throw new Error("Unimplemented abstract class"); }
181
+
182
+ /**
183
+ * @param {AbstractTTSSound} sound
184
+ * @return {PromiseLike} promise called once playing finished
185
+ */
186
+ playSound(sound) {
187
+ this.activeSound = sound;
188
+ if (!this.activeSound.loaded) this.opts.onLoadingStart();
189
+ return this.activeSound.play();
190
+ }
191
+
192
+ /** Convenience wrapper for {@see AbstractTTSEngine.getBestVoice} */
193
+ getBestVoice() {
194
+ return AbstractTTSEngine.getBestBookVoice(this.getVoices(), this.opts.bookLanguage);
195
+ }
196
+
197
+ /**
198
+ * @private
199
+ * Find the best voice to use given the available voices, the book language, and the user's
200
+ * languages.
201
+ * @param {SpeechSynthesisVoice[]} voices
202
+ * @param {ISO6391} bookLanguage
203
+ * @param {string[]} userLanguages languages in BCP47 format (e.g. en-US). Ordered by preference.
204
+ * @return {SpeechSynthesisVoice | undefined}
205
+ */
206
+ static getBestBookVoice(voices, bookLanguage, userLanguages = navigator.languages) {
207
+ const bookLangVoices = voices.filter(v => v.lang.startsWith(bookLanguage));
208
+ // navigator.languages browser support isn't great yet, so get just 1 language otherwise
209
+ // Sample navigator.languages: ["en-CA", "fr-CA", "fr", "en-US", "en", "de-DE", "de"]
210
+ userLanguages = userLanguages || (navigator.language ? [navigator.language] : []);
211
+
212
+ // user languages that match the book language
213
+ const matchingUserLangs = userLanguages.filter(lang => lang.startsWith(bookLanguage));
214
+
215
+ // Try to find voices that intersect these two sets
216
+ return AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices) ||
217
+ // no user languages match the books; let's return the best voice for the book language
218
+ (bookLangVoices.find(v => v.default) || bookLangVoices[0])
219
+ // No voices match the book language? let's find a voice in the user's language
220
+ // and ignore book lang
221
+ || AbstractTTSEngine.getMatchingVoice(userLanguages, voices)
222
+ // C'mon! Ok, just read with whatever we got!
223
+ || (voices.find(v => v.default) || voices[0]);
224
+ }
225
+
226
+ /**
227
+ * @private
228
+ * Get the best voice that matches one of the BCP47 languages (order by preference)
229
+ * @param {string[]} languages in BCP 47 format (e.g. 'en-US', or 'en'); ordered by preference
230
+ * @param {SpeechSynthesisVoice[]} voices voices to choose from
231
+ * @return {SpeechSynthesisVoice | undefined}
232
+ */
233
+ static getMatchingVoice(languages, voices) {
234
+ for (const lang of languages) {
235
+ // Chrome Android was returning voice languages like `en_US` instead of `en-US`
236
+ const matchingVoices = voices.filter(v => v.lang.replace('_', '-').startsWith(lang));
237
+ if (matchingVoices.length) {
238
+ return matchingVoices.find(v => v.default) || matchingVoices[0];
239
+ }
240
+ }
241
+ }
242
+ }