@internetarchive/bookreader 5.0.0-28-remove-url-defaults → 5.0.0-29

Sign up to get free protection for your applications and to get access to all the features.
Files changed (226) hide show
  1. package/.husky/_/husky.sh +30 -0
  2. package/BookReader/BookReader.css +1 -1
  3. package/BookReader/BookReader.js +1 -1
  4. package/BookReader/BookReader.js.map +1 -1
  5. package/BookReader/bookreader-component-bundle.js +570 -542
  6. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +23 -0
  7. package/BookReader/bookreader-component-bundle.js.map +1 -1
  8. package/BookReader/plugins/plugin.search.js +1 -1
  9. package/BookReader/plugins/plugin.search.js.map +1 -1
  10. package/BookReaderDemo/BookReaderDemo.css +14 -1
  11. package/BookReaderDemo/IADemoBr.js +104 -0
  12. package/BookReaderDemo/demo-internetarchive.html +65 -98
  13. package/CHANGELOG.md +4 -0
  14. package/package.json +9 -6
  15. package/src/BookNavigator/assets/ia-logo.js +17 -0
  16. package/src/BookNavigator/book-navigator.js +521 -0
  17. package/src/BookNavigator/bookmarks/bookmark-button.js +2 -1
  18. package/src/BookNavigator/bookmarks/bookmarks-provider.js +20 -8
  19. package/src/BookNavigator/bookmarks/ia-bookmarks.js +84 -51
  20. package/src/BookNavigator/downloads/downloads-provider.js +5 -9
  21. package/src/BookNavigator/downloads/downloads.js +1 -0
  22. package/src/BookNavigator/search/search-provider.js +15 -8
  23. package/src/BookNavigator/sharing.js +27 -0
  24. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +9 -8
  25. package/src/BookNavigator/volumes/volumes-provider.js +3 -4
  26. package/src/BookReader/options.js +6 -0
  27. package/src/BookReader.js +20 -8
  28. package/src/BookReaderComponent/BookReaderComponent.js +53 -32
  29. package/src/css/_BRComponent.scss +1 -1
  30. package/src/plugins/search/plugin.search.js +9 -9
  31. package/{src → stat}/BookNavigator/BookModel.js +0 -0
  32. package/{src → stat}/BookNavigator/BookNavigator.js +109 -102
  33. package/stat/BookNavigator/assets/bookmark-colors.js +15 -0
  34. package/stat/BookNavigator/assets/button-base.js +61 -0
  35. package/stat/BookNavigator/assets/ia-logo.js +17 -0
  36. package/stat/BookNavigator/assets/icon_checkmark.js +6 -0
  37. package/stat/BookNavigator/assets/icon_close.js +3 -0
  38. package/stat/BookNavigator/assets/icon_sort_asc.js +5 -0
  39. package/stat/BookNavigator/assets/icon_sort_desc.js +5 -0
  40. package/stat/BookNavigator/assets/icon_sort_neutral.js +5 -0
  41. package/stat/BookNavigator/assets/icon_volumes.js +11 -0
  42. package/stat/BookNavigator/bookmarks/bookmark-button.js +64 -0
  43. package/stat/BookNavigator/bookmarks/bookmark-edit.js +215 -0
  44. package/stat/BookNavigator/bookmarks/bookmarks-list.js +285 -0
  45. package/stat/BookNavigator/bookmarks/bookmarks-loginCTA.js +28 -0
  46. package/stat/BookNavigator/bookmarks/bookmarks-provider.js +56 -0
  47. package/stat/BookNavigator/bookmarks/ia-bookmarks.js +523 -0
  48. package/{src → stat}/BookNavigator/br-fullscreen-mgr.js +1 -2
  49. package/stat/BookNavigator/delete-modal-actions.js +49 -0
  50. package/stat/BookNavigator/downloads/downloads-provider.js +72 -0
  51. package/stat/BookNavigator/downloads/downloads.js +139 -0
  52. package/stat/BookNavigator/provider-config.js +0 -0
  53. package/stat/BookNavigator/search/a-search-result.js +55 -0
  54. package/stat/BookNavigator/search/search-provider.js +180 -0
  55. package/stat/BookNavigator/search/search-results.js +360 -0
  56. package/{src/ItemNavigator/providers → stat/BookNavigator}/sharing.js +3 -5
  57. package/stat/BookNavigator/visual-adjustments/visual-adjustments-provider.js +94 -0
  58. package/stat/BookNavigator/visual-adjustments/visual-adjustments.js +280 -0
  59. package/stat/BookNavigator/volumes/volumes-provider.js +83 -0
  60. package/stat/BookNavigator/volumes/volumes.js +178 -0
  61. package/stat/BookReader/BookModel.js +518 -0
  62. package/stat/BookReader/DebugConsole.js +54 -0
  63. package/stat/BookReader/DragScrollable.js +233 -0
  64. package/stat/BookReader/ImageCache.js +116 -0
  65. package/stat/BookReader/Mode1Up.js +102 -0
  66. package/stat/BookReader/Mode1UpLit.js +434 -0
  67. package/stat/BookReader/Mode2Up.js +1372 -0
  68. package/stat/BookReader/ModeSmoothZoom.js +177 -0
  69. package/stat/BookReader/ModeThumb.js +344 -0
  70. package/stat/BookReader/Navbar/Navbar.js +310 -0
  71. package/stat/BookReader/PageContainer.js +120 -0
  72. package/stat/BookReader/ReduceSet.js +26 -0
  73. package/stat/BookReader/Toolbar/Toolbar.js +384 -0
  74. package/stat/BookReader/events.js +20 -0
  75. package/stat/BookReader/options.js +324 -0
  76. package/stat/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  77. package/stat/BookReader/utils/classes.js +36 -0
  78. package/stat/BookReader/utils.js +240 -0
  79. package/stat/BookReader.js +2550 -0
  80. package/stat/BookReaderComponent/BookReaderComponent.js +117 -0
  81. package/stat/assets/icons/1up.svg +12 -0
  82. package/stat/assets/icons/2up.svg +15 -0
  83. package/stat/assets/icons/advance.svg +26 -0
  84. package/stat/assets/icons/chevron-right.svg +1 -0
  85. package/stat/assets/icons/close-circle-dark.svg +1 -0
  86. package/stat/assets/icons/close-circle.svg +1 -0
  87. package/stat/assets/icons/fullscreen.svg +17 -0
  88. package/stat/assets/icons/fullscreen_exit.svg +17 -0
  89. package/stat/assets/icons/hamburger.svg +15 -0
  90. package/stat/assets/icons/left-arrow.svg +12 -0
  91. package/stat/assets/icons/magnify-minus.svg +16 -0
  92. package/stat/assets/icons/magnify-plus.svg +17 -0
  93. package/stat/assets/icons/magnify.svg +15 -0
  94. package/stat/assets/icons/pause.svg +23 -0
  95. package/stat/assets/icons/play.svg +22 -0
  96. package/stat/assets/icons/playback-speed.svg +34 -0
  97. package/stat/assets/icons/read-aloud.svg +22 -0
  98. package/stat/assets/icons/review.svg +22 -0
  99. package/stat/assets/icons/thumbnails.svg +17 -0
  100. package/stat/assets/icons/voice.svg +1 -0
  101. package/stat/assets/icons/volume-full.svg +22 -0
  102. package/stat/assets/images/BRicons.png +0 -0
  103. package/stat/assets/images/BRicons.svg +94 -0
  104. package/stat/assets/images/BRicons_ia.png +0 -0
  105. package/stat/assets/images/back_pages.png +0 -0
  106. package/stat/assets/images/book_bottom_icon.png +0 -0
  107. package/stat/assets/images/book_down_icon.png +0 -0
  108. package/stat/assets/images/book_left_icon.png +0 -0
  109. package/stat/assets/images/book_leftmost_icon.png +0 -0
  110. package/stat/assets/images/book_right_icon.png +0 -0
  111. package/stat/assets/images/book_rightmost_icon.png +0 -0
  112. package/stat/assets/images/book_top_icon.png +0 -0
  113. package/stat/assets/images/book_up_icon.png +0 -0
  114. package/stat/assets/images/books_graphic.svg +177 -0
  115. package/stat/assets/images/booksplit.png +0 -0
  116. package/stat/assets/images/control_pause_icon.png +0 -0
  117. package/stat/assets/images/control_play_icon.png +0 -0
  118. package/stat/assets/images/embed_icon.png +0 -0
  119. package/stat/assets/images/icon-home-ia.png +0 -0
  120. package/stat/assets/images/icon_OL-logo-xs.png +0 -0
  121. package/stat/assets/images/icon_alert-xs.png +0 -0
  122. package/stat/assets/images/icon_book.svg +12 -0
  123. package/stat/assets/images/icon_bookmark.svg +12 -0
  124. package/stat/assets/images/icon_close-pop.png +0 -0
  125. package/stat/assets/images/icon_download.png +0 -0
  126. package/stat/assets/images/icon_gear.svg +14 -0
  127. package/stat/assets/images/icon_hamburger.svg +20 -0
  128. package/stat/assets/images/icon_home.png +0 -0
  129. package/stat/assets/images/icon_home.svg +21 -0
  130. package/stat/assets/images/icon_home_ia.png +0 -0
  131. package/stat/assets/images/icon_indicator.png +0 -0
  132. package/stat/assets/images/icon_info.svg +11 -0
  133. package/stat/assets/images/icon_one_page.svg +8 -0
  134. package/stat/assets/images/icon_pause.svg +1 -0
  135. package/stat/assets/images/icon_play.svg +1 -0
  136. package/stat/assets/images/icon_playback-rate.svg +15 -0
  137. package/stat/assets/images/icon_return.png +0 -0
  138. package/stat/assets/images/icon_search_button.svg +8 -0
  139. package/stat/assets/images/icon_share.svg +9 -0
  140. package/stat/assets/images/icon_skip-ahead.svg +6 -0
  141. package/stat/assets/images/icon_skip-back.svg +13 -0
  142. package/stat/assets/images/icon_speaker.svg +18 -0
  143. package/stat/assets/images/icon_speaker_open.svg +10 -0
  144. package/stat/assets/images/icon_thumbnails.svg +12 -0
  145. package/stat/assets/images/icon_toc.svg +5 -0
  146. package/stat/assets/images/icon_two_pages.svg +9 -0
  147. package/stat/assets/images/icon_zoomer.png +0 -0
  148. package/stat/assets/images/loading.gif +0 -0
  149. package/stat/assets/images/logo_icon.png +0 -0
  150. package/stat/assets/images/marker_chap-off.png +0 -0
  151. package/stat/assets/images/marker_chap-off.svg +11 -0
  152. package/stat/assets/images/marker_chap-off_ia.png +0 -0
  153. package/stat/assets/images/marker_chap-on.png +0 -0
  154. package/stat/assets/images/marker_chap-on.svg +11 -0
  155. package/stat/assets/images/marker_srch-on.svg +11 -0
  156. package/stat/assets/images/marker_srchchap-off.png +0 -0
  157. package/stat/assets/images/marker_srchchap-on.png +0 -0
  158. package/stat/assets/images/nav_control-dn.png +0 -0
  159. package/stat/assets/images/nav_control-dn_ia.png +0 -0
  160. package/stat/assets/images/nav_control-up.png +0 -0
  161. package/stat/assets/images/nav_control-up_ia.png +0 -0
  162. package/stat/assets/images/nav_control.png +0 -0
  163. package/stat/assets/images/one_page_mode_icon.png +0 -0
  164. package/stat/assets/images/paper-badge.png +0 -0
  165. package/stat/assets/images/print_icon.png +0 -0
  166. package/stat/assets/images/progressbar.gif +0 -0
  167. package/stat/assets/images/right_edges.png +0 -0
  168. package/stat/assets/images/slider.png +0 -0
  169. package/stat/assets/images/slider_ia.png +0 -0
  170. package/stat/assets/images/thumbnail_mode_icon.png +0 -0
  171. package/stat/assets/images/transparent.png +0 -0
  172. package/stat/assets/images/two_page_mode_icon.png +0 -0
  173. package/stat/assets/images/zoom_in_icon.png +0 -0
  174. package/stat/assets/images/zoom_out_icon.png +0 -0
  175. package/stat/css/BookReader.scss +89 -0
  176. package/stat/css/_BRBookmarks.scss +29 -0
  177. package/stat/css/_BRComponent.scss +13 -0
  178. package/stat/css/_BRfloat.scss +197 -0
  179. package/stat/css/_BRicon.scss +48 -0
  180. package/stat/css/_BRmain.scss +251 -0
  181. package/stat/css/_BRnav.scss +359 -0
  182. package/stat/css/_BRpages.scss +139 -0
  183. package/stat/css/_BRsearch.scss +226 -0
  184. package/stat/css/_BRtoolbar.scss +84 -0
  185. package/stat/css/_BRvendor.scss +5 -0
  186. package/stat/css/_MobileNav.scss +194 -0
  187. package/stat/css/_TextSelection.scss +32 -0
  188. package/stat/css/_colorbox.scss +52 -0
  189. package/stat/css/_controls.scss +253 -0
  190. package/stat/css/_icons.scss +121 -0
  191. package/stat/jquery-wrapper.js +4 -0
  192. package/stat/plugins/plugin.archive_analytics.js +86 -0
  193. package/stat/plugins/plugin.autoplay.js +129 -0
  194. package/stat/plugins/plugin.chapters.js +248 -0
  195. package/stat/plugins/plugin.iframe.js +48 -0
  196. package/stat/plugins/plugin.mobile_nav.js +288 -0
  197. package/stat/plugins/plugin.resume.js +68 -0
  198. package/stat/plugins/plugin.text_selection.js +291 -0
  199. package/stat/plugins/plugin.url.js +198 -0
  200. package/stat/plugins/plugin.vendor-fullscreen.js +247 -0
  201. package/stat/plugins/search/plugin.search.js +439 -0
  202. package/stat/plugins/search/view.js +439 -0
  203. package/stat/plugins/tts/AbstractTTSEngine.js +249 -0
  204. package/stat/plugins/tts/FestivalTTSEngine.js +169 -0
  205. package/stat/plugins/tts/PageChunk.js +107 -0
  206. package/stat/plugins/tts/PageChunkIterator.js +163 -0
  207. package/stat/plugins/tts/WebTTSEngine.js +357 -0
  208. package/stat/plugins/tts/plugin.tts.js +357 -0
  209. package/stat/plugins/tts/tooltip_dict.js +15 -0
  210. package/stat/plugins/tts/utils.js +91 -0
  211. package/stat/util/browserSniffing.js +30 -0
  212. package/stat/util/debouncer.js +26 -0
  213. package/stat/util/docCookies.js +67 -0
  214. package/stat/util/strings.js +34 -0
  215. package/tests/e2e/viewmode.test.js +30 -30
  216. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +64 -52
  217. package/tests/karma/BookNavigator/book-navigator.test.js +413 -108
  218. package/tests/karma/BookNavigator/bookmarks/bookmark-button.test.js +44 -0
  219. package/tests/karma/BookNavigator/downloads/downloads-provider.test.js +6 -3
  220. package/tests/karma/BookNavigator/search/search-provider.test.js +106 -6
  221. package/tests/karma/BookNavigator/search/search-results.test.js +0 -2
  222. package/tests/karma/BookNavigator/sharing/sharing-provider.test.js +29 -20
  223. package/tests/karma/BookNavigator/volumes/volumes-provider.test.js +41 -17
  224. package/.nvmrc +0 -1
  225. package/src/BookNavigator/assets/book-loader.js +0 -27
  226. package/src/ItemNavigator/ItemNavigator.js +0 -377
@@ -0,0 +1,439 @@
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;
@@ -0,0 +1,249 @@
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
+ }