@internetarchive/bookreader 5.0.0-97 → 5.0.0-99
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.css +23 -20
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/bergamot-translator-worker.js +1 -1
- package/BookReader/ia-bookreader-bundle.js +156 -156
- package/BookReader/ia-bookreader-bundle.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.translate.js +135 -1
- package/BookReader/plugins/plugin.translate.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/silence.mp3 +0 -0
- package/package.json +2 -2
- package/src/BookReader/options.js +12 -8
- package/src/BookReader.js +53 -5
- package/src/assets/silence.mp3 +0 -0
- package/src/css/_BRpages.scss +1 -9
- package/src/css/_BRsearch.scss +1 -0
- package/src/css/_TextSelection.scss +23 -11
- package/src/plugins/plugin.text_selection.js +14 -168
- package/src/plugins/translate/TranslationManager.js +19 -24
- package/src/plugins/translate/plugin.translate.js +189 -78
- package/src/plugins/tts/AbstractTTSEngine.js +3 -4
- package/src/plugins/tts/PageChunk.js +28 -9
- package/src/plugins/tts/WebTTSEngine.js +5 -7
- package/src/plugins/tts/plugin.tts.js +40 -4
- package/src/plugins/tts/utils.js +15 -5
- package/src/util/TextSelectionManager.js +282 -0
package/src/plugins/tts/utils.js
CHANGED
|
@@ -19,38 +19,48 @@ export function isAndroid(userAgent = navigator.userAgent) {
|
|
|
19
19
|
return /android/i.test(userAgent);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** @type {{[lang: string]: string}} */
|
|
23
|
+
// Handle odd one-off language pairs that might show up over time
|
|
24
|
+
const specialLangs = {
|
|
25
|
+
"zh-hans": "中文 (Zhōngwén)",
|
|
26
|
+
};
|
|
22
27
|
/**
|
|
23
28
|
* @typedef {string} ISO6391
|
|
24
29
|
* Language code in ISO 639-1 format. e.g. en, fr, zh
|
|
30
|
+
* Can also retrieve language native name + ISO 639-1 code
|
|
25
31
|
**/
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* @param {string} language in some format
|
|
35
|
+
* @param {boolean?} getName gets Native Name + language code if true
|
|
29
36
|
* @return {ISO6391?}
|
|
30
37
|
*/
|
|
31
|
-
export function toISO6391(language) {
|
|
38
|
+
export function toISO6391(language, getName = false) {
|
|
32
39
|
if (!language) return null;
|
|
33
40
|
language = language.toLowerCase();
|
|
34
41
|
|
|
35
|
-
return searchForISO6391(language, ['iso639_1']) ||
|
|
36
|
-
searchForISO6391(language, ['iso639_2T']) ||
|
|
37
|
-
searchForISO6391(language, ['iso639_2B', 'nativeName', 'name']);
|
|
42
|
+
return searchForISO6391(language, ['iso639_1'], getName) ||
|
|
43
|
+
searchForISO6391(language, ['iso639_2T'], getName) ||
|
|
44
|
+
searchForISO6391(language, ['iso639_2B', 'nativeName', 'name'], getName);
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
/**
|
|
41
48
|
* Searches for the given long in the given columns.
|
|
42
49
|
* @param {string} language
|
|
43
50
|
* @param {Array<keyof import('iso-language-codes').Code>} columnsToSearch
|
|
51
|
+
* @param {boolean?} getName
|
|
44
52
|
* @return {ISO6391?}
|
|
45
53
|
*/
|
|
46
|
-
function searchForISO6391(language, columnsToSearch) {
|
|
54
|
+
function searchForISO6391(language, columnsToSearch, getName) {
|
|
47
55
|
for (const lang of langs) {
|
|
48
56
|
for (const colName of columnsToSearch) {
|
|
49
57
|
if (lang[colName].split(', ').map(x => x.toLowerCase()).indexOf(language) != -1) {
|
|
58
|
+
if (getName) return `${lang.nativeName.split(", ")[0]} (${lang.iso639_1})`;
|
|
50
59
|
return lang.iso639_1;
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
}
|
|
63
|
+
if (specialLangs[language]) return `${specialLangs[language]} (${language})`;
|
|
54
64
|
return null;
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { SelectionObserver } from "../BookReader/utils/SelectionObserver.js";
|
|
3
|
+
|
|
4
|
+
export class TextSelectionManager {
|
|
5
|
+
options = {
|
|
6
|
+
// Current Translation plugin implementation does not have words, will limit to one BRlineElement for now
|
|
7
|
+
maxProtectedWords: 200,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} layer Selector for the text layer to manage
|
|
12
|
+
* @param {import('../BookReader.js').default} br
|
|
13
|
+
* @param {Object} options
|
|
14
|
+
* @param {string[]} options.selectionElement CSS selector for elements that count as "words" for selection limiting
|
|
15
|
+
* @param {number} [maxWords] Maximum number of words allowed to be selected
|
|
16
|
+
*/
|
|
17
|
+
constructor (layer, br, { selectionElement }, maxWords) {
|
|
18
|
+
/** @type {string} */
|
|
19
|
+
this.layer = layer;
|
|
20
|
+
/** @type {import('../BookReader.js').default} */
|
|
21
|
+
this.br = br;
|
|
22
|
+
/** @type {string[]} */
|
|
23
|
+
this.selectionElement = selectionElement;
|
|
24
|
+
this.selectionObserver = new SelectionObserver(this.layer, this._onSelectionChange);
|
|
25
|
+
this.options.maxProtectedWords = maxWords ? maxWords : 200;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
init() {
|
|
29
|
+
this.attach();
|
|
30
|
+
new SelectionObserver(this.layer, (selectEvent) => {
|
|
31
|
+
// Track how often selection is used
|
|
32
|
+
if (selectEvent == 'started') {
|
|
33
|
+
this.br.plugins.archiveAnalytics?.sendEvent('BookReader', 'SelectStart');
|
|
34
|
+
|
|
35
|
+
// Set a class on the page to avoid hiding it when zooming/etc
|
|
36
|
+
this.br.refs.$br.find('.BRpagecontainer--hasSelection').removeClass('BRpagecontainer--hasSelection');
|
|
37
|
+
$(window.getSelection().anchorNode).closest('.BRpagecontainer').addClass('BRpagecontainer--hasSelection');
|
|
38
|
+
}
|
|
39
|
+
}).attach();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Need attach + detach methods to toggle w/ Translation plugin
|
|
43
|
+
attach() {
|
|
44
|
+
this.selectionObserver.attach();
|
|
45
|
+
if (this.br.protected) {
|
|
46
|
+
document.addEventListener('selectionchange', this._limitSelection);
|
|
47
|
+
// Prevent right clicking when selected text
|
|
48
|
+
$(document.body).on('contextmenu dragstart copy', (e) => {
|
|
49
|
+
const selection = document.getSelection();
|
|
50
|
+
if (selection?.toString()) {
|
|
51
|
+
const intersectsTextLayer = $(this.layer)
|
|
52
|
+
.toArray()
|
|
53
|
+
.some(el => selection.containsNode(el, true));
|
|
54
|
+
if (intersectsTextLayer) {
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
detach() {
|
|
64
|
+
this.selectionObserver.detach();
|
|
65
|
+
if (this.br.protected) {
|
|
66
|
+
document.removeEventListener('selectionchange', this._limitSelection);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {'started' | 'cleared'} type
|
|
72
|
+
* @param {HTMLElement} target
|
|
73
|
+
*/
|
|
74
|
+
_onSelectionChange = (type, target) => {
|
|
75
|
+
if (type === 'started') {
|
|
76
|
+
this.textSelectingMode(target);
|
|
77
|
+
} else if (type === 'cleared') {
|
|
78
|
+
this.defaultMode(target);
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error(`Unknown type ${type}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Intercept copied text to remove any styling applied to it
|
|
86
|
+
* @param {JQuery} $container
|
|
87
|
+
*/
|
|
88
|
+
interceptCopy ($container) {
|
|
89
|
+
$container[0].addEventListener('copy', (event) => {
|
|
90
|
+
const selection = document.getSelection();
|
|
91
|
+
event.clipboardData.setData('text/plain', selection.toString());
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initializes text selection modes if there is a text layer on the page
|
|
98
|
+
* @param {JQuery} $container
|
|
99
|
+
*/
|
|
100
|
+
stopPageFlip($container) {
|
|
101
|
+
/** @type {JQuery<HTMLElement>} */
|
|
102
|
+
const $textLayer = $container.find(this.layer);
|
|
103
|
+
if (!$textLayer.length) return;
|
|
104
|
+
$textLayer.each((i, s) => this.defaultMode(s));
|
|
105
|
+
if (!this.br.protected) {
|
|
106
|
+
this.interceptCopy($container);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Applies mouse events when in default mode
|
|
112
|
+
* @param {HTMLElement} textLayer
|
|
113
|
+
*/
|
|
114
|
+
defaultMode (textLayer) {
|
|
115
|
+
const $pageContainer = $(textLayer).closest('.BRpagecontainer');
|
|
116
|
+
textLayer.style.pointerEvents = "none";
|
|
117
|
+
$pageContainer.find("img").css("pointer-events", "auto");
|
|
118
|
+
|
|
119
|
+
$(textLayer).off(".textSelectPluginHandler");
|
|
120
|
+
const startedMouseDown = this.mouseIsDown;
|
|
121
|
+
let skipNextMouseup = this.mouseIsDown;
|
|
122
|
+
if (startedMouseDown) {
|
|
123
|
+
textLayer.style.pointerEvents = "auto";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Need to stop propagation to prevent DragScrollable from
|
|
127
|
+
// blocking selection
|
|
128
|
+
$(textLayer).on("mousedown.textSelectPluginHandler", (event) => {
|
|
129
|
+
this.mouseIsDown = true;
|
|
130
|
+
if ($(event.target).is(this.selectionElement.join(", "))) {
|
|
131
|
+
event.stopPropagation();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
$(textLayer).on("mouseup.textSelectPluginHandler", (event) => {
|
|
136
|
+
this.mouseIsDown = false;
|
|
137
|
+
textLayer.style.pointerEvents = "none";
|
|
138
|
+
if (skipNextMouseup) {
|
|
139
|
+
skipNextMouseup = false;
|
|
140
|
+
event.stopPropagation();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* This mode is active while there is a selection on the given textLayer
|
|
147
|
+
* @param {HTMLElement} textLayer
|
|
148
|
+
*/
|
|
149
|
+
textSelectingMode(textLayer) {
|
|
150
|
+
const $pageContainer = $(textLayer).closest('.BRpagecontainer');
|
|
151
|
+
// Make text layer consume all events
|
|
152
|
+
textLayer.style.pointerEvents = "all";
|
|
153
|
+
// Block img from getting long-press to save while selecting
|
|
154
|
+
$pageContainer.find("img").css("pointer-events", "none");
|
|
155
|
+
|
|
156
|
+
$(textLayer).off(".textSelectPluginHandler");
|
|
157
|
+
|
|
158
|
+
$(textLayer).on("mousedown.textSelectPluginHandler", (event) => {
|
|
159
|
+
this.mouseIsDown = true;
|
|
160
|
+
event.stopPropagation();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Prevent page flip on click
|
|
164
|
+
$(textLayer).on('mouseup.textSelectPluginHandler', (event) => {
|
|
165
|
+
this.mouseIsDown = false;
|
|
166
|
+
event.stopPropagation();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
_limitSelection = () => {
|
|
172
|
+
const selection = window.getSelection();
|
|
173
|
+
if (!selection.rangeCount) return;
|
|
174
|
+
|
|
175
|
+
const range = selection.getRangeAt(0);
|
|
176
|
+
|
|
177
|
+
// Check if range.startContainer is inside the sub-tree of .BRContainer
|
|
178
|
+
const startInBr = !!range.startContainer.parentElement.closest('.BRcontainer');
|
|
179
|
+
const endInBr = !!range.endContainer.parentElement.closest('.BRcontainer');
|
|
180
|
+
if (!startInBr && !endInBr) return;
|
|
181
|
+
if (!startInBr || !endInBr) {
|
|
182
|
+
// weird case, just clear the selection
|
|
183
|
+
selection.removeAllRanges();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Find the last allowed word in the selection
|
|
188
|
+
const lastAllowedWord = genAt(
|
|
189
|
+
genFilter(
|
|
190
|
+
walkBetweenNodes(range.startContainer, range.endContainer),
|
|
191
|
+
(node) => node.classList?.contains(
|
|
192
|
+
this.selectionElement[0].replace(".", ""),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
this.options.maxProtectedWords - 1,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (!lastAllowedWord || range.endContainer.parentNode == lastAllowedWord) return;
|
|
199
|
+
|
|
200
|
+
const newRange = document.createRange();
|
|
201
|
+
newRange.setStart(range.startContainer, range.startOffset);
|
|
202
|
+
newRange.setEnd(lastAllowedWord.firstChild, lastAllowedWord.textContent.length);
|
|
203
|
+
|
|
204
|
+
selection.removeAllRanges();
|
|
205
|
+
selection.addRange(newRange);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @template T
|
|
211
|
+
* Get the i-th element of an iterable
|
|
212
|
+
* @param {Iterable<T>} iterable
|
|
213
|
+
* @param {number} index
|
|
214
|
+
*/
|
|
215
|
+
export function genAt(iterable, index) {
|
|
216
|
+
let i = 0;
|
|
217
|
+
for (const x of iterable) {
|
|
218
|
+
if (i == index) {
|
|
219
|
+
return x;
|
|
220
|
+
}
|
|
221
|
+
i++;
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @template T
|
|
228
|
+
* Generator version of filter
|
|
229
|
+
* @param {Iterable<T>} iterable
|
|
230
|
+
* @param {function(T): boolean} fn
|
|
231
|
+
*/
|
|
232
|
+
export function* genFilter(iterable, fn) {
|
|
233
|
+
for (const x of iterable) {
|
|
234
|
+
if (fn(x)) yield x;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Depth traverse the DOM tree starting at `start`, and ending at `end`.
|
|
240
|
+
* @param {Node} start
|
|
241
|
+
* @param {Node} end
|
|
242
|
+
* @returns {Generator<Node>}
|
|
243
|
+
*/
|
|
244
|
+
export function* walkBetweenNodes(start, end) {
|
|
245
|
+
let done = false;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {Node} node
|
|
249
|
+
*/
|
|
250
|
+
function* walk(node, {children = true, parents = true, siblings = true} = {}) {
|
|
251
|
+
if (node === end) {
|
|
252
|
+
done = true;
|
|
253
|
+
yield node;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// yield self
|
|
258
|
+
yield node;
|
|
259
|
+
|
|
260
|
+
// First iterate children (depth-first traversal)
|
|
261
|
+
if (children && node.firstChild) {
|
|
262
|
+
yield* walk(node.firstChild, {children: true, parents: false, siblings: true});
|
|
263
|
+
if (done) return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Then iterate siblings
|
|
267
|
+
if (siblings) {
|
|
268
|
+
for (let sib = node.nextSibling; sib; sib = sib.nextSibling) {
|
|
269
|
+
yield* walk(sib, {children: true, parents: false, siblings: false});
|
|
270
|
+
if (done) return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Finally, move up the tree
|
|
275
|
+
if (parents && node.parentNode) {
|
|
276
|
+
yield* walk(node.parentNode, {children: false, parents: true, siblings: true});
|
|
277
|
+
if (done) return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
yield* walk(start);
|
|
282
|
+
}
|