@internetarchive/bookreader 5.0.0-6-14 → 5.0.0-60

Sign up to get free protection for your applications and to get access to all the features.
Files changed (271) hide show
  1. package/.eslintrc.js +17 -15
  2. package/.github/workflows/node.js.yml +72 -10
  3. package/.github/workflows/npm-publish.yml +6 -20
  4. package/.testcaferc.js +10 -0
  5. package/BookReader/BookReader.css +241 -140
  6. package/BookReader/BookReader.js +1 -1
  7. package/BookReader/BookReader.js.LICENSE.txt +24 -20
  8. package/BookReader/BookReader.js.map +1 -1
  9. package/BookReader/ia-bookreader-bundle.js +1519 -0
  10. package/BookReader/ia-bookreader-bundle.js.LICENSE.txt +17 -0
  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/pause.svg +1 -1
  16. package/BookReader/icons/playback-speed.svg +1 -1
  17. package/BookReader/icons/read-aloud.svg +1 -1
  18. package/BookReader/icons/voice.svg +1 -0
  19. package/BookReader/images/BRicons.svg +2 -2
  20. package/BookReader/images/books_graphic.svg +1 -1
  21. package/BookReader/images/icon_book.svg +1 -1
  22. package/BookReader/images/icon_gear.svg +1 -1
  23. package/BookReader/images/icon_info.svg +1 -1
  24. package/BookReader/images/icon_playback-rate.svg +1 -1
  25. package/BookReader/images/icon_search_button.svg +1 -1
  26. package/BookReader/images/icon_share.svg +1 -1
  27. package/BookReader/images/icon_speaker.svg +1 -1
  28. package/BookReader/images/icon_speaker_open.svg +1 -1
  29. package/BookReader/images/marker_chap-off.svg +1 -1
  30. package/BookReader/images/marker_chap-on.svg +1 -1
  31. package/BookReader/images/marker_srch-on.svg +1 -1
  32. package/BookReader/jquery-3.js +2 -0
  33. package/BookReader/jquery-3.js.LICENSE.txt +24 -0
  34. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  35. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  36. package/BookReader/plugins/plugin.autoplay.js +1 -1
  37. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  38. package/BookReader/plugins/plugin.chapters.js +1 -1
  39. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  40. package/BookReader/plugins/plugin.iframe.js +1 -1
  41. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  42. package/BookReader/plugins/plugin.mobile_nav.js +1 -1
  43. package/BookReader/plugins/plugin.mobile_nav.js.map +1 -1
  44. package/BookReader/plugins/plugin.resume.js +1 -1
  45. package/BookReader/plugins/plugin.resume.js.map +1 -1
  46. package/BookReader/plugins/plugin.search.js +1 -1
  47. package/BookReader/plugins/plugin.search.js.map +1 -1
  48. package/BookReader/plugins/plugin.text_selection.js +1 -1
  49. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  50. package/BookReader/plugins/plugin.tts.js +1 -1
  51. package/BookReader/plugins/plugin.tts.js.map +1 -1
  52. package/BookReader/plugins/plugin.url.js +1 -1
  53. package/BookReader/plugins/plugin.url.js.map +1 -1
  54. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  55. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  56. package/BookReader/webcomponents-bundle.js +3 -0
  57. package/BookReader/webcomponents-bundle.js.LICENSE.txt +9 -0
  58. package/BookReader/webcomponents-bundle.js.map +1 -0
  59. package/BookReaderDemo/BookReaderDemo.css +14 -1
  60. package/BookReaderDemo/BookReaderJSAutoplay.js +4 -1
  61. package/BookReaderDemo/BookReaderJSSimple.js +1 -0
  62. package/BookReaderDemo/IADemoBr.js +147 -0
  63. package/BookReaderDemo/demo-advanced.html +2 -2
  64. package/BookReaderDemo/demo-autoplay.html +2 -1
  65. package/BookReaderDemo/demo-embed-iframe-src.html +2 -1
  66. package/BookReaderDemo/demo-fullscreen-mobile.html +2 -1
  67. package/BookReaderDemo/demo-fullscreen.html +2 -1
  68. package/BookReaderDemo/demo-iiif.html +2 -1
  69. package/BookReaderDemo/demo-internetarchive.html +84 -17
  70. package/BookReaderDemo/demo-multiple.html +2 -1
  71. package/BookReaderDemo/demo-preview-pages.html +2 -1
  72. package/BookReaderDemo/demo-simple.html +2 -1
  73. package/BookReaderDemo/demo-vendor-fullscreen.html +2 -1
  74. package/BookReaderDemo/ia-multiple-volumes-manifest.js +170 -0
  75. package/BookReaderDemo/immersion-1up.html +2 -1
  76. package/BookReaderDemo/immersion-mode.html +2 -1
  77. package/BookReaderDemo/toggle_controls.html +2 -1
  78. package/BookReaderDemo/view_mode.html +2 -1
  79. package/BookReaderDemo/viewmode-cycle.html +2 -3
  80. package/CHANGELOG.md +244 -0
  81. package/README.md +14 -1
  82. package/babel.config.js +19 -0
  83. package/codecov.yml +6 -0
  84. package/index.html +3 -0
  85. package/jsconfig.json +19 -0
  86. package/netlify.toml +5 -0
  87. package/package.json +70 -59
  88. package/renovate.json +52 -0
  89. package/scripts/preversion.js +4 -1
  90. package/src/BookNavigator/assets/bookmark-colors.js +1 -1
  91. package/src/BookNavigator/assets/button-base.js +9 -2
  92. package/src/BookNavigator/assets/ia-logo.js +17 -0
  93. package/src/BookNavigator/assets/icon_checkmark.js +1 -1
  94. package/src/BookNavigator/assets/icon_close.js +1 -1
  95. package/src/BookNavigator/assets/icon_sort_asc.js +5 -0
  96. package/src/BookNavigator/assets/icon_sort_desc.js +5 -0
  97. package/src/BookNavigator/assets/icon_sort_neutral.js +5 -0
  98. package/src/BookNavigator/assets/icon_volumes.js +11 -0
  99. package/src/BookNavigator/book-navigator.js +585 -0
  100. package/src/BookNavigator/bookmarks/bookmark-button.js +3 -2
  101. package/src/BookNavigator/bookmarks/bookmark-edit.js +3 -4
  102. package/src/BookNavigator/bookmarks/bookmarks-list.js +2 -3
  103. package/src/BookNavigator/bookmarks/bookmarks-loginCTA.js +3 -8
  104. package/src/BookNavigator/bookmarks/bookmarks-provider.js +27 -17
  105. package/src/BookNavigator/bookmarks/ia-bookmarks.js +116 -67
  106. package/src/BookNavigator/delete-modal-actions.js +1 -1
  107. package/src/BookNavigator/downloads/downloads-provider.js +36 -21
  108. package/src/BookNavigator/downloads/downloads.js +41 -25
  109. package/src/BookNavigator/search/search-provider.js +80 -28
  110. package/src/BookNavigator/search/search-results.js +34 -25
  111. package/src/BookNavigator/sharing.js +27 -0
  112. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +11 -10
  113. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +3 -3
  114. package/src/BookNavigator/volumes/volumes-provider.js +111 -0
  115. package/src/BookNavigator/volumes/volumes.js +188 -0
  116. package/src/BookReader/BookModel.js +59 -30
  117. package/src/BookReader/DebugConsole.js +3 -3
  118. package/src/BookReader/DragScrollable.js +233 -0
  119. package/src/BookReader/Mode1Up.js +56 -351
  120. package/src/BookReader/Mode1UpLit.js +391 -0
  121. package/src/BookReader/Mode2Up.js +73 -1318
  122. package/src/BookReader/Mode2UpLit.js +781 -0
  123. package/src/BookReader/ModeCoordinateSpace.js +29 -0
  124. package/src/BookReader/ModeSmoothZoom.js +211 -0
  125. package/src/BookReader/ModeThumb.js +17 -11
  126. package/src/BookReader/Navbar/Navbar.js +10 -36
  127. package/src/BookReader/PageContainer.js +69 -6
  128. package/src/BookReader/ReduceSet.js +1 -1
  129. package/src/BookReader/Toolbar/Toolbar.js +10 -37
  130. package/src/BookReader/events.js +2 -0
  131. package/src/BookReader/options.js +24 -2
  132. package/src/BookReader/utils/HTMLDimensionsCacher.js +44 -0
  133. package/src/BookReader/utils/ScrollClassAdder.js +31 -0
  134. package/src/BookReader/utils.js +108 -13
  135. package/src/BookReader.js +481 -828
  136. package/src/assets/icons/close-circle-dark.svg +1 -0
  137. package/src/assets/icons/magnify-minus.svg +3 -7
  138. package/src/assets/icons/magnify-plus.svg +3 -7
  139. package/src/assets/icons/voice.svg +1 -0
  140. package/src/css/_BRBookmarks.scss +1 -1
  141. package/src/css/_BRComponent.scss +1 -1
  142. package/src/css/_BRmain.scss +33 -0
  143. package/src/css/_BRnav.scss +4 -26
  144. package/src/css/_BRpages.scss +147 -40
  145. package/src/css/_BRsearch.scss +25 -11
  146. package/src/css/_TextSelection.scss +16 -17
  147. package/src/css/_colorbox.scss +2 -2
  148. package/src/css/_controls.scss +17 -5
  149. package/src/css/_icons.scss +7 -1
  150. package/src/ia-bookreader/ia-bookreader.js +224 -0
  151. package/src/plugins/plugin.archive_analytics.js +3 -3
  152. package/src/plugins/plugin.autoplay.js +4 -9
  153. package/src/plugins/plugin.chapters.js +28 -35
  154. package/src/plugins/plugin.mobile_nav.js +11 -10
  155. package/src/plugins/plugin.resume.js +3 -3
  156. package/src/plugins/plugin.text_selection.js +32 -41
  157. package/src/plugins/plugin.vendor-fullscreen.js +4 -4
  158. package/src/plugins/search/plugin.search.js +179 -103
  159. package/src/plugins/search/view.js +59 -44
  160. package/src/plugins/tts/AbstractTTSEngine.js +46 -37
  161. package/src/plugins/tts/FestivalTTSEngine.js +13 -14
  162. package/src/plugins/tts/PageChunk.js +15 -21
  163. package/src/plugins/tts/PageChunkIterator.js +8 -12
  164. package/src/plugins/tts/WebTTSEngine.js +87 -71
  165. package/src/plugins/tts/plugin.tts.js +94 -125
  166. package/src/plugins/tts/utils.js +0 -25
  167. package/src/plugins/url/UrlPlugin.js +193 -0
  168. package/src/plugins/{plugin.url.js → url/plugin.url.js} +45 -16
  169. package/src/util/docCookies.js +21 -2
  170. package/tests/e2e/README.md +37 -0
  171. package/tests/e2e/autoplay.test.js +2 -2
  172. package/tests/e2e/base.test.js +7 -7
  173. package/tests/e2e/helpers/base.js +28 -23
  174. package/tests/e2e/helpers/debug.js +1 -1
  175. package/tests/e2e/helpers/desktopSearch.js +14 -13
  176. package/tests/e2e/helpers/mobileSearch.js +3 -3
  177. package/tests/e2e/helpers/params.js +17 -0
  178. package/tests/e2e/helpers/rightToLeft.js +4 -10
  179. package/tests/e2e/models/Navigation.js +13 -4
  180. package/tests/e2e/rightToLeft.test.js +4 -5
  181. package/tests/e2e/viewmode.test.js +40 -33
  182. package/tests/jest/BookNavigator/book-navigator.test.js +658 -0
  183. package/tests/jest/BookNavigator/bookmarks/bookmark-button.test.js +43 -0
  184. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmark-edit.test.js +25 -26
  185. package/tests/{karma → jest}/BookNavigator/bookmarks/bookmarks-list.test.js +41 -42
  186. package/tests/jest/BookNavigator/bookmarks/ia-bookmarks.test.js +45 -0
  187. package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +67 -0
  188. package/tests/jest/BookNavigator/downloads/downloads.test.js +53 -0
  189. package/tests/jest/BookNavigator/search/search-provider.test.js +167 -0
  190. package/tests/{karma/BookNavigator → jest/BookNavigator/search}/search-results.test.js +104 -60
  191. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +49 -0
  192. package/tests/jest/BookNavigator/visual-adjustments.test.js +200 -0
  193. package/tests/jest/BookNavigator/volumes/volumes-provider.test.js +184 -0
  194. package/tests/jest/BookNavigator/volumes/volumes.test.js +97 -0
  195. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +59 -14
  196. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +193 -0
  197. package/tests/{BookReader → jest/BookReader}/DebugConsole.test.js +1 -1
  198. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +4 -4
  199. package/tests/jest/BookReader/Mode1UpLit.test.js +73 -0
  200. package/tests/jest/BookReader/Mode2Up.test.js +98 -0
  201. package/tests/jest/BookReader/Mode2UpLit.test.js +190 -0
  202. package/tests/jest/BookReader/ModeCoordinateSpace.test.js +16 -0
  203. package/tests/jest/BookReader/ModeSmoothZoom.test.js +175 -0
  204. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  205. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +10 -10
  206. package/tests/{BookReader → jest/BookReader}/PageContainer.test.js +88 -6
  207. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +1 -1
  208. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +2 -2
  209. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +59 -0
  210. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +49 -0
  211. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +1 -1
  212. package/tests/jest/BookReader/utils.test.js +217 -0
  213. package/tests/jest/BookReader.keyboard.test.js +190 -0
  214. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +9 -1
  215. package/tests/{BookReader.test.js → jest/BookReader.test.js} +26 -37
  216. package/tests/{plugins → jest/plugins}/plugin.archive_analytics.test.js +2 -2
  217. package/tests/{plugins → jest/plugins}/plugin.autoplay.test.js +4 -4
  218. package/tests/{plugins → jest/plugins}/plugin.chapters.test.js +10 -11
  219. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +2 -2
  220. package/tests/{plugins → jest/plugins}/plugin.mobile_nav.test.js +5 -5
  221. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +3 -3
  222. package/tests/{plugins → jest/plugins}/plugin.text_selection.test.js +39 -47
  223. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +2 -2
  224. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +57 -47
  225. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +35 -6
  226. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +9 -9
  227. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +4 -4
  228. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +1 -1
  229. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +3 -3
  230. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +47 -1
  231. package/tests/{plugins → jest/plugins}/tts/utils.test.js +1 -60
  232. package/tests/jest/plugins/url/UrlPlugin.test.js +190 -0
  233. package/tests/{plugins → jest/plugins/url}/plugin.url.test.js +53 -14
  234. package/tests/jest/setup.js +3 -0
  235. package/tests/{util → jest/util}/browserSniffing.test.js +1 -1
  236. package/tests/jest/util/docCookies.test.js +24 -0
  237. package/tests/{util → jest/util}/strings.test.js +1 -1
  238. package/tests/{utils.js → jest/utils.js} +38 -0
  239. package/webpack.config.js +11 -5
  240. package/.babelrc +0 -12
  241. package/.dependabot/config.yml +0 -6
  242. package/.testcaferc.json +0 -5
  243. package/BookReader/bookreader-component-bundle.js +0 -1450
  244. package/BookReader/bookreader-component-bundle.js.LICENSE.txt +0 -38
  245. package/BookReader/bookreader-component-bundle.js.map +0 -1
  246. package/BookReader/jquery-1.10.1.js +0 -2
  247. package/BookReader/jquery-1.10.1.js.LICENSE.txt +0 -24
  248. package/BookReader/plugins/plugin.menu_toggle.js +0 -2
  249. package/BookReader/plugins/plugin.menu_toggle.js.map +0 -1
  250. package/BookReaderDemo/bookreader-template-bundle.js +0 -7178
  251. package/BookReaderDemo/demo-plugin-menu-toggle.html +0 -34
  252. package/karma.conf.js +0 -23
  253. package/src/BookNavigator/BookModel.js +0 -14
  254. package/src/BookNavigator/BookNavigator.js +0 -438
  255. package/src/BookNavigator/assets/book-loader.js +0 -27
  256. package/src/BookNavigator/br-fullscreen-mgr.js +0 -83
  257. package/src/BookNavigator/search/a-search-result.js +0 -55
  258. package/src/BookReaderComponent/BookReaderComponent.js +0 -112
  259. package/src/ItemNavigator/ItemNavigator.js +0 -372
  260. package/src/ItemNavigator/providers/sharing.js +0 -29
  261. package/src/dragscrollable-br.js +0 -261
  262. package/src/plugins/menu_toggle/plugin.menu_toggle.js +0 -324
  263. package/tests/BookReader/BookReaderPublicFunctions.test.js +0 -171
  264. package/tests/BookReader/Mode1Up.test.js +0 -164
  265. package/tests/BookReader/Mode2Up.test.js +0 -247
  266. package/tests/BookReader/utils.test.js +0 -109
  267. package/tests/e2e/ia-production/ia-prod-base.js +0 -17
  268. package/tests/karma/BookNavigator/book-navigator.test.js +0 -132
  269. package/tests/karma/BookNavigator/visual-adjustments.test.js +0 -201
  270. package/tests/plugins/menu_toggle/plugin.menu_toggle.test.js +0 -68
  271. package/tests/util/docCookies.test.js +0 -15
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  /* global BookReader */
2
3
  /**
3
4
  * Plugin for Archive.org book search
@@ -20,8 +21,16 @@
20
21
  * the book has not had OCR text indexed yet. Receives `instance`
21
22
  * @event BookReader:SearchCallbackEmpty - When no results found. Receives
22
23
  * `instance`
24
+ * @event BookReader:SearchCanceled - When no results found. Receives
25
+ * `instance`
23
26
  */
27
+ import { poll } from '../../BookReader/utils.js';
28
+ import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.js';
24
29
  import SearchView from './view.js';
30
+ /** @typedef {import('../../BookReader/PageContainer').PageContainer} PageContainer */
31
+ /** @typedef {import('../../BookReader/BookModel').PageIndex} PageIndex */
32
+ /** @typedef {import('../../BookReader/BookModel').LeafNum} LeafNum */
33
+ /** @typedef {import('../../BookReader/BookModel').PageNumString} PageNumString */
25
34
 
26
35
  jQuery.extend(BookReader.defaultOptions, {
27
36
  server: 'ia600609.us.archive.org',
@@ -49,10 +58,14 @@ BookReader.prototype.setup = (function (super_) {
49
58
  this.subPrefix = options.subPrefix;
50
59
  this.bookPath = options.bookPath;
51
60
 
52
- if (this.searchView) { return; }
53
- this.searchView = new SearchView({
54
- br: this,
55
- });
61
+ this.searchXHR = null;
62
+ this._cancelSearch.bind(this);
63
+ this.cancelSearchRequest.bind(this);
64
+
65
+ /** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
66
+ this._searchBoxesByIndex = {};
67
+
68
+ this.searchView = undefined;
56
69
  };
57
70
  })(BookReader.prototype.setup);
58
71
 
@@ -60,11 +73,26 @@ BookReader.prototype.setup = (function (super_) {
60
73
  BookReader.prototype.init = (function (super_) {
61
74
  return function () {
62
75
  super_.call(this);
63
-
76
+ // give SearchView the most complete bookreader state
77
+ this.searchView = new SearchView({
78
+ br: this,
79
+ searchCancelledCallback: () => {
80
+ this._cancelSearch();
81
+ this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
82
+ }
83
+ });
64
84
  if (this.options.enableSearch && this.options.initialSearchTerm) {
85
+ /**
86
+ * this.search() take two parameter
87
+ * 1. this.options.initialSearchTerm - search term
88
+ * 2. {
89
+ * goToFirstResult: this.options.goToFirstResult,
90
+ * suppressFragmentChange: false // always want to change fragment in URL
91
+ * }
92
+ */
65
93
  this.search(
66
94
  this.options.initialSearchTerm,
67
- { goToFirstResult: this.options.goToFirstResult, suppressFragmentChange: true }
95
+ { goToFirstResult: this.options.goToFirstResult, suppressFragmentChange: false }
68
96
  );
69
97
  }
70
98
  };
@@ -82,6 +110,25 @@ BookReader.prototype.buildToolbarElement = (function (super_) {
82
110
  };
83
111
  })(BookReader.prototype.buildToolbarElement);
84
112
 
113
+ /** @override */
114
+ BookReader.prototype._createPageContainer = (function (super_) {
115
+ return function (index) {
116
+ const pageContainer = super_.call(this, index);
117
+ if (this.enableSearch && pageContainer.page && index in this._searchBoxesByIndex) {
118
+ const pageIndex = pageContainer.page.index;
119
+ const boxes = this._searchBoxesByIndex[pageIndex];
120
+ renderBoxesInPageContainerLayer(
121
+ 'searchHiliteLayer',
122
+ boxes,
123
+ pageContainer.page,
124
+ pageContainer.$container[0],
125
+ boxes.map(b => `match-index-${b.matchIndex}`),
126
+ );
127
+ }
128
+ return pageContainer;
129
+ };
130
+ })(BookReader.prototype._createPageContainer);
131
+
85
132
  /**
86
133
  * @typedef {object} SearchOptions
87
134
  * @property {boolean} goToFirstResult
@@ -96,7 +143,7 @@ BookReader.prototype.buildToolbarElement = (function (super_) {
96
143
  * @param {string} term
97
144
  * @param {SearchOptions} overrides
98
145
  */
99
- BookReader.prototype.search = function(term = '', overrides = {}) {
146
+ BookReader.prototype.search = async function(term = '', overrides = {}) {
100
147
  /** @type {SearchOptions} */
101
148
  const defaultOptions = {
102
149
  goToFirstResult: false, /* jump to the first result (default=false) */
@@ -108,6 +155,7 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
108
155
  };
109
156
  const options = jQuery.extend({}, defaultOptions, overrides);
110
157
  this.suppressFragmentChange = options.suppressFragmentChange;
158
+ this.searchCancelled = false;
111
159
 
112
160
  // strip slashes, since this goes in the url
113
161
  this.searchTerm = term.replace(/\//g, ' ');
@@ -143,12 +191,16 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
143
191
 
144
192
  const url = `${baseUrl}${paramStr}`;
145
193
 
146
- const processSearchResults = (searchInsideResults) => {
194
+ const callSearchResultsCallback = (searchInsideResults) => {
195
+ if (this.searchCancelled) {
196
+ return;
197
+ }
147
198
  const responseHasError = searchInsideResults.error || !searchInsideResults.matches.length;
148
199
  const hasCustomError = typeof options.error === 'function';
149
200
  const hasCustomSuccess = typeof options.success === 'function';
150
201
 
151
202
  if (responseHasError) {
203
+ console.error('Search Inside Response Error', searchInsideResults.error || 'matches.length == 0');
152
204
  hasCustomError
153
205
  ? options.error.call(this, searchInsideResults, options)
154
206
  : this.BRSearchCallbackError(searchInsideResults, options);
@@ -159,11 +211,39 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
159
211
  }
160
212
  };
161
213
 
162
- this.trigger('SearchStarted', { term: this.searchTerm });
163
- return $.ajax({
214
+ this.trigger('SearchStarted', { term: this.searchTerm, instance: this });
215
+ callSearchResultsCallback(await $.ajax({
164
216
  url: url,
165
- dataType: 'jsonp'
166
- }).then(processSearchResults);
217
+ dataType: 'jsonp',
218
+ cache: true,
219
+ beforeSend: xhr => { this.searchXHR = xhr; },
220
+ }));
221
+ };
222
+
223
+ /**
224
+ * cancels AJAX Call
225
+ * emits custom event
226
+ */
227
+ BookReader.prototype._cancelSearch = function () {
228
+ this.searchXHR?.abort();
229
+ this.searchView.clearSearchFieldAndResults(false);
230
+ this.searchTerm = '';
231
+ this.searchXHR = null;
232
+ this.searchCancelled = true;
233
+ this.searchResults = [];
234
+ };
235
+
236
+ /**
237
+ * External function to cancel search
238
+ * checks for term & xhr in flight before running
239
+ */
240
+ BookReader.prototype.cancelSearchRequest = function () {
241
+ this.searchCancelled = true;
242
+ if (this.searchXHR !== null) {
243
+ this._cancelSearch();
244
+ this.searchView.toggleSearchPending();
245
+ this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
246
+ }
167
247
  };
168
248
 
169
249
  /**
@@ -174,10 +254,13 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
174
254
  * @property {number} b
175
255
  * @property {number} t
176
256
  * @property {HTMLDivElement} [div]
257
+ * @property {number} matchIndex This is a fake field! not part of the API response. The index of the match that contains this box in total search results matches.
177
258
  */
178
259
 
179
260
  /**
180
261
  * @typedef {object} SearchInsideMatch
262
+ * @property {number} matchIndex This is a fake field! Not part of the API response. It is added by the JS.
263
+ * @property {string} displayPageNumber (fake field) The page number as it should be displayed in the UI.
181
264
  * @property {string} text
182
265
  * @property {Array<{ page: number, boxes: SearchInsideMatchBox[] }>} par
183
266
  */
@@ -189,24 +272,42 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
189
272
  * @property {boolean} indexed
190
273
  */
191
274
 
275
+ /**
276
+ * Attach some fields to search inside results
277
+ * @param {SearchInsideResults} results
278
+ * @param {(pageNum: LeafNum) => PageNumString} displayPageNumberFn
279
+ */
280
+ export function marshallSearchResults(results, displayPageNumberFn) {
281
+ // Attach matchIndex to a few things to make it easier to identify
282
+ // an active/selected match
283
+ for (const [index, match] of results.matches.entries()) {
284
+ match.matchIndex = index;
285
+ match.displayPageNumber = displayPageNumberFn(match.par[0].page);
286
+ for (const par of match.par) {
287
+ for (const box of par.boxes) {
288
+ box.matchIndex = index;
289
+ }
290
+ }
291
+ }
292
+ }
293
+
192
294
  /**
193
295
  * Search Results return handler
194
- * @callback
195
296
  * @param {SearchInsideResults} results
196
297
  * @param {object} options
197
298
  * @param {boolean} options.goToFirstResult
198
299
  */
199
300
  BookReader.prototype.BRSearchCallback = function(results, options) {
200
- this.searchResults = results;
301
+ marshallSearchResults(results, pageNum => this.book.getPageNum(this.book.leafNumToIndex(pageNum)));
302
+ this.searchResults = results || [];
201
303
 
202
304
  this.updateSearchHilites();
203
305
  this.removeProgressPopup();
204
306
  if (options.goToFirstResult) {
205
- const pageIndex = this._models.book.leafNumToIndex(results.matches[0].par[0].page);
206
- this._searchPluginGoToResult(pageIndex);
307
+ this._searchPluginGoToResult(0);
207
308
  }
208
309
  this.trigger('SearchCallback', { results, options, instance: this });
209
- }
310
+ };
210
311
 
211
312
  /**
212
313
  * Main search results error handler
@@ -246,95 +347,41 @@ BookReader.prototype._BRSearchCallbackError = function(results) {
246
347
  * updates search on-page highlights controller
247
348
  */
248
349
  BookReader.prototype.updateSearchHilites = function() {
249
- if (this.constMode2up == this.mode) {
250
- this.updateSearchHilites2UP();
251
- return;
350
+ /** @type {SearchInsideMatch[]} */
351
+ const matches = this.searchResults?.matches || [];
352
+ /** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
353
+ const boxesByIndex = {};
354
+
355
+ // Clear any existing svg layers
356
+ this.removeSearchHilites();
357
+
358
+ // Group by pageIndex
359
+ for (const match of matches) {
360
+ for (const box of match.par[0].boxes) {
361
+ const pageIndex = this.book.leafNumToIndex(box.page);
362
+ const pageBoxes = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
363
+ pageBoxes.push(box);
364
+ }
252
365
  }
253
- this.updateSearchHilites1UP();
254
- };
255
366
 
256
- /**
257
- * update search on-page highlights in 1up mode
258
- */
259
- BookReader.prototype.updateSearchHilites1UP = function() {
260
- const results = this.searchResults;
261
- if (null == results) return;
262
- results.matches.forEach(match => {
263
- match.par[0].boxes.forEach(box => {
264
- const pageIndex = this.leafNumToIndex(box.page);
265
- const pageIsInView = jQuery.inArray(pageIndex, this.displayedIndices) >= 0;
266
- if (pageIsInView) {
267
- if (!box.div) {
268
- //create a div for the search highlight, and stash it in the box object
269
- box.div = document.createElement('div');
270
- $(box.div).prop('className', 'BookReaderSearchHilite').appendTo(this.$(`.pagediv${pageIndex}`));
271
- }
272
- const page = this._models.book.getPage(pageIndex);
273
- const highlight = {
274
- width: this._modes.mode1Up.physicalInchesToDisplayPixels((box.r - box.l) / page.ppi),
275
- height: this._modes.mode1Up.physicalInchesToDisplayPixels((box.b - box.t) / page.ppi),
276
- left: this._modes.mode1Up.physicalInchesToDisplayPixels(box.l / page.ppi),
277
- top: this._modes.mode1Up.physicalInchesToDisplayPixels(box.t / page.ppi),
278
- };
279
- $(box.div).css(highlight);
280
- } else {
281
- if (box.div) {
282
- $(box.div).remove();
283
- box.div = null;
284
- }
285
- }
286
- });
287
- });
288
- };
289
-
290
- /**
291
- * update search on-page highlights in 2up mode
292
- */
293
- BookReader.prototype.updateSearchHilites2UP = function() {
294
- const results = this.searchResults;
367
+ // update any already created pages
368
+ for (const [pageIndexString, boxes] of Object.entries(boxesByIndex)) {
369
+ const pageIndex = parseFloat(pageIndexString);
370
+ const page = this.book.getPage(pageIndex);
371
+ const pageContainers = this.getActivePageContainerElementsForIndex(pageIndex);
372
+ for (const container of pageContainers) {
373
+ renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container, boxes.map(b => `match-index-${b.matchIndex}`));
374
+ }
375
+ }
295
376
 
296
- if (results === null) return;
297
-
298
- const { matches } = results;
299
- matches.forEach((match) => {
300
- match.par[0].boxes.forEach(box => {
301
- const pageIndex = this.leafNumToIndex(match.par[0].page);
302
- const pageIsInView = jQuery.inArray(pageIndex, this.displayedIndices) >= 0;
303
- const { isViewable } = this._models.book.getPage(pageIndex);
304
-
305
- if (pageIsInView && isViewable) {
306
- if (!box.div) {
307
- //create a div for the search highlight, and stash it in the box object
308
- box.div = document.createElement('div');
309
- $(box.div).addClass('BookReaderSearchHilite')
310
- .appendTo(this.refs.$brTwoPageView);
311
- }
312
- this.setHilightCss2UP(box.div, pageIndex, box.l, box.r, box.t, box.b);
313
- } else {
314
- // clear stale reference
315
- if (box.div) {
316
- $(box.div).remove();
317
- box.div = null;
318
- }
319
- }
320
- });
321
- });
377
+ this._searchBoxesByIndex = boxesByIndex;
322
378
  };
323
379
 
324
380
  /**
325
381
  * remove search highlights
326
382
  */
327
383
  BookReader.prototype.removeSearchHilites = function() {
328
- const results = this.searchResults;
329
- if (null == results || !results.matches) { return; }
330
- results.matches.forEach(match => {
331
- match.par[0].boxes.forEach(box => {
332
- if (null != box.div) {
333
- $(box.div).remove();
334
- box.div = null;
335
- }
336
- });
337
- });
384
+ $(this.getActivePageContainerElements()).find('.searchHiliteLayer').remove();
338
385
  };
339
386
 
340
387
  /**
@@ -342,11 +389,14 @@ BookReader.prototype.removeSearchHilites = function() {
342
389
  * Goes to the page specified. If the page is not viewable, tries to load the page
343
390
  * FIXME Most of this logic is IA specific, and should be less integrated into here
344
391
  * or at least more configurable.
345
- * @param {PageIndex} pageIndex
392
+ * @param {number} matchIndex
346
393
  */
347
- BookReader.prototype._searchPluginGoToResult = async function (pageIndex) {
348
- const { book } = this._models;
394
+ BookReader.prototype._searchPluginGoToResult = async function (matchIndex) {
395
+ const match = this.searchResults?.matches[matchIndex];
396
+ const book = this.book;
397
+ const pageIndex = book.leafNumToIndex(match.par[0].page);
349
398
  const page = book.getPage(pageIndex);
399
+ const onNearbyPage = Math.abs(this.currentIndex() - pageIndex) < 3;
350
400
  let makeUnviewableAtEnd = false;
351
401
  if (!page.isViewable) {
352
402
  const resp = await fetch('/services/bookreader/request_page?' + new URLSearchParams({
@@ -365,15 +415,41 @@ BookReader.prototype._searchPluginGoToResult = async function (pageIndex) {
365
415
  book.getPage(pageIndex).makeViewable();
366
416
  makeUnviewableAtEnd = true;
367
417
  }
418
+
419
+ // Trigger an update of book
420
+ this._modes.mode1Up.mode1UpLit.updatePages();
421
+ await this._modes.mode1Up.mode1UpLit.updateComplete;
368
422
  }
369
423
  /* this updates the URL */
370
- this.suppressFragmentChange = false;
371
- this.jumpToIndex(pageIndex);
424
+ if (!this._isIndexDisplayed(pageIndex)) {
425
+ this.suppressFragmentChange = false;
426
+ this.jumpToIndex(pageIndex);
427
+ }
372
428
 
373
429
  // Reset it to unviewable if it wasn't resolved
374
430
  if (makeUnviewableAtEnd) {
375
431
  book.getPage(pageIndex).makeViewable(false);
376
432
  }
433
+
434
+ // Scroll/flash in the ui
435
+ const $boxes = await poll(() => $(`rect.match-index-${match.matchIndex}`), { until: result => result.length > 0 });
436
+ if ($boxes.length) {
437
+ $boxes.css('animation', 'none');
438
+ $boxes[0].scrollIntoView({
439
+ // Only vertically center the highlight if we're in 1up or in full screen. In
440
+ // 2up, if we're not fullscreen, the whole body gets scrolled around to try to
441
+ // center the highlight 🙄 See:
442
+ // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
443
+ // Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
444
+ // full-width, and covers up the last line of the highlight.
445
+ block: this.constMode1up == this.mode || this.isFullscreenActive ? 'center' : 'nearest',
446
+ inline: 'center',
447
+ behavior: onNearbyPage ? 'smooth' : 'auto',
448
+ });
449
+ // wait for animation to start
450
+ await new Promise(resolve => setTimeout(resolve, 100));
451
+ $boxes.removeAttr("style");
452
+ }
377
453
  };
378
454
 
379
455
  /**
@@ -407,7 +483,7 @@ BookReader.prototype.searchHighlightVisible = function() {
407
483
 
408
484
  results.matches.some(match => {
409
485
  return match.par[0].boxes.some(box => {
410
- const pageIndex = this.leafNumToIndex(box.page);
486
+ const pageIndex = this.book.leafNumToIndex(box.page);
411
487
  if (jQuery.inArray(pageIndex, visiblePages) >= 0) {
412
488
  return true;
413
489
  }
@@ -1,21 +1,25 @@
1
+ import { escapeHTML } from "../../BookReader/utils.js";
2
+
1
3
  class SearchView {
2
4
  /**
3
5
  * @param {object} params
4
6
  * @param {object} params.br The BookReader instance
7
+ * @param {function} params.cancelSearch callback when a user wants to cancel search
5
8
  *
6
9
  * @event BookReader:SearchResultsCleared - when the search results nav gets cleared
7
10
  * @event BookReader:ToggleSearchMenu - when search results menu should toggle
8
11
  */
9
- constructor({ br }) {
12
+ constructor({ br, searchCancelledCallback = () => {} }) {
10
13
  this.br = br;
11
14
 
12
15
  // Search results are returned as a text blob with the hits wrapped in
13
16
  // triple mustaches. Hits occasionally include text beyond the search
14
17
  // term, so everything within the staches is captured and wrapped.
15
- this.matcher = new RegExp('{{{(.+?)}}}', 'g');
18
+ this.matcher = new RegExp('{{{([^]+?)}}}', 'g'); // [^] matches any character, including line breaks
16
19
  this.matches = [];
17
20
  this.cacheDOMElements();
18
21
  this.bindEvents();
22
+ this.cancelSearch = searchCancelledCallback;
19
23
  }
20
24
 
21
25
  cacheDOMElements() {
@@ -39,13 +43,15 @@ class SearchView {
39
43
  this.br.$('.BRnavpos .BRsearch').remove();
40
44
  }
41
45
 
42
- clearSearchFieldAndResults() {
46
+ clearSearchFieldAndResults(dispatchEventWhenComplete = true) {
43
47
  this.br.removeSearchResults();
44
48
  this.removeResultPins();
45
49
  this.emptyMatches();
46
50
  this.setQuery('');
47
51
  this.teardownSearchNavigation();
48
- this.br.trigger('SearchResultsCleared');
52
+ if (dispatchEventWhenComplete) {
53
+ this.br.trigger('SearchResultsCleared');
54
+ }
49
55
  }
50
56
 
51
57
  toggleSidebar() {
@@ -138,17 +144,19 @@ class SearchView {
138
144
  const start = pool.slice(0, pool.length / 2);
139
145
  const end = pool.slice(pool.length / 2);
140
146
  return closestTo((comparisonFn(start, end, comparator) ? start : end), comparator);
141
- }
147
+ };
142
148
 
143
149
  const closestPage = closestTo(matchPages, currentPage);
144
150
  return this.matches.indexOf(this.matches.find((m) => m.par[0].page === closestPage));
145
151
  }
146
152
 
147
153
  updateResultsPosition() {
154
+ if (!this.dom.searchNavigation) return;
148
155
  this.dom.searchNavigation.find('[data-id=resultsCount]').text(this.resultsPosition());
149
156
  }
150
157
 
151
158
  updateSearchNavigationButtons() {
159
+ if (!this.dom.searchNavigation) return;
152
160
  this.dom.searchNavigation.find('.prev').attr('disabled', !this.currentMatchIndex);
153
161
  this.dom.searchNavigation.find('.next').attr('disabled', this.currentMatchIndex + 1 === this.matches.length);
154
162
  }
@@ -223,22 +231,23 @@ class SearchView {
223
231
  renderPins(matches) {
224
232
  matches.forEach((match) => {
225
233
  const queryString = match.text;
226
- const pageIndex = this.br.leafNumToIndex(match.par[0].page);
227
- const pageNumber = this.br.getPageNum(pageIndex);
234
+ const pageIndex = this.br.book.leafNumToIndex(match.par[0].page);
228
235
  const uiStringSearch = "Search result"; // i18n
229
- const uiStringPage = "Page"; // i18n
230
236
 
231
- const percentThrough = this.br.constructor.util.cssPercentage(pageIndex, this.br.getNumLeafs() - 1);
237
+ const percentThrough = this.br.constructor.util.cssPercentage(pageIndex, this.br.book.getNumLeafs() - 1);
232
238
 
233
- const queryStringWithB = queryString.replace(this.matcher, '<b>$1</b>');
239
+ const escapedQueryString = escapeHTML(queryString);
240
+ const queryStringWithB = escapedQueryString.replace(this.matcher, '<b>$1</b>');
234
241
 
235
242
  let queryStringWithBTruncated = '';
236
243
 
237
244
  if (queryString.length > 100) {
238
- queryStringWithBTruncated = queryString
239
- .replace(/^(.{100}[^\s]*).*/, "$1")
245
+ queryStringWithBTruncated = queryString.replace(/^(.{100}[^\s]*).*/, "$1");
246
+
247
+ // If truncating, we must escape *after* truncation occurs (but before wrapping in <b>)
248
+ queryStringWithBTruncated = escapeHTML(queryStringWithBTruncated)
240
249
  .replace(this.matcher, '<b>$1</b>')
241
- + '...';
250
+ + '...';
242
251
  }
243
252
 
244
253
  // draw marker
@@ -251,34 +260,26 @@ class SearchView {
251
260
  .append(`
252
261
  <div class="BRquery">
253
262
  <div>${queryStringWithBTruncated || queryStringWithB}</div>
254
- <div>${uiStringPage} ${pageNumber}</div>
263
+ <div>Page ${match.displayPageNumber}</div>
255
264
  </div>
256
265
  `)
257
- .data({ pageIndex })
258
266
  .appendTo(this.br.$('.BRnavline'))
259
- .hover(
260
- (event) => {
261
- // remove from other markers then turn on just for this
262
- // XXX should be done when nav slider moves
263
- const marker = event.currentTarget;
264
- const tooltip = marker.querySelector('.BRquery');
265
- const tooltipOffset = tooltip.getBoundingClientRect();
266
- const targetOffset = marker.getBoundingClientRect();
267
- const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
268
- if (tooltipOffset.x - boxSizeAdjust < 0) {
269
- tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
270
- }
271
- $('.BRsearch,.BRchapter').removeClass('front');
272
- $(event.target).addClass('front');
273
- },
274
- (event) => $(event.target).removeClass('front'))
275
- .click(function (event) {
276
- // closures are nested and deep, using an arrow function breaks references.
277
- // Todo: update to arrow function & clean up closures
278
- // to remove `bind` dependency
279
- this.br._searchPluginGoToResult(+$(event.target).data('pageIndex'));
280
- this.br.updateSearchHilites();
281
- }.bind(this));
267
+ .on("mouseenter", (event) => {
268
+ // remove from other markers then turn on just for this
269
+ // XXX should be done when nav slider moves
270
+ const marker = event.currentTarget;
271
+ const tooltip = marker.querySelector('.BRquery');
272
+ const tooltipOffset = tooltip.getBoundingClientRect();
273
+ const targetOffset = marker.getBoundingClientRect();
274
+ const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
275
+ if (tooltipOffset.x - boxSizeAdjust < 0) {
276
+ tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
277
+ }
278
+ $('.BRsearch,.BRchapter').removeClass('front');
279
+ $(event.target).addClass('front');
280
+ })
281
+ .on("mouseleave", (event) => $(event.target).removeClass('front'))
282
+ .on("click", () => { this.br._searchPluginGoToResult(match.matchIndex); });
282
283
  });
283
284
  }
284
285
 
@@ -287,18 +288,27 @@ class SearchView {
287
288
  */
288
289
  toggleSearchPending(bool) {
289
290
  if (bool) {
290
- this.br.showProgressPopup("Search results will appear below...");
291
+ this.br.showProgressPopup("Search results will appear below...", () => this.progressPopupClosed());
291
292
  }
292
293
  else {
293
294
  this.br.removeProgressPopup();
294
295
  }
295
296
  }
296
297
 
297
- renderErrorModal() {
298
+ /**
299
+ * Primary callback when user cancels search popup
300
+ */
301
+ progressPopupClosed() {
302
+ this.toggleSearchPending();
303
+ this.cancelSearch();
304
+ }
305
+
306
+ renderErrorModal(textIsProcessing = false) {
307
+ const errorDetails = `${!textIsProcessing ? 'The text may still be processing. ' : ''}Please try again.`;
298
308
  this.renderModalMessage(`
299
309
  Sorry, there was an error with your search.
300
310
  <br />
301
- The text may still be processing.
311
+ ${errorDetails}
302
312
  `);
303
313
  this.delayModalRemovalFor(4000);
304
314
  }
@@ -390,9 +400,14 @@ class SearchView {
390
400
  this.setQuery(this.br.searchTerm);
391
401
  }
392
402
 
393
- handleSearchCallbackError() {
403
+ /**
404
+ * Event listener for: `BookReader:SearchCallbackError`
405
+ * @param {CustomEvent} event
406
+ */
407
+ handleSearchCallbackError(event = {}) {
394
408
  this.toggleSearchPending(false);
395
- this.renderErrorModal();
409
+ const isIndexed = event?.detail?.props?.results?.indexed;
410
+ this.renderErrorModal(isIndexed);
396
411
  }
397
412
 
398
413
  handleSearchCallbackBookNotIndexed() {
@@ -408,10 +423,10 @@ class SearchView {
408
423
  bindEvents() {
409
424
  const namespace = 'BookReader:';
410
425
 
426
+ window.addEventListener(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this));
411
427
  $(document).on(`${namespace}SearchCallback`, this.handleSearchCallback.bind(this))
412
428
  .on(`${namespace}navToggled`, this.handleNavToggledCallback.bind(this))
413
429
  .on(`${namespace}SearchStarted`, this.handleSearchStarted.bind(this))
414
- .on(`${namespace}SearchCallbackError`, this.handleSearchCallbackError.bind(this))
415
430
  .on(`${namespace}SearchCallbackBookNotIndexed`, this.handleSearchCallbackBookNotIndexed.bind(this))
416
431
  .on(`${namespace}SearchCallbackEmpty`, this.handleSearchCallbackEmpty.bind(this))
417
432
  .on(`${namespace}pageChanged`, this.updateSearchNavigation.bind(this));