@internetarchive/bookreader 5.0.0-110 → 5.0.0-112
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 +59 -0
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +74 -77
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/plugins/plugin.chapters.js +1 -1
- package/BookReader/plugins/plugin.chapters.js.map +1 -1
- package/BookReader/plugins/plugin.experiments.js +1 -1
- package/BookReader/plugins/plugin.experiments.js.map +1 -1
- package/BookReader/plugins/plugin.search.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +23 -1
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.translate.js +25 -3
- package/BookReader/plugins/plugin.translate.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/plugins/plugin.url.js +2 -1
- package/BookReader/plugins/plugin.url.js.LICENSE.txt +1 -0
- package/BookReader/plugins/plugin.url.js.map +1 -1
- package/package.json +2 -1
- package/src/BookReader/Mode2UpLit.js +1 -3
- package/src/BookReader/Navbar/Navbar.js +9 -10
- package/src/BookReader/utils/SelectionObserver.js +9 -1
- package/src/BookReader.js +17 -2
- package/src/css/_TextSelection.scss +59 -0
- package/src/plugins/plugin.chapters.js +8 -3
- package/src/plugins/plugin.experiments.js +21 -3
- package/src/plugins/plugin.text_selection.js +17 -0
- package/src/plugins/translate/plugin.translate.js +68 -22
- package/src/plugins/tts/utils.js +1 -1
- package/src/plugins/url/UrlPlugin.js +23 -3
- package/src/plugins/url/plugin.url.js +63 -7
- package/src/util/TextSelectionManager.js +252 -2
|
@@ -314,7 +314,7 @@ export class Navbar {
|
|
|
314
314
|
const $sliders = this.$root.find('.BRpager').slider({
|
|
315
315
|
animate: true,
|
|
316
316
|
min: 0,
|
|
317
|
-
max: br.book.getNumLeafs() - 1,
|
|
317
|
+
max: Math.max(0, br.book.getNumLeafs() - 1),
|
|
318
318
|
value: br.currentIndex(),
|
|
319
319
|
range: "min",
|
|
320
320
|
});
|
|
@@ -324,8 +324,8 @@ export class Navbar {
|
|
|
324
324
|
$sliders.find('.ui-slider-handle').attr({
|
|
325
325
|
'role': 'slider',
|
|
326
326
|
'aria-label': 'Page navigation slider',
|
|
327
|
-
'aria-valuemin':
|
|
328
|
-
'aria-valuemax': br.book.getNumLeafs(),
|
|
327
|
+
'aria-valuemin': 0,
|
|
328
|
+
'aria-valuemax': Math.max(0, br.book.getNumLeafs() - 1),
|
|
329
329
|
});
|
|
330
330
|
|
|
331
331
|
// Ignore up/down arrow keys and page up/down keys, since they're confusingly different
|
|
@@ -396,7 +396,6 @@ export class Navbar {
|
|
|
396
396
|
*/
|
|
397
397
|
getNavPageNumString(index) {
|
|
398
398
|
const { br } = this;
|
|
399
|
-
// Accessible index starts at 0 (alas) so we add 1 to make human
|
|
400
399
|
const pageNum = br.book.getPageNum(index);
|
|
401
400
|
const pageType = br.book.getPageProp(index, 'pageType');
|
|
402
401
|
const numLeafs = br.book.getNumLeafs();
|
|
@@ -425,7 +424,7 @@ export class Navbar {
|
|
|
425
424
|
updateNavPageNum(index) {
|
|
426
425
|
this.$root.find('.BRcurrentpage').html(this.getNavPageNumString(index));
|
|
427
426
|
this.$root.find('.ui-slider-handle').attr({
|
|
428
|
-
'aria-valuenow': index
|
|
427
|
+
'aria-valuenow': index,
|
|
429
428
|
'aria-valuetext': this.getNavPageNumString(index),
|
|
430
429
|
});
|
|
431
430
|
}
|
|
@@ -446,19 +445,19 @@ export class Navbar {
|
|
|
446
445
|
|
|
447
446
|
/**
|
|
448
447
|
* Renders the html for the page string
|
|
449
|
-
* @param {
|
|
450
|
-
* @param {number} numLeafs
|
|
448
|
+
* @param {PageIndex} index 0-indexed page index (0..numLeafs-1)
|
|
449
|
+
* @param {number} numLeafs total number of leaves
|
|
451
450
|
* @param {number|string} pageNum
|
|
452
451
|
* @param {*} pageType - Deprecated
|
|
453
452
|
* @param {number} maxPageNum
|
|
454
|
-
* @return {string}
|
|
453
|
+
* @return {string} e.g. "Page 14 (3/39)"
|
|
455
454
|
*/
|
|
456
455
|
export function getNavPageNumHtml(index, numLeafs, pageNum, pageType, maxPageNum) {
|
|
457
456
|
const pageIsAsserted = pageNum[0] != 'n';
|
|
458
|
-
const pageIndex = index + 1;
|
|
459
457
|
|
|
460
458
|
if (!pageIsAsserted) {
|
|
461
459
|
pageNum = '—';
|
|
462
460
|
}
|
|
463
|
-
|
|
461
|
+
const lastIndex = Math.max(0, numLeafs - 1);
|
|
462
|
+
return `Page ${pageNum} (${index}/${lastIndex})`;
|
|
464
463
|
}
|
|
@@ -4,10 +4,12 @@ export class SelectionObserver {
|
|
|
4
4
|
startedInSelector = false;
|
|
5
5
|
/** @type {HTMLElement} */
|
|
6
6
|
target = null;
|
|
7
|
+
/** @type {Node} */
|
|
8
|
+
lastKnownFocusNode = null;
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* @param {string} selector
|
|
10
|
-
* @param {function('started' | 'cleared', HTMLElement): any} handler
|
|
12
|
+
* @param {function('started' | 'cleared' | 'focusChanged', HTMLElement): any} handler
|
|
11
13
|
*/
|
|
12
14
|
constructor(selector, handler) {
|
|
13
15
|
this.selector = selector;
|
|
@@ -34,9 +36,15 @@ export class SelectionObserver {
|
|
|
34
36
|
if (!target) return;
|
|
35
37
|
this.target = target;
|
|
36
38
|
this.selecting = true;
|
|
39
|
+
this.lastKnownFocusNode = sel.focusNode;
|
|
37
40
|
this.handler('started', this.target);
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
if (this.selecting && (this.lastKnownFocusNode != sel.focusNode || sel.toString() && !sel.isCollapsed)) {
|
|
44
|
+
this.lastKnownFocusNode = sel.focusNode;
|
|
45
|
+
this.handler('focusChanged', this.target);
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
if (this.selecting && (sel.isCollapsed || !sel.toString() || !$(sel.anchorNode).closest(this.selector)[0])) {
|
|
41
49
|
this.selecting = false;
|
|
42
50
|
this.handler('cleared', this.target);
|
package/src/BookReader.js
CHANGED
|
@@ -1970,10 +1970,25 @@ BookReader.prototype.queryStringFromParams = function(
|
|
|
1970
1970
|
if (params.search && urlMode === 'history') {
|
|
1971
1971
|
newParams.set('q', params.search);
|
|
1972
1972
|
}
|
|
1973
|
+
|
|
1974
|
+
let textFragmentParam = '';
|
|
1975
|
+
// Need to pull out text separately to avoid the spaces becoming encoded as +, which
|
|
1976
|
+
// the browser seems not to handle with the text fragment
|
|
1977
|
+
if (newParams.get('text')) {
|
|
1978
|
+
newParams.delete('text');
|
|
1979
|
+
textFragmentParam = `text=${this.urlPlugin.retrieveTextFragment(currQueryString)}`;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1973
1982
|
// https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString
|
|
1974
1983
|
// Note: This method returns the query string without the question mark.
|
|
1975
|
-
|
|
1976
|
-
|
|
1984
|
+
let result = newParams.toString();
|
|
1985
|
+
if (textFragmentParam) {
|
|
1986
|
+
if (result) result += '&';
|
|
1987
|
+
result += textFragmentParam;
|
|
1988
|
+
}
|
|
1989
|
+
if (result) result = '?' + result;
|
|
1990
|
+
|
|
1991
|
+
return result;
|
|
1977
1992
|
};
|
|
1978
1993
|
|
|
1979
1994
|
/**
|
|
@@ -150,4 +150,63 @@
|
|
|
150
150
|
|
|
151
151
|
.BRtranslateLayer .BRparagraphElement.BRtranslateHidden {
|
|
152
152
|
display: none;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.br-select-menu__root {
|
|
156
|
+
display: none;
|
|
157
|
+
border-radius: 20px;
|
|
158
|
+
background-color: #333;
|
|
159
|
+
color-scheme: dark;
|
|
160
|
+
padding: 2px;
|
|
161
|
+
overflow: hidden;
|
|
162
|
+
transition: border-radius 0.2s;
|
|
163
|
+
}
|
|
164
|
+
.br-select-menu__root:hover {
|
|
165
|
+
border-radius: 8px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.br-select-menu__root:hover .br-select-menu__option {
|
|
169
|
+
border-radius: 6px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.br-select-menu__option {
|
|
173
|
+
--iconWidth: 15px;
|
|
174
|
+
--iconHeight: 15px;
|
|
175
|
+
--iconFillColor: currentColor;
|
|
176
|
+
background: transparent;
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
border-radius: 20px;
|
|
180
|
+
font-family: inherit;
|
|
181
|
+
border: 0;
|
|
182
|
+
transition: background-color 0.2s, border-radius 0.2s;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
text-wrap: nowrap;
|
|
185
|
+
padding: 4px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.br-select-menu__option:hover {
|
|
189
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.br-select-menu__icon {
|
|
193
|
+
display: block;
|
|
194
|
+
flex-shrink: 0;
|
|
195
|
+
width: 17px;
|
|
196
|
+
height: 17px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.br-select-menu__label {
|
|
200
|
+
width: 0px;
|
|
201
|
+
opacity: 0;
|
|
202
|
+
interpolate-size: allow-keywords;
|
|
203
|
+
transition: width 0.2s;
|
|
204
|
+
font-size: 12px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
.br-select-menu__root:hover .br-select-menu__label {
|
|
209
|
+
width: auto;
|
|
210
|
+
margin-left: 4px;
|
|
211
|
+
opacity: 1;
|
|
153
212
|
}
|
|
@@ -6,6 +6,7 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|
|
6
6
|
import '@internetarchive/icon-toc/icon-toc.js';
|
|
7
7
|
import { BookReaderPlugin } from "../BookReaderPlugin.js";
|
|
8
8
|
import { applyVariables } from "../util/strings.js";
|
|
9
|
+
import { promisifyEvent } from "../BookReader/utils.js";
|
|
9
10
|
/** @typedef {import('@/src/BookReader/BookModel.js').PageIndex} PageIndex */
|
|
10
11
|
/** @typedef {import('@/src/BookReader/BookModel.js').PageString} PageString */
|
|
11
12
|
/** @typedef {import('@/src/BookReader/BookModel.js').LeafNum} LeafNum */
|
|
@@ -94,11 +95,15 @@ export class ChaptersPlugin extends BookReaderPlugin {
|
|
|
94
95
|
this._tocEntries = rawTableOfContents
|
|
95
96
|
.map(rawTOCEntry => (Object.assign({}, rawTOCEntry, {
|
|
96
97
|
pageIndex: (
|
|
97
|
-
typeof
|
|
98
|
-
rawTOCEntry.
|
|
99
|
-
|
|
98
|
+
typeof rawTOCEntry.page_index === 'number' ? rawTOCEntry.page_index :
|
|
99
|
+
typeof(rawTOCEntry.leaf) == 'number' ? this.br.book.leafNumToIndex(rawTOCEntry.leaf) :
|
|
100
|
+
rawTOCEntry.pagenum ? this.br.book.getPageIndex(rawTOCEntry.pagenum) :
|
|
101
|
+
undefined
|
|
100
102
|
),
|
|
101
103
|
})));
|
|
104
|
+
if (!this.br.shell) {
|
|
105
|
+
await promisifyEvent(window, 'BookReader:PostInit');
|
|
106
|
+
}
|
|
102
107
|
this._render();
|
|
103
108
|
this.br.bind(BookReader.eventNames.pageChanged, () => this._updateCurrent());
|
|
104
109
|
}
|
|
@@ -51,11 +51,27 @@ export class ExperimentsPlugin extends BookReaderPlugin {
|
|
|
51
51
|
localStorageKey: 'BrExperiments',
|
|
52
52
|
|
|
53
53
|
/** The experiments that should be shown in the experiments panel */
|
|
54
|
-
enabledExperiments: ['translate'],
|
|
54
|
+
enabledExperiments: ['translate', 'copyLinkToHighlight'],
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** @type {ExperimentModel[]} */
|
|
58
58
|
allExperiments = [
|
|
59
|
+
new class extends ExperimentModel {
|
|
60
|
+
name = 'copyLinkToHighlight';
|
|
61
|
+
title = 'Copy to Selection URL';
|
|
62
|
+
description = 'Share text selection via URL';
|
|
63
|
+
learnMore = 'none';
|
|
64
|
+
icon = null;
|
|
65
|
+
enabled = false;
|
|
66
|
+
async enable ({ manual = false }) {
|
|
67
|
+
this.br.plugins.textSelection.enableSelectionMenu();
|
|
68
|
+
}
|
|
69
|
+
async disable() {
|
|
70
|
+
sleep(0).then(() => {
|
|
71
|
+
window.location.reload();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}(),
|
|
59
75
|
new class extends ExperimentModel {
|
|
60
76
|
name = 'translate';
|
|
61
77
|
title = 'Translate Plugin';
|
|
@@ -123,7 +139,9 @@ export class ExperimentsPlugin extends BookReaderPlugin {
|
|
|
123
139
|
for (const experiment of this.allExperiments) {
|
|
124
140
|
// TODO: imagesBaseURL should be replaced with assetRoot everywhere
|
|
125
141
|
experiment.assetRoot = this.br.options.imagesBaseURL.replace(/images\/$/, '');
|
|
126
|
-
|
|
142
|
+
if (experiment.icon) {
|
|
143
|
+
experiment.icon = experiment.buildAssetPath(experiment.icon);
|
|
144
|
+
}
|
|
127
145
|
experiment.br = this.br;
|
|
128
146
|
}
|
|
129
147
|
|
|
@@ -269,7 +287,7 @@ export class BrExperimentToggle extends LitElement {
|
|
|
269
287
|
return html`
|
|
270
288
|
<div class="experiment-card" style="margin-bottom: 10px;">
|
|
271
289
|
<div style="display: flex; align-items: center; gap: 10px;">
|
|
272
|
-
|
|
290
|
+
${this.icon ? html`<img src="${this.icon}" style="width: 20px; height: 20px;" alt="" />` : ''}
|
|
273
291
|
<div style="flex-grow: 1; font-weight: bold;">${this.title}</div>
|
|
274
292
|
</div>
|
|
275
293
|
<p style="opacity: 0.9">
|
|
@@ -53,10 +53,21 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
53
53
|
init() {
|
|
54
54
|
if (!this.options.enabled) return;
|
|
55
55
|
|
|
56
|
+
this.br.on('pageVisible', (_, {pageContainerEl}) => {
|
|
57
|
+
if (pageContainerEl.querySelector('.BRtextLayer')) {
|
|
58
|
+
this.br.trigger('textLayerVisible', {pageContainerEl});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
56
62
|
this.loadData();
|
|
57
63
|
this.textSelectionManager.init();
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
enableSelectionMenu() {
|
|
67
|
+
this.textSelectionManager.selectionMenuEnabled = true;
|
|
68
|
+
this.textSelectionManager.renderSelectionMenu();
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
/**
|
|
61
72
|
* @override
|
|
62
73
|
* @param {PageContainer} pageContainer
|
|
@@ -188,6 +199,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
188
199
|
paragEl.style.marginTop = `${newTop}px`;
|
|
189
200
|
yAdded += newTop;
|
|
190
201
|
textLayer.appendChild(paragEl);
|
|
202
|
+
textLayer.appendChild(document.createTextNode('\n'));
|
|
191
203
|
}
|
|
192
204
|
$container.append(textLayer);
|
|
193
205
|
this.textSelectionManager.stopPageFlip($container);
|
|
@@ -195,6 +207,11 @@ export class TextSelectionPlugin extends BookReaderPlugin {
|
|
|
195
207
|
pageIndex,
|
|
196
208
|
pageContainer,
|
|
197
209
|
});
|
|
210
|
+
|
|
211
|
+
// Check if page is visible
|
|
212
|
+
if ($container.hasClass('BRpage-visible')) {
|
|
213
|
+
this.br.trigger('textLayerVisible', {pageContainerEl: $container[0]});
|
|
214
|
+
}
|
|
198
215
|
}
|
|
199
216
|
|
|
200
217
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
import { html, LitElement } from 'lit';
|
|
2
|
+
import { css, html, LitElement } from 'lit';
|
|
3
3
|
import { BookReaderPlugin } from '../../BookReaderPlugin.js';
|
|
4
|
-
import { customElement, property } from 'lit/decorators.js';
|
|
4
|
+
import { customElement, property, query } from 'lit/decorators.js';
|
|
5
5
|
import { TranslationManager } from "./TranslationManager.js";
|
|
6
|
-
import { toISO6391 } from '../tts/utils.js';
|
|
6
|
+
import { toISO6391, toNativeName } from '../tts/utils.js';
|
|
7
7
|
import { sortBy } from '../../../src/BookReader/utils.js';
|
|
8
8
|
import { TextSelectionManager } from '../../../src/util/TextSelectionManager.js';
|
|
9
9
|
import '@internetarchive/ia-activity-indicator';
|
|
@@ -63,14 +63,12 @@ export class TranslatePlugin extends BookReaderPlugin {
|
|
|
63
63
|
textSelectionManager = new TextSelectionManager('.BRtranslateLayer', this.br, {selectionElement: [".BRlineElement"]}, 1);
|
|
64
64
|
|
|
65
65
|
async init() {
|
|
66
|
-
|
|
66
|
+
if (!this.options.enabled) return;
|
|
67
|
+
|
|
68
|
+
const bookLanguage = this.br.options.bookLanguage || '';
|
|
69
|
+
const currentLanguage = toISO6391(bookLanguage);
|
|
67
70
|
this.langFromCode = currentLanguage ?? "en";
|
|
68
71
|
this.textSelectionManager.init();
|
|
69
|
-
|
|
70
|
-
if (!this.options.enabled) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
72
|
this.translationManager.publicPath = this.br.options.imagesBaseURL.replace(/\/+$/, '') + '/..';
|
|
75
73
|
|
|
76
74
|
/**
|
|
@@ -307,6 +305,14 @@ export class TranslatePlugin extends BookReaderPlugin {
|
|
|
307
305
|
}
|
|
308
306
|
|
|
309
307
|
handleToggleTranslation = async () => {
|
|
308
|
+
const shouldEnableTranslation = !this.userToggleTranslate;
|
|
309
|
+
if (shouldEnableTranslation && !this.isSourceLanguageSupported()) {
|
|
310
|
+
this.userToggleTranslate = false;
|
|
311
|
+
this.translationManager.active = false;
|
|
312
|
+
this._render();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
310
316
|
this.userToggleTranslate = !this.userToggleTranslate;
|
|
311
317
|
this.translationManager.active = this.userToggleTranslate;
|
|
312
318
|
|
|
@@ -325,6 +331,10 @@ export class TranslatePlugin extends BookReaderPlugin {
|
|
|
325
331
|
/**
|
|
326
332
|
* Update translation side menu
|
|
327
333
|
*/
|
|
334
|
+
isSourceLanguageSupported() {
|
|
335
|
+
return this.translationManager.fromLanguages.some((lang) => lang.code == this.langFromCode);
|
|
336
|
+
}
|
|
337
|
+
|
|
328
338
|
_render() {
|
|
329
339
|
this.br.shell.menuProviders['translate'] = {
|
|
330
340
|
id: 'translate',
|
|
@@ -339,6 +349,7 @@ export class TranslatePlugin extends BookReaderPlugin {
|
|
|
339
349
|
this._panel.detectedToLang = this.langToCode;
|
|
340
350
|
this._panel.detectedFromLang = this.langFromCode;
|
|
341
351
|
this._panel.loadingModel = this.loadingModel;
|
|
352
|
+
this._panel.sourceLanguageSupported = this.isSourceLanguageSupported();
|
|
342
353
|
}
|
|
343
354
|
}"
|
|
344
355
|
@langFromChanged="${this.handleFromLangChange}"
|
|
@@ -351,6 +362,7 @@ export class TranslatePlugin extends BookReaderPlugin {
|
|
|
351
362
|
.detectedFromLang=${this.langFromCode}
|
|
352
363
|
.detectedToLang=${this.langToCode}
|
|
353
364
|
.loadingModel=${this.loadingModel}
|
|
365
|
+
.sourceLanguageSupported=${this.isSourceLanguageSupported()}
|
|
354
366
|
class="translate-panel"
|
|
355
367
|
/>`,
|
|
356
368
|
};
|
|
@@ -369,12 +381,27 @@ export class BrTranslatePanel extends LitElement {
|
|
|
369
381
|
@property({ type: String }) detectedFromLang = '';
|
|
370
382
|
@property({ type: String }) detectedToLang = '';
|
|
371
383
|
@property({ type: Boolean }) loadingModel;
|
|
384
|
+
@property({ type: Boolean }) sourceLanguageSupported = true;
|
|
385
|
+
|
|
386
|
+
@query('#lang-from') langFromDropdown;
|
|
387
|
+
@query('#lang-to') langToDropdown;
|
|
388
|
+
|
|
389
|
+
static styles = css`
|
|
390
|
+
.disclaimer {
|
|
391
|
+
padding: 10px;
|
|
392
|
+
border-radius: 8px;
|
|
393
|
+
font-size: 12px;
|
|
394
|
+
margin: 0 0 10px;
|
|
395
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
396
|
+
color: rgba(255, 255, 255, 0.9);
|
|
397
|
+
}
|
|
372
398
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
399
|
+
.disclaimer--warning {
|
|
400
|
+
margin: 10px 0;
|
|
401
|
+
background-color: rgba(255, 186, 8, 0.18);
|
|
402
|
+
color: #ffe7a3;
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
378
405
|
|
|
379
406
|
connectedCallback() {
|
|
380
407
|
super.connectedCallback();
|
|
@@ -382,11 +409,12 @@ export class BrTranslatePanel extends LitElement {
|
|
|
382
409
|
}
|
|
383
410
|
|
|
384
411
|
render() {
|
|
412
|
+
const statusWarning = this._statusWarning();
|
|
413
|
+
|
|
385
414
|
return html`<div class="app" style="margin-top: 5%;padding-right: 5px;">
|
|
386
415
|
<div
|
|
387
416
|
class="disclaimer"
|
|
388
417
|
id="disclaimerMessage"
|
|
389
|
-
style="background-color: rgba(255,255,255,0.1);padding: 10px;border-radius: 8px;font-size: 12px;margin-bottom: 10px;color: rgba(255,255,255, 0.9);"
|
|
390
418
|
>${this.disclaimerMessage}</div>
|
|
391
419
|
|
|
392
420
|
<div class="panel panel--to" style="padding: 0 10px;">
|
|
@@ -405,7 +433,7 @@ export class BrTranslatePanel extends LitElement {
|
|
|
405
433
|
</div>
|
|
406
434
|
|
|
407
435
|
<div class="panel panel--start" style="text-align: right;padding: 0 10px;/*! font-size: 18px; */margin-top: 10px;">
|
|
408
|
-
|
|
436
|
+
<button class="start-translation-brn" @click="${this._toggleTranslation}" ?disabled=${!this.sourceLanguageSupported}>
|
|
409
437
|
${this.userTranslationActive ? "Stop Translating" : "Translate"}
|
|
410
438
|
</button>
|
|
411
439
|
</div>
|
|
@@ -426,9 +454,14 @@ export class BrTranslatePanel extends LitElement {
|
|
|
426
454
|
})
|
|
427
455
|
}
|
|
428
456
|
</select>
|
|
429
|
-
|
|
457
|
+
</details>
|
|
458
|
+
</div>
|
|
430
459
|
<div class="footer" id="status" style="margin-top:5%">
|
|
431
|
-
|
|
460
|
+
${
|
|
461
|
+
statusWarning ? html`
|
|
462
|
+
<div class="disclaimer disclaimer--warning">${statusWarning}</div>
|
|
463
|
+
` : ''
|
|
464
|
+
}
|
|
432
465
|
</div>
|
|
433
466
|
|
|
434
467
|
<div class="lang-models-loading">
|
|
@@ -469,17 +502,27 @@ export class BrTranslatePanel extends LitElement {
|
|
|
469
502
|
}
|
|
470
503
|
|
|
471
504
|
_getSelectedLang(type) {
|
|
472
|
-
|
|
473
|
-
const dropdown = this.querySelector(`#lang-${type}`);
|
|
505
|
+
const dropdown = type === 'from' ? this.langFromDropdown : this.langToDropdown;
|
|
474
506
|
return dropdown ? dropdown.value : '';
|
|
475
507
|
}
|
|
476
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Resolves a human-readable language name for a language code.
|
|
511
|
+
* Prefers names from loaded translation language lists, then falls back to
|
|
512
|
+
* ISO language metadata via toNativeName.
|
|
513
|
+
* @param {string} code
|
|
514
|
+
* @returns {string}
|
|
515
|
+
*/
|
|
477
516
|
_getLangName(code) {
|
|
478
|
-
const lang = [...this.fromLanguages, ...this.toLanguages]
|
|
479
|
-
|
|
517
|
+
const lang = [...this.fromLanguages, ...this.toLanguages]
|
|
518
|
+
.find(lang => lang.code === code);
|
|
519
|
+
return lang?.name || toNativeName(code) || code;
|
|
480
520
|
}
|
|
481
521
|
|
|
482
522
|
_toggleTranslation(event) {
|
|
523
|
+
if (!this.sourceLanguageSupported) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
483
526
|
const toggleTranslateEvent = new CustomEvent('toggleTranslation', {
|
|
484
527
|
detail: {value: event.target.value},
|
|
485
528
|
bubbles: true,
|
|
@@ -491,6 +534,9 @@ export class BrTranslatePanel extends LitElement {
|
|
|
491
534
|
|
|
492
535
|
// TODO: Hardcoded warning message for now but should add more statuses
|
|
493
536
|
_statusWarning() {
|
|
537
|
+
if (!this.sourceLanguageSupported) {
|
|
538
|
+
return `Translating from ${this._getLangName(this.detectedFromLang)} is not supported`;
|
|
539
|
+
}
|
|
494
540
|
if (this.detectedFromLang == this.detectedToLang) {
|
|
495
541
|
return "Translate To language is the same as the Source language";
|
|
496
542
|
}
|
package/src/plugins/tts/utils.js
CHANGED
|
@@ -161,7 +161,7 @@ export class UrlPlugin {
|
|
|
161
161
|
* If it was changeed, update the urlState
|
|
162
162
|
*/
|
|
163
163
|
listenForHashChanges() {
|
|
164
|
-
this.oldLocationHash =
|
|
164
|
+
this.oldLocationHash = this.getHash();
|
|
165
165
|
if (this.urlLocationPollId) {
|
|
166
166
|
clearInterval(this.urlLocationPollId);
|
|
167
167
|
this.urlLocationPollId = null;
|
|
@@ -169,7 +169,7 @@ export class UrlPlugin {
|
|
|
169
169
|
|
|
170
170
|
// check if the URL changes
|
|
171
171
|
const updateHash = () => {
|
|
172
|
-
const newFragment =
|
|
172
|
+
const newFragment = this.getHash();
|
|
173
173
|
const hasFragmentChange = newFragment != this.oldLocationHash;
|
|
174
174
|
|
|
175
175
|
if (!hasFragmentChange) { return; }
|
|
@@ -182,10 +182,30 @@ export class UrlPlugin {
|
|
|
182
182
|
/**
|
|
183
183
|
* Will read either the hash or URL and return the bookreader fragment
|
|
184
184
|
*/
|
|
185
|
-
pullFromAddressBar
|
|
185
|
+
pullFromAddressBar(location = window.location) {
|
|
186
186
|
const path = this.urlMode === 'history'
|
|
187
187
|
? (location.pathname.substr(this.urlHistoryBasePath.length) + location.search)
|
|
188
188
|
: location.hash.substr(1);
|
|
189
189
|
this.urlState = this.urlStringToUrlState(path);
|
|
190
190
|
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get the hash out of the current URL. Also augments it with the text
|
|
194
|
+
* from the main part of the URL, since that is not readable by JS
|
|
195
|
+
* from the actual hash
|
|
196
|
+
* @returns
|
|
197
|
+
*/
|
|
198
|
+
getHash() {
|
|
199
|
+
const text = this.retrieveTextFragment(window.location.search);
|
|
200
|
+
const textFragment = text ? `:~:text=${text[0]}` : '';
|
|
201
|
+
return `${window.location.hash.slice(1)}${textFragment}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {string} urlString
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
retrieveTextFragment(urlString) {
|
|
209
|
+
return urlString.match(/(?<=[&?]?text=)[^&]*/);
|
|
210
|
+
}
|
|
191
211
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* global BookReader */
|
|
2
2
|
|
|
3
3
|
import { UrlPlugin } from "./UrlPlugin.js";
|
|
4
|
+
import { sleep } from "../../BookReader/utils.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Plugin for URL management in BookReader
|
|
@@ -25,7 +26,7 @@ jQuery.extend(BookReader.defaultOptions, {
|
|
|
25
26
|
urlHistoryBasePath: '/',
|
|
26
27
|
|
|
27
28
|
/** Only these params will be reflected onto the URL */
|
|
28
|
-
urlTrackedParams: ['page', 'search', 'mode', 'region', 'highlight', 'view'],
|
|
29
|
+
urlTrackedParams: ['page', 'search', 'mode', 'region', 'highlight', 'view', 'text'],
|
|
29
30
|
|
|
30
31
|
/** If true, don't update the URL when `page == n0 (eg "/page/n0")` */
|
|
31
32
|
urlTrackIndex0: false,
|
|
@@ -42,6 +43,10 @@ BookReader.prototype.setup = (function(super_) {
|
|
|
42
43
|
this.locationPollId = null;
|
|
43
44
|
this.oldLocationHash = null;
|
|
44
45
|
this.oldUserHash = null;
|
|
46
|
+
// Should include the :~:text= prefix
|
|
47
|
+
this.textFragment = null;
|
|
48
|
+
// Tracks the original textFragment page num when first loaded
|
|
49
|
+
this.textFragmentPage = null;
|
|
45
50
|
};
|
|
46
51
|
})(BookReader.prototype.setup);
|
|
47
52
|
|
|
@@ -140,11 +145,23 @@ BookReader.prototype.urlUpdateFragment = function() {
|
|
|
140
145
|
return validParams;
|
|
141
146
|
}, {});
|
|
142
147
|
|
|
148
|
+
// eg 'page/3/mode/2up'; no query params (in hash mode, it might have /search/term)
|
|
149
|
+
// Does NOT have the :~:text fragment
|
|
143
150
|
const newFragment = this.fragmentFromParams(params, this.options.urlMode);
|
|
151
|
+
const newFragmentWithSlash = newFragment === '' ? '' : `/${newFragment}`;
|
|
152
|
+
// eg 'page/3/mode/2up'; no query params
|
|
153
|
+
// WILL CONTAIN the :~:text fragment in hash mode (!)
|
|
144
154
|
const currFragment = this.urlReadFragment();
|
|
155
|
+
// This should have both ?q=foo&text=bar (and any other params) as an encoded string
|
|
145
156
|
const currQueryString = this.getLocationSearch();
|
|
157
|
+
// Eg ?q=foo&text=bar; only query params, no fragment
|
|
146
158
|
const newQueryString = this.queryStringFromParams(params, currQueryString, this.options.urlMode);
|
|
147
|
-
|
|
159
|
+
|
|
160
|
+
// NOTE: If ?text is in the URL, we will fire fragment change events on every render; which is
|
|
161
|
+
// not desireable, but currently don't have a way to handle re-writing ?text to the hash text
|
|
162
|
+
// fragment form, :~:text=foo.
|
|
163
|
+
const hasTextParam = this.urlPlugin.retrieveTextFragment(currQueryString);
|
|
164
|
+
if (currFragment === newFragment && currQueryString === newQueryString && !hasTextParam) {
|
|
148
165
|
return;
|
|
149
166
|
}
|
|
150
167
|
|
|
@@ -153,12 +170,19 @@ BookReader.prototype.urlUpdateFragment = function() {
|
|
|
153
170
|
this.options.urlMode = 'hash';
|
|
154
171
|
} else {
|
|
155
172
|
const baseWithoutSlash = this.options.urlHistoryBasePath.replace(/\/+$/, '');
|
|
156
|
-
const
|
|
157
|
-
|
|
173
|
+
const textFragment = this.urlPlugin.retrieveTextFragment(newQueryString);
|
|
158
174
|
const newUrlPath = `${baseWithoutSlash}${newFragmentWithSlash}${newQueryString}`;
|
|
175
|
+
const extractedPage = this.urlPlugin.urlStringToUrlState(newFragmentWithSlash)?.page;
|
|
176
|
+
if (!this.textFragmentPage && textFragment) {
|
|
177
|
+
this.textFragmentPage = extractedPage ? extractedPage : null;
|
|
178
|
+
this.textFragment = `:~:text=${textFragment}`;
|
|
179
|
+
}
|
|
159
180
|
try {
|
|
160
181
|
window.history.replaceState({}, null, newUrlPath);
|
|
161
182
|
this.oldLocationHash = newFragment + newQueryString;
|
|
183
|
+
if (textFragment) {
|
|
184
|
+
this.oldLocationHash += `:~:text=${textFragment[0]}`;
|
|
185
|
+
}
|
|
162
186
|
} catch (e) {
|
|
163
187
|
// DOMException on Chrome when in sandboxed iframe
|
|
164
188
|
this.options.urlMode = 'hash';
|
|
@@ -168,8 +192,22 @@ BookReader.prototype.urlUpdateFragment = function() {
|
|
|
168
192
|
|
|
169
193
|
if (this.options.urlMode === 'hash') {
|
|
170
194
|
const newQueryStringSearch = this.urlParamsFiltersOnlySearch(this.readQueryString());
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
let textFragment = this.urlPlugin.retrieveTextFragment(this.readQueryString());
|
|
196
|
+
const extractedPage = this.urlPlugin.urlStringToUrlState(newFragmentWithSlash)?.page;
|
|
197
|
+
|
|
198
|
+
if (textFragment) {
|
|
199
|
+
textFragment = `:~:text=${textFragment[0]}`;
|
|
200
|
+
} else {
|
|
201
|
+
textFragment = '';
|
|
202
|
+
}
|
|
203
|
+
if (!this.textFragmentPage && textFragment) {
|
|
204
|
+
this.textFragmentPage = extractedPage ? extractedPage : null;
|
|
205
|
+
this.textFragment = textFragment;
|
|
206
|
+
} else if (this.textFragmentPage && extractedPage != this.textFragmentPage) {
|
|
207
|
+
textFragment = '';
|
|
208
|
+
}
|
|
209
|
+
window.location.replace('#' + newFragment + newQueryStringSearch + textFragment);
|
|
210
|
+
this.oldLocationHash = newFragment + newQueryStringSearch + textFragment;
|
|
173
211
|
}
|
|
174
212
|
};
|
|
175
213
|
|
|
@@ -195,7 +233,7 @@ BookReader.prototype.urlReadFragment = function() {
|
|
|
195
233
|
if (urlMode === 'history') {
|
|
196
234
|
return window.location.pathname.substr(urlHistoryBasePath.length);
|
|
197
235
|
} else {
|
|
198
|
-
return
|
|
236
|
+
return this.urlPlugin.getHash();
|
|
199
237
|
}
|
|
200
238
|
};
|
|
201
239
|
|
|
@@ -210,6 +248,24 @@ export class BookreaderUrlPlugin extends BookReader {
|
|
|
210
248
|
init() {
|
|
211
249
|
if (this.options.enableUrlPlugin) {
|
|
212
250
|
this.urlPlugin = new UrlPlugin(this.options);
|
|
251
|
+
const location = this.getLocationSearch();
|
|
252
|
+
if (location.includes("text=")) {
|
|
253
|
+
this.on('textLayerVisible', async (_, {pageContainerEl}) => {
|
|
254
|
+
const visiblePageNum = pageContainerEl.getAttribute('data-page-num');
|
|
255
|
+
|
|
256
|
+
// Hack: More time mode 1up page "settle down" from user scrolling
|
|
257
|
+
await sleep(this.mode === 1 ? 900 : 100);
|
|
258
|
+
|
|
259
|
+
// No textFragment found or the textFragment stored doesn't match current visible page loaded
|
|
260
|
+
if (!this.textFragment || this.textFragmentPage !== visiblePageNum) return;
|
|
261
|
+
if (this.options.urlMode === 'history') {
|
|
262
|
+
window.location.replace(`#${this.textFragment}`);
|
|
263
|
+
} else {
|
|
264
|
+
// for urlMode hash, textFragment is stored in oldLocationHash already
|
|
265
|
+
window.location.replace(`#${this.oldLocationHash}`);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
213
269
|
this.bind(BookReader.eventNames.PostInit, () => {
|
|
214
270
|
const { urlMode } = this.options;
|
|
215
271
|
|