Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make TextSelectionPlugin extend new BookReaderPlugin class #1372

Merged
merged 1 commit into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.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 @@
// 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 @@
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 @@
* @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 @@
} 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 @@
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 @@
}
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 @@
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 @@

/** @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 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 @@
}
}

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 @@
}
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 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 😎



/**
Expand Down
Loading
Loading