Skip to content

Commit

Permalink
Make TextSelectionPlugin extend new BookReaderPlugin class
Browse files Browse the repository at this point in the history
  • Loading branch information
cdrini committed Jan 30, 2025
1 parent 4bf48dc commit b7fb64f
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 106 deletions.
28 changes: 23 additions & 5 deletions src/BookReader.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ BookReader.constModeThumb = 3;
BookReader.PLUGINS = {
/** @type {typeof import('./plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin | null}*/
archiveAnalytics: null,
/** @type {typeof import('./plugins/plugin.text_selection.js').TextSelectionPlugin | null}*/
textSelection: null,
};

/**
Expand Down Expand Up @@ -259,8 +261,14 @@ BookReader.prototype.setup = function(options) {
// Construct the usual suspects first to get type hints
this._plugins = {
archiveAnalytics: BookReader.PLUGINS.archiveAnalytics ? new BookReader.PLUGINS.archiveAnalytics(this) : null,
textSelection: BookReader.PLUGINS.textSelection ? new BookReader.PLUGINS.textSelection(this) : null,
};

// Delete anything that's null
for (const [pluginName, plugin] of Object.entries(this._plugins)) {
if (!plugin) delete this._plugins[pluginName];
}

// Now construct the rest of the plugins
for (const [pluginName, PluginClass] of Object.entries(BookReader.PLUGINS)) {
if (this._plugins[pluginName] || !PluginClass) continue;
Expand All @@ -271,6 +279,9 @@ BookReader.prototype.setup = function(options) {
for (const [pluginName, plugin] of Object.entries(this._plugins)) {
try {
plugin.setup(this.options.plugins?.[pluginName] ?? {});
// Write the options back; this way the plugin is the source of truth,
// and BR just contains a reference to it.
this.options.plugins[pluginName] = plugin.options;
} catch (e) {
console.error(`Error setting up plugin ${pluginName}`, e);
}
Expand Down Expand Up @@ -824,11 +835,18 @@ BookReader.prototype.drawLeafs = function() {
* @param {PageIndex} index
*/
BookReader.prototype._createPageContainer = function(index) {
return new PageContainer(this.book.getPage(index, false), {
const pageContainer = new PageContainer(this.book.getPage(index, false), {
isProtected: this.protected,
imageCache: this.imageCache,
loadingImage: this.imagesBaseURL + 'loading.gif',
});

// Call plugin handlers
for (const plugin of Object.values(this._plugins)) {
plugin._configurePageContainer(pageContainer);
}

return pageContainer;
};

BookReader.prototype.bindGestures = function(jElement) {
Expand Down Expand Up @@ -877,7 +895,7 @@ BookReader.prototype.zoom = function(direction) {
} else {
this.activeMode.zoom('out');
}
this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer);
this._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);

Check warning on line 898 in src/BookReader.js

View check run for this annotation

Codecov / codecov/patch

src/BookReader.js#L898

Added line #L898 was not covered by tests
return;
};

Expand Down Expand Up @@ -1108,7 +1126,7 @@ BookReader.prototype.switchMode = function(
const eventName = mode + 'PageViewSelected';
this.trigger(BookReader.eventNames[eventName]);

this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer);
this._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
};

BookReader.prototype.updateBrClasses = function() {
Expand Down Expand Up @@ -1180,7 +1198,7 @@ BookReader.prototype.enterFullscreen = async function(bindKeyboardControls = tru
}
this.jumpToIndex(currentIndex);

this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer);
this._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
// Add "?view=theater"
this.trigger(BookReader.eventNames.fragmentChange);
// trigger event here, so that animations,
Expand Down Expand Up @@ -1226,7 +1244,7 @@ BookReader.prototype.exitFullScreen = async function () {
await this.activeMode.mode1UpLit.updateComplete;
}

this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer);
this._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
// Remove "?view=theater"
this.trigger(BookReader.eventNames.fragmentChange);
this.refs.$br.removeClass('BRfullscreenAnimation');
Expand Down
2 changes: 1 addition & 1 deletion src/BookReader/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const DEFAULT_OPTIONS = {
plugins: {
/** @type {import('../plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin['options']}*/
archiveAnalytics: null,
/** @type {import('../plugins/plugin.text_selection.js').TextSelectionPluginOptions} */
/** @type {import('../plugins/plugin.text_selection.js').TextSelectionPlugin['options']} */
textSelection: null,
},

Expand Down
8 changes: 8 additions & 0 deletions src/BookReaderPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,12 @@ export class BookReaderPlugin {

/** @abstract */
init() {}

/**
* @abstract
* @protected
* @param {import ("./BookReader/PageContainer.js").PageContainer} pageContainer
*/
_configurePageContainer(pageContainer) {

Check warning on line 34 in src/BookReaderPlugin.js

View check run for this annotation

Codecov / codecov/patch

src/BookReaderPlugin.js#L34

Added line #L34 was not covered by tests
}
}
144 changes: 68 additions & 76 deletions src/plugins/plugin.text_selection.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
//@ts-check
import { createDIVPageLayer } from '../BookReader/PageContainer.js';
import { SelectionObserver } from '../BookReader/utils/SelectionObserver.js';
import { BookReaderPlugin } from '../BookReaderPlugin.js';
import { applyVariables } from '../util/strings.js';
/** @typedef {import('../util/strings.js').StringWithVars} StringWithVars */
/** @typedef {import('../BookReader/PageContainer.js').PageContainer} PageContainer */

const BookReader = /** @type {typeof import('../BookReader').default} */(window.BookReader);

export const DEFAULT_OPTIONS = {
enabled: true,
/** @type {StringWithVars} The URL to fetch the entire DJVU xml. Supports options.vars */
fullDjvuXmlUrl: null,
/** @type {StringWithVars} The URL to fetch a single page of the DJVU xml. Supports options.vars. Also has {{pageIndex}} */
singlePageDjvuXmlUrl: null,
/** Whether to fetch the XML as a jsonp */
jsonp: false,
};
/** @typedef {typeof DEFAULT_OPTIONS} TextSelectionPluginOptions */

/**
* @template T
*/
Expand All @@ -39,30 +29,73 @@ export class Cache {
}
}

export class TextSelectionPlugin {
export class TextSelectionPlugin extends BookReaderPlugin {
options = {
enabled: true,
/** @type {StringWithVars} The URL to fetch the entire DJVU xml. Supports options.vars */
fullDjvuXmlUrl: null,
/** @type {StringWithVars} The URL to fetch a single page of the DJVU xml. Supports options.vars. Also has {{pageIndex}} */
singlePageDjvuXmlUrl: null,
/** Whether to fetch the XML as a jsonp */
jsonp: false,
}

/**@type {PromiseLike<JQuery<HTMLElement>|undefined>} */
djvuPagesPromise = null;

/** @type {Cache<{index: number, response: any}>} */
pageTextCache = new Cache();

/**
* @param {'lr' | 'rl'} pageProgression In the future this should be in the ocr file
* since a book being right to left doesn't mean the ocr is right to left. But for
* now we do make that assumption.
* Sometimes there are too many words on a page, and the browser becomes near
* unusable. For now don't render text layer for pages with too many words.
*/
constructor(options = DEFAULT_OPTIONS, optionVariables, pageProgression = 'lr') {
this.options = options;
this.optionVariables = optionVariables;
/**@type {PromiseLike<JQuery<HTMLElement>|undefined>} */
this.djvuPagesPromise = null;
maxWordRendered = 2500;

/**
* @param {import('../BookReader.js').default} br
*/
constructor(br) {
super(br);
// In the future this should be in the ocr file
// since a book being right to left doesn't mean the ocr is right to left. But for
// now we do make that assumption.
/** Whether the book is right-to-left */
this.rtl = pageProgression === 'rl';
this.rtl = this.br.pageProgression === 'rl';
this.selectionObserver = new SelectionObserver('.BRtextLayer', this._onSelectionChange);
}

/** @type {Cache<{index: number, response: any}>} */
this.pageTextCache = new Cache();
/** @override */
init() {
if (!this.options.enabled) return;

/**
* Sometimes there are too many words on a page, and the browser becomes near
* unusable. For now don't render text layer for pages with too many words.
*/
this.maxWordRendered = 2500;
this.loadData();

this.selectionObserver = new SelectionObserver('.BRtextLayer', this._onSelectionChange);
this.selectionObserver.attach();
new SelectionObserver('.BRtextLayer', (selectEvent) => {
// Track how often selection is used
if (selectEvent == 'started') {
this.br._plugins.archiveAnalytics?.sendEvent('BookReader', 'SelectStart');

Check warning on line 78 in src/plugins/plugin.text_selection.js

View check run for this annotation

Codecov / codecov/patch

src/plugins/plugin.text_selection.js#L78

Added line #L78 was not covered by tests

// Set a class on the page to avoid hiding it when zooming/etc
this.br.refs.$br.find('.BRpagecontainer--hasSelection').removeClass('BRpagecontainer--hasSelection');
$(window.getSelection().anchorNode).closest('.BRpagecontainer').addClass('BRpagecontainer--hasSelection');

Check warning on line 82 in src/plugins/plugin.text_selection.js

View check run for this annotation

Codecov / codecov/patch

src/plugins/plugin.text_selection.js#L81-L82

Added lines #L81 - L82 were not covered by tests
}
}).attach();
}

/**
* @override
* @param {PageContainer} pageContainer
* @returns {PageContainer}
*/
_configurePageContainer(pageContainer) {
// Disable if thumb mode; it's too janky
// .page can be null for "pre-cover" region
if (this.br.mode !== this.br.constModeThumb && pageContainer.page) {
this.createTextLayer(pageContainer);
}
return pageContainer;
}

/**
Expand All @@ -79,18 +112,16 @@ export class TextSelectionPlugin {
}
}

init() {
this.selectionObserver.attach();

loadData() {
// Only fetch the full djvu xml if the single page url isn't there
if (this.options.singlePageDjvuXmlUrl) return;
this.djvuPagesPromise = $.ajax({
type: "GET",
url: applyVariables(this.options.fullDjvuXmlUrl, this.optionVariables),
url: applyVariables(this.options.fullDjvuXmlUrl, this.br.options.vars),
dataType: this.options.jsonp ? "jsonp" : "html",
cache: true,
xhrFields: {
withCredentials: window.br.protected,
withCredentials: this.br.protected,
},
error: (e) => undefined,
}).then((res) => {
Expand All @@ -115,11 +146,11 @@ export class TextSelectionPlugin {
}
const res = await $.ajax({
type: "GET",
url: applyVariables(this.options.singlePageDjvuXmlUrl, this.optionVariables, { pageIndex: index }),
url: applyVariables(this.options.singlePageDjvuXmlUrl, this.br.options.vars, { pageIndex: index }),
dataType: this.options.jsonp ? "jsonp" : "html",
cache: true,
xhrFields: {
withCredentials: window.br.protected,
withCredentials: this.br.protected,
},
error: (e) => undefined,
});
Expand Down Expand Up @@ -410,46 +441,7 @@ export class TextSelectionPlugin {
}
}

export class BookreaderWithTextSelection extends BookReader {
init() {
const options = Object.assign({}, DEFAULT_OPTIONS, this.options.plugins.textSelection);
if (options.enabled) {
this.textSelectionPlugin = new TextSelectionPlugin(options, this.options.vars, this.pageProgression);
// Write this back; this way the plugin is the source of truth, and BR just
// contains a reference to it.
this.options.plugins.textSelection = options;
this.textSelectionPlugin.init();

new SelectionObserver('.BRtextLayer', (selectEvent) => {
// Track how often selection is used
if (selectEvent == 'started') {
this._plugins.archiveAnalytics?.sendEvent('BookReader', 'SelectStart');

// Set a class on the page to avoid hiding it when zooming/etc
this.refs.$br.find('.BRpagecontainer--hasSelection').removeClass('BRpagecontainer--hasSelection');
$(window.getSelection().anchorNode).closest('.BRpagecontainer').addClass('BRpagecontainer--hasSelection');
}
}).attach();
}

super.init();
}

/**
* @param {number} index
*/
_createPageContainer(index) {
const pageContainer = super._createPageContainer(index);
// Disable if thumb mode; it's too janky
// .page can be null for "pre-cover" region
if (this.mode !== this.constModeThumb && pageContainer.page) {
this.textSelectionPlugin?.createTextLayer(pageContainer);
}
return pageContainer;
}
}
window.BookReader = BookreaderWithTextSelection;
export default BookreaderWithTextSelection;
BookReader?.registerPlugin('textSelection', TextSelectionPlugin);


/**
Expand Down
Loading

0 comments on commit b7fb64f

Please sign in to comment.