@internetarchive/bookreader 5.0.0-91 → 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 (43) 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 +12 -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/Toolbar/Toolbar.js +5 -0
  30. package/src/BookReader/options.js +9 -7
  31. package/src/BookReader.js +31 -15
  32. package/src/BookReaderPlugin.js +8 -0
  33. package/src/plugins/plugin.text_selection.js +3 -1
  34. package/src/plugins/search/plugin.search.js +330 -376
  35. package/src/plugins/search/view.js +13 -9
  36. package/tests/e2e/helpers/mockSearch.js +1 -1
  37. package/tests/jest/BookNavigator/book-navigator.test.js +8 -3
  38. package/tests/jest/BookNavigator/search/search-provider.test.js +16 -4
  39. package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +1 -1
  40. package/tests/jest/BookReader.test.js +26 -1
  41. package/tests/jest/plugins/search/plugin.search.test.js +17 -42
  42. package/tests/jest/plugins/search/plugin.search.view.test.js +10 -18
  43. package/tests/jest/plugins/url/plugin.url.test.js +1 -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,15 @@
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
+
1
13
  # 5.0.0-91
2
14
  - Refactor: Migrate ChaptersPlugin to BookReaderPlugin system @cdrini
3
15
  - Breaking changes:
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-91",
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;
@@ -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,19 +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.chapters.js').ChaptersPlugin['options']} */
148
+ /** @type {Partial<import('../plugins/plugin.chapters.js').ChaptersPlugin['options']>} */
149
149
  chapters: null,
150
- /** @type {import('../plugins/plugin.iiif.js').IiifPlugin['options']} */
150
+ /** @type {Partial<import('../plugins/plugin.iiif.js').IiifPlugin['options']>} */
151
151
  iiif: null,
152
- /** @type {import('../plugins/plugin.resume.js').ResumePlugin['options']} */
152
+ /** @type {Partial<import('../plugins/plugin.resume.js').ResumePlugin['options']>} */
153
153
  resume: null,
154
- /** @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']>} */
155
157
  textSelection: null,
156
- /** @type {import('../plugins/tts/plugin.tts.js').TtsPlugin['options']} */
158
+ /** @type {Partial<import('../plugins/tts/plugin.tts.js').TtsPlugin['options']>} */
157
159
  tts: null,
158
160
  },
159
161
 
package/src/BookReader.js CHANGED
@@ -74,6 +74,8 @@ BookReader.PLUGINS = {
74
74
  chapters: null,
75
75
  /** @type {typeof import('./plugins/plugin.resume.js').ResumePlugin | null}*/
76
76
  resume: null,
77
+ /** @type {typeof import('./plugins/search/plugin.search.js').SearchPlugin | null}*/
78
+ search: null,
77
79
  /** @type {typeof import('./plugins/plugin.text_selection.js').TextSelectionPlugin | null}*/
78
80
  textSelection: null,
79
81
  /** @type {typeof import('./plugins/tts/plugin.tts.js').TtsPlugin | null}*/
@@ -121,11 +123,22 @@ BookReader.prototype.setup = function(options) {
121
123
  /** @type {import('@/src/BookNavigator/book-navigator.js').BookNavigator} */
122
124
  this.shell;
123
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
+
124
136
  // Construct the usual plugins first to get type hints
125
137
  this._plugins = {
126
138
  archiveAnalytics: BookReader.PLUGINS.archiveAnalytics ? new BookReader.PLUGINS.archiveAnalytics(this) : null,
127
139
  autoplay: BookReader.PLUGINS.autoplay ? new BookReader.PLUGINS.autoplay(this) : null,
128
140
  chapters: BookReader.PLUGINS.chapters ? new BookReader.PLUGINS.chapters(this) : null,
141
+ search: BookReader.PLUGINS.search ? new BookReader.PLUGINS.search(this) : null,
129
142
  resume: BookReader.PLUGINS.resume ? new BookReader.PLUGINS.resume(this) : null,
130
143
  textSelection: BookReader.PLUGINS.textSelection ? new BookReader.PLUGINS.textSelection(this) : null,
131
144
  tts: BookReader.PLUGINS.tts ? new BookReader.PLUGINS.tts(this) : null,
@@ -154,12 +167,14 @@ BookReader.prototype.setup = function(options) {
154
167
  }
155
168
  }
156
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
+
157
175
  /** @type {number} @deprecated some past iterations set this */
158
176
  this.numLeafs = undefined;
159
177
 
160
- /** Overridden by plugin.search.js */
161
- this.enableSearch = false;
162
-
163
178
  /**
164
179
  * Store viewModeOrder states
165
180
  * @var {boolean}
@@ -447,23 +462,24 @@ BookReader.prototype.initParams = function() {
447
462
  }
448
463
 
449
464
  // Check for Search plugin
450
- if (this.options.enableSearch) {
465
+ if (this._plugins.search?.options.enabled) {
466
+ const sp = this._plugins.search;
451
467
  // Go to first result only if no default or URL page
452
- this.options.goToFirstResult = !params.pageFound;
468
+ sp.options.goToFirstResult = !params.pageFound;
453
469
 
454
470
  // If initialSearchTerm not set
455
- if (!this.options.initialSearchTerm) {
471
+ if (!sp.initialSearchTerm) {
456
472
  // Look for any term in URL
457
473
  if (params.search) {
458
474
  // Old style: /search/[term]
459
- this.options.initialSearchTerm = params.search;
460
- this.searchTerm = params.search;
475
+ sp.options.initialSearchTerm = params.search;
476
+ sp.searchTerm = params.search;
461
477
  } else {
462
478
  // If we have a query string: q=[term]
463
479
  const searchParams = new URLSearchParams(this.readQueryString());
464
480
  const searchTerm = searchParams.get('q');
465
481
  if (searchTerm) {
466
- this.options.initialSearchTerm = utils.decodeURIComponentPlus(searchTerm);
482
+ sp.options.initialSearchTerm = utils.decodeURIComponentPlus(searchTerm);
467
483
  }
468
484
  }
469
485
  }
@@ -644,7 +660,7 @@ BookReader.prototype.init = function() {
644
660
  }
645
661
 
646
662
  // If not searching, set to allow on-going fragment changes
647
- if (!this.options.initialSearchTerm) {
663
+ if (!this._plugins.search?.options.initialSearchTerm) {
648
664
  this.suppressFragmentChange = false;
649
665
  }
650
666
 
@@ -1308,7 +1324,7 @@ BookReader.prototype.updateFirstIndex = function(
1308
1324
  // If there's an initial search we stop suppressing global URL changes
1309
1325
  // when local suppression ends
1310
1326
  // This seems to correctly handle multiple calls during mode/1up
1311
- if (this.options.initialSearchTerm && !suppressFragmentChange) {
1327
+ if (this._plugins.search.options.initialSearchTerm && !suppressFragmentChange) {
1312
1328
  this.suppressFragmentChange = false;
1313
1329
  }
1314
1330
 
@@ -1637,8 +1653,8 @@ BookReader.prototype.updateFromParams = function(params) {
1637
1653
  // process /search
1638
1654
  // @deprecated for urlMode 'history'
1639
1655
  // Continues to work for urlMode 'hash'
1640
- if (this.enableSearch && 'undefined' != typeof(params.search)) {
1641
- if (this.searchTerm !== params.search) {
1656
+ if (this._plugins.search?.enabled && 'undefined' != typeof(params.search)) {
1657
+ if (this._plugins.search.searchTerm !== params.search) {
1642
1658
  this.$('.BRsearchInput').val(params.search);
1643
1659
  }
1644
1660
  }
@@ -1842,8 +1858,8 @@ BookReader.prototype.paramsFromCurrent = function() {
1842
1858
  params.view = fullscreenView;
1843
1859
  }
1844
1860
  // Search
1845
- if (this.enableSearch) {
1846
- params.search = this.searchTerm;
1861
+ if (this._plugins.search?.enabled) {
1862
+ params.search = this._plugins.search.searchTerm;
1847
1863
  }
1848
1864
 
1849
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
 
@@ -265,7 +265,9 @@ export class TextSelectionPlugin extends BookReaderPlugin {
265
265
  const $textLayer = $container.find('.BRtextLayer');
266
266
  if (!$textLayer.length) return;
267
267
  $textLayer.each((i, s) => this.defaultMode(s));
268
- this.interceptCopy($container);
268
+ if (!this.br.protected) {
269
+ this.interceptCopy($container);
270
+ }
269
271
  }
270
272
 
271
273
  /**