@internetarchive/bookreader 5.0.0-38 → 5.0.0-39

Sign up to get free protection for your applications and to get access to all the features.
Files changed (222) hide show
  1. package/BookReader/BookReader.css +8 -0
  2. package/BookReader/BookReader.js +1 -1
  3. package/BookReader/BookReader.js.map +1 -1
  4. package/BookReader/ia-bookreader-bundle.js +99 -75
  5. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  6. package/BookReader/icons/magnify-minus.svg +1 -1
  7. package/BookReader/icons/magnify-plus.svg +1 -1
  8. package/BookReader/plugins/plugin.autoplay.js +1 -1
  9. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  10. package/BookReader/plugins/plugin.chapters.js +1 -1
  11. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  12. package/BookReader/plugins/plugin.mobile_nav.js +1 -1
  13. package/BookReader/plugins/plugin.mobile_nav.js.map +1 -1
  14. package/BookReader/plugins/plugin.resume.js +1 -1
  15. package/BookReader/plugins/plugin.resume.js.map +1 -1
  16. package/BookReader/plugins/plugin.search.js +1 -1
  17. package/BookReader/plugins/plugin.search.js.map +1 -1
  18. package/BookReader/plugins/plugin.text_selection.js +1 -1
  19. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  20. package/BookReader/plugins/plugin.tts.js +1 -1
  21. package/BookReader/plugins/plugin.tts.js.map +1 -1
  22. package/BookReader/plugins/plugin.url.js +1 -1
  23. package/BookReader/plugins/plugin.url.js.map +1 -1
  24. package/CHANGELOG.md +5 -0
  25. package/README.md +13 -0
  26. package/package.json +14 -14
  27. package/renovate.json +1 -1
  28. package/src/BookReader/Mode1UpLit.js +7 -1
  29. package/src/BookReader/Mode2Up.js +11 -0
  30. package/src/BookReader/ModeSmoothZoom.js +2 -0
  31. package/src/BookReader/PageContainer.js +10 -4
  32. package/src/BookReader/utils/ScrollClassAdder.js +31 -0
  33. package/src/assets/icons/magnify-minus.svg +3 -7
  34. package/src/assets/icons/magnify-plus.svg +3 -7
  35. package/src/css/_TextSelection.scss +13 -0
  36. package/tests/jest/BookReader/PageContainer.test.js +5 -4
  37. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  38. package/.husky/_/husky.sh +0 -30
  39. package/stat/BookNavigator/BookModel.js +0 -14
  40. package/stat/BookNavigator/BookNavigator.js +0 -524
  41. package/stat/BookNavigator/assets/bookmark-colors.js +0 -15
  42. package/stat/BookNavigator/assets/button-base.js +0 -61
  43. package/stat/BookNavigator/assets/ia-logo.js +0 -17
  44. package/stat/BookNavigator/assets/icon_checkmark.js +0 -6
  45. package/stat/BookNavigator/assets/icon_close.js +0 -3
  46. package/stat/BookNavigator/assets/icon_sort_asc.js +0 -5
  47. package/stat/BookNavigator/assets/icon_sort_desc.js +0 -5
  48. package/stat/BookNavigator/assets/icon_sort_neutral.js +0 -5
  49. package/stat/BookNavigator/assets/icon_volumes.js +0 -11
  50. package/stat/BookNavigator/bookmarks/bookmark-button.js +0 -64
  51. package/stat/BookNavigator/bookmarks/bookmark-edit.js +0 -215
  52. package/stat/BookNavigator/bookmarks/bookmarks-list.js +0 -285
  53. package/stat/BookNavigator/bookmarks/bookmarks-loginCTA.js +0 -28
  54. package/stat/BookNavigator/bookmarks/bookmarks-provider.js +0 -56
  55. package/stat/BookNavigator/bookmarks/ia-bookmarks.js +0 -523
  56. package/stat/BookNavigator/br-fullscreen-mgr.js +0 -82
  57. package/stat/BookNavigator/delete-modal-actions.js +0 -49
  58. package/stat/BookNavigator/downloads/downloads-provider.js +0 -72
  59. package/stat/BookNavigator/downloads/downloads.js +0 -139
  60. package/stat/BookNavigator/provider-config.js +0 -0
  61. package/stat/BookNavigator/search/a-search-result.js +0 -55
  62. package/stat/BookNavigator/search/search-provider.js +0 -180
  63. package/stat/BookNavigator/search/search-results.js +0 -360
  64. package/stat/BookNavigator/sharing.js +0 -31
  65. package/stat/BookNavigator/visual-adjustments/visual-adjustments-provider.js +0 -94
  66. package/stat/BookNavigator/visual-adjustments/visual-adjustments.js +0 -280
  67. package/stat/BookNavigator/volumes/volumes-provider.js +0 -83
  68. package/stat/BookNavigator/volumes/volumes.js +0 -178
  69. package/stat/BookReader/BookModel.js +0 -518
  70. package/stat/BookReader/DebugConsole.js +0 -54
  71. package/stat/BookReader/DragScrollable.js +0 -233
  72. package/stat/BookReader/ImageCache.js +0 -116
  73. package/stat/BookReader/Mode1Up.js +0 -102
  74. package/stat/BookReader/Mode1UpLit.js +0 -434
  75. package/stat/BookReader/Mode2Up.js +0 -1372
  76. package/stat/BookReader/ModeSmoothZoom.js +0 -177
  77. package/stat/BookReader/ModeThumb.js +0 -344
  78. package/stat/BookReader/Navbar/Navbar.js +0 -310
  79. package/stat/BookReader/PageContainer.js +0 -120
  80. package/stat/BookReader/ReduceSet.js +0 -26
  81. package/stat/BookReader/Toolbar/Toolbar.js +0 -384
  82. package/stat/BookReader/events.js +0 -20
  83. package/stat/BookReader/options.js +0 -324
  84. package/stat/BookReader/utils/HTMLDimensionsCacher.js +0 -44
  85. package/stat/BookReader/utils/classes.js +0 -36
  86. package/stat/BookReader/utils.js +0 -240
  87. package/stat/BookReader.js +0 -2550
  88. package/stat/BookReaderComponent/BookReaderComponent.js +0 -117
  89. package/stat/assets/icons/1up.svg +0 -12
  90. package/stat/assets/icons/2up.svg +0 -15
  91. package/stat/assets/icons/advance.svg +0 -26
  92. package/stat/assets/icons/chevron-right.svg +0 -1
  93. package/stat/assets/icons/close-circle-dark.svg +0 -1
  94. package/stat/assets/icons/close-circle.svg +0 -1
  95. package/stat/assets/icons/fullscreen.svg +0 -17
  96. package/stat/assets/icons/fullscreen_exit.svg +0 -17
  97. package/stat/assets/icons/hamburger.svg +0 -15
  98. package/stat/assets/icons/left-arrow.svg +0 -12
  99. package/stat/assets/icons/magnify-minus.svg +0 -16
  100. package/stat/assets/icons/magnify-plus.svg +0 -17
  101. package/stat/assets/icons/magnify.svg +0 -15
  102. package/stat/assets/icons/pause.svg +0 -23
  103. package/stat/assets/icons/play.svg +0 -22
  104. package/stat/assets/icons/playback-speed.svg +0 -34
  105. package/stat/assets/icons/read-aloud.svg +0 -22
  106. package/stat/assets/icons/review.svg +0 -22
  107. package/stat/assets/icons/thumbnails.svg +0 -17
  108. package/stat/assets/icons/voice.svg +0 -1
  109. package/stat/assets/icons/volume-full.svg +0 -22
  110. package/stat/assets/images/BRicons.png +0 -0
  111. package/stat/assets/images/BRicons.svg +0 -94
  112. package/stat/assets/images/BRicons_ia.png +0 -0
  113. package/stat/assets/images/back_pages.png +0 -0
  114. package/stat/assets/images/book_bottom_icon.png +0 -0
  115. package/stat/assets/images/book_down_icon.png +0 -0
  116. package/stat/assets/images/book_left_icon.png +0 -0
  117. package/stat/assets/images/book_leftmost_icon.png +0 -0
  118. package/stat/assets/images/book_right_icon.png +0 -0
  119. package/stat/assets/images/book_rightmost_icon.png +0 -0
  120. package/stat/assets/images/book_top_icon.png +0 -0
  121. package/stat/assets/images/book_up_icon.png +0 -0
  122. package/stat/assets/images/books_graphic.svg +0 -177
  123. package/stat/assets/images/booksplit.png +0 -0
  124. package/stat/assets/images/control_pause_icon.png +0 -0
  125. package/stat/assets/images/control_play_icon.png +0 -0
  126. package/stat/assets/images/embed_icon.png +0 -0
  127. package/stat/assets/images/icon-home-ia.png +0 -0
  128. package/stat/assets/images/icon_OL-logo-xs.png +0 -0
  129. package/stat/assets/images/icon_alert-xs.png +0 -0
  130. package/stat/assets/images/icon_book.svg +0 -12
  131. package/stat/assets/images/icon_bookmark.svg +0 -12
  132. package/stat/assets/images/icon_close-pop.png +0 -0
  133. package/stat/assets/images/icon_download.png +0 -0
  134. package/stat/assets/images/icon_gear.svg +0 -14
  135. package/stat/assets/images/icon_hamburger.svg +0 -20
  136. package/stat/assets/images/icon_home.png +0 -0
  137. package/stat/assets/images/icon_home.svg +0 -21
  138. package/stat/assets/images/icon_home_ia.png +0 -0
  139. package/stat/assets/images/icon_indicator.png +0 -0
  140. package/stat/assets/images/icon_info.svg +0 -11
  141. package/stat/assets/images/icon_one_page.svg +0 -8
  142. package/stat/assets/images/icon_pause.svg +0 -1
  143. package/stat/assets/images/icon_play.svg +0 -1
  144. package/stat/assets/images/icon_playback-rate.svg +0 -15
  145. package/stat/assets/images/icon_return.png +0 -0
  146. package/stat/assets/images/icon_search_button.svg +0 -8
  147. package/stat/assets/images/icon_share.svg +0 -9
  148. package/stat/assets/images/icon_skip-ahead.svg +0 -6
  149. package/stat/assets/images/icon_skip-back.svg +0 -13
  150. package/stat/assets/images/icon_speaker.svg +0 -18
  151. package/stat/assets/images/icon_speaker_open.svg +0 -10
  152. package/stat/assets/images/icon_thumbnails.svg +0 -12
  153. package/stat/assets/images/icon_toc.svg +0 -5
  154. package/stat/assets/images/icon_two_pages.svg +0 -9
  155. package/stat/assets/images/icon_zoomer.png +0 -0
  156. package/stat/assets/images/loading.gif +0 -0
  157. package/stat/assets/images/logo_icon.png +0 -0
  158. package/stat/assets/images/marker_chap-off.png +0 -0
  159. package/stat/assets/images/marker_chap-off.svg +0 -11
  160. package/stat/assets/images/marker_chap-off_ia.png +0 -0
  161. package/stat/assets/images/marker_chap-on.png +0 -0
  162. package/stat/assets/images/marker_chap-on.svg +0 -11
  163. package/stat/assets/images/marker_srch-on.svg +0 -11
  164. package/stat/assets/images/marker_srchchap-off.png +0 -0
  165. package/stat/assets/images/marker_srchchap-on.png +0 -0
  166. package/stat/assets/images/nav_control-dn.png +0 -0
  167. package/stat/assets/images/nav_control-dn_ia.png +0 -0
  168. package/stat/assets/images/nav_control-up.png +0 -0
  169. package/stat/assets/images/nav_control-up_ia.png +0 -0
  170. package/stat/assets/images/nav_control.png +0 -0
  171. package/stat/assets/images/one_page_mode_icon.png +0 -0
  172. package/stat/assets/images/paper-badge.png +0 -0
  173. package/stat/assets/images/print_icon.png +0 -0
  174. package/stat/assets/images/progressbar.gif +0 -0
  175. package/stat/assets/images/right_edges.png +0 -0
  176. package/stat/assets/images/slider.png +0 -0
  177. package/stat/assets/images/slider_ia.png +0 -0
  178. package/stat/assets/images/thumbnail_mode_icon.png +0 -0
  179. package/stat/assets/images/transparent.png +0 -0
  180. package/stat/assets/images/two_page_mode_icon.png +0 -0
  181. package/stat/assets/images/zoom_in_icon.png +0 -0
  182. package/stat/assets/images/zoom_out_icon.png +0 -0
  183. package/stat/css/BookReader.scss +0 -89
  184. package/stat/css/_BRBookmarks.scss +0 -29
  185. package/stat/css/_BRComponent.scss +0 -13
  186. package/stat/css/_BRfloat.scss +0 -197
  187. package/stat/css/_BRicon.scss +0 -48
  188. package/stat/css/_BRmain.scss +0 -251
  189. package/stat/css/_BRnav.scss +0 -359
  190. package/stat/css/_BRpages.scss +0 -139
  191. package/stat/css/_BRsearch.scss +0 -226
  192. package/stat/css/_BRtoolbar.scss +0 -84
  193. package/stat/css/_BRvendor.scss +0 -5
  194. package/stat/css/_MobileNav.scss +0 -194
  195. package/stat/css/_TextSelection.scss +0 -32
  196. package/stat/css/_colorbox.scss +0 -52
  197. package/stat/css/_controls.scss +0 -253
  198. package/stat/css/_icons.scss +0 -121
  199. package/stat/jquery-wrapper.js +0 -4
  200. package/stat/plugins/plugin.archive_analytics.js +0 -86
  201. package/stat/plugins/plugin.autoplay.js +0 -129
  202. package/stat/plugins/plugin.chapters.js +0 -248
  203. package/stat/plugins/plugin.iframe.js +0 -48
  204. package/stat/plugins/plugin.mobile_nav.js +0 -288
  205. package/stat/plugins/plugin.resume.js +0 -68
  206. package/stat/plugins/plugin.text_selection.js +0 -291
  207. package/stat/plugins/plugin.url.js +0 -198
  208. package/stat/plugins/plugin.vendor-fullscreen.js +0 -247
  209. package/stat/plugins/search/plugin.search.js +0 -439
  210. package/stat/plugins/search/view.js +0 -439
  211. package/stat/plugins/tts/AbstractTTSEngine.js +0 -249
  212. package/stat/plugins/tts/FestivalTTSEngine.js +0 -169
  213. package/stat/plugins/tts/PageChunk.js +0 -107
  214. package/stat/plugins/tts/PageChunkIterator.js +0 -163
  215. package/stat/plugins/tts/WebTTSEngine.js +0 -357
  216. package/stat/plugins/tts/plugin.tts.js +0 -357
  217. package/stat/plugins/tts/tooltip_dict.js +0 -15
  218. package/stat/plugins/tts/utils.js +0 -91
  219. package/stat/util/browserSniffing.js +0 -30
  220. package/stat/util/debouncer.js +0 -26
  221. package/stat/util/docCookies.js +0 -67
  222. package/stat/util/strings.js +0 -34
@@ -1,439 +0,0 @@
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
- .on("mouseenter", (event) => {
264
- // remove from other markers then turn on just for this
265
- // XXX should be done when nav slider moves
266
- const marker = event.currentTarget;
267
- const tooltip = marker.querySelector('.BRquery');
268
- const tooltipOffset = tooltip.getBoundingClientRect();
269
- const targetOffset = marker.getBoundingClientRect();
270
- const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
271
- if (tooltipOffset.x - boxSizeAdjust < 0) {
272
- tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
273
- }
274
- $('.BRsearch,.BRchapter').removeClass('front');
275
- $(event.target).addClass('front');
276
- })
277
- .on("mouseleave", (event) => $(event.target).removeClass('front'))
278
- .on("click", function (event) {
279
- // closures are nested and deep, using an arrow function breaks references.
280
- // Todo: update to arrow function & clean up closures
281
- // to remove `bind` dependency
282
- this.br._searchPluginGoToResult(+$(event.target).data('pageIndex'));
283
- }.bind(this));
284
- });
285
- }
286
-
287
- /**
288
- * @param {boolean} bool
289
- */
290
- toggleSearchPending(bool) {
291
- if (bool) {
292
- this.br.showProgressPopup("Search results will appear below...", () => this.progressPopupClosed());
293
- }
294
- else {
295
- this.br.removeProgressPopup();
296
- }
297
- }
298
-
299
- /**
300
- * Primary callback when user cancels search popup
301
- */
302
- progressPopupClosed() {
303
- this.toggleSearchPending();
304
- this.cancelSearch();
305
- }
306
-
307
- renderErrorModal(textIsProcessing = false) {
308
- const errorDetails = `${!textIsProcessing ? 'The text may still be processing. ' : ''}Please try again.`;
309
- this.renderModalMessage(`
310
- Sorry, there was an error with your search.
311
- <br />
312
- ${errorDetails}
313
- `);
314
- this.delayModalRemovalFor(4000);
315
- }
316
-
317
- renderBookNotIndexedModal() {
318
- this.renderModalMessage(`
319
- <p>
320
- This book hasn't been indexed for searching yet.
321
- We've just started indexing it, so search should be available soon.
322
- <br />
323
- Please try again later. Thanks!
324
- </p>
325
- `);
326
- this.delayModalRemovalFor(5000);
327
- }
328
-
329
- renderResultsEmptyModal() {
330
- this.renderModalMessage('No matches were found.');
331
- this.delayModalRemovalFor(2000);
332
- }
333
-
334
- /**
335
- * @param {string} messageHTML The innerHTML string used to popupate the modal contents
336
- */
337
- renderModalMessage(messageHTML) {
338
- const modal = document.createElement('div');
339
- modal.classList.add('BRprogresspopup', 'search_modal');
340
- modal.innerHTML = messageHTML;
341
- document.querySelector(this.br.el).append(modal);
342
- }
343
-
344
- /**
345
- * @param {number} timeoutMS
346
- */
347
- delayModalRemovalFor(timeoutMS) {
348
- setTimeout(this.br.removeProgressPopup.bind(this.br), timeoutMS);
349
- }
350
-
351
- /**
352
- * @param {Event} e
353
- */
354
- submitHandler(e) {
355
- e.preventDefault();
356
- const query = e.target.querySelector('[name="query"]').value;
357
- if (!query.length) { return false; }
358
- this.br.search(query);
359
- this.emptyMatches();
360
- this.toggleSearchPending(true);
361
- return false;
362
- }
363
-
364
- /**
365
- * @param {Event} e
366
- * @param {object} properties
367
- * @param {object} properties.results
368
- * @param {object} properties.options
369
- */
370
- handleSearchCallback(e, { results, options }) {
371
- this.matches = results.matches;
372
- this.setCurrentMatchIndex();
373
- this.teardownSearchNavigation();
374
- this.renderSearchNavigation();
375
- this.bindSearchNavigationEvents();
376
- this.renderPins(results.matches);
377
- this.toggleSearchPending(false);
378
- if (options.goToFirstResult) {
379
- $(document).one('BookReader:pageChanged', () => {
380
- this.br.resize();
381
- });
382
- } else {
383
- this.br.resize();
384
- }
385
- }
386
-
387
- /**
388
- * @param {Event} e
389
- */
390
- handleNavToggledCallback(e) {
391
- const is_visible = this.br.navigationIsVisible();
392
- this.togglePinsFor(is_visible);
393
- }
394
-
395
- handleSearchStarted() {
396
- this.emptyMatches();
397
- this.br.removeSearchHilites();
398
- this.removeResultPins();
399
- this.toggleSearchPending(true);
400
- this.teardownSearchNavigation();
401
- this.setQuery(this.br.searchTerm);
402
- }
403
-
404
- /**
405
- * Event listener for: `BookReader:SearchCallbackError`
406
- * @param {CustomEvent} event
407
- */
408
- handleSearchCallbackError(event = {}) {
409
- this.toggleSearchPending(false);
410
- const isIndexed = event?.detail?.props?.results?.indexed;
411
- this.renderErrorModal(isIndexed);
412
- }
413
-
414
- handleSearchCallbackBookNotIndexed() {
415
- this.toggleSearchPending(false);
416
- this.renderBookNotIndexedModal();
417
- }
418
-
419
- handleSearchCallbackEmpty() {
420
- this.toggleSearchPending(false);
421
- this.renderResultsEmptyModal();
422
- }
423
-
424
- bindEvents() {
425
- const namespace = 'BookReader:';
426
-
427
- window.addEventListener(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this));
428
- $(document).on(`${namespace}SearchCallback`, this.handleSearchCallback.bind(this))
429
- .on(`${namespace}navToggled`, this.handleNavToggledCallback.bind(this))
430
- .on(`${namespace}SearchStarted`, this.handleSearchStarted.bind(this))
431
- .on(`${namespace}SearchCallbackBookNotIndexed`, this.handleSearchCallbackBookNotIndexed.bind(this))
432
- .on(`${namespace}SearchCallbackEmpty`, this.handleSearchCallbackEmpty.bind(this))
433
- .on(`${namespace}pageChanged`, this.updateSearchNavigation.bind(this));
434
-
435
- this.dom.toolbarSearch.querySelector('form').addEventListener('submit', this.submitHandler.bind(this));
436
- }
437
- }
438
-
439
- export default SearchView;
@@ -1,249 +0,0 @@
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
- * @property {SpeechSynthesisVoice => void} setVoice
32
- **/
33
-
34
- /** Handling bookreader's text-to-speech */
35
- export default class AbstractTTSEngine {
36
- /**
37
- * @protected
38
- * @param {TTSEngineOptions} options
39
- */
40
- constructor(options) {
41
- this.playing = false;
42
- this.paused = false;
43
- this.opts = options;
44
- /** @type {PageChunkIterator} */
45
- this._chunkIterator = null;
46
- /** @type {AbstractTTSSound} */
47
- this.activeSound = null;
48
- this.playbackRate = 1;
49
- /** Events we can bind to */
50
- this.events = $({});
51
- /** @type {SpeechSynthesisVoice} */
52
- this.voice = null;
53
- // Listen for voice changes (fired by subclasses)
54
- this.events.on('voiceschanged', () => {
55
- this.voice = AbstractTTSEngine.getBestBookVoice(this.getVoices(), this.opts.bookLanguage);
56
- });
57
- this.events.trigger('voiceschanged');
58
- }
59
-
60
- /**
61
- * @abstract
62
- * @return {boolean}
63
- */
64
- static isSupported() { throw new Error("Unimplemented abstract class"); }
65
-
66
- /**
67
- * @abstract
68
- * @return {SpeechSynthesisVoice[]}
69
- */
70
- getVoices() { throw new Error("Unimplemented abstract class"); }
71
-
72
- /** @abstract */
73
- init() { return null; }
74
-
75
- /**
76
- * @param {number} leafIndex
77
- * @param {number} numLeafs total number of leafs in the current book
78
- */
79
- start(leafIndex, numLeafs) {
80
- this.playing = true;
81
- this.opts.onLoadingStart();
82
-
83
- this._chunkIterator = new PageChunkIterator(numLeafs, leafIndex, {
84
- server: this.opts.server,
85
- bookPath: this.opts.bookPath,
86
- pageBufferSize: 5,
87
- });
88
-
89
- this.step();
90
- this.events.trigger('start');
91
- }
92
-
93
- stop() {
94
- if (this.activeSound) this.activeSound.stop();
95
- this.playing = false;
96
- this._chunkIterator = null;
97
- this.activeSound = null;
98
- this.events.trigger('stop');
99
- }
100
-
101
- /** @public */
102
- pause() {
103
- const fireEvent = !this.paused && this.activeSound;
104
- this.paused = true;
105
- if (this.activeSound) this.activeSound.pause();
106
- if (fireEvent) this.events.trigger('pause');
107
- }
108
-
109
- /** @public */
110
- resume() {
111
- const fireEvent = this.paused && this.activeSound;
112
- this.paused = false;
113
- if (this.activeSound) this.activeSound.resume();
114
- if (fireEvent) this.events.trigger('resume');
115
- }
116
-
117
- togglePlayPause() {
118
- if (this.paused) this.resume();
119
- else this.pause();
120
- }
121
-
122
- /** @public */
123
- jumpForward() {
124
- if (this.activeSound) this.activeSound.finish();
125
- }
126
-
127
- /** @public */
128
- jumpBackward() {
129
- Promise.all([
130
- this.activeSound.stop(),
131
- this._chunkIterator.decrement()
132
- .then(() => this._chunkIterator.decrement())
133
- ])
134
- .then(() => this.step());
135
- }
136
-
137
- /** @param {number} newRate */
138
- setVoice(voiceURI) {
139
- this.voice = this.getVoices().find(voice => voice.voiceURI === voiceURI);
140
- if (this.activeSound) this.activeSound.setVoice(this.voice);
141
- }
142
-
143
- /** @param {number} newRate */
144
- setPlaybackRate(newRate) {
145
- this.playbackRate = newRate;
146
- if (this.activeSound) this.activeSound.setPlaybackRate(newRate);
147
- }
148
-
149
- /** @private */
150
- step() {
151
- this._chunkIterator.next()
152
- .then(chunk => {
153
- if (chunk == PageChunkIterator.AT_END) {
154
- this.stop();
155
- this.opts.onDone();
156
- return;
157
- }
158
-
159
- this.opts.onLoadingStart();
160
- const sound = this.createSound(chunk);
161
- sound.chunk = chunk;
162
- sound.rate = this.playbackRate;
163
- sound.voice = this.voice;
164
- sound.load(() => this.opts.onLoadingComplete());
165
-
166
- this.opts.onLoadingComplete();
167
- return this.opts.beforeChunkPlay(chunk).then(() => sound);
168
- })
169
- .then(sound => {
170
- if (!this.playing) return;
171
-
172
- const playPromise = this.playSound(sound)
173
- .then(() => this.opts.afterChunkPlay(sound.chunk));
174
- if (this.paused) this.pause();
175
- return playPromise;
176
- })
177
- .then(() => {
178
- if (this.playing) return this.step();
179
- });
180
- }
181
-
182
- /**
183
- * @abstract
184
- * @param {PageChunk} chunk
185
- * @return {AbstractTTSSound}
186
- */
187
- createSound(chunk) { throw new Error("Unimplemented abstract class"); }
188
-
189
- /**
190
- * @param {AbstractTTSSound} sound
191
- * @return {PromiseLike} promise called once playing finished
192
- */
193
- playSound(sound) {
194
- this.activeSound = sound;
195
- if (!this.activeSound.loaded) this.opts.onLoadingStart();
196
- return this.activeSound.play();
197
- }
198
-
199
- /** Convenience wrapper for {@see AbstractTTSEngine.getBestVoice} */
200
- getBestVoice() {
201
- return AbstractTTSEngine.getBestBookVoice(this.getVoices(), this.opts.bookLanguage);
202
- }
203
-
204
- /**
205
- * @private
206
- * Find the best voice to use given the available voices, the book language, and the user's
207
- * languages.
208
- * @param {SpeechSynthesisVoice[]} voices
209
- * @param {ISO6391} bookLanguage
210
- * @param {string[]} userLanguages languages in BCP47 format (e.g. en-US). Ordered by preference.
211
- * @return {SpeechSynthesisVoice | undefined}
212
- */
213
- static getBestBookVoice(voices, bookLanguage, userLanguages = navigator.languages) {
214
- const bookLangVoices = voices.filter(v => v.lang.startsWith(bookLanguage));
215
- // navigator.languages browser support isn't great yet, so get just 1 language otherwise
216
- // Sample navigator.languages: ["en-CA", "fr-CA", "fr", "en-US", "en", "de-DE", "de"]
217
- userLanguages = userLanguages || (navigator.language ? [navigator.language] : []);
218
-
219
- // user languages that match the book language
220
- const matchingUserLangs = userLanguages.filter(lang => lang.startsWith(bookLanguage));
221
-
222
- // Try to find voices that intersect these two sets
223
- return AbstractTTSEngine.getMatchingVoice(matchingUserLangs, bookLangVoices) ||
224
- // no user languages match the books; let's return the best voice for the book language
225
- (bookLangVoices.find(v => v.default) || bookLangVoices[0])
226
- // No voices match the book language? let's find a voice in the user's language
227
- // and ignore book lang
228
- || AbstractTTSEngine.getMatchingVoice(userLanguages, voices)
229
- // C'mon! Ok, just read with whatever we got!
230
- || (voices.find(v => v.default) || voices[0]);
231
- }
232
-
233
- /**
234
- * @private
235
- * Get the best voice that matches one of the BCP47 languages (order by preference)
236
- * @param {string[]} languages in BCP 47 format (e.g. 'en-US', or 'en'); ordered by preference
237
- * @param {SpeechSynthesisVoice[]} voices voices to choose from
238
- * @return {SpeechSynthesisVoice | undefined}
239
- */
240
- static getMatchingVoice(languages, voices) {
241
- for (const lang of languages) {
242
- // Chrome Android was returning voice languages like `en_US` instead of `en-US`
243
- const matchingVoices = voices.filter(v => v.lang.replace('_', '-').startsWith(lang));
244
- if (matchingVoices.length) {
245
- return matchingVoices.find(v => v.default) || matchingVoices[0];
246
- }
247
- }
248
- }
249
- }