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 59888e5
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 79 deletions.
14 changes: 12 additions & 2 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 @@ -258,7 +260,8 @@ 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,
archiveAnalytics: new BookReader.PLUGINS.archiveAnalytics?.(this),
textSelection: BookReader.PLUGINS.textSelection ? new BookReader.PLUGINS.textSelection(this) : null,
};

// Now construct the rest of the plugins
Expand Down Expand Up @@ -824,11 +827,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
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
11 changes: 11 additions & 0 deletions src/BookReaderPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,19 @@ export class BookReaderPlugin {
**/
setup(options) {
this.options = Object.assign({}, this.options, options);
// Write this back; this way the plugin is the source of truth, and BR just
// contains a reference to it.
// TODO this.br.options.plugins[NAME??] = this.options;
}

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

/**
* @abstract
* @protected
* @param {import ("./BookReader/PageContainer.js").PageContainer} pageContainer
*/
_configurePageContainer(pageContainer) {
}
}
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');

// 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');
}
}).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

0 comments on commit 59888e5

Please sign in to comment.