@internetarchive/bookreader 5.0.0-90 → 5.0.0-92

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 (54) hide show
  1. package/BookReader/BookReader.js +1 -1
  2. package/BookReader/BookReader.js.map +1 -1
  3. package/BookReader/ia-bookreader-bundle.js +2 -2
  4. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  5. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  6. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  7. package/BookReader/plugins/plugin.autoplay.js +1 -1
  8. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  9. package/BookReader/plugins/plugin.chapters.js +2 -2
  10. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  11. package/BookReader/plugins/plugin.iiif.js +1 -1
  12. package/BookReader/plugins/plugin.iiif.js.map +1 -1
  13. package/BookReader/plugins/plugin.resume.js +1 -1
  14. package/BookReader/plugins/plugin.resume.js.map +1 -1
  15. package/BookReader/plugins/plugin.search.js +1 -1
  16. package/BookReader/plugins/plugin.search.js.map +1 -1
  17. package/BookReader/plugins/plugin.text_selection.js +1 -1
  18. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  19. package/BookReader/plugins/plugin.tts.js +1 -1
  20. package/BookReader/plugins/plugin.tts.js.map +1 -1
  21. package/BookReaderDemo/IADemoBr.js +29 -1
  22. package/BookReaderDemo/ia-multiple-volumes-manifest.js +0 -1
  23. package/CHANGELOG.md +28 -0
  24. package/README.md +1 -1
  25. package/package.json +1 -1
  26. package/src/BookNavigator/book-navigator.js +5 -2
  27. package/src/BookNavigator/search/search-provider.js +13 -7
  28. package/src/BookNavigator/sharing.js +1 -1
  29. package/src/BookReader/BookModel.js +5 -4
  30. package/src/BookReader/Toolbar/Toolbar.js +5 -0
  31. package/src/BookReader/options.js +10 -6
  32. package/src/BookReader.js +49 -23
  33. package/src/BookReaderPlugin.js +8 -0
  34. package/src/plugins/plugin.chapters.js +220 -157
  35. package/src/plugins/plugin.text_selection.js +19 -1
  36. package/src/plugins/search/plugin.search.js +330 -376
  37. package/src/plugins/search/view.js +13 -9
  38. package/src/plugins/tts/WebTTSEngine.js +67 -41
  39. package/src/plugins/tts/plugin.tts.js +1 -3
  40. package/src/plugins/tts/utils.js +13 -0
  41. package/src/util/browserSniffing.js +11 -1
  42. package/tests/e2e/helpers/mockSearch.js +1 -1
  43. package/tests/jest/BookNavigator/book-navigator.test.js +8 -3
  44. package/tests/jest/BookNavigator/search/search-provider.test.js +16 -4
  45. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +1 -1
  46. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +70 -0
  47. package/tests/jest/BookReader.test.js +26 -1
  48. package/tests/jest/plugins/plugin.chapters.test.js +56 -58
  49. package/tests/jest/plugins/search/plugin.search.test.js +17 -42
  50. package/tests/jest/plugins/search/plugin.search.view.test.js +10 -18
  51. package/tests/jest/plugins/tts/WebTTSEngine.test.js +18 -12
  52. package/tests/jest/plugins/url/plugin.url.test.js +1 -1
  53. package/tests/jest/util/browserSniffing.test.js +9 -3
  54. package/tests/jest/utils.js +4 -1
@@ -1,5 +1,4 @@
1
1
  // @ts-check
2
- /* global BookReader */
3
2
  /**
4
3
  * Plugin for Archive.org book search
5
4
  * Events fired at various points throughout search processing are published
@@ -25,66 +24,70 @@ import { poll } from '../../BookReader/utils.js';
25
24
  import { renderBoxesInPageContainerLayer } from '../../BookReader/PageContainer.js';
26
25
  import SearchView from './view.js';
27
26
  import { marshallSearchResults } from './utils.js';
27
+ import { BookReaderPlugin } from '../../BookReaderPlugin.js';
28
+ import { applyVariables } from '../../util/strings.js';
28
29
  /** @typedef {import('../../BookReader/PageContainer').PageContainer} PageContainer */
29
30
  /** @typedef {import('../../BookReader/BookModel').PageIndex} PageIndex */
30
31
  /** @typedef {import('../../BookReader/BookModel').LeafNum} LeafNum */
31
32
  /** @typedef {import('../../BookReader/BookModel').PageNumString} PageNumString */
32
33
 
33
- jQuery.extend(BookReader.defaultOptions, {
34
- server: 'ia600609.us.archive.org',
35
- bookId: '',
36
- subPrefix: '',
37
- bookPath: '',
38
- enableSearch: true,
39
- searchInsideProtocol: 'https',
40
- searchInsideUrl: '/fulltext/inside.php',
41
- searchInsidePreTag: '{{{',
42
- searchInsidePostTag: '}}}',
43
- initialSearchTerm: null,
44
- });
45
-
46
- /** @override */
47
- BookReader.prototype.setup = (function (super_) {
48
- return function (options) {
49
- super_.call(this, options);
34
+ // @ts-ignore
35
+ const BookReader = /** @type {typeof import('@/src/BookReader.js').default} */(window.BookReader);
36
+
37
+ export class SearchPlugin extends BookReaderPlugin {
38
+ options = {
39
+ enabled: true,
40
+ preTag: '{{{',
41
+ postTag: '}}}',
42
+ /**
43
+ * @type {import('@/src/util/strings.js').StringWithVars}
44
+ * Provides the variables: `query`, `preTag`, and `postTag` (from these options)
45
+ */
46
+ searchInsideUrl: '/fulltext/inside.php?' + [
47
+ 'q={{query|urlencode}}',
48
+ 'pre_tag={{preTag|urlencode}}',
49
+ 'post_tag={{postTag|urlencode}}',
50
+ ].join('&'),
51
+ /** @type {string} */
52
+ initialSearchTerm: null,
53
+ goToFirstResult: false,
54
+ }
55
+
56
+ /**
57
+ * @override
58
+ * @param {Partial<SearchPlugin['options']>} options
59
+ **/
60
+ setup(options) {
61
+ super.setup(options);
50
62
 
51
63
  this.searchTerm = '';
64
+ /** @type {SearchInsideResults | null} */
52
65
  this.searchResults = null;
53
- this.searchInsideUrl = options.searchInsideUrl;
54
- this.enableSearch = options.enableSearch;
55
-
56
- // Base server used by some api calls
57
- this.bookId = options.bookId;
58
- this.server = options.server;
59
- this.subPrefix = options.subPrefix;
60
- this.bookPath = options.bookPath;
61
-
66
+ this.searchInsideUrl = this.options.searchInsideUrl;
67
+ this.enabled = this.options.enabled;
62
68
  this.searchXHR = null;
63
- this._cancelSearch.bind(this);
64
- this.cancelSearchRequest.bind(this);
65
69
 
66
70
  /** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
67
71
  this._searchBoxesByIndex = {};
68
72
 
69
- if (this.enableSearch) {
73
+ if (this.enabled) {
70
74
  this.searchView = new SearchView({
71
- br: this,
75
+ br: this.br,
72
76
  searchCancelledCallback: () => {
73
77
  this._cancelSearch();
74
- this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
78
+ this.br.trigger('SearchCanceled', { term: this.searchTerm, instance: this.br });
75
79
  },
76
80
  });
77
81
  } else {
78
82
  this.searchView = null;
79
83
  }
80
- };
81
- })(BookReader.prototype.setup);
84
+ }
85
+
86
+ /** @override */
87
+ init() {
88
+ super.init();
82
89
 
83
- /** @override */
84
- BookReader.prototype.init = (function (super_) {
85
- return function () {
86
- super_.call(this);
87
- if (!this.enableSearch) return;
90
+ if (!this.enabled) return;
88
91
 
89
92
  this.searchView.init();
90
93
  if (this.options.initialSearchTerm) {
@@ -101,26 +104,27 @@ BookReader.prototype.init = (function (super_) {
101
104
  { goToFirstResult: this.options.goToFirstResult, suppressFragmentChange: false },
102
105
  );
103
106
  }
104
- };
105
- })(BookReader.prototype.init);
106
-
107
- /** @override */
108
- BookReader.prototype.buildToolbarElement = (function (super_) {
109
- return function () {
110
- const $el = super_.call(this);
111
- if (!this.enableSearch) { return; }
107
+ }
108
+
109
+ /**
110
+ * @override
111
+ * @protected
112
+ * @param {JQuery<HTMLElement>} $toolbarElement
113
+ */
114
+ _configureToolbar($toolbarElement) {
115
+ if (!this.enabled) { return; }
112
116
  if (this.searchView.dom.toolbarSearch) {
113
- $el.find('.BRtoolbarSectionInfo').after(this.searchView.dom.toolbarSearch);
117
+ $toolbarElement.find('.BRtoolbarSectionInfo').after(this.searchView.dom.toolbarSearch);
114
118
  }
115
- return $el;
116
- };
117
- })(BookReader.prototype.buildToolbarElement);
118
-
119
- /** @override */
120
- BookReader.prototype._createPageContainer = (function (super_) {
121
- return function (index) {
122
- const pageContainer = super_.call(this, index);
123
- if (this.enableSearch && pageContainer.page && index in this._searchBoxesByIndex) {
119
+ return $toolbarElement;
120
+ }
121
+
122
+ /**
123
+ * @override
124
+ * @param {import ("@/src/BookReader/PageContainer.js").PageContainer} pageContainer
125
+ */
126
+ _configurePageContainer(pageContainer) {
127
+ if (this.enabled && pageContainer.page && pageContainer.page.index in this._searchBoxesByIndex) {
124
128
  const pageIndex = pageContainer.page.index;
125
129
  const boxes = this._searchBoxesByIndex[pageIndex];
126
130
  renderBoxesInPageContainerLayer(
@@ -132,362 +136,312 @@ BookReader.prototype._createPageContainer = (function (super_) {
132
136
  );
133
137
  }
134
138
  return pageContainer;
135
- };
136
- })(BookReader.prototype._createPageContainer);
137
-
138
- /**
139
- * @typedef {object} SearchOptions
140
- * @property {boolean} goToFirstResult
141
- * @property {boolean} disablePopup
142
- * @property {(null|function)} error - @deprecated at v.5.0
143
- * @property {(null|function)} success - @deprecated at v.5.0
144
- */
145
-
146
- /**
147
- * Submits search request
148
- *
149
- * @param {string} term
150
- * @param {SearchOptions} overrides
151
- */
152
- BookReader.prototype.search = async function(term = '', overrides = {}) {
153
- /** @type {SearchOptions} */
154
- const defaultOptions = {
155
- goToFirstResult: false, /* jump to the first result (default=false) */
156
- disablePopup: false, /* don't show the modal progress (default=false) */
157
- suppressFragmentChange: false, /* don't change the URL on initial load */
158
- error: null, /* optional error handler (default=null) */
159
- success: null, /* optional success handler (default=null) */
160
-
161
- };
162
- const options = jQuery.extend({}, defaultOptions, overrides);
163
- this.suppressFragmentChange = options.suppressFragmentChange;
164
- this.searchCancelled = false;
165
-
166
- // strip slashes, since this goes in the url
167
- this.searchTerm = term.replace(/\//g, ' ');
168
-
169
- if (!options.suppressFragmentChange) {
170
- this.trigger(BookReader.eventNames.fragmentChange);
171
139
  }
172
140
 
173
- // Add quotes to the term. This is to compenstate for the backends default OR query
174
- // term = term.replace(/['"]+/g, '');
175
- // term = '"' + term + '"';
176
-
177
- // Remove the port and userdir
178
- const serverPath = this.server.replace(/:.+/, '');
179
- const baseUrl = `${this.options.searchInsideProtocol}://${serverPath}${this.searchInsideUrl}?`;
141
+ /**
142
+ * Submits search request
143
+ *
144
+ * @param {string} term
145
+ * @param {Partial<SearchOptions>} overrides
146
+ */
147
+ async search(term = '', overrides = {}) {
148
+ /** @type {SearchOptions} */
149
+ const defaultOptions = {
150
+ goToFirstResult: false, /* jump to the first result (default=false) */
151
+ disablePopup: false, /* don't show the modal progress (default=false) */
152
+ suppressFragmentChange: false, /* don't change the URL on initial load */
153
+ error: null, /* optional error handler (default=null) */
154
+ success: null, /* optional success handler (default=null) */
155
+
156
+ };
157
+ const options = jQuery.extend({}, defaultOptions, overrides);
158
+ this.suppressFragmentChange = options.suppressFragmentChange;
159
+ this.searchCancelled = false;
160
+
161
+ // strip slashes, since this goes in the url
162
+ this.searchTerm = term.replace(/\//g, ' ');
163
+
164
+ if (!options.suppressFragmentChange) {
165
+ this.br.trigger(BookReader.eventNames.fragmentChange);
166
+ }
180
167
 
181
- // Remove subPrefix from end of path
182
- let path = this.bookPath;
183
- const subPrefixWithSlash = `/${this.subPrefix}`;
184
- if (this.bookPath.length - this.bookPath.lastIndexOf(subPrefixWithSlash) == subPrefixWithSlash.length) {
185
- path = this.bookPath.substr(0, this.bookPath.length - subPrefixWithSlash.length);
186
- }
168
+ // Add quotes to the term. This is to compenstate for the backends default OR query
169
+ // term = term.replace(/['"]+/g, '');
170
+ // term = '"' + term + '"';
187
171
 
188
- const urlParams = {
189
- item_id: this.bookId,
190
- doc: this.subPrefix,
191
- path,
192
- q: term,
193
- pre_tag: this.options.searchInsidePreTag,
194
- post_tag: this.options.searchInsidePostTag,
195
- };
172
+ const url = applyVariables(this.options.searchInsideUrl, this.br.options.vars, {
173
+ query: term,
174
+ preTag: this.options.preTag,
175
+ postTag: this.options.postTag,
176
+ });
196
177
 
197
- // NOTE that the API does not expect / (slashes) to be encoded. (%2F) won't work
198
- const paramStr = $.param(urlParams).replace(/%2F/g, '/');
178
+ const callSearchResultsCallback = (searchInsideResults) => {
179
+ if (this.searchCancelled) {
180
+ return;
181
+ }
182
+ const responseHasError = searchInsideResults.error || !searchInsideResults.matches.length;
183
+ const hasCustomError = typeof options.error === 'function';
184
+ const hasCustomSuccess = typeof options.success === 'function';
185
+
186
+ if (responseHasError) {
187
+ console.error('Search Inside Response Error', searchInsideResults.error || 'matches.length == 0');
188
+ hasCustomError
189
+ ? options.error.call(this, searchInsideResults, options)
190
+ : this.BRSearchCallbackError(searchInsideResults);
191
+ } else {
192
+ hasCustomSuccess
193
+ ? options.success.call(this, searchInsideResults, options)
194
+ : this.BRSearchCallback(searchInsideResults, options);
195
+ }
196
+ };
197
+
198
+ this.br.trigger('SearchStarted', { term: this.searchTerm, instance: this.br });
199
+ callSearchResultsCallback(await $.ajax({
200
+ url: url,
201
+ cache: true,
202
+ xhrFields: {
203
+ withCredentials: this.br.protected,
204
+ },
205
+ beforeSend: xhr => { this.searchXHR = xhr; },
206
+ }));
207
+ }
199
208
 
200
- const url = `${baseUrl}${paramStr}`;
209
+ /**
210
+ * cancels AJAX Call
211
+ * emits custom event
212
+ */
213
+ _cancelSearch() {
214
+ this.searchXHR?.abort();
215
+ this.searchView.clearSearchFieldAndResults(false);
216
+ this.searchTerm = '';
217
+ this.searchXHR = null;
218
+ this.searchCancelled = true;
219
+ this.searchResults = null;
220
+ }
201
221
 
202
- const callSearchResultsCallback = (searchInsideResults) => {
203
- if (this.searchCancelled) {
204
- return;
222
+ /**
223
+ * External function to cancel search
224
+ * checks for term & xhr in flight before running
225
+ */
226
+ cancelSearchRequest() {
227
+ this.searchCancelled = true;
228
+ if (this.searchXHR !== null) {
229
+ this._cancelSearch();
230
+ this.searchView.toggleSearchPending();
231
+ this.br.trigger('SearchCanceled', { term: this.searchTerm, instance: this.br });
205
232
  }
206
- const responseHasError = searchInsideResults.error || !searchInsideResults.matches.length;
207
- const hasCustomError = typeof options.error === 'function';
208
- const hasCustomSuccess = typeof options.success === 'function';
209
-
210
- if (responseHasError) {
211
- console.error('Search Inside Response Error', searchInsideResults.error || 'matches.length == 0');
212
- hasCustomError
213
- ? options.error.call(this, searchInsideResults, options)
214
- : this.BRSearchCallbackError(searchInsideResults, options);
215
- } else {
216
- hasCustomSuccess
217
- ? options.success.call(this, searchInsideResults, options)
218
- : this.BRSearchCallback(searchInsideResults, options);
219
- }
220
- };
221
-
222
- this.trigger('SearchStarted', { term: this.searchTerm, instance: this });
223
- callSearchResultsCallback(await $.ajax({
224
- url: url,
225
- cache: true,
226
- xhrFields: {
227
- withCredentials: this.protected,
228
- },
229
- beforeSend: xhr => { this.searchXHR = xhr; },
230
- }));
231
- };
232
-
233
- /**
234
- * cancels AJAX Call
235
- * emits custom event
236
- */
237
- BookReader.prototype._cancelSearch = function () {
238
- this.searchXHR?.abort();
239
- this.searchView.clearSearchFieldAndResults(false);
240
- this.searchTerm = '';
241
- this.searchXHR = null;
242
- this.searchCancelled = true;
243
- this.searchResults = [];
244
- };
245
-
246
- /**
247
- * External function to cancel search
248
- * checks for term & xhr in flight before running
249
- */
250
- BookReader.prototype.cancelSearchRequest = function () {
251
- this.searchCancelled = true;
252
- if (this.searchXHR !== null) {
253
- this._cancelSearch();
254
- this.searchView.toggleSearchPending();
255
- this.trigger('SearchCanceled', { term: this.searchTerm, instance: this });
256
233
  }
257
- };
258
234
 
259
- /**
260
- * @typedef {object} SearchInsideMatchBox
261
- * @property {number} page
262
- * @property {number} r
263
- * @property {number} l
264
- * @property {number} b
265
- * @property {number} t
266
- * @property {HTMLDivElement} [div]
267
- * @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.
268
- */
269
-
270
- /**
271
- * @typedef {object} SearchInsideMatch
272
- * @property {number} matchIndex This is a fake field! Not part of the API response. It is added by the JS.
273
- * @property {string} displayPageNumber (fake field) The page number as it should be displayed in the UI.
274
- * @property {string} html (computed field) The html-escaped raw html to display in the UI.
275
- * @property {string} text
276
- * @property {Array<{ page: number, boxes: SearchInsideMatchBox[] }>} par
277
- */
235
+ /**
236
+ * Search Results return handler
237
+ * @param {SearchInsideResults} results
238
+ * @param {object} options
239
+ * @param {boolean} options.goToFirstResult
240
+ */
241
+ BRSearchCallback(results, options) {
242
+ marshallSearchResults(
243
+ results,
244
+ pageNum => this.br.book.getPageNum(this.br.book.leafNumToIndex(pageNum)),
245
+ this.options.preTag,
246
+ this.options.postTag,
247
+ );
248
+ this.searchResults = results || null;
249
+
250
+ this.updateSearchHilites();
251
+ this.br.removeProgressPopup();
252
+ if (options.goToFirstResult) {
253
+ this.jumpToMatch(0);
254
+ }
255
+ this.br.trigger('SearchCallback', { results, options, instance: this.br });
256
+ }
278
257
 
279
- /**
280
- * @typedef {object} SearchInsideResults
281
- * @property {string} error
282
- * @property {SearchInsideMatch[]} matches
283
- * @property {boolean} indexed
284
- */
258
+ /**
259
+ * Main search results error handler
260
+ * @param {SearchInsideResults} results
261
+ */
262
+ BRSearchCallbackError(results) {
263
+ this._BRSearchCallbackError(results);
264
+ }
285
265
 
286
- /**
287
- * Search Results return handler
288
- * @param {SearchInsideResults} results
289
- * @param {object} options
290
- * @param {boolean} options.goToFirstResult
291
- */
292
- BookReader.prototype.BRSearchCallback = function(results, options) {
293
- marshallSearchResults(
294
- results,
295
- pageNum => this.book.getPageNum(this.book.leafNumToIndex(pageNum)),
296
- this.options.searchInsidePreTag,
297
- this.options.searchInsidePostTag,
298
- );
299
- this.searchResults = results || [];
300
-
301
- this.updateSearchHilites();
302
- this.removeProgressPopup();
303
- if (options.goToFirstResult) {
304
- this._searchPluginGoToResult(0);
266
+ /**
267
+ * @private draws search results error
268
+ * @param {SearchInsideResults} results
269
+ */
270
+ _BRSearchCallbackError(results) {
271
+ this.searchResults = results;
272
+ const payload = {
273
+ term: this.searchTerm,
274
+ results,
275
+ instance: this.br,
276
+ };
277
+ if (results.error) {
278
+ this.br.trigger('SearchCallbackError', payload);
279
+ } else if (0 == results.matches.length) {
280
+ if (false === results.indexed) {
281
+ this.br.trigger('SearchCallbackBookNotIndexed', payload);
282
+ return;
283
+ }
284
+ this.br.trigger('SearchCallbackEmpty', payload);
285
+ }
305
286
  }
306
- this.trigger('SearchCallback', { results, options, instance: this });
307
- };
308
287
 
309
- /**
310
- * Main search results error handler
311
- * @callback
312
- * @param {SearchInsideResults} results
313
- */
314
- BookReader.prototype.BRSearchCallbackError = function(results) {
315
- this._BRSearchCallbackError(results);
316
- };
288
+ /**
289
+ * updates search on-page highlights controller
290
+ */
291
+ updateSearchHilites() {
292
+ /** @type {SearchInsideMatch[]} */
293
+ const matches = this.searchResults?.matches || [];
294
+ /** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
295
+ const boxesByIndex = {};
317
296
 
318
- /**
319
- * @private draws search results error
320
- * @callback
321
- * @param {SearchInsideResults} results
322
- * @param {jQuery} $el
323
- * @param {boolean} fade
324
- */
325
- BookReader.prototype._BRSearchCallbackError = function(results) {
326
- this.searchResults = results;
327
- const basePayload = {
328
- term: this.searchTerm,
329
- instance: this,
330
- };
331
- if (results.error) {
332
- const payload = Object.assign({}, basePayload, { results });
333
- this.trigger('SearchCallbackError', payload);
334
- } else if (0 == results.matches.length) {
335
- if (false === results.indexed) {
336
- this.trigger('SearchCallbackBookNotIndexed', basePayload);
337
- return;
297
+ // Clear any existing svg layers
298
+ this.removeSearchHilites();
299
+
300
+ // Group by pageIndex
301
+ for (const match of matches) {
302
+ for (const box of match.par[0].boxes) {
303
+ const pageIndex = this.br.book.leafNumToIndex(box.page);
304
+ const pageBoxes = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
305
+ pageBoxes.push(box);
306
+ }
338
307
  }
339
- this.trigger('SearchCallbackEmpty', basePayload);
340
- }
341
- };
342
308
 
343
- /**
344
- * updates search on-page highlights controller
345
- */
346
- BookReader.prototype.updateSearchHilites = function() {
347
- /** @type {SearchInsideMatch[]} */
348
- const matches = this.searchResults?.matches || [];
349
- /** @type { {[pageIndex: number]: SearchInsideMatchBox[]} } */
350
- const boxesByIndex = {};
351
-
352
- // Clear any existing svg layers
353
- this.removeSearchHilites();
354
-
355
- // Group by pageIndex
356
- for (const match of matches) {
357
- for (const box of match.par[0].boxes) {
358
- const pageIndex = this.book.leafNumToIndex(box.page);
359
- const pageBoxes = boxesByIndex[pageIndex] || (boxesByIndex[pageIndex] = []);
360
- pageBoxes.push(box);
309
+ // update any already created pages
310
+ for (const [pageIndexString, boxes] of Object.entries(boxesByIndex)) {
311
+ const pageIndex = parseFloat(pageIndexString);
312
+ const page = this.br.book.getPage(pageIndex);
313
+ const pageContainers = this.br.getActivePageContainerElementsForIndex(pageIndex);
314
+ for (const container of pageContainers) {
315
+ renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container, boxes.map(b => `match-index-${b.matchIndex}`));
316
+ }
361
317
  }
318
+
319
+ this._searchBoxesByIndex = boxesByIndex;
362
320
  }
363
321
 
364
- // update any already created pages
365
- for (const [pageIndexString, boxes] of Object.entries(boxesByIndex)) {
366
- const pageIndex = parseFloat(pageIndexString);
367
- const page = this.book.getPage(pageIndex);
368
- const pageContainers = this.getActivePageContainerElementsForIndex(pageIndex);
369
- for (const container of pageContainers) {
370
- renderBoxesInPageContainerLayer('searchHiliteLayer', boxes, page, container, boxes.map(b => `match-index-${b.matchIndex}`));
371
- }
322
+ /**
323
+ * remove search highlights
324
+ */
325
+ removeSearchHilites() {
326
+ $(this.br.getActivePageContainerElements()).find('.searchHiliteLayer').remove();
372
327
  }
373
328
 
374
- this._searchBoxesByIndex = boxesByIndex;
375
- };
329
+ /**
330
+ * Goes to the page specified. If the page is not viewable, tries to load the page
331
+ * FIXME Most of this logic is IA specific, and should be less integrated into here
332
+ * or at least more configurable.
333
+ * @param {number} matchIndex
334
+ */
335
+ async jumpToMatch(matchIndex) {
336
+ const match = this.searchResults?.matches[matchIndex];
337
+ const book = this.br.book;
338
+ const pageIndex = book.leafNumToIndex(match.par[0].page);
339
+ const page = book.getPage(pageIndex);
340
+ const onNearbyPage = Math.abs(this.br.currentIndex() - pageIndex) < 3;
341
+ let makeUnviewableAtEnd = false;
342
+ if (!page.isViewable) {
343
+ const resp = await fetch('/services/bookreader/request_page?' + new URLSearchParams({
344
+ id: this.options.bookId,
345
+ subprefix: this.options.subPrefix,
346
+ leafNum: page.leafNum,
347
+ })).then(r => r.json());
348
+
349
+ for (const leafNum of resp.value) {
350
+ book.getPage(book.leafNumToIndex(leafNum)).makeViewable();
351
+ }
376
352
 
377
- /**
378
- * remove search highlights
379
- */
380
- BookReader.prototype.removeSearchHilites = function() {
381
- $(this.getActivePageContainerElements()).find('.searchHiliteLayer').remove();
382
- };
353
+ // not able to show page; make the page viewable anyways so that it can
354
+ // actually open. On IA, it has a fallback to a special error page.
355
+ if (!resp.value.length) {
356
+ book.getPage(pageIndex).makeViewable();
357
+ makeUnviewableAtEnd = true;
358
+ }
383
359
 
384
- /**
385
- * @private
386
- * Goes to the page specified. If the page is not viewable, tries to load the page
387
- * FIXME Most of this logic is IA specific, and should be less integrated into here
388
- * or at least more configurable.
389
- * @param {number} matchIndex
390
- */
391
- BookReader.prototype._searchPluginGoToResult = async function (matchIndex) {
392
- const match = this.searchResults?.matches[matchIndex];
393
- const book = this.book;
394
- const pageIndex = book.leafNumToIndex(match.par[0].page);
395
- const page = book.getPage(pageIndex);
396
- const onNearbyPage = Math.abs(this.currentIndex() - pageIndex) < 3;
397
- let makeUnviewableAtEnd = false;
398
- if (!page.isViewable) {
399
- const resp = await fetch('/services/bookreader/request_page?' + new URLSearchParams({
400
- id: this.options.bookId,
401
- subprefix: this.options.subPrefix,
402
- leafNum: page.leafNum,
403
- })).then(r => r.json());
404
-
405
- for (const leafNum of resp.value) {
406
- book.getPage(book.leafNumToIndex(leafNum)).makeViewable();
360
+ // Trigger an update of book
361
+ this.br._modes.mode1Up.mode1UpLit.updatePages();
362
+ if (this.br.activeMode == this.br._modes.mode1Up) {
363
+ await this.br._modes.mode1Up.mode1UpLit.updateComplete;
364
+ }
407
365
  }
408
-
409
- // not able to show page; make the page viewable anyways so that it can
410
- // actually open. On IA, it has a fallback to a special error page.
411
- if (!resp.value.length) {
412
- book.getPage(pageIndex).makeViewable();
413
- makeUnviewableAtEnd = true;
366
+ /* this updates the URL */
367
+ if (!this.br._isIndexDisplayed(pageIndex)) {
368
+ this.suppressFragmentChange = false;
369
+ this.br.jumpToIndex(pageIndex);
414
370
  }
415
371
 
416
- // Trigger an update of book
417
- this._modes.mode1Up.mode1UpLit.updatePages();
418
- if (this.activeMode == this._modes.mode1Up) {
419
- await this._modes.mode1Up.mode1UpLit.updateComplete;
372
+ // Reset it to unviewable if it wasn't resolved
373
+ if (makeUnviewableAtEnd) {
374
+ book.getPage(pageIndex).makeViewable(false);
420
375
  }
421
- }
422
- /* this updates the URL */
423
- if (!this._isIndexDisplayed(pageIndex)) {
424
- this.suppressFragmentChange = false;
425
- this.jumpToIndex(pageIndex);
426
- }
427
376
 
428
- // Reset it to unviewable if it wasn't resolved
429
- if (makeUnviewableAtEnd) {
430
- book.getPage(pageIndex).makeViewable(false);
377
+ // Scroll/flash in the ui
378
+ const $boxes = await poll(() => $(`rect.match-index-${match.matchIndex}`), { until: result => result.length > 0 });
379
+ if ($boxes.length) {
380
+ $boxes.css('animation', 'none');
381
+ $boxes[0].scrollIntoView({
382
+ // Only vertically center the highlight if we're in 1up or in full screen. In
383
+ // 2up, if we're not fullscreen, the whole body gets scrolled around to try to
384
+ // center the highlight 🙄 See:
385
+ // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
386
+ // Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
387
+ // full-width, and covers up the last line of the highlight.
388
+ block: this.br.constMode1up == this.br.mode || this.br.isFullscreenActive ? 'center' : 'nearest',
389
+ inline: 'center',
390
+ behavior: onNearbyPage ? 'smooth' : 'auto',
391
+ });
392
+ // wait for animation to start
393
+ await new Promise(resolve => setTimeout(resolve, 100));
394
+ $boxes.removeAttr("style");
395
+ }
431
396
  }
432
397
 
433
- // Scroll/flash in the ui
434
- const $boxes = await poll(() => $(`rect.match-index-${match.matchIndex}`), { until: result => result.length > 0 });
435
- if ($boxes.length) {
436
- $boxes.css('animation', 'none');
437
- $boxes[0].scrollIntoView({
438
- // Only vertically center the highlight if we're in 1up or in full screen. In
439
- // 2up, if we're not fullscreen, the whole body gets scrolled around to try to
440
- // center the highlight 🙄 See:
441
- // https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move/11041376
442
- // Note: nearest doesn't quite work great, because the ReadAloud toolbar is now
443
- // full-width, and covers up the last line of the highlight.
444
- block: this.constMode1up == this.mode || this.isFullscreenActive ? 'center' : 'nearest',
445
- inline: 'center',
446
- behavior: onNearbyPage ? 'smooth' : 'auto',
447
- });
448
- // wait for animation to start
449
- await new Promise(resolve => setTimeout(resolve, 100));
450
- $boxes.removeAttr("style");
398
+ /**
399
+ * Removes all search pins
400
+ */
401
+ removeSearchResults(suppressFragmentChange = false) {
402
+ this.removeSearchHilites(); //be sure to set all box.divs to null
403
+ this.searchTerm = null;
404
+ this.searchResults = null;
405
+ if (!suppressFragmentChange) {
406
+ this.br.trigger(BookReader.eventNames.fragmentChange);
407
+ }
451
408
  }
452
- };
409
+ }
410
+ BookReader?.registerPlugin('search', SearchPlugin);
453
411
 
454
412
  /**
455
- * Removes all search pins
413
+ * @typedef {object} SearchOptions
414
+ * @property {boolean} goToFirstResult
415
+ * @property {boolean} disablePopup
416
+ * @property {boolean} suppressFragmentChange
417
+ * @property {(null|function)} error (deprecated)
418
+ * @property {(null|function)} success (deprecated)
456
419
  */
457
- BookReader.prototype.removeSearchResults = function(suppressFragmentChange = false) {
458
- this.removeSearchHilites(); //be sure to set all box.divs to null
459
- this.searchTerm = null;
460
- this.searchResults = null;
461
- if (!suppressFragmentChange) {
462
- this.trigger(BookReader.eventNames.fragmentChange);
463
- }
464
- };
465
420
 
466
421
  /**
467
- * Returns true if a search highlight is currently being displayed
468
- * @returns {boolean}
422
+ * @typedef {object} SearchInsideMatchBox
423
+ * @property {number} page
424
+ * @property {number} r
425
+ * @property {number} l
426
+ * @property {number} b
427
+ * @property {number} t
428
+ * @property {HTMLDivElement} [div]
429
+ * @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.
469
430
  */
470
- BookReader.prototype.searchHighlightVisible = function() {
471
- const results = this.searchResults;
472
- let visiblePages = [];
473
- if (null == results) return false;
474
-
475
- if (this.constMode2up == this.mode) {
476
- visiblePages = [this.twoPage.currentIndexL, this.twoPage.currentIndexR];
477
- } else if (this.constMode1up == this.mode) {
478
- visiblePages = [this.currentIndex()];
479
- } else {
480
- return false;
481
- }
482
431
 
483
- results.matches.some(match => {
484
- return match.par[0].boxes.some(box => {
485
- const pageIndex = this.book.leafNumToIndex(box.page);
486
- if (jQuery.inArray(pageIndex, visiblePages) >= 0) {
487
- return true;
488
- }
489
- });
490
- });
432
+ /**
433
+ * @typedef {object} SearchInsideMatch
434
+ * @property {number} matchIndex This is a fake field! Not part of the API response. It is added by the JS.
435
+ * @property {string} displayPageNumber (fake field) The page number as it should be displayed in the UI.
436
+ * @property {string} html (computed field) The html-escaped raw html to display in the UI.
437
+ * @property {string} text
438
+ * @property {Array<{ page: number, boxes: SearchInsideMatchBox[] }>} par
439
+ */
491
440
 
492
- return false;
493
- };
441
+ /**
442
+ * @typedef {object} SearchInsideResults
443
+ * @property {string} q
444
+ * @property {string} error
445
+ * @property {SearchInsideMatch[]} matches
446
+ * @property {boolean} indexed
447
+ */