@internetarchive/bookreader 5.0.0-4 → 5.0.0-40-a1

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 (228) hide show
  1. package/.eslintrc.js +17 -15
  2. package/.github/workflows/node.js.yml +75 -4
  3. package/.github/workflows/npm-publish.yml +2 -16
  4. package/.testcaferc.js +10 -0
  5. package/BookReader/BookReader.css +83 -323
  6. package/BookReader/BookReader.js +1 -1
  7. package/BookReader/BookReader.js.LICENSE.txt +24 -0
  8. package/BookReader/BookReader.js.map +1 -1
  9. package/BookReader/ia-bookreader-bundle.js +1623 -0
  10. package/BookReader/{bookreader-component-bundle.js.LICENSE.txt → ia-bookreader-bundle.js.LICENSE.txt} +14 -10
  11. package/BookReader/ia-bookreader-bundle.js.map +1 -0
  12. package/BookReader/icons/close-circle-dark.svg +1 -0
  13. package/BookReader/icons/magnify-minus.svg +1 -1
  14. package/BookReader/icons/magnify-plus.svg +1 -1
  15. package/BookReader/icons/voice.svg +1 -0
  16. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  17. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  18. package/BookReader/plugins/plugin.autoplay.js +1 -1
  19. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  20. package/BookReader/plugins/plugin.chapters.js +1 -1
  21. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  22. package/BookReader/plugins/plugin.iframe.js +1 -1
  23. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  24. package/BookReader/plugins/plugin.mobile_nav.js +1 -1
  25. package/BookReader/plugins/plugin.mobile_nav.js.map +1 -1
  26. package/BookReader/plugins/plugin.resume.js +1 -1
  27. package/BookReader/plugins/plugin.resume.js.map +1 -1
  28. package/BookReader/plugins/plugin.search.js +1 -1
  29. package/BookReader/plugins/plugin.search.js.map +1 -1
  30. package/BookReader/plugins/plugin.text_selection.js +1 -1
  31. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  32. package/BookReader/plugins/plugin.tts.js +1 -1
  33. package/BookReader/plugins/plugin.tts.js.map +1 -1
  34. package/BookReader/plugins/plugin.url.js +1 -1
  35. package/BookReader/plugins/plugin.url.js.map +1 -1
  36. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  37. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  38. package/BookReader/webcomponents-bundle.js +3 -0
  39. package/BookReader/webcomponents-bundle.js.LICENSE.txt +9 -0
  40. package/BookReader/webcomponents-bundle.js.map +1 -0
  41. package/BookReaderDemo/BookReaderDemo.css +14 -1
  42. package/BookReaderDemo/IADemoBr.js +120 -0
  43. package/BookReaderDemo/demo-advanced.html +1 -1
  44. package/BookReaderDemo/demo-autoplay.html +1 -0
  45. package/BookReaderDemo/demo-embed-iframe-src.html +1 -0
  46. package/BookReaderDemo/demo-fullscreen-mobile.html +1 -0
  47. package/BookReaderDemo/demo-fullscreen.html +1 -0
  48. package/BookReaderDemo/demo-iiif.html +1 -0
  49. package/BookReaderDemo/demo-internetarchive.html +74 -17
  50. package/BookReaderDemo/demo-multiple.html +1 -0
  51. package/BookReaderDemo/demo-preview-pages.html +1 -0
  52. package/BookReaderDemo/demo-simple.html +1 -0
  53. package/BookReaderDemo/demo-vendor-fullscreen.html +1 -0
  54. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  55. package/BookReaderDemo/immersion-1up.html +1 -0
  56. package/BookReaderDemo/immersion-mode.html +1 -0
  57. package/BookReaderDemo/toggle_controls.html +1 -0
  58. package/BookReaderDemo/view_mode.html +1 -0
  59. package/BookReaderDemo/viewmode-cycle.html +1 -2
  60. package/CHANGELOG.md +166 -0
  61. package/README.md +14 -1
  62. package/babel.config.js +18 -0
  63. package/codecov.yml +6 -0
  64. package/index.html +3 -0
  65. package/jsconfig.json +19 -0
  66. package/package.json +62 -47
  67. package/renovate.json +43 -0
  68. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  69. package/src/BookNavigator/assets/button-base.js +9 -2
  70. package/src/BookNavigator/assets/ia-logo.js +17 -0
  71. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  72. package/src/BookNavigator/assets/icon_close.js +1 -1
  73. package/src/BookNavigator/assets/icon_sort_asc.js +5 -0
  74. package/src/BookNavigator/assets/icon_sort_desc.js +5 -0
  75. package/src/BookNavigator/assets/icon_sort_neutral.js +5 -0
  76. package/src/BookNavigator/assets/icon_volumes.js +11 -0
  77. package/src/BookNavigator/book-navigator.js +556 -0
  78. package/src/BookNavigator/bookmarks/bookmark-button.js +3 -2
  79. package/src/BookNavigator/bookmarks/bookmark-edit.js +4 -4
  80. package/src/BookNavigator/bookmarks/bookmarks-list.js +3 -3
  81. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +3 -8
  82. package/src/BookNavigator/bookmarks/bookmarks-provider.js +23 -12
  83. package/src/BookNavigator/bookmarks/ia-bookmarks.js +98 -62
  84. package/src/BookNavigator/delete-modal-actions.js +1 -1
  85. package/src/BookNavigator/downloads/downloads-provider.js +23 -17
  86. package/src/BookNavigator/downloads/downloads.js +17 -25
  87. package/src/BookNavigator/search/a-search-result.js +3 -3
  88. package/src/BookNavigator/search/search-provider.js +57 -24
  89. package/src/BookNavigator/search/search-results.js +8 -20
  90. package/src/BookNavigator/sharing.js +27 -0
  91. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +11 -13
  92. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +4 -3
  93. package/src/BookNavigator/volumes/volumes-provider.js +114 -0
  94. package/src/BookNavigator/volumes/volumes.js +188 -0
  95. package/src/BookReader/DebugConsole.js +3 -3
  96. package/src/BookReader/DragScrollable.js +233 -0
  97. package/src/BookReader/Mode1Up.js +51 -351
  98. package/src/BookReader/Mode1UpLit.js +441 -0
  99. package/src/BookReader/Mode2Up.js +104 -71
  100. package/src/BookReader/ModeSmoothZoom.js +179 -0
  101. package/src/BookReader/ModeThumb.js +16 -8
  102. package/src/BookReader/Navbar/Navbar.js +2 -31
  103. package/src/BookReader/PageContainer.js +57 -6
  104. package/src/BookReader/ReduceSet.js +1 -1
  105. package/src/BookReader/Toolbar/Toolbar.js +7 -7
  106. package/src/BookReader/options.js +10 -0
  107. package/src/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  108. package/src/BookReader/utils/ScrollClassAdder.js +31 -0
  109. package/src/BookReader/utils.js +68 -13
  110. package/src/BookReader.js +375 -289
  111. package/src/assets/icons/close-circle-dark.svg +1 -0
  112. package/src/assets/icons/magnify-minus.svg +3 -7
  113. package/src/assets/icons/magnify-plus.svg +3 -7
  114. package/src/assets/icons/voice.svg +1 -0
  115. package/src/css/BookReader.scss +0 -12
  116. package/src/css/_BRComponent.scss +1 -1
  117. package/src/css/_BRmain.scss +19 -24
  118. package/src/css/_BRnav.scss +4 -26
  119. package/src/css/_BRpages.scss +35 -0
  120. package/src/css/_BRsearch.scss +11 -215
  121. package/src/css/_TextSelection.scss +14 -17
  122. package/src/css/_colorbox.scss +2 -2
  123. package/src/css/_controls.scss +16 -3
  124. package/src/css/_icons.scss +6 -0
  125. package/src/ia-bookreader/ia-bookreader.js +224 -0
  126. package/src/plugins/plugin.chapters.js +26 -33
  127. package/src/plugins/plugin.mobile_nav.js +11 -10
  128. package/src/plugins/plugin.resume.js +3 -3
  129. package/src/plugins/plugin.text_selection.js +26 -39
  130. package/src/plugins/plugin.vendor-fullscreen.js +4 -4
  131. package/src/plugins/search/plugin.search.js +106 -107
  132. package/src/plugins/search/view.js +50 -163
  133. package/src/plugins/tts/AbstractTTSEngine.js +46 -37
  134. package/src/plugins/tts/FestivalTTSEngine.js +12 -13
  135. package/src/plugins/tts/PageChunk.js +15 -21
  136. package/src/plugins/tts/PageChunkIterator.js +8 -12
  137. package/src/plugins/tts/WebTTSEngine.js +64 -68
  138. package/src/plugins/tts/plugin.tts.js +79 -108
  139. package/src/plugins/url/UrlPlugin.js +184 -0
  140. package/src/plugins/{plugin.url.js → url/plugin.url.js} +28 -6
  141. package/tests/e2e/README.md +37 -0
  142. package/tests/e2e/autoplay.test.js +2 -2
  143. package/tests/e2e/base.test.js +7 -7
  144. package/tests/e2e/helpers/base.js +8 -3
  145. package/tests/e2e/helpers/debug.js +1 -1
  146. package/tests/e2e/helpers/desktopSearch.js +14 -13
  147. package/tests/e2e/helpers/mobileSearch.js +3 -3
  148. package/tests/e2e/helpers/params.js +17 -0
  149. package/tests/e2e/models/Navigation.js +12 -3
  150. package/tests/e2e/rightToLeft.test.js +4 -5
  151. package/tests/e2e/viewmode.test.js +38 -33
  152. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +3 -3
  153. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +176 -0
  154. package/tests/{BookReader → jest/BookReader}/DebugConsole.test.js +1 -1
  155. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +4 -4
  156. package/tests/jest/BookReader/Mode1UpLit.test.js +88 -0
  157. package/tests/{BookReader → jest/BookReader}/Mode2Up.test.js +5 -7
  158. package/tests/jest/BookReader/ModeSmoothZoom.test.js +149 -0
  159. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  160. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +7 -7
  161. package/tests/{BookReader → jest/BookReader}/PageContainer.test.js +79 -6
  162. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +1 -1
  163. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +2 -2
  164. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  165. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  166. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +1 -1
  167. package/tests/jest/BookReader/utils.test.js +136 -0
  168. package/tests/jest/BookReader.keyboard.test.js +190 -0
  169. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +9 -1
  170. package/tests/{BookReader.test.js → jest/BookReader.test.js} +20 -4
  171. package/tests/{plugins → jest/plugins}/plugin.archive_analytics.test.js +2 -2
  172. package/tests/{plugins → jest/plugins}/plugin.autoplay.test.js +2 -2
  173. package/tests/{plugins → jest/plugins}/plugin.chapters.test.js +8 -8
  174. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +2 -2
  175. package/tests/{plugins → jest/plugins}/plugin.mobile_nav.test.js +5 -5
  176. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +3 -3
  177. package/tests/{plugins → jest/plugins}/plugin.text_selection.test.js +39 -47
  178. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +2 -2
  179. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +24 -25
  180. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +6 -6
  181. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +6 -6
  182. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +4 -4
  183. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +1 -1
  184. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +3 -3
  185. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +1 -1
  186. package/tests/{plugins → jest/plugins}/tts/utils.test.js +3 -3
  187. package/tests/jest/plugins/url/UrlPlugin.test.js +190 -0
  188. package/tests/{plugins → jest/plugins/url}/plugin.url.test.js +33 -14
  189. package/tests/{util → jest/util}/browserSniffing.test.js +1 -1
  190. package/tests/{util → jest/util}/docCookies.test.js +1 -1
  191. package/tests/{util → jest/util}/strings.test.js +1 -1
  192. package/tests/{utils.js → jest/utils.js} +38 -0
  193. package/tests/karma/BookNavigator/book-navigator.test.js +501 -0
  194. package/tests/karma/BookNavigator/bookmarks/bookmark-button.test.js +44 -0
  195. package/tests/karma/BookNavigator/bookmarks/bookmark-edit.test.js +1 -3
  196. package/tests/karma/BookNavigator/bookmarks/bookmarks-list.test.js +3 -4
  197. package/tests/karma/BookNavigator/bookmarks/ia-bookmarks.test.js +57 -0
  198. package/tests/karma/BookNavigator/downloads/downloads-provider.test.js +67 -0
  199. package/tests/karma/BookNavigator/downloads/downloads.test.js +54 -0
  200. package/tests/karma/BookNavigator/search/search-provider.test.js +123 -0
  201. package/tests/karma/BookNavigator/{search-results.test.js → search/search-results.test.js} +1 -4
  202. package/tests/karma/BookNavigator/sharing/sharing-provider.test.js +49 -0
  203. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -2
  204. package/tests/karma/BookNavigator/volumes/volumes-provider.test.js +184 -0
  205. package/tests/karma/BookNavigator/volumes/volumes.test.js +98 -0
  206. package/webpack.config.js +10 -4
  207. package/.babelrc +0 -12
  208. package/.dependabot/config.yml +0 -6
  209. package/.testcaferc.json +0 -5
  210. package/BookReader/bookreader-component-bundle.js +0 -1450
  211. package/BookReader/bookreader-component-bundle.js.map +0 -1
  212. package/BookReader/plugins/plugin.menu_toggle.js +0 -2
  213. package/BookReader/plugins/plugin.menu_toggle.js.map +0 -1
  214. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
  215. package/BookReaderDemo/demo-plugin-menu-toggle.html +0 -34
  216. package/src/BookNavigator/BookModel.js +0 -14
  217. package/src/BookNavigator/BookNavigator.js +0 -435
  218. package/src/BookNavigator/assets/book-loader.js +0 -27
  219. package/src/BookNavigator/br-fullscreen-mgr.js +0 -83
  220. package/src/BookReaderComponent/BookReaderComponent.js +0 -112
  221. package/src/ItemNavigator/ItemNavigator.js +0 -372
  222. package/src/ItemNavigator/providers/sharing.js +0 -29
  223. package/src/dragscrollable-br.js +0 -261
  224. package/src/plugins/menu_toggle/plugin.menu_toggle.js +0 -324
  225. package/tests/BookReader/BookReaderPublicFunctions.test.js +0 -171
  226. package/tests/BookReader/Mode1Up.test.js +0 -164
  227. package/tests/BookReader/utils.test.js +0 -109
  228. package/tests/plugins/menu_toggle/plugin.menu_toggle.test.js +0 -68
@@ -20,8 +20,13 @@
20
20
  * the book has not had OCR text indexed yet. Receives `instance`
21
21
  * @event BookReader:SearchCallbackEmpty - When no results found. Receives
22
22
  * `instance`
23
+ * @event BookReader:SearchCanceled - When no results found. Receives
24
+ * `instance`
23
25
  */
26
+ import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.js';
24
27
  import SearchView from './view.js';
28
+ /** @typedef {import('../../BookReader/PageContainer').PageContainer} PageContainer */
29
+ /** @typedef {import('../../BookReader/BookModel').PageIndex} PageIndex */
25
30
 
26
31
  jQuery.extend(BookReader.defaultOptions, {
27
32
  server: 'ia600609.us.archive.org',
@@ -42,7 +47,6 @@ BookReader.prototype.setup = (function (super_) {
42
47
  this.searchResults = null;
43
48
  this.searchInsideUrl = options.searchInsideUrl;
44
49
  this.enableSearch = options.enableSearch;
45
- this.goToFirstResult = false;
46
50
 
47
51
  // Base server used by some api calls
48
52
  this.bookId = options.bookId;
@@ -50,11 +54,14 @@ BookReader.prototype.setup = (function (super_) {
50
54
  this.subPrefix = options.subPrefix;
51
55
  this.bookPath = options.bookPath;
52
56
 
53
- if (this.searchView) { return; }
54
- this.searchView = new SearchView({
55
- br: this,
56
- selector: '#BRsearch_tray',
57
- });
57
+ this.searchXHR = null;
58
+ this._cancelSearch.bind(this);
59
+ this.cancelSearchRequest.bind(this);
60
+
61
+ /** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
62
+ this._searchBoxesByIndex = {};
63
+
64
+ this.searchView = undefined;
58
65
  };
59
66
  })(BookReader.prototype.setup);
60
67
 
@@ -62,28 +69,31 @@ BookReader.prototype.setup = (function (super_) {
62
69
  BookReader.prototype.init = (function (super_) {
63
70
  return function () {
64
71
  super_.call(this);
65
-
72
+ // give SearchView the most complete bookreader state
73
+ this.searchView = new SearchView({
74
+ br: this,
75
+ searchCancelledCallback: () => {
76
+ this._cancelSearch();
77
+ this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
78
+ }
79
+ });
66
80
  if (this.options.enableSearch && this.options.initialSearchTerm) {
81
+ /**
82
+ * this.search() take two parameter
83
+ * 1. this.options.initialSearchTerm - search term
84
+ * 2. {
85
+ * goToFirstResult: this.options.goToFirstResult,
86
+ * suppressFragmentChange: false // always want to change fragment in URL
87
+ * }
88
+ */
67
89
  this.search(
68
90
  this.options.initialSearchTerm,
69
- { goToFirstResult: this.goToFirstResult, suppressFragmentChange: true }
91
+ { goToFirstResult: this.options.goToFirstResult, suppressFragmentChange: false }
70
92
  );
71
93
  }
72
94
  };
73
95
  })(BookReader.prototype.init);
74
96
 
75
- /** @override */
76
- BookReader.prototype.buildMobileDrawerElement = (function (super_) {
77
- return function () {
78
- const $el = super_.call(this);
79
- if (!this.enableSearch) { return; }
80
- if (this.searchView.dom.mobileSearch) {
81
- $el.find('.BRmobileMenu__moreInfoRow').after(this.searchView.dom.mobileSearch);
82
- }
83
- return $el;
84
- };
85
- })(BookReader.prototype.buildMobileDrawerElement);
86
-
87
97
  /** @override */
88
98
  BookReader.prototype.buildToolbarElement = (function (super_) {
89
99
  return function () {
@@ -96,6 +106,18 @@ BookReader.prototype.buildToolbarElement = (function (super_) {
96
106
  };
97
107
  })(BookReader.prototype.buildToolbarElement);
98
108
 
109
+ /** @override */
110
+ BookReader.prototype._createPageContainer = (function (super_) {
111
+ return function (index) {
112
+ const pageContainer = super_.call(this, index);
113
+ if (this.enableSearch && pageContainer.page && index in this._searchBoxesByIndex) {
114
+ const pageIndex = pageContainer.page.index;
115
+ renderBoxesInPageContainerLayer('searchHiliteLayer', this._searchBoxesByIndex[pageIndex], pageContainer.page, pageContainer.$container[0]);
116
+ }
117
+ return pageContainer;
118
+ };
119
+ })(BookReader.prototype._createPageContainer);
120
+
99
121
  /**
100
122
  * @typedef {object} SearchOptions
101
123
  * @property {boolean} goToFirstResult
@@ -110,7 +132,7 @@ BookReader.prototype.buildToolbarElement = (function (super_) {
110
132
  * @param {string} term
111
133
  * @param {SearchOptions} overrides
112
134
  */
113
- BookReader.prototype.search = function(term = '', overrides = {}) {
135
+ BookReader.prototype.search = async function(term = '', overrides = {}) {
114
136
  /** @type {SearchOptions} */
115
137
  const defaultOptions = {
116
138
  goToFirstResult: false, /* jump to the first result (default=false) */
@@ -122,6 +144,7 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
122
144
  };
123
145
  const options = jQuery.extend({}, defaultOptions, overrides);
124
146
  this.suppressFragmentChange = options.suppressFragmentChange;
147
+ this.searchCancelled = false;
125
148
 
126
149
  // strip slashes, since this goes in the url
127
150
  this.searchTerm = term.replace(/\//g, ' ');
@@ -158,6 +181,9 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
158
181
  const url = `${baseUrl}${paramStr}`;
159
182
 
160
183
  const processSearchResults = (searchInsideResults) => {
184
+ if (this.searchCancelled) {
185
+ return;
186
+ }
161
187
  const responseHasError = searchInsideResults.error || !searchInsideResults.matches.length;
162
188
  const hasCustomError = typeof options.error === 'function';
163
189
  const hasCustomSuccess = typeof options.success === 'function';
@@ -173,11 +199,39 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
173
199
  }
174
200
  };
175
201
 
176
- this.trigger('SearchStarted', { term: this.searchTerm });
177
- return $.ajax({
202
+ this.trigger('SearchStarted', { term: this.searchTerm, instance: this });
203
+ return processSearchResults(await $.ajax({
178
204
  url: url,
179
- dataType: 'jsonp'
180
- }).then(processSearchResults);
205
+ dataType: 'jsonp',
206
+ cache: true,
207
+ beforeSend: xhr => { this.searchXHR = xhr; },
208
+ }));
209
+ };
210
+
211
+ /**
212
+ * cancels AJAX Call
213
+ * emits custom event
214
+ */
215
+ BookReader.prototype._cancelSearch = function () {
216
+ this.searchXHR?.abort();
217
+ this.searchView.clearSearchFieldAndResults(false);
218
+ this.searchTerm = '';
219
+ this.searchXHR = null;
220
+ this.searchCancelled = true;
221
+ this.searchResults = [];
222
+ };
223
+
224
+ /**
225
+ * External function to cancel search
226
+ * checks for term & xhr in flight before running
227
+ */
228
+ BookReader.prototype.cancelSearchRequest = function () {
229
+ this.searchCancelled = true;
230
+ if (this.searchXHR !== null) {
231
+ this._cancelSearch();
232
+ this.searchView.toggleSearchPending();
233
+ this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
234
+ }
181
235
  };
182
236
 
183
237
  /**
@@ -211,15 +265,16 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
211
265
  * @param {boolean} options.goToFirstResult
212
266
  */
213
267
  BookReader.prototype.BRSearchCallback = function(results, options) {
214
- this.searchResults = results;
268
+ this.searchResults = results || [];
215
269
 
216
270
  this.updateSearchHilites();
217
271
  this.removeProgressPopup();
218
272
  if (options.goToFirstResult) {
219
- this._searchPluginGoToResult(results.matches[0].par[0].page);
273
+ const pageIndex = this._models.book.leafNumToIndex(results.matches[0].par[0].page);
274
+ this._searchPluginGoToResult(pageIndex);
220
275
  }
221
276
  this.trigger('SearchCallback', { results, options, instance: this });
222
- }
277
+ };
223
278
 
224
279
  /**
225
280
  * Main search results error handler
@@ -259,95 +314,39 @@ BookReader.prototype._BRSearchCallbackError = function(results) {
259
314
  * updates search on-page highlights controller
260
315
  */
261
316
  BookReader.prototype.updateSearchHilites = function() {
262
- if (this.constMode2up == this.mode) {
263
- this.updateSearchHilites2UP();
264
- return;
265
- }
266
- this.updateSearchHilites1UP();
267
- };
317
+ /** @type {SearchInsideMatch[]} */
318
+ const matches = this.searchResults?.matches || [];
319
+ /** @type { {[pageIndex: number]: SearchInsideMatch[]} } */
320
+ const boxesByIndex = {};
268
321
 
269
- /**
270
- * update search on-page highlights in 1up mode
271
- */
272
- BookReader.prototype.updateSearchHilites1UP = function() {
273
- const results = this.searchResults;
274
- if (null == results) return;
275
- results.matches.forEach(match => {
276
- match.par[0].boxes.forEach(box => {
322
+ // Clear any existing svg layers
323
+ this.removeSearchHilites();
324
+
325
+ // Group by pageIndex
326
+ for (const match of matches) {
327
+ for (const box of match.par[0].boxes) {
277
328
  const pageIndex = this.leafNumToIndex(box.page);
278
- const pageIsInView = jQuery.inArray(pageIndex, this.displayedIndices) >= 0;
279
- if (pageIsInView) {
280
- if (!box.div) {
281
- //create a div for the search highlight, and stash it in the box object
282
- box.div = document.createElement('div');
283
- $(box.div).prop('className', 'BookReaderSearchHilite').appendTo(this.$(`.pagediv${pageIndex}`));
284
- }
285
- const page = this._models.book.getPage(pageIndex);
286
- const highlight = {
287
- width: this._modes.mode1Up.physicalInchesToDisplayPixels((box.r - box.l) / page.ppi),
288
- height: this._modes.mode1Up.physicalInchesToDisplayPixels((box.b - box.t) / page.ppi),
289
- left: this._modes.mode1Up.physicalInchesToDisplayPixels(box.l / page.ppi),
290
- top: this._modes.mode1Up.physicalInchesToDisplayPixels(box.t / page.ppi),
291
- };
292
- $(box.div).css(highlight);
293
- } else {
294
- if (box.div) {
295
- $(box.div).remove();
296
- box.div = null;
297
- }
298
- }
299
- });
300
- });
301
- };
329
+ const pageMatches = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
330
+ pageMatches.push(box);
331
+ }
332
+ }
302
333
 
303
- /**
304
- * update search on-page highlights in 2up mode
305
- */
306
- BookReader.prototype.updateSearchHilites2UP = function() {
307
- const results = this.searchResults;
334
+ // update any already created pages
335
+ for (const [pageIndexString, boxes] of Object.entries(boxesByIndex)) {
336
+ const pageIndex = parseFloat(pageIndexString);
337
+ const page = this._models.book.getPage(pageIndex);
338
+ const pageContainers = this.getActivePageContainerElementsForIndex(pageIndex);
339
+ pageContainers.forEach(container => renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container));
340
+ }
308
341
 
309
- if (results === null) return;
310
-
311
- const { matches } = results;
312
- matches.forEach((match) => {
313
- match.par[0].boxes.forEach(box => {
314
- const pageIndex = this.leafNumToIndex(match.par[0].page);
315
- const pageIsInView = jQuery.inArray(pageIndex, this.displayedIndices) >= 0;
316
- const { isViewable } = this._models.book.getPage(pageIndex);
317
-
318
- if (pageIsInView && isViewable) {
319
- if (!box.div) {
320
- //create a div for the search highlight, and stash it in the box object
321
- box.div = document.createElement('div');
322
- $(box.div).addClass('BookReaderSearchHilite')
323
- .appendTo(this.refs.$brTwoPageView);
324
- }
325
- this.setHilightCss2UP(box.div, pageIndex, box.l, box.r, box.t, box.b);
326
- } else {
327
- // clear stale reference
328
- if (box.div) {
329
- $(box.div).remove();
330
- box.div = null;
331
- }
332
- }
333
- });
334
- });
342
+ this._searchBoxesByIndex = boxesByIndex;
335
343
  };
336
344
 
337
345
  /**
338
346
  * remove search highlights
339
347
  */
340
348
  BookReader.prototype.removeSearchHilites = function() {
341
- const results = this.searchResults;
342
- if (null == results || !results.matches) { return; }
343
- results.matches.forEach(match => {
344
- match.par[0].boxes.forEach(box => {
345
- if (null != box.div) {
346
- $(box.div).remove();
347
- box.div = null;
348
- }
349
- });
350
- });
349
+ $(this.getActivePageContainerElements()).find('.searchHiliteLayer').remove();
351
350
  };
352
351
 
353
352
  /**
@@ -1,75 +1,31 @@
1
1
  class SearchView {
2
2
  /**
3
3
  * @param {object} params
4
- * @param {string} params.selector A selector for the element that the search tray will be rendered in
5
- * @param {string} params.query An existing query string
6
- * @param {object} params.br The BookReader instance
4
+ * @param {object} params.br The BookReader instance
5
+ * @param {function} params.cancelSearch callback when a user wants to cancel search
7
6
  *
8
7
  * @event BookReader:SearchResultsCleared - when the search results nav gets cleared
9
8
  * @event BookReader:ToggleSearchMenu - when search results menu should toggle
10
9
  */
11
- constructor(params) {
12
- if (!params.selector) {
13
- console.warn('BookReader::Search - SearchView must be passed a valid CSS selector');
14
- return;
15
- }
16
-
17
- this.br = params.br;
10
+ constructor({ br, searchCancelledCallback = () => {} }) {
11
+ this.br = br;
18
12
 
19
13
  // Search results are returned as a text blob with the hits wrapped in
20
14
  // triple mustaches. Hits occasionally include text beyond the search
21
15
  // term, so everything within the staches is captured and wrapped.
22
16
  this.matcher = new RegExp('{{{(.+?)}}}', 'g');
23
17
  this.matches = [];
24
- this.cacheDOMElements(params.selector);
18
+ this.cacheDOMElements();
25
19
  this.bindEvents();
20
+ this.cancelSearch = searchCancelledCallback;
26
21
  }
27
22
 
28
- /**
29
- * @param {string} selector A selector for the element that the search tray will be rendered in
30
- */
31
- cacheDOMElements(selector) {
23
+ cacheDOMElements() {
32
24
  this.dom = {};
33
-
34
- // The parent search tray in mobile menu
35
- this.dom.searchTray = this.renderSearchTray(selector);
36
- // Container for rendered search results
37
- this.dom.results = this.dom.searchTray.querySelector('[data-id="results"]');
38
- // Element used to display number of results
39
- this.dom.resultsCount = this.dom.searchTray.querySelector('[data-id="results_count"]');
40
- // Search input within the mobile search tray
41
- this.dom.searchField = this.dom.searchTray.querySelector('[name="query"]');
42
- // Waiting indicator displayed while waiting for a search request
43
- this.dom.searchPending = this.dom.searchTray.querySelector('[data-id="searchPending"]');
44
- // The element added to the mobile menu that is animated into view when
45
- // the "search" nav item is clicked
46
- this.dom.mobileSearch = this.buildMobileDrawer();
47
25
  // Search input within the top toolbar. Will be removed once the mobile menu is replaced.
48
26
  this.dom.toolbarSearch = this.buildToolbarSearch();
49
27
  }
50
28
 
51
- /**
52
- * @param {boolean} bool
53
- */
54
- toggleSearchTray(bool = this.dom.searchTray.classList.contains('hidden')) {
55
- this.dom.searchTray.classList.toggle('hidden', !bool);
56
- }
57
-
58
- /**
59
- * @param {boolean} bool
60
- */
61
- toggleResultsCount(bool) {
62
- this.dom.resultsCount.classList.toggle('visible', bool);
63
- }
64
-
65
- /**
66
- * @param {SearchInsideResults} results
67
- */
68
- updateResultsCount(results) {
69
- this.dom.resultsCount.innerText = `(${results} result${results != 1 ? 's' : ''})`;
70
- this.toggleResultsCount(true);
71
- }
72
-
73
29
  /**
74
30
  * @param {string} query
75
31
  */
@@ -78,7 +34,6 @@ class SearchView {
78
34
  }
79
35
 
80
36
  emptyMatches() {
81
- this.dom.results.innerHTML = '';
82
37
  this.matches = [];
83
38
  }
84
39
 
@@ -86,48 +41,20 @@ class SearchView {
86
41
  this.br.$('.BRnavpos .BRsearch').remove();
87
42
  }
88
43
 
89
- clearSearchFieldAndResults() {
44
+ clearSearchFieldAndResults(dispatchEventWhenComplete = true) {
90
45
  this.br.removeSearchResults();
91
- this.toggleResultsCount(false);
92
46
  this.removeResultPins();
93
47
  this.emptyMatches();
94
48
  this.setQuery('');
95
49
  this.teardownSearchNavigation();
96
- this.br.trigger('SearchResultsCleared');
50
+ if (dispatchEventWhenComplete) {
51
+ this.br.trigger('SearchResultsCleared');
52
+ }
97
53
  }
98
54
 
99
55
  toggleSidebar() {
100
56
  this.br.trigger('ToggleSearchMenu');
101
57
  }
102
- /**
103
- * @param {string} selector The ID attribute to be used for the search tray
104
- */
105
- renderSearchTray(selector) {
106
- const searchTray = document.createElement('div');
107
- searchTray.setAttribute('id', selector.replace(/^#/, ''));
108
- searchTray.innerHTML = `
109
- <header>
110
- <div>
111
- <h3>Search inside</h3>
112
- <p data-id="results_count"></p>
113
- </div>
114
- <a href="#" class="close"></a>
115
- </header>
116
- <form action="" method="get">
117
- <fieldset>
118
- <input name="all_files" id="all_files" type="checkbox" />
119
- <label class="checkbox" for="all_files">Search all files</label>
120
- <input type="search" name="query" placeholder="Enter a search term" />
121
- </fieldset>
122
- </form>
123
- <div data-id="searchPending" id="search_pending">
124
- <p>Your search results will appear below</p>
125
- <div class="loader tc mt20"></div>
126
- </div>
127
- <ul data-id="results"></ul>
128
- `;
129
- return searchTray;
130
- }
131
58
 
132
59
  renderSearchNavigation() {
133
60
  const selector = 'BRsearch-navigation';
@@ -215,17 +142,19 @@ class SearchView {
215
142
  const start = pool.slice(0, pool.length / 2);
216
143
  const end = pool.slice(pool.length / 2);
217
144
  return closestTo((comparisonFn(start, end, comparator) ? start : end), comparator);
218
- }
145
+ };
219
146
 
220
147
  const closestPage = closestTo(matchPages, currentPage);
221
148
  return this.matches.indexOf(this.matches.find((m) => m.par[0].page === closestPage));
222
149
  }
223
150
 
224
151
  updateResultsPosition() {
152
+ if (!this.dom.searchNavigation) return;
225
153
  this.dom.searchNavigation.find('[data-id=resultsCount]').text(this.resultsPosition());
226
154
  }
227
155
 
228
156
  updateSearchNavigationButtons() {
157
+ if (!this.dom.searchNavigation) return;
229
158
  this.dom.searchNavigation.find('.prev').attr('disabled', !this.currentMatchIndex);
230
159
  this.dom.searchNavigation.find('.next').attr('disabled', this.currentMatchIndex + 1 === this.matches.length);
231
160
  }
@@ -272,19 +201,6 @@ class SearchView {
272
201
  this.updateSearchNavigationButtons();
273
202
  }
274
203
 
275
- /**
276
- * @param {array} matches
277
- */
278
- renderMatches(matches) {
279
- const items = matches.map((match) => `
280
- <li data-page="${match.par[0].page}" data-page-index="${this.br.leafNumToIndex(match.par[0].page)}">
281
- <h4>Page ${match.par[0].page}</h4>
282
- <p>${match.text.replace(this.matcher, '<mark>$1</mark>')}</p>
283
- </li>
284
- `);
285
- this.dom.results.innerHTML = items.join('');
286
- }
287
-
288
204
  /**
289
205
  * @param {boolean} bool
290
206
  */
@@ -293,23 +209,6 @@ class SearchView {
293
209
  this.br.refs.$BRfooter.find('.BRsearch').css({ visibility: pinsVisibleState });
294
210
  }
295
211
 
296
- buildMobileDrawer() {
297
- const mobileSearch = document.createElement('li');
298
- mobileSearch.innerHTML = `
299
- <span>
300
- <span class="DrawerIconWrapper">
301
- <img class="DrawerIcon" src="${this.br.imagesBaseURL}icon_search_button.svg" />
302
- </span>
303
- Search
304
- </span>
305
- <div data-id="search_slot">
306
- </div>
307
- `;
308
- mobileSearch.querySelector('[data-id="search_slot"]').appendChild(this.dom.searchTray);
309
- mobileSearch.classList.add('BRmobileMenu__search');
310
- return mobileSearch;
311
- }
312
-
313
212
  buildToolbarSearch() {
314
213
  const toolbarSearch = document.createElement('span');
315
214
  toolbarSearch.classList.add('BRtoolbarSection', 'BRtoolbarSectionSearch');
@@ -363,28 +262,26 @@ class SearchView {
363
262
  `)
364
263
  .data({ pageIndex })
365
264
  .appendTo(this.br.$('.BRnavline'))
366
- .hover(
367
- (event) => {
368
- // remove from other markers then turn on just for this
369
- // XXX should be done when nav slider moves
370
- const marker = event.currentTarget;
371
- const tooltip = marker.querySelector('.BRquery');
372
- const tooltipOffset = tooltip.getBoundingClientRect();
373
- const targetOffset = marker.getBoundingClientRect();
374
- const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
375
- if (tooltipOffset.x - boxSizeAdjust < 0) {
376
- tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
377
- }
378
- $('.BRsearch,.BRchapter').removeClass('front');
379
- $(event.target).addClass('front');
380
- },
381
- (event) => $(event.target).removeClass('front'))
382
- .click(function (event) {
265
+ .on("mouseenter", (event) => {
266
+ // remove from other markers then turn on just for this
267
+ // XXX should be done when nav slider moves
268
+ const marker = event.currentTarget;
269
+ const tooltip = marker.querySelector('.BRquery');
270
+ const tooltipOffset = tooltip.getBoundingClientRect();
271
+ const targetOffset = marker.getBoundingClientRect();
272
+ const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
273
+ if (tooltipOffset.x - boxSizeAdjust < 0) {
274
+ tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
275
+ }
276
+ $('.BRsearch,.BRchapter').removeClass('front');
277
+ $(event.target).addClass('front');
278
+ })
279
+ .on("mouseleave", (event) => $(event.target).removeClass('front'))
280
+ .on("click", function (event) {
383
281
  // closures are nested and deep, using an arrow function breaks references.
384
282
  // Todo: update to arrow function & clean up closures
385
283
  // to remove `bind` dependency
386
284
  this.br._searchPluginGoToResult(+$(event.target).data('pageIndex'));
387
- this.br.updateSearchHilites();
388
285
  }.bind(this));
389
286
  });
390
287
  }
@@ -393,20 +290,28 @@ class SearchView {
393
290
  * @param {boolean} bool
394
291
  */
395
292
  toggleSearchPending(bool) {
396
- this.dom.searchPending.classList.toggle('visible', bool);
397
293
  if (bool) {
398
- this.br.showProgressPopup("Search results will appear below...");
294
+ this.br.showProgressPopup("Search results will appear below...", () => this.progressPopupClosed());
399
295
  }
400
296
  else {
401
297
  this.br.removeProgressPopup();
402
298
  }
403
299
  }
404
300
 
405
- renderErrorModal() {
301
+ /**
302
+ * Primary callback when user cancels search popup
303
+ */
304
+ progressPopupClosed() {
305
+ this.toggleSearchPending();
306
+ this.cancelSearch();
307
+ }
308
+
309
+ renderErrorModal(textIsProcessing = false) {
310
+ const errorDetails = `${!textIsProcessing ? 'The text may still be processing. ' : ''}Please try again.`;
406
311
  this.renderModalMessage(`
407
312
  Sorry, there was an error with your search.
408
313
  <br />
409
- The text may still be processing.
314
+ ${errorDetails}
410
315
  `);
411
316
  this.delayModalRemovalFor(4000);
412
317
  }
@@ -445,14 +350,6 @@ class SearchView {
445
350
  setTimeout(this.br.removeProgressPopup.bind(this.br), timeoutMS);
446
351
  }
447
352
 
448
- openMobileMenu() {
449
- this.br.refs.$mmenu.data('mmenu').open();
450
- }
451
-
452
- closeMobileMenu() {
453
- this.br.refs.$mmenu.data('mmenu').close();
454
- }
455
-
456
353
  /**
457
354
  * @param {Event} e
458
355
  */
@@ -461,7 +358,6 @@ class SearchView {
461
358
  const query = e.target.querySelector('[name="query"]').value;
462
359
  if (!query.length) { return false; }
463
360
  this.br.search(query);
464
- this.dom.searchField.blur();
465
361
  this.emptyMatches();
466
362
  this.toggleSearchPending(true);
467
363
  return false;
@@ -479,9 +375,7 @@ class SearchView {
479
375
  this.teardownSearchNavigation();
480
376
  this.renderSearchNavigation();
481
377
  this.bindSearchNavigationEvents();
482
- this.renderMatches(results.matches);
483
378
  this.renderPins(results.matches);
484
- this.updateResultsCount(results.matches.length);
485
379
  this.toggleSearchPending(false);
486
380
  if (options.goToFirstResult) {
487
381
  $(document).one('BookReader:pageChanged', () => {
@@ -498,7 +392,6 @@ class SearchView {
498
392
  handleNavToggledCallback(e) {
499
393
  const is_visible = this.br.navigationIsVisible();
500
394
  this.togglePinsFor(is_visible);
501
- this.toggleSearchTray(is_visible ? !!this.dom.results.querySelector('li') : false);
502
395
  }
503
396
 
504
397
  handleSearchStarted() {
@@ -510,9 +403,14 @@ class SearchView {
510
403
  this.setQuery(this.br.searchTerm);
511
404
  }
512
405
 
513
- handleSearchCallbackError() {
406
+ /**
407
+ * Event listener for: `BookReader:SearchCallbackError`
408
+ * @param {CustomEvent} event
409
+ */
410
+ handleSearchCallbackError(event = {}) {
514
411
  this.toggleSearchPending(false);
515
- this.renderErrorModal();
412
+ const isIndexed = event?.detail?.props?.results?.indexed;
413
+ this.renderErrorModal(isIndexed);
516
414
  }
517
415
 
518
416
  handleSearchCallbackBookNotIndexed() {
@@ -528,26 +426,15 @@ class SearchView {
528
426
  bindEvents() {
529
427
  const namespace = 'BookReader:';
530
428
 
429
+ window.addEventListener(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this));
531
430
  $(document).on(`${namespace}SearchCallback`, this.handleSearchCallback.bind(this))
532
431
  .on(`${namespace}navToggled`, this.handleNavToggledCallback.bind(this))
533
432
  .on(`${namespace}SearchStarted`, this.handleSearchStarted.bind(this))
534
- .on(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this))
535
433
  .on(`${namespace}SearchCallbackBookNotIndexed`, this.handleSearchCallbackBookNotIndexed.bind(this))
536
434
  .on(`${namespace}SearchCallbackEmpty`, this.handleSearchCallbackEmpty.bind(this))
537
435
  .on(`${namespace}pageChanged`, this.updateSearchNavigation.bind(this));
538
436
 
539
- this.dom.searchTray.addEventListener('submit', this.submitHandler.bind(this));
540
437
  this.dom.toolbarSearch.querySelector('form').addEventListener('submit', this.submitHandler.bind(this));
541
- this.dom.searchField.addEventListener('search', () => {
542
- if (this.dom.searchField.value) { return; }
543
- this.clearSearchFieldAndResults();
544
- });
545
-
546
- $(this.dom.results).on('click', 'li', (e) => {
547
- this.br._searchPluginGoToResult(+e.currentTarget.dataset.pageIndex);
548
- this.br.updateSearchHilites();
549
- this.closeMobileMenu();
550
- });
551
438
  }
552
439
  }
553
440