@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
@@ -45,6 +45,23 @@ BookReader.optionOverrides.imagesBaseURL = '/BookReader/images/';
45
45
  const initializeBookReader = (brManifest) => {
46
46
  console.log('initializeBookReader', brManifest);
47
47
 
48
+ const {bookPath, subPrefix} = brManifest.data.brOptions;
49
+ let path = bookPath;
50
+ const subPrefixWithSlash = `/${subPrefix}`;
51
+ if (bookPath.length - bookPath.lastIndexOf(subPrefixWithSlash) == subPrefixWithSlash.length) {
52
+ path = bookPath.substr(0, bookPath.length - subPrefixWithSlash.length);
53
+ }
54
+
55
+ const searchInsideUrl = '//{{server}}/fulltext/inside.php?' + [
56
+ 'item_id={{bookId|urlencode}}',
57
+ 'doc={{subPrefix|urlencode}}',
58
+ 'q={{query|urlencode}}',
59
+ // This endpoint doesn't expect the path to be url encoded
60
+ `path=${encodeURIComponent(path).replace(/%2F/g, '/')}`,
61
+ 'pre_tag={{preTag|urlencode}}',
62
+ 'post_tag={{postTag|urlencode}}',
63
+ ].join('&');
64
+
48
65
  const options = {
49
66
  el: '#BookReader',
50
67
  /* Url plugin - IA uses History mode for URL */
@@ -59,7 +76,6 @@ const initializeBookReader = (brManifest) => {
59
76
  enableBookTitleLink: false,
60
77
  bookUrlText: null,
61
78
  startFullscreen: openFullImmersionTheater,
62
- initialSearchTerm: searchTerm ? searchTerm : '',
63
79
  // leaving this option commented out bc we change given user agent on archive.org
64
80
  // onePage: { autofit: <?=json_encode($this->ios ? 'width' : 'auto')?> },
65
81
  showToolbar: getFromUrl('options.showToolbar', 'false') === 'true',
@@ -70,6 +86,18 @@ const initializeBookReader = (brManifest) => {
70
86
  /* End multiple volumes */
71
87
  enableBookmarks: true, // turn this on
72
88
  enableFSLogoShortcut: true,
89
+
90
+ // TMP: To be replaced once BookReaderJSIA is updated to provide
91
+ // these in the right spot.
92
+ plugins: {
93
+ search: {
94
+ enabled: true,
95
+ initialSearchTerm: searchTerm ? searchTerm : '',
96
+ searchInsideUrl,
97
+ preTag: brManifest.data.brOptions.searchInsidePreTag,
98
+ postTag: brManifest.data.brOptions.searchInsidePostTag,
99
+ },
100
+ },
73
101
  };
74
102
 
75
103
  // we want to show item as embedded when ?ui=embed is in URI
@@ -5,7 +5,6 @@ const extraVolOptions = {
5
5
  "enableBookTitleLink": false,
6
6
  "bookUrlText": null,
7
7
  "startFullscreen": false,
8
- "initialSearchTerm": null,
9
8
  "onePage": {
10
9
  "autofit": "auto"
11
10
  },
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # 5.0.0-92
2
+ - Refactor: Migrate search plugin to BookReaderPlugin system @cdrini
3
+ - Breaking changes:
4
+ - Search options are now nested under the plugin (eg `searchInsideUrl` → `options.plugins.search.searchInsideUrl`), and mostly no longer include the `search` prefix; specifically:
5
+ - `searchInsidePreTag`, `searchInsidePostTag` → `options.plugins.search.preTag` and `postTag`
6
+ - `searchInsideUrl` is now a `StringWithVars` and supports including `vars` from the root options, e.g. `searchInsideUrl: "https://{{server}}/search_endpoint?q={{query|urlencode}}"` . It also has access to the `preTag` and `postTag` from the options. Note this allows the search inside URL to now be fully customizable from the options, and is no longer hard-coded internally to Internet Archive specific logic.
7
+ - `initialSearchTerm` → `options.plugins.search.initialSearchTerm`
8
+ - `searchInsideProtocol` → deprecated in favour of variables in `searchInsideUrl`
9
+ - Internal search related methods/properties are now nested in the `SearchPlugin` and no longer accessible from the root bookreader object. Notable exception: `br.search` is still made available for convenience.
10
+ - Moved: `searchResults`, `searchXHR`, `searchView`, `cancelSearchRequest`, `BRSearchCallback`, `updateSearchHilites`, `removeSearchResults`, `removeSearchHilites`, `searchHighlightVisible`
11
+
12
+
13
+ # 5.0.0-91
14
+ - Refactor: Migrate ChaptersPlugin to BookReaderPlugin system @cdrini
15
+ - Breaking changes:
16
+ - Options moved:
17
+ - `options.olHost` → `options.plugins.chapters.olHost`
18
+ - `options.enableChaptersPlugin` → `options.plugins.chapters.enabled`
19
+ - Note `table_of_contents` _did not_ move; that is still root-level
20
+ - All chapters state/methods moved from `BookReader` to `BookReader.plugins.chapters`. Full list:
21
+ - eg `BookReader._chapters*` has been moved to `BookReader.plugins.chapters._*`
22
+ - See https://github.com/internetarchive/bookreader/pull/1381
23
+ - Feature: TextSelectionPlugin now supports `protected` option @cdrini
24
+ - Feature: New option for ChaptersPlugin for explicit `openLibraryId` @cdrini
25
+ - Fix: No longer error when book ends on unviewable page @cdrini
26
+ - Fix: ReadAloud no longer jumps to top of page every time it scrolls @cdrini
27
+ - Fix: Fix various ReadAloud issues -- ReadAloud stuttering, not stopping, getting stuck, etc. @cdrini
28
+
1
29
  # 5.0.0-90
2
30
  - Fix: Festival TTS not working @cdrini
3
31
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Build Status](https://github.com/internetarchive/bookreader/actions/workflows/node.js.yml/badge.svg?branch=master) [![codecov](https://codecov.io/gh/internetarchive/bookreader/branch/master/graph/badge.svg)](https://codecov.io/gh/internetarchive/bookreader)
4
4
 
5
- **Disclaimer: BookReader v5 is currently in beta. It's stable enough for production use (and is what is being used on archive.org), but there will be some breaking changes in the next ~month or so to public BookReader APIs.**
5
+ **Disclaimer: BookReader v5 is currently in beta. It is stable enough for production use and is actively deployed on archive.org. Future updates while in v5 beta may introduce breaking changes to public BookReader APIs, although these will be noted in the CHANGELOG.**
6
6
 
7
7
 
8
8
  <p align="center">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-90",
3
+ "version": "5.0.0-92",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,6 +10,7 @@ import BookmarksProvider from './bookmarks/bookmarks-provider.js';
10
10
  import SharingProvider from './sharing.js';
11
11
  import ViewableFilesProvider from './viewable-files.js';
12
12
  import iaLogo from './assets/ia-logo.js';
13
+ /** @typedef {import('@/src/BookReader.js').default} BookReader */
13
14
 
14
15
  const events = {
15
16
  menuUpdated: 'menuUpdated',
@@ -183,7 +184,9 @@ export class BookNavigator extends LitElement {
183
184
  providers.downloads = new DownloadProvider(this.baseProviderConfig);
184
185
  }
185
186
 
186
- if (this.bookreader.options.enableSearch) {
187
+ // Note plugins will never be null-ish in runtime, but some of the unit tests
188
+ // stub BR with a nullish value there.
189
+ if (this.bookreader.options.plugins?.search?.enabled) {
187
190
  providers.search = new SearchProvider({
188
191
  ...this.baseProviderConfig,
189
192
  /**
@@ -193,7 +196,7 @@ export class BookNavigator extends LitElement {
193
196
  */
194
197
  onProviderChange: (brInstance = null, searchUpdates = {}) => {
195
198
  if (brInstance) {
196
- /* refresh br instance reference */
199
+ /** @type {BookReader} refresh br instance reference */
197
200
  this.bookreader = brInstance;
198
201
  }
199
202
 
@@ -1,7 +1,10 @@
1
+ // @ts-check
1
2
  import { html, nothing } from 'lit';
2
3
  import '@internetarchive/icon-search/icon-search';
3
4
  import './search-results';
4
5
  /** @typedef {import('@/src/plugins/search/plugin.search.js').SearchInsideMatch} SearchInsideMatch */
6
+ /** @typedef {import('@/src/plugins/search/plugin.search.js').SearchInsideResults} SearchInsideResults */
7
+ /** @typedef {import('@/src/BookReader.js').default} BookReader */
5
8
 
6
9
  let searchState = {
7
10
  query: '',
@@ -99,11 +102,14 @@ export default class SearchProvider {
99
102
  this.bookreader.search(searchState.query);
100
103
  }
101
104
 
105
+ /**
106
+ * @param {CustomEvent<{props: {instance: BookReader, results: SearchInsideResults}}>} event
107
+ */
102
108
  onSearchRequestError(event, errorType = 'default') {
103
- const { detail: { props = {} } } = event;
104
- const { instance = null } = props;
109
+ const { detail: { props } } = event;
110
+ const { instance, results } = props;
105
111
  if (instance) {
106
- /* keep bookreader instance reference up-to-date */
112
+ /** @type {BookReader} keep bookreader instance reference up-to-date */
107
113
  this.bookreader = instance;
108
114
  }
109
115
  const errorMessages = {
@@ -114,7 +120,7 @@ export default class SearchProvider {
114
120
  };
115
121
 
116
122
  const messageToShow = errorMessages[errorType] ?? errorMessages.default;
117
- searchState.query = instance?.searchResults?.q || '';
123
+ searchState.query = results?.q || '';
118
124
  searchState.results = [];
119
125
  searchState.resultsCount = 0;
120
126
  searchState.queryInProgress = false;
@@ -137,7 +143,7 @@ export default class SearchProvider {
137
143
  }
138
144
 
139
145
  searchCanceledInMenu() {
140
- this.bookreader?.cancelSearchRequest();
146
+ this.bookreader._plugins.search.cancelSearchRequest();
141
147
  }
142
148
 
143
149
  onSearchResultsCleared() {
@@ -149,7 +155,7 @@ export default class SearchProvider {
149
155
  errorMessage: '',
150
156
  };
151
157
  this.updateMenu({ openMenu: false });
152
- this.bookreader?.searchView?.clearSearchFieldAndResults(false);
158
+ this.bookreader._plugins.search.searchView.clearSearchFieldAndResults(false);
153
159
  if (this.bookreader.urlPlugin) {
154
160
  this.updateSearchInUrl();
155
161
  }
@@ -198,6 +204,6 @@ export default class SearchProvider {
198
204
  * @param {{ detail: {match: SearchInsideMatch} }} param0
199
205
  */
200
206
  onSearchResultsClicked({ detail }) {
201
- this.bookreader._searchPluginGoToResult(detail.match.matchIndex);
207
+ this.bookreader._plugins.search.jumpToMatch(detail.match.matchIndex);
202
208
  }
203
209
  }
@@ -10,7 +10,7 @@ export default class SharingProvider {
10
10
  }) {
11
11
  const { identifier, creator, title } = item?.metadata;
12
12
  const creatorToUse = Array.isArray(creator) ? creator[0] : creator;
13
- const subPrefix = bookreader.options.subPrefix || '';
13
+ const subPrefix = bookreader.subPrefix || '';
14
14
  const label = `Share this book`;
15
15
  this.icon = html`${iauxShareIcon}`;
16
16
  this.label = label;
@@ -274,6 +274,7 @@ export class BookModel {
274
274
  * @param {number} [arg0.end] exclusive
275
275
  * @param {boolean} [arg0.combineConsecutiveUnviewables] Yield only first unviewable
276
276
  * of a chunk of unviewable pages instead of each page
277
+ * @return {Generator<PageModel>}
277
278
  */
278
279
  * pagesIterator({ start = 0, end = Infinity, combineConsecutiveUnviewables = false } = {}) {
279
280
  start = Math.max(0, start);
@@ -483,7 +484,7 @@ export class PageModel {
483
484
  * @param {object} [arg0]
484
485
  * @param {boolean} [arg0.combineConsecutiveUnviewables] Whether to only yield the first page
485
486
  * of a series of unviewable pages instead of each page
486
- * @return {PageModel|void}
487
+ * @return {PageModel|undefined}
487
488
  */
488
489
  findNext({ combineConsecutiveUnviewables = false } = {}) {
489
490
  return this.book
@@ -495,7 +496,7 @@ export class PageModel {
495
496
  * @param {object} [arg0]
496
497
  * @param {boolean} [arg0.combineConsecutiveUnviewables] Whether to only yield the first page
497
498
  * of a series of unviewable pages instead of each page
498
- * @return {PageModel|void}
499
+ * @return {PageModel|undefined}
499
500
  */
500
501
  findPrev({ combineConsecutiveUnviewables = false } = {}) {
501
502
  if (this.index == 0) return undefined;
@@ -518,7 +519,7 @@ export class PageModel {
518
519
  * @param {object} [arg0]
519
520
  * @param {boolean} [arg0.combineConsecutiveUnviewables] Whether to only yield the first page
520
521
  * of a series of unviewable pages instead of each page
521
- * @return {PageModel|void}
522
+ * @return {PageModel|undefined}
522
523
  */
523
524
  findLeft({ combineConsecutiveUnviewables = false } = {}) {
524
525
  return this.book.pageProgression === 'lr' ? this.findPrev({ combineConsecutiveUnviewables }) : this.findNext({ combineConsecutiveUnviewables });
@@ -528,7 +529,7 @@ export class PageModel {
528
529
  * @param {object} [arg0]
529
530
  * @param {boolean} [arg0.combineConsecutiveUnviewables] Whether to only yield the first page
530
531
  * of a series of unviewable pages instead of each page
531
- * @return {PageModel|void}
532
+ * @return {PageModel|undefined}
532
533
  */
533
534
  findRight({ combineConsecutiveUnviewables = false } = {}) {
534
535
  return this.book.pageProgression === 'lr' ? this.findNext({ combineConsecutiveUnviewables }) : this.findPrev({ combineConsecutiveUnviewables });
@@ -59,6 +59,11 @@ export class Toolbar {
59
59
  $titleSectionEl.append(br.bookUrlText || br.bookTitle);
60
60
  }
61
61
 
62
+ // Call _bindNavigationHandlers on the plugins
63
+ for (const plugin of Object.values(br._plugins)) {
64
+ plugin._configureToolbar(br.refs.$BRtoolbar);
65
+ }
66
+
62
67
  // const $hamburger = br.refs.$BRtoolbar.find('BRtoolbarHamburger');
63
68
  return br.refs.$BRtoolbar;
64
69
  }
@@ -141,17 +141,21 @@ export const DEFAULT_OPTIONS = {
141
141
  * but going forward we'll keep them here.
142
142
  **/
143
143
  plugins: {
144
- /** @type {import('../plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin['options']}*/
144
+ /** @type {Partial<import('../plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin['options'>]}*/
145
145
  archiveAnalytics: null,
146
- /** @type {import('../plugins/plugin.autoplay.js').AutoplayPlugin['options']}*/
146
+ /** @type {Partial<import('../plugins/plugin.autoplay.js').AutoplayPlugin['options'>]}*/
147
147
  autoplay: null,
148
- /** @type {import('../plugins/plugin.iiif.js').IiifPlugin['options']} */
148
+ /** @type {Partial<import('../plugins/plugin.chapters.js').ChaptersPlugin['options']>} */
149
+ chapters: null,
150
+ /** @type {Partial<import('../plugins/plugin.iiif.js').IiifPlugin['options']>} */
149
151
  iiif: null,
150
- /** @type {import('../plugins/plugin.resume.js').ResumePlugin['options']} */
152
+ /** @type {Partial<import('../plugins/plugin.resume.js').ResumePlugin['options']>} */
151
153
  resume: null,
152
- /** @type {import('../plugins/plugin.text_selection.js').TextSelectionPlugin['options']} */
154
+ /** @type {Partial<import('../plugins/search/plugin.search.js').SearchPlugin['options']>} */
155
+ search: null,
156
+ /** @type {Partial<import('../plugins/plugin.text_selection.js').TextSelectionPlugin['options']>} */
153
157
  textSelection: null,
154
- /** @type {import('../plugins/tts/plugin.tts.js').TtsPlugin['options']} */
158
+ /** @type {Partial<import('../plugins/tts/plugin.tts.js').TtsPlugin['options']>} */
155
159
  tts: null,
156
160
  },
157
161
 
package/src/BookReader.js CHANGED
@@ -44,7 +44,7 @@ import { NAMED_REDUCE_SETS } from './BookReader/ReduceSet';
44
44
 
45
45
  /**
46
46
  * BookReader
47
- * @param {BookReaderOptions} options
47
+ * @param {Partial<BookReaderOptions>} overrides
48
48
  * TODO document all options properties
49
49
  * @constructor
50
50
  */
@@ -70,8 +70,12 @@ BookReader.PLUGINS = {
70
70
  archiveAnalytics: null,
71
71
  /** @type {typeof import('./plugins/plugin.autoplay.js').AutoplayPlugin | null}*/
72
72
  autoplay: null,
73
+ /** @type {typeof import('./plugins/plugin.chapters.js').ChaptersPlugin | null}*/
74
+ chapters: null,
73
75
  /** @type {typeof import('./plugins/plugin.resume.js').ResumePlugin | null}*/
74
76
  resume: null,
77
+ /** @type {typeof import('./plugins/search/plugin.search.js').SearchPlugin | null}*/
78
+ search: null,
75
79
  /** @type {typeof import('./plugins/plugin.text_selection.js').TextSelectionPlugin | null}*/
76
80
  textSelection: null,
77
81
  /** @type {typeof import('./plugins/tts/plugin.tts.js').TtsPlugin | null}*/
@@ -80,7 +84,7 @@ BookReader.PLUGINS = {
80
84
 
81
85
  /**
82
86
  * @param {string} pluginName
83
- * @param {typeof import('./BookReaderPlugin.js').BookReaderPlugin} plugin
87
+ * @param {new (...args: any[]) => import('./BookReaderPlugin.js').BookReaderPlugin} plugin
84
88
  */
85
89
  BookReader.registerPlugin = function(pluginName, plugin) {
86
90
  if (BookReader.PLUGINS[pluginName]) {
@@ -116,10 +120,25 @@ BookReader.prototype.setup = function(options) {
116
120
  // Store the options used to setup bookreader
117
121
  this.options = options;
118
122
 
123
+ /** @type {import('@/src/BookNavigator/book-navigator.js').BookNavigator} */
124
+ this.shell;
125
+
126
+ // Base server used by some api calls
127
+ /** @deprecated */
128
+ this.bookId = options.bookId;
129
+ /** @deprecated */
130
+ this.server = options.server;
131
+ /** @deprecated */
132
+ this.subPrefix = options.subPrefix;
133
+ /** @deprecated */
134
+ this.bookPath = options.bookPath;
135
+
119
136
  // Construct the usual plugins first to get type hints
120
137
  this._plugins = {
121
138
  archiveAnalytics: BookReader.PLUGINS.archiveAnalytics ? new BookReader.PLUGINS.archiveAnalytics(this) : null,
122
139
  autoplay: BookReader.PLUGINS.autoplay ? new BookReader.PLUGINS.autoplay(this) : null,
140
+ chapters: BookReader.PLUGINS.chapters ? new BookReader.PLUGINS.chapters(this) : null,
141
+ search: BookReader.PLUGINS.search ? new BookReader.PLUGINS.search(this) : null,
123
142
  resume: BookReader.PLUGINS.resume ? new BookReader.PLUGINS.resume(this) : null,
124
143
  textSelection: BookReader.PLUGINS.textSelection ? new BookReader.PLUGINS.textSelection(this) : null,
125
144
  tts: BookReader.PLUGINS.tts ? new BookReader.PLUGINS.tts(this) : null,
@@ -148,12 +167,14 @@ BookReader.prototype.setup = function(options) {
148
167
  }
149
168
  }
150
169
 
170
+ if (this._plugins.search?.options.enabled) {
171
+ // Expose the search method for convenience / backward compat
172
+ this.search = this._plugins.search.search.bind(this._plugins.search);
173
+ }
174
+
151
175
  /** @type {number} @deprecated some past iterations set this */
152
176
  this.numLeafs = undefined;
153
177
 
154
- /** Overridden by plugin.search.js */
155
- this.enableSearch = false;
156
-
157
178
  /**
158
179
  * Store viewModeOrder states
159
180
  * @var {boolean}
@@ -441,23 +462,24 @@ BookReader.prototype.initParams = function() {
441
462
  }
442
463
 
443
464
  // Check for Search plugin
444
- if (this.options.enableSearch) {
465
+ if (this._plugins.search?.options.enabled) {
466
+ const sp = this._plugins.search;
445
467
  // Go to first result only if no default or URL page
446
- this.options.goToFirstResult = !params.pageFound;
468
+ sp.options.goToFirstResult = !params.pageFound;
447
469
 
448
470
  // If initialSearchTerm not set
449
- if (!this.options.initialSearchTerm) {
471
+ if (!sp.initialSearchTerm) {
450
472
  // Look for any term in URL
451
473
  if (params.search) {
452
474
  // Old style: /search/[term]
453
- this.options.initialSearchTerm = params.search;
454
- this.searchTerm = params.search;
475
+ sp.options.initialSearchTerm = params.search;
476
+ sp.searchTerm = params.search;
455
477
  } else {
456
478
  // If we have a query string: q=[term]
457
479
  const searchParams = new URLSearchParams(this.readQueryString());
458
480
  const searchTerm = searchParams.get('q');
459
481
  if (searchTerm) {
460
- this.options.initialSearchTerm = utils.decodeURIComponentPlus(searchTerm);
482
+ sp.options.initialSearchTerm = utils.decodeURIComponentPlus(searchTerm);
461
483
  }
462
484
  }
463
485
  }
@@ -638,7 +660,7 @@ BookReader.prototype.init = function() {
638
660
  }
639
661
 
640
662
  // If not searching, set to allow on-going fragment changes
641
- if (!this.options.initialSearchTerm) {
663
+ if (!this._plugins.search?.options.initialSearchTerm) {
642
664
  this.suppressFragmentChange = false;
643
665
  }
644
666
 
@@ -1052,16 +1074,20 @@ BookReader.prototype._isIndexDisplayed = function(index) {
1052
1074
  BookReader.prototype.jumpToIndex = function(index, pageX, pageY, noAnimate) {
1053
1075
  // Don't jump into specific unviewable page
1054
1076
  const page = this.book.getPage(index);
1077
+
1055
1078
  if (!page.isViewable && page.unviewablesStart != page.index) {
1056
1079
  // If already in unviewable range, jump to end of that range
1057
1080
  const alreadyInPreview = this._isIndexDisplayed(page.unviewablesStart);
1058
- const newIndex = alreadyInPreview ? page.findNext({ combineConsecutiveUnviewables: true }).index : page.unviewablesStart;
1059
- return this.jumpToIndex(newIndex, pageX, pageY, noAnimate);
1060
- }
1061
-
1062
- this.trigger(BookReader.eventNames.stop);
1081
+ const newIndex = alreadyInPreview ? page.findNext({ combineConsecutiveUnviewables: true })?.index : page.unviewablesStart;
1082
+ // Rare, but a book can end on an unviewable page, so this could be undefined
1083
+ if (typeof newIndex !== 'undefined') {
1084
+ this.jumpToIndex(newIndex, pageX, pageY, noAnimate);
1085
+ }
1086
+ } else {
1087
+ this.trigger(BookReader.eventNames.stop);
1063
1088
 
1064
- this.activeMode.jumpToIndex(index, pageX, pageY, noAnimate);
1089
+ this.activeMode.jumpToIndex(index, pageX, pageY, noAnimate);
1090
+ }
1065
1091
  };
1066
1092
 
1067
1093
  /**
@@ -1298,7 +1324,7 @@ BookReader.prototype.updateFirstIndex = function(
1298
1324
  // If there's an initial search we stop suppressing global URL changes
1299
1325
  // when local suppression ends
1300
1326
  // This seems to correctly handle multiple calls during mode/1up
1301
- if (this.options.initialSearchTerm && !suppressFragmentChange) {
1327
+ if (this._plugins.search.options.initialSearchTerm && !suppressFragmentChange) {
1302
1328
  this.suppressFragmentChange = false;
1303
1329
  }
1304
1330
 
@@ -1627,8 +1653,8 @@ BookReader.prototype.updateFromParams = function(params) {
1627
1653
  // process /search
1628
1654
  // @deprecated for urlMode 'history'
1629
1655
  // Continues to work for urlMode 'hash'
1630
- if (this.enableSearch && 'undefined' != typeof(params.search)) {
1631
- if (this.searchTerm !== params.search) {
1656
+ if (this._plugins.search?.enabled && 'undefined' != typeof(params.search)) {
1657
+ if (this._plugins.search.searchTerm !== params.search) {
1632
1658
  this.$('.BRsearchInput').val(params.search);
1633
1659
  }
1634
1660
  }
@@ -1832,8 +1858,8 @@ BookReader.prototype.paramsFromCurrent = function() {
1832
1858
  params.view = fullscreenView;
1833
1859
  }
1834
1860
  // Search
1835
- if (this.enableSearch) {
1836
- params.search = this.searchTerm;
1861
+ if (this._plugins.search?.enabled) {
1862
+ params.search = this._plugins.search.searchTerm;
1837
1863
  }
1838
1864
 
1839
1865
  return params;
@@ -34,6 +34,14 @@ export class BookReaderPlugin {
34
34
  _configurePageContainer(pageContainer) {
35
35
  }
36
36
 
37
+ /**
38
+ * @abstract
39
+ * @protected
40
+ * @param {JQuery<HTMLElement>} $toolbarElement
41
+ */
42
+ _configureToolbar($toolbarElement) {
43
+ }
44
+
37
45
  /** @abstract @protected */
38
46
  _bindNavigationHandlers() {}
39
47