Skip to content

Commit aeadc08

Browse files
authored
Merge pull request #1440 from schu96/translation-layer-click-behavior
Use TextSelectionManager for Text Select and Translate plugins
2 parents 59d096f + 19be28b commit aeadc08

File tree

8 files changed

+459
-263
lines changed

8 files changed

+459
-263
lines changed

src/BookReader.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,8 @@ BookReader.prototype.zoom = function(direction) {
10051005
} else {
10061006
this.activeMode.zoom('out');
10071007
}
1008-
this.plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
1008+
this.plugins.textSelection?.textSelectionManager.stopPageFlip(this.refs.$brContainer);
1009+
this.plugins.translate?.textSelectionManager.stopPageFlip(this.refs.$brContainer);
10091010
return;
10101011
};
10111012

@@ -1252,7 +1253,8 @@ BookReader.prototype.switchMode = function(
12521253
const eventName = mode + 'PageViewSelected';
12531254
this.trigger(BookReader.eventNames[eventName]);
12541255

1255-
this.plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
1256+
this.plugins.textSelection?.textSelectionManager.stopPageFlip(this.refs.$brContainer);
1257+
this.plugins.translate?.textSelectionManager.stopPageFlip(this.refs.$brContainer);
12561258
};
12571259

12581260
BookReader.prototype.updateBrClasses = function() {
@@ -1324,7 +1326,8 @@ BookReader.prototype.enterFullscreen = async function(bindKeyboardControls = tru
13241326
}
13251327
this.jumpToIndex(currentIndex);
13261328

1327-
this.plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
1329+
this.plugins.textSelection?.textSelectionManager.stopPageFlip(this.refs.$brContainer);
1330+
this.plugins.translate?.textSelectionManager.stopPageFlip(this.refs.$brContainer);
13281331
// Add "?view=theater"
13291332
this.trigger(BookReader.eventNames.fragmentChange);
13301333
// trigger event here, so that animations,
@@ -1370,7 +1373,8 @@ BookReader.prototype.exitFullScreen = async function () {
13701373
await this.activeMode.mode1UpLit.updateComplete;
13711374
}
13721375

1373-
this.plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
1376+
this.plugins.textSelection?.textSelectionManager.stopPageFlip(this.refs.$brContainer);
1377+
this.plugins.translate?.textSelectionManager.stopPageFlip(this.refs.$brContainer);
13741378
// Remove "?view=theater"
13751379
this.trigger(BookReader.eventNames.fragmentChange);
13761380
this.refs.$br.removeClass('BRfullscreenAnimation');

src/css/_BRpages.scss

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,8 @@
9191
}
9292
}
9393

94-
svg.BRPageLayer {
95-
position: absolute;
96-
top: 0;
97-
left: 0;
98-
right: 0;
99-
bottom: 0;
100-
}
101-
10294
// Hides page layers during page flip animation
103-
.BRpageFlipping .BRPageLayer {
95+
.BRpageFlipping .BRtextLayer {
10496
display: none;
10597
}
10698

src/css/_TextSelection.scss

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@
6161
color: transparent;
6262
}
6363

64+
.BRtranslateLayer ::selection {
65+
background: hsla(210, 74%, 62%, 0.4);
66+
}
67+
68+
.BRtranslateLayer ::-moz-selection {
69+
background: hsla(210, 74%, 62%, 0.4);
70+
}
71+
6472
.BRparagraphElement br {
6573
visibility: hidden;
6674
}
@@ -137,8 +145,7 @@
137145
}
138146

139147
.BRtextLayer.showingTranslation {
140-
visibility: hidden;
141-
pointer-events: none;
148+
display: none;
142149
}
143150

144151
.BRtranslateLayer .BRparagraphElement.BRtranslateHidden {

src/plugins/plugin.text_selection.js

Lines changed: 5 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
//@ts-check
22
import { createDIVPageLayer } from '../BookReader/PageContainer.js';
3-
import { SelectionObserver } from '../BookReader/utils/SelectionObserver.js';
43
import { BookReaderPlugin } from '../BookReaderPlugin.js';
54
import { applyVariables } from '../util/strings.js';
65
import { Cache } from '../util/cache.js';
76
import { toISO6391 } from './tts/utils.js';
7+
import { TextSelectionManager } from '../util/TextSelectionManager.js';
88
/** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
99
/** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */
1010

@@ -20,7 +20,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
2020
singlePageDjvuXmlUrl: null,
2121
/** Whether to fetch the XML as a jsonp */
2222
jsonp: false,
23-
/** Mox words tha can be selected when the text layer is protected */
23+
/** Mox words that can be selected when the text layer is protected */
2424
maxProtectedWords: 200,
2525
}
2626

@@ -46,80 +46,17 @@ export class TextSelectionPlugin extends BookReaderPlugin {
4646
// now we do make that assumption.
4747
/** Whether the book is right-to-left */
4848
this.rtl = this.br.pageProgression === 'rl';
49-
this.selectionObserver = new SelectionObserver('.BRtextLayer', this._onSelectionChange);
49+
this.textSelectionManager = new TextSelectionManager('.BRtextLayer', this.br, {selectionElement: ['.BRwordElement', '.BRspace']}, this.options.maxProtectedWords);
5050
}
5151

5252
/** @override */
5353
init() {
5454
if (!this.options.enabled) return;
5555

5656
this.loadData();
57-
58-
this.selectionObserver.attach();
59-
new SelectionObserver('.BRtextLayer', (selectEvent) => {
60-
// Track how often selection is used
61-
if (selectEvent == 'started') {
62-
this.br.plugins.archiveAnalytics?.sendEvent('BookReader', 'SelectStart');
63-
64-
// Set a class on the page to avoid hiding it when zooming/etc
65-
this.br.refs.$br.find('.BRpagecontainer--hasSelection').removeClass('BRpagecontainer--hasSelection');
66-
$(window.getSelection().anchorNode).closest('.BRpagecontainer').addClass('BRpagecontainer--hasSelection');
67-
}
68-
}).attach();
69-
70-
if (this.br.protected) {
71-
document.addEventListener('selectionchange', this._limitSelection);
72-
// Prevent right clicking when selected text
73-
$(document.body).on('contextmenu dragstart copy', (e) => {
74-
const selection = document.getSelection();
75-
if (selection?.toString()) {
76-
const intersectsTextLayer = $('.BRtextLayer')
77-
.toArray()
78-
.some(el => selection.containsNode(el, true));
79-
if (intersectsTextLayer) {
80-
e.preventDefault();
81-
return false;
82-
}
83-
}
84-
});
85-
}
57+
this.textSelectionManager.init();
8658
}
8759

88-
_limitSelection = () => {
89-
const selection = window.getSelection();
90-
if (!selection.rangeCount) return;
91-
92-
const range = selection.getRangeAt(0);
93-
94-
// Check if range.startContainer is inside the sub-tree of .BRContainer
95-
const startInBr = !!range.startContainer.parentElement.closest('.BRcontainer');
96-
const endInBr = !!range.endContainer.parentElement.closest('.BRcontainer');
97-
if (!startInBr && !endInBr) return;
98-
if (!startInBr || !endInBr) {
99-
// weird case, just clear the selection
100-
selection.removeAllRanges();
101-
return;
102-
}
103-
104-
// Find the last allowed word in the selection
105-
const lastAllowedWord = genAt(
106-
genFilter(
107-
walkBetweenNodes(range.startContainer, range.endContainer),
108-
(node) => node.classList?.contains('BRwordElement'),
109-
),
110-
this.options.maxProtectedWords - 1,
111-
);
112-
113-
if (!lastAllowedWord || range.endContainer.parentNode == lastAllowedWord) return;
114-
115-
const newRange = document.createRange();
116-
newRange.setStart(range.startContainer, range.startOffset);
117-
newRange.setEnd(lastAllowedWord.firstChild, lastAllowedWord.textContent.length);
118-
119-
selection.removeAllRanges();
120-
selection.addRange(newRange);
121-
};
122-
12360
/**
12461
* @override
12562
* @param {PageContainer} pageContainer
@@ -134,20 +71,6 @@ export class TextSelectionPlugin extends BookReaderPlugin {
13471
return pageContainer;
13572
}
13673

137-
/**
138-
* @param {'started' | 'cleared'} type
139-
* @param {HTMLElement} target
140-
*/
141-
_onSelectionChange = (type, target) => {
142-
if (type === 'started') {
143-
this.textSelectingMode(target);
144-
} else if (type === 'cleared') {
145-
this.defaultMode(target);
146-
} else {
147-
throw new Error(`Unknown type ${type}`);
148-
}
149-
}
150-
15174
loadData() {
15275
// Only fetch the full djvu xml if the single page url isn't there
15376
if (this.options.singlePageDjvuXmlUrl) return;
@@ -204,92 +127,6 @@ export class TextSelectionPlugin extends BookReaderPlugin {
204127
}
205128
}
206129

207-
/**
208-
* Intercept copied text to remove any styling applied to it
209-
* @param {JQuery} $container
210-
*/
211-
interceptCopy($container) {
212-
$container[0].addEventListener('copy', (event) => {
213-
const selection = document.getSelection();
214-
event.clipboardData.setData('text/plain', selection.toString());
215-
event.preventDefault();
216-
});
217-
}
218-
219-
/**
220-
* Applies mouse events when in default mode
221-
* @param {HTMLElement} textLayer
222-
*/
223-
defaultMode(textLayer) {
224-
const $pageContainer = $(textLayer).closest('.BRpagecontainer');
225-
textLayer.style.pointerEvents = "none";
226-
$pageContainer.find("img").css("pointer-events", "auto");
227-
228-
$(textLayer).off(".textSelectPluginHandler");
229-
const startedMouseDown = this.mouseIsDown;
230-
let skipNextMouseup = this.mouseIsDown;
231-
if (startedMouseDown) {
232-
textLayer.style.pointerEvents = "auto";
233-
}
234-
235-
// Need to stop propagation to prevent DragScrollable from
236-
// blocking selection
237-
$(textLayer).on("mousedown.textSelectPluginHandler", (event) => {
238-
this.mouseIsDown = true;
239-
if ($(event.target).is(".BRwordElement, .BRspace")) {
240-
event.stopPropagation();
241-
}
242-
});
243-
244-
$(textLayer).on("mouseup.textSelectPluginHandler", (event) => {
245-
this.mouseIsDown = false;
246-
textLayer.style.pointerEvents = "none";
247-
if (skipNextMouseup) {
248-
skipNextMouseup = false;
249-
event.stopPropagation();
250-
}
251-
});
252-
}
253-
254-
/**
255-
* This mode is active while there is a selection on the given textLayer
256-
* @param {HTMLElement} textLayer
257-
*/
258-
textSelectingMode(textLayer) {
259-
const $pageContainer = $(textLayer).closest('.BRpagecontainer');
260-
// Make text layer consume all events
261-
textLayer.style.pointerEvents = "all";
262-
// Block img from getting long-press to save while selecting
263-
$pageContainer.find("img").css("pointer-events", "none");
264-
265-
$(textLayer).off(".textSelectPluginHandler");
266-
267-
$(textLayer).on('mousedown.textSelectPluginHandler', (event) => {
268-
this.mouseIsDown = true;
269-
event.stopPropagation();
270-
});
271-
272-
// Prevent page flip on click
273-
$(textLayer).on('mouseup.textSelectPluginHandler', (event) => {
274-
this.mouseIsDown = false;
275-
event.stopPropagation();
276-
});
277-
}
278-
279-
/**
280-
* Initializes text selection modes if there is a text layer on the page
281-
* @param {JQuery} $container
282-
*/
283-
stopPageFlip($container) {
284-
/** @type {JQuery<HTMLElement>} */
285-
const $textLayer = $container.find('.BRtextLayer');
286-
if (!$textLayer.length) return;
287-
$textLayer.each((i, s) => this.defaultMode(s));
288-
if (!this.br.protected) {
289-
this.interceptCopy($container);
290-
}
291-
}
292-
293130
/**
294131
* @param {PageContainer} pageContainer
295132
*/
@@ -344,7 +181,7 @@ export class TextSelectionPlugin extends BookReaderPlugin {
344181
textLayer.appendChild(paragEl);
345182
}
346183
$container.append(textLayer);
347-
this.stopPageFlip($container);
184+
this.textSelectionManager.stopPageFlip($container);
348185
this.br.trigger('textLayerRendered', {
349186
pageIndex,
350187
pageContainer,

src/plugins/translate/plugin.translate.js

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { customElement, property } from 'lit/decorators.js';
55
import { TranslationManager } from "./TranslationManager.js";
66
import { toISO6391 } from '../tts/utils.js';
77
import { sortBy } from '../../../src/BookReader/utils.js';
8+
import { TextSelectionManager } from '../../../src/util/TextSelectionManager.js';
89
import '@internetarchive/ia-activity-indicator/ia-activity-indicator.js';
910

1011
// @ts-ignore
@@ -183,23 +184,6 @@ export class TranslatePlugin extends BookReaderPlugin {
183184
"width": $(originalParagraphStyle).css("width"),
184185
"font-size": fontSize,
185186
});
186-
187-
// Note: We'll likely want to switch to using the same logic as
188-
// TextSelectionPlugin's selection, which allows for e.g. click-to-flip
189-
// to work simultaneously with text selection.
190-
translatedParagraph.addEventListener('mousedown', (e) => {
191-
e.stopPropagation();
192-
e.stopImmediatePropagation();
193-
});
194-
195-
translatedParagraph.addEventListener('mouseup', (e) => {
196-
e.stopPropagation();
197-
e.stopImmediatePropagation();
198-
});
199-
200-
translatedParagraph.addEventListener('dragstart', (e) =>{
201-
e.preventDefault();
202-
});
203187
pageTranslationLayer.append(translatedParagraph);
204188
}
205189

@@ -227,6 +211,8 @@ export class TranslatePlugin extends BookReaderPlugin {
227211
}
228212
}
229213
});
214+
215+
this.textSelectionManager?.stopPageFlip(this.br.refs.$brContainer);
230216
await Promise.all(paragraphTranslationPromises);
231217
this.br.trigger('translateLayerRendered', {
232218
leafIndex: pageIndex,
@@ -257,6 +243,7 @@ export class TranslatePlugin extends BookReaderPlugin {
257243

258244
clearAllTranslations() {
259245
document.querySelectorAll('.BRtranslateLayer').forEach(el => el.remove());
246+
document.querySelectorAll('.showingTranslation').forEach(el => el.classList.remove('showingTranslation'));
260247
}
261248

262249
/**
@@ -331,13 +318,20 @@ export class TranslatePlugin extends BookReaderPlugin {
331318
handleToggleTranslation = async () => {
332319
this.userToggleTranslate = !this.userToggleTranslate;
333320
this.translationManager.active = this.userToggleTranslate;
321+
// Init textSelectionManager only after the translation is active
322+
if (!this.textSelectionManager) {
323+
this.textSelectionManager = new TextSelectionManager('.BRtranslateLayer', this.br, {selectionElement: [".BRlineElement"]}, 1);
324+
this.textSelectionManager.init();
325+
}
334326
this._render();
335327
if (!this.userToggleTranslate) {
336328
this.clearAllTranslations();
337329
this.br.trigger('translationDisabled', { });
330+
this.textSelectionManager.detach();
338331
} else {
339332
this.br.trigger('translationEnabled', { });
340333
this.translateActivePageContainerElements();
334+
this.textSelectionManager.attach();
341335
}
342336
}
343337

0 commit comments

Comments
 (0)