@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.
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +2 -2
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
- package/BookReader/plugins/plugin.autoplay.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js.map +1 -1
- package/BookReader/plugins/plugin.chapters.js +2 -2
- package/BookReader/plugins/plugin.chapters.js.map +1 -1
- package/BookReader/plugins/plugin.iiif.js +1 -1
- package/BookReader/plugins/plugin.iiif.js.map +1 -1
- package/BookReader/plugins/plugin.resume.js +1 -1
- package/BookReader/plugins/plugin.resume.js.map +1 -1
- package/BookReader/plugins/plugin.search.js +1 -1
- package/BookReader/plugins/plugin.search.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +1 -1
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReaderDemo/IADemoBr.js +29 -1
- package/BookReaderDemo/ia-multiple-volumes-manifest.js +0 -1
- package/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/BookNavigator/book-navigator.js +5 -2
- package/src/BookNavigator/search/search-provider.js +13 -7
- package/src/BookNavigator/sharing.js +1 -1
- package/src/BookReader/BookModel.js +5 -4
- package/src/BookReader/Toolbar/Toolbar.js +5 -0
- package/src/BookReader/options.js +10 -6
- package/src/BookReader.js +49 -23
- package/src/BookReaderPlugin.js +8 -0
- package/src/plugins/plugin.chapters.js +220 -157
- package/src/plugins/plugin.text_selection.js +19 -1
- package/src/plugins/search/plugin.search.js +330 -376
- package/src/plugins/search/view.js +13 -9
- package/src/plugins/tts/WebTTSEngine.js +67 -41
- package/src/plugins/tts/plugin.tts.js +1 -3
- package/src/plugins/tts/utils.js +13 -0
- package/src/util/browserSniffing.js +11 -1
- package/tests/e2e/helpers/mockSearch.js +1 -1
- package/tests/jest/BookNavigator/book-navigator.test.js +8 -3
- package/tests/jest/BookNavigator/search/search-provider.test.js +16 -4
- package/tests/jest/BookNavigator/sharing/sharing-provider.test.js +1 -1
- package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +70 -0
- package/tests/jest/BookReader.test.js +26 -1
- package/tests/jest/plugins/plugin.chapters.test.js +56 -58
- package/tests/jest/plugins/search/plugin.search.test.js +17 -42
- package/tests/jest/plugins/search/plugin.search.view.test.js +10 -18
- package/tests/jest/plugins/tts/WebTTSEngine.test.js +18 -12
- package/tests/jest/plugins/url/plugin.url.test.js +1 -1
- package/tests/jest/util/browserSniffing.test.js +9 -3
- package/tests/jest/utils.js +4 -1
@@ -1,85 +1,240 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
.
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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.
|
268
|
+
if (!this.br.protected) {
|
269
|
+
this.interceptCopy($container);
|
270
|
+
}
|
253
271
|
}
|
254
272
|
|
255
273
|
/**
|