@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,85 +1,240 @@
1
- /* global BookReader */
1
+ // @ts-check
2
2
  import { css, html, LitElement, nothing } from "lit";
3
3
  import { customElement, property } from 'lit/decorators.js';
4
4
  import { ifDefined } from 'lit/directives/if-defined.js';
5
5
  import { styleMap } from 'lit/directives/style-map.js';
6
6
  import '@internetarchive/icon-toc/icon-toc';
7
- /** @typedef {import('@/src/BookNavigator/book-navigator.js').BookNavigator} BookNavigator */
7
+ import { BookReaderPlugin } from "../BookReaderPlugin";
8
+ import { applyVariables } from "../util/strings.js";
9
+ /** @typedef {import('@/src/BookReader/BookModel.js').PageIndex} PageIndex */
10
+ /** @typedef {import('@/src/BookReader/BookModel.js').PageString} PageString */
11
+ /** @typedef {import('@/src/BookReader/BookModel.js').LeafNum} LeafNum */
12
+
13
+ // @ts-ignore
14
+ const BookReader = /** @type {typeof import('@/src/BookReader.js').default} */(window.BookReader);
8
15
 
9
16
  /**
10
17
  * Plugin for chapter markers in BookReader. Fetches from openlibrary.org
11
18
  * Could be forked, or extended to alter behavior
12
19
  */
20
+ export class ChaptersPlugin extends BookReaderPlugin {
21
+ options = {
22
+ enabled: true,
23
+
24
+ /**
25
+ * The Open Library host to fetch/query Open Library.
26
+ * @type {import('@/src/util/strings.js').StringWithVars}
27
+ */
28
+ openLibraryHost: 'https://openlibrary.org',
29
+
30
+ /**
31
+ * The Open Library edition to fetch the table of contents from.
32
+ * E.g. 'OL12345M'
33
+ *
34
+ * @type {import('@/src/util/strings.js').StringWithVars}
35
+ */
36
+ openLibraryId: '',
37
+
38
+ /**
39
+ * The Internet Archive identifier to fetch the Open Library table
40
+ * of contents from.
41
+ * E.g. 'goody'
42
+ *
43
+ * @type {import('@/src/util/strings.js').StringWithVars}
44
+ */
45
+ internetArchiveId: '',
46
+
47
+ /**
48
+ * @deprecated
49
+ * Old name for openLibraryHost
50
+ * @type {import('@/src/util/strings.js').StringWithVars}
51
+ */
52
+ olHost: 'https://openlibrary.org',
53
+
54
+ /**
55
+ * @deprecated
56
+ * Old name for internetArchiveId
57
+ * @type {import('@/src/util/strings.js').StringWithVars}
58
+ */
59
+ bookId: '{{bookId}}',
60
+ }
61
+
62
+ /** @type {TocEntry[]} */
63
+ _tocEntries;
13
64
 
14
- jQuery.extend(BookReader.defaultOptions, {
15
- olHost: 'https://openlibrary.org',
16
- enableChaptersPlugin: true,
17
- bookId: '',
18
- });
19
-
20
- /** @override Extend to call Open Library for TOC */
21
- BookReader.prototype.init = (function(super_) {
22
- return function() {
23
- super_.call(this);
24
- if (this.options.enableChaptersPlugin && this.ui !== 'embed') {
25
- this._chapterInit();
65
+ /** @type {BRChaptersPanel} */
66
+ _chaptersPanel;
67
+
68
+ /** @override */
69
+ setup(options) {
70
+ super.setup(options);
71
+ this.options.internetArchiveId = this.options.internetArchiveId || this.options.bookId;
72
+ this.options.openLibraryHost = this.options.openLibraryHost || this.options.olHost;
73
+ }
74
+
75
+ /** @override Extend to call Open Library for TOC */
76
+ async init() {
77
+ if (!this.options.enabled || this.br.ui === 'embed') {
78
+ return;
26
79
  }
27
- };
28
- })(BookReader.prototype.init);
29
-
30
- BookReader.prototype._chapterInit = async function() {
31
- let rawTableOfContents = null;
32
- // Prefer IA TOC for now, until we update the second half to check for
33
- // `openlibrary_edition` on the IA metadata instead of making a bunch of
34
- // requests to OL.
35
- if (this.options.table_of_contents?.length) {
36
- rawTableOfContents = this.options.table_of_contents;
37
- } else {
38
- const olEdition = await this.getOpenLibraryRecord(this.options.olHost, this.options.bookId);
39
- if (olEdition?.table_of_contents?.length) {
40
- rawTableOfContents = olEdition.table_of_contents;
80
+
81
+ let rawTableOfContents = null;
82
+ // Prefer explicit TOC if specified
83
+ if (this.br.options.table_of_contents?.length) {
84
+ rawTableOfContents = this.br.options.table_of_contents;
85
+ } else {
86
+ // Otherwise fetch from OL
87
+ const olEdition = await this.getOpenLibraryRecord();
88
+ if (olEdition?.table_of_contents?.length) {
89
+ rawTableOfContents = olEdition.table_of_contents;
90
+ }
91
+ }
92
+
93
+ if (rawTableOfContents) {
94
+ this._tocEntries = rawTableOfContents
95
+ .map(rawTOCEntry => (Object.assign({}, rawTOCEntry, {
96
+ pageIndex: (
97
+ typeof(rawTOCEntry.leaf) == 'number' ? this.br.book.leafNumToIndex(rawTOCEntry.leaf) :
98
+ rawTOCEntry.pagenum ? this.br.book.getPageIndex(rawTOCEntry.pagenum) :
99
+ undefined
100
+ ),
101
+ })));
102
+ this._render();
103
+ this.br.bind(BookReader.eventNames.pageChanged, () => this._updateCurrent());
41
104
  }
42
105
  }
43
106
 
44
- if (rawTableOfContents) {
45
- this._tocEntries = rawTableOfContents
46
- .map(rawTOCEntry => (Object.assign({}, rawTOCEntry, {
47
- pageIndex: (
48
- typeof(rawTOCEntry.leaf) == 'number' ? this.book.leafNumToIndex(rawTOCEntry.leaf) :
49
- rawTOCEntry.pagenum ? this.book.getPageIndex(rawTOCEntry.pagenum) :
50
- undefined
51
- ),
52
- })));
53
- this._chaptersRender(this._tocEntries);
54
- this.bind(BookReader.eventNames.pageChanged, () => this._chaptersUpdateCurrent());
107
+ /**
108
+ * Update the table of contents based on array of TOC entries.
109
+ */
110
+ _render() {
111
+ this.br.shell.menuProviders['chapters'] = {
112
+ id: 'chapters',
113
+ icon: html`<ia-icon-toc style="width: var(--iconWidth); height: var(--iconHeight);"></ia-icon-toc>`,
114
+ label: 'Table of Contents',
115
+ component: html`<br-chapters-panel
116
+ .contents="${this._tocEntries}"
117
+ .jumpToPage="${(pageIndex) => {
118
+ this._updateCurrent(pageIndex);
119
+ this.br.jumpToIndex(pageIndex);
120
+ }}"
121
+ @connected="${(e) => {
122
+ this._chaptersPanel = e.target;
123
+ this._updateCurrent();
124
+ }}"
125
+ />`,
126
+ };
127
+ this.br.shell.addMenuShortcut('chapters');
128
+ this.br.shell.updateMenuContents();
129
+ this._tocEntries.forEach((tocEntry, i) => this._renderMarker(tocEntry, i));
55
130
  }
56
- };
57
131
 
58
- /**
59
- * Update the table of contents based on array of TOC entries.
60
- */
61
- BookReader.prototype._chaptersRender = function() {
62
- const shell = /** @type {BookNavigator} */(this.shell);
63
- shell.menuProviders['chapters'] = {
64
- id: 'chapters',
65
- icon: html`<ia-icon-toc style="width: var(--iconWidth); height: var(--iconHeight);"></ia-icon-toc>`,
66
- label: 'Table of Contents',
67
- component: html`<br-chapters-panel
68
- .contents="${this._tocEntries}"
69
- .jumpToPage="${(pageIndex) => {
70
- this._chaptersUpdateCurrent(pageIndex);
71
- this.jumpToIndex(pageIndex);
72
- }}"
73
- @connected="${(e) => {
74
- this._chaptersPanel = e.target;
75
- this._chaptersUpdateCurrent();
76
- }}"
77
- />`,
78
- };
79
- shell.addMenuShortcut('chapters');
80
- shell.updateMenuContents();
81
- this._tocEntries.forEach((tocEntry, i) => this._chaptersRenderMarker(tocEntry, i));
82
- };
132
+ /**
133
+ * @param {TocEntry} tocEntry
134
+ * @param {number} entryIndex
135
+ */
136
+ _renderMarker(tocEntry, entryIndex) {
137
+ if (tocEntry.pageIndex == undefined) return;
138
+
139
+ //creates a string with non-void tocEntry.label and tocEntry.title
140
+ const chapterStr = [tocEntry.label, tocEntry.title]
141
+ .filter(x => x)
142
+ .join(' ') || `Chapter ${entryIndex + 1}`;
143
+
144
+ const percentThrough = BookReader.util.cssPercentage(tocEntry.pageIndex, this.br.book.getNumLeafs() - 1);
145
+ $(`<div></div>`)
146
+ .append(
147
+ $('<div />')
148
+ .text(chapterStr)
149
+ .append(
150
+ $('<div class="BRchapterPage" />')
151
+ .text(this.br.book.getPageName(tocEntry.pageIndex)),
152
+ ),
153
+ )
154
+ .addClass('BRchapter')
155
+ .css({ left: percentThrough })
156
+ .appendTo(this.br.$('.BRnavline'))
157
+ .on("mouseenter", event => {
158
+ // remove hover effect from other markers then turn on just for this
159
+ const marker = event.currentTarget;
160
+ const tooltip = marker.querySelector('div');
161
+ const tooltipOffset = tooltip.getBoundingClientRect();
162
+ const targetOffset = marker.getBoundingClientRect();
163
+ const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
164
+ if (tooltipOffset.x - boxSizeAdjust < 0) {
165
+ tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
166
+ }
167
+ this.br.$('.BRsearch,.BRchapter').removeClass('front');
168
+ $(event.target).addClass('front');
169
+ })
170
+ .on("mouseleave", event => $(event.target).removeClass('front'))
171
+ .on('click', () => {
172
+ this._updateCurrent(tocEntry.pageIndex);
173
+ this.br.jumpToIndex(tocEntry.pageIndex);
174
+ });
175
+
176
+ this.br.$('.BRchapter, .BRsearch').each((i, el) => {
177
+ const $el = $(el);
178
+ $el
179
+ .on("mouseenter", () => $el.addClass('front'))
180
+ .on("mouseleave", () => $el.removeClass('front'));
181
+ });
182
+ }
183
+
184
+ /**
185
+ * This makes a call to OL API and calls the given callback function with the
186
+ * response from the API.
187
+ */
188
+ async getOpenLibraryRecord() {
189
+ const olHost = applyVariables(this.options.openLibraryHost, this.br.options.vars);
190
+
191
+ if (this.options.openLibraryId) {
192
+ const openLibraryId = applyVariables(this.options.openLibraryId, this.br.options.vars);
193
+ return await $.ajax({ url: `${olHost}/books/${openLibraryId}.json` });
194
+ }
195
+
196
+ if (this.options.internetArchiveId) {
197
+ const ocaid = applyVariables(this.options.internetArchiveId, this.br.options.vars);
198
+
199
+ // Try looking up by ocaid first, then by source_record
200
+ const baseQueryUrl = `${olHost}/query.json?type=/type/edition&*=&`;
201
+
202
+ let data = await $.ajax({
203
+ url: baseQueryUrl + new URLSearchParams({ ocaid, limit: '1' }),
204
+ });
205
+
206
+ if (!data || !data.length) {
207
+ // try source_records
208
+ data = await $.ajax({
209
+ url: baseQueryUrl + new URLSearchParams({ source_records: `ia:${ocaid}`, limit: '1' }),
210
+ });
211
+ }
212
+
213
+ return data?.[0];
214
+ }
215
+
216
+ return null;
217
+ }
218
+
219
+ /**
220
+ * @private
221
+ * Highlights the current chapter based on current page
222
+ * @param {PageIndex} curIndex
223
+ */
224
+ _updateCurrent(
225
+ curIndex = (this.br.mode == 2 ? Math.max(...this.br.displayedIndices) : this.br.firstIndex),
226
+ ) {
227
+ const tocEntriesIndexed = this._tocEntries.filter((el) => el.pageIndex != undefined).reverse();
228
+ const currChapter = tocEntriesIndexed[
229
+ // subtract one so that 2up shows the right label
230
+ tocEntriesIndexed.findIndex((chapter) => chapter.pageIndex <= curIndex)
231
+ ];
232
+ if (this._chaptersPanel) {
233
+ this._chaptersPanel.currentChapter = currChapter;
234
+ }
235
+ }
236
+ }
237
+ BookReader?.registerPlugin('chapters', ChaptersPlugin);
83
238
 
84
239
  /**
85
240
  * @typedef {Object} TocEntry
@@ -99,98 +254,6 @@ BookReader.prototype._chaptersRender = function() {
99
254
  * }
100
255
  */
101
256
 
102
- /**
103
- * @param {TocEntry} tocEntry
104
- * @param {number} entryIndex
105
- */
106
- BookReader.prototype._chaptersRenderMarker = function(tocEntry, entryIndex) {
107
- if (tocEntry.pageIndex == undefined) return;
108
-
109
- //creates a string with non-void tocEntry.label and tocEntry.title
110
- const chapterStr = [tocEntry.label, tocEntry.title]
111
- .filter(x => x)
112
- .join(' ') || `Chapter ${entryIndex + 1}`;
113
-
114
- const percentThrough = BookReader.util.cssPercentage(tocEntry.pageIndex, this.book.getNumLeafs() - 1);
115
- $(`<div></div>`)
116
- .append(
117
- $('<div />')
118
- .text(chapterStr)
119
- .append(
120
- $('<div class="BRchapterPage" />')
121
- .text(this.book.getPageName(tocEntry.pageIndex)),
122
- ),
123
- )
124
- .addClass('BRchapter')
125
- .css({ left: percentThrough })
126
- .appendTo(this.$('.BRnavline'))
127
- .on("mouseenter", event => {
128
- // remove hover effect from other markers then turn on just for this
129
- const marker = event.currentTarget;
130
- const tooltip = marker.querySelector('div');
131
- const tooltipOffset = tooltip.getBoundingClientRect();
132
- const targetOffset = marker.getBoundingClientRect();
133
- const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
134
- if (tooltipOffset.x - boxSizeAdjust < 0) {
135
- tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
136
- }
137
- this.$('.BRsearch,.BRchapter').removeClass('front');
138
- $(event.target).addClass('front');
139
- })
140
- .on("mouseleave", event => $(event.target).removeClass('front'))
141
- .on('click', () => {
142
- this._chaptersUpdateCurrent(tocEntry.pageIndex);
143
- this.jumpToIndex(tocEntry.pageIndex);
144
- });
145
-
146
- this.$('.BRchapter, .BRsearch').each((i, el) => {
147
- const $el = $(el);
148
- $el
149
- .on("mouseenter", () => $el.addClass('front'))
150
- .on("mouseleave", () => $el.removeClass('front'));
151
- });
152
- };
153
-
154
- /**
155
- * This makes a call to OL API and calls the given callback function with the
156
- * response from the API.
157
- *
158
- * @param {string} olHost
159
- * @param {string} ocaid
160
- */
161
- BookReader.prototype.getOpenLibraryRecord = async function (olHost, ocaid) {
162
- // Try looking up by ocaid first, then by source_record
163
- const baseURL = `${olHost}/query.json?type=/type/edition&*=`;
164
- const fetchUrlByBookId = `${baseURL}&ocaid=${ocaid}`;
165
-
166
- let data = await $.ajax({ url: fetchUrlByBookId });
167
-
168
- if (!data || !data.length) {
169
- // try sourceid
170
- data = await $.ajax({ url: `${baseURL}&source_records=ia:${ocaid}` });
171
- }
172
-
173
- return data?.[0];
174
- };
175
-
176
- /**
177
- * @private
178
- * Highlights the current chapter based on current page
179
- * @param {PageIndex} curIndex
180
- */
181
- BookReader.prototype._chaptersUpdateCurrent = function(
182
- curIndex = (this.mode == 2 ? Math.max(...this.displayedIndices) : this.firstIndex),
183
- ) {
184
- const tocEntriesIndexed = this._tocEntries.filter((el) => el.pageIndex != undefined).reverse();
185
- const currChapter = tocEntriesIndexed[
186
- // subtract one so that 2up shows the right label
187
- tocEntriesIndexed.findIndex((chapter) => chapter.pageIndex <= curIndex)
188
- ];
189
- if (this._chaptersPanel) {
190
- this._chaptersPanel.currentChapter = currChapter;
191
- }
192
- };
193
-
194
257
  @customElement('br-chapters-panel')
195
258
  export class BRChaptersPanel extends LitElement {
196
259
  /** @type {TocEntry[]} */
@@ -82,6 +82,22 @@ export class TextSelectionPlugin extends BookReaderPlugin {
82
82
  $(window.getSelection().anchorNode).closest('.BRpagecontainer').addClass('BRpagecontainer--hasSelection');
83
83
  }
84
84
  }).attach();
85
+
86
+ if (this.br.protected) {
87
+ // Prevent right clicking when selected text
88
+ $(document.body).on('contextmenu dragstart copy', (e) => {
89
+ const selection = document.getSelection();
90
+ if (selection?.toString()) {
91
+ const intersectsTextLayer = $('.BRtextLayer')
92
+ .toArray()
93
+ .some(el => selection.containsNode(el, true));
94
+ if (intersectsTextLayer) {
95
+ e.preventDefault();
96
+ return false;
97
+ }
98
+ }
99
+ });
100
+ }
85
101
  }
86
102
 
87
103
  /**
@@ -249,7 +265,9 @@ export class TextSelectionPlugin extends BookReaderPlugin {
249
265
  const $textLayer = $container.find('.BRtextLayer');
250
266
  if (!$textLayer.length) return;
251
267
  $textLayer.each((i, s) => this.defaultMode(s));
252
- this.interceptCopy($container);
268
+ if (!this.br.protected) {
269
+ this.interceptCopy($container);
270
+ }
253
271
  }
254
272
 
255
273
  /**