@internetarchive/bookreader 5.0.0-88 → 5.0.0-89
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 +14 -0
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js +1 -1
- package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
- package/BookReader/plugins/plugin.autoplay.js +1 -1
- package/BookReader/plugins/plugin.autoplay.js.map +1 -1
- package/BookReader/plugins/plugin.iiif.js +1 -1
- package/BookReader/plugins/plugin.iiif.js.map +1 -1
- package/BookReader/plugins/plugin.resume.js +1 -1
- package/BookReader/plugins/plugin.resume.js.map +1 -1
- package/BookReader/plugins/plugin.text_selection.js +1 -1
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/plugins/plugin.url.js +1 -1
- package/BookReader/plugins/plugin.url.js.map +1 -1
- package/CHANGELOG.md +10 -0
- package/codecov.yml +1 -1
- package/package.json +1 -1
- package/src/BookReader/ImageCache.js +48 -15
- package/src/BookReader/Mode2UpLit.js +3 -2
- package/src/BookReader/PageContainer.js +41 -22
- package/src/BookReader/options.js +24 -3
- package/src/BookReader/utils.js +10 -0
- package/src/BookReader.js +89 -38
- package/src/BookReaderPlugin.js +16 -0
- package/src/css/_BRpages.scss +21 -2
- package/src/plugins/plugin.autoplay.js +98 -102
- package/src/plugins/plugin.iiif.js +16 -30
- package/src/plugins/plugin.resume.js +54 -51
- package/src/plugins/plugin.text_selection.js +68 -76
- package/src/plugins/tts/AbstractTTSEngine.js +2 -4
- package/src/plugins/tts/PageChunk.js +5 -9
- package/src/plugins/tts/PageChunkIterator.js +3 -5
- package/src/plugins/tts/plugin.tts.js +309 -329
- package/src/plugins/url/plugin.url.js +1 -1
- package/src/util/strings.js +1 -0
- package/tests/e2e/autoplay.test.js +8 -5
- package/tests/e2e/helpers/base.js +2 -2
- package/tests/e2e/helpers/mockSearch.js +6 -9
- package/tests/jest/BookReader/PageContainer.test.js +96 -55
- package/tests/jest/BookReader/utils.test.js +21 -0
- package/tests/jest/BookReader.test.js +13 -12
- package/tests/jest/plugins/plugin.autoplay.test.js +9 -22
- package/tests/jest/plugins/plugin.resume.test.js +19 -32
- package/tests/jest/plugins/plugin.text_selection.test.js +23 -24
@@ -7,8 +7,10 @@
|
|
7
7
|
/** @typedef {import("./BookModel").BookModel} BookModel */
|
8
8
|
/** @typedef {import("./BookModel").PageIndex} PageIndex */
|
9
9
|
/** @typedef {import("./ReduceSet").ReduceSet} ReduceSet */
|
10
|
+
/** @typedef {import("./options").BookReaderOptions} BookReaderOptions */
|
10
11
|
|
11
12
|
import { Pow2ReduceSet } from "./ReduceSet";
|
13
|
+
import { DEFAULT_OPTIONS } from "./options";
|
12
14
|
|
13
15
|
export class ImageCache {
|
14
16
|
/**
|
@@ -16,11 +18,25 @@ export class ImageCache {
|
|
16
18
|
* @param {object} opts
|
17
19
|
* @param {boolean} [opts.useSrcSet]
|
18
20
|
* @param {ReduceSet} [opts.reduceSet]
|
21
|
+
* @param {BookReaderOptions['renderPageURI']} [opts.renderPageURI]
|
19
22
|
*/
|
20
|
-
constructor(
|
23
|
+
constructor(
|
24
|
+
book,
|
25
|
+
{
|
26
|
+
useSrcSet = false,
|
27
|
+
reduceSet = Pow2ReduceSet,
|
28
|
+
renderPageURI = DEFAULT_OPTIONS.renderPageURI,
|
29
|
+
} = {},
|
30
|
+
) {
|
31
|
+
/** @type {BookModel} */
|
21
32
|
this.book = book;
|
33
|
+
/** @type {boolean} */
|
22
34
|
this.useSrcSet = useSrcSet;
|
35
|
+
/** @type {ReduceSet} */
|
23
36
|
this.reduceSet = reduceSet;
|
37
|
+
/** @type {BookReaderOptions['renderPageURI']} */
|
38
|
+
this.renderPageURI = renderPageURI;
|
39
|
+
|
24
40
|
/** @type {{ [index: number]: { reduce: number, loaded: boolean }[] }} */
|
25
41
|
this.cache = {};
|
26
42
|
this.defaultScale = 8;
|
@@ -33,19 +49,35 @@ export class ImageCache {
|
|
33
49
|
*
|
34
50
|
* @param {PageIndex} index
|
35
51
|
* @param {Number} reduce
|
52
|
+
* @param {HTMLImageElement?} [img]
|
36
53
|
*/
|
37
|
-
image(index, reduce) {
|
54
|
+
image(index, reduce, img = null) {
|
55
|
+
const finalReduce = this.getFinalReduce(index, reduce);
|
56
|
+
return this._serveImageElement(index, finalReduce, img);
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Get the final reduce factor to use for the given index
|
61
|
+
*
|
62
|
+
* @param {PageIndex} index
|
63
|
+
* @param {Number} reduce
|
64
|
+
*/
|
65
|
+
getFinalReduce(index, reduce) {
|
38
66
|
const cachedImages = this.cache[index] || [];
|
39
67
|
const sufficientImages = cachedImages
|
40
68
|
.filter(x => x.loaded && x.reduce <= reduce);
|
69
|
+
|
41
70
|
if (sufficientImages.length) {
|
42
71
|
// Choose the largest reduction factor that meets our needs
|
43
72
|
const bestReduce = Math.max(...sufficientImages.map(e => e.reduce));
|
44
|
-
|
73
|
+
// Don't need to floor here, since we know the image is in the cache
|
74
|
+
// and hence was already floor'd by the below `else` clause before
|
75
|
+
// it was added
|
76
|
+
return bestReduce;
|
45
77
|
} else {
|
46
78
|
// Don't use a cache entry; i.e. a fresh fetch will be made
|
47
79
|
// for this reduce
|
48
|
-
return this.
|
80
|
+
return this.reduceSet.floor(reduce);
|
49
81
|
}
|
50
82
|
}
|
51
83
|
|
@@ -87,26 +119,27 @@ export class ImageCache {
|
|
87
119
|
*
|
88
120
|
* @param {PageIndex} index
|
89
121
|
* @param {number} reduce
|
122
|
+
* @param {HTMLImageElement?} [img]
|
90
123
|
* @returns {JQuery<HTMLImageElement>} with base image classes
|
91
124
|
*/
|
92
|
-
_serveImageElement(index, reduce) {
|
93
|
-
|
94
|
-
let cacheEntry = this.cache[index]?.find(e => e.reduce == validReduce);
|
125
|
+
_serveImageElement(index, reduce, img = null) {
|
126
|
+
let cacheEntry = this.cache[index]?.find(e => e.reduce == reduce);
|
95
127
|
if (!cacheEntry) {
|
96
|
-
cacheEntry = { reduce
|
128
|
+
cacheEntry = { reduce, loaded: false };
|
97
129
|
const entries = this.cache[index] || (this.cache[index] = []);
|
98
130
|
entries.push(cacheEntry);
|
99
131
|
}
|
100
132
|
const page = this.book.getPage(index);
|
101
133
|
|
102
|
-
const
|
103
|
-
|
104
|
-
'
|
105
|
-
|
106
|
-
|
107
|
-
.data('
|
134
|
+
const uri = page.getURI(reduce, 0);
|
135
|
+
const $img = $(img || document.createElement('img'))
|
136
|
+
.addClass('BRpageimage')
|
137
|
+
.attr('alt', 'Book page image')
|
138
|
+
.data('reduce', reduce)
|
139
|
+
.data('src', uri);
|
140
|
+
this.renderPageURI($img[0], uri);
|
108
141
|
if (this.useSrcSet) {
|
109
|
-
$img.attr('srcset', page.getURISrcSet(
|
142
|
+
$img.attr('srcset', page.getURISrcSet(reduce));
|
110
143
|
}
|
111
144
|
if (!cacheEntry.loaded) {
|
112
145
|
$img.one('load', () => cacheEntry.loaded = true);
|
@@ -493,13 +493,14 @@ export class Mode2UpLit extends LitElement {
|
|
493
493
|
/**
|
494
494
|
* @param {'left' | 'right' | 'next' | 'prev' | PageIndex | PageModel | {left: PageModel | null, right: PageModel | null}} nextSpread
|
495
495
|
*/
|
496
|
-
async flipAnimation(nextSpread, { animate = true } = {}) {
|
496
|
+
async flipAnimation(nextSpread, { animate = true, flipSpeed = this.flipSpeed } = {}) {
|
497
497
|
const curSpread = (this.pageLeft || this.pageRight)?.spread;
|
498
498
|
if (!curSpread) {
|
499
499
|
// Nothings been actually rendered yet! Will be corrected during initFirstRender
|
500
500
|
return;
|
501
501
|
}
|
502
502
|
|
503
|
+
flipSpeed = flipSpeed || this.flipSpeed; // Handle null
|
503
504
|
nextSpread = this.parseNextSpread(nextSpread);
|
504
505
|
if (this.activeFlip || !nextSpread) return;
|
505
506
|
|
@@ -559,7 +560,7 @@ export class Mode2UpLit extends LitElement {
|
|
559
560
|
|
560
561
|
/** @type {KeyframeAnimationOptions} */
|
561
562
|
const animationStyle = {
|
562
|
-
duration:
|
563
|
+
duration: flipSpeed + this.activeFlip.pagesFlippingCount,
|
563
564
|
easing: 'ease-in',
|
564
565
|
fill: 'none',
|
565
566
|
};
|
@@ -2,6 +2,8 @@
|
|
2
2
|
/** @typedef {import('./BookModel.js').PageModel} PageModel */
|
3
3
|
/** @typedef {import('./ImageCache.js').ImageCache} ImageCache */
|
4
4
|
|
5
|
+
import { sleep } from './utils.js';
|
6
|
+
|
5
7
|
|
6
8
|
export class PageContainer {
|
7
9
|
/**
|
@@ -9,12 +11,10 @@ export class PageContainer {
|
|
9
11
|
* @param {object} opts
|
10
12
|
* @param {boolean} opts.isProtected Whether we're in a protected book
|
11
13
|
* @param {ImageCache} opts.imageCache
|
12
|
-
* @param {string} opts.loadingImage
|
13
14
|
*/
|
14
|
-
constructor(page, {isProtected, imageCache
|
15
|
+
constructor(page, {isProtected, imageCache}) {
|
15
16
|
this.page = page;
|
16
17
|
this.imageCache = imageCache;
|
17
|
-
this.loadingImage = loadingImage;
|
18
18
|
this.$container = $('<div />', {
|
19
19
|
'class': `BRpagecontainer ${page ? `pagediv${page.index}` : 'BRemptypage'}`,
|
20
20
|
css: { position: 'absolute' },
|
@@ -43,39 +43,58 @@ export class PageContainer {
|
|
43
43
|
return;
|
44
44
|
}
|
45
45
|
|
46
|
-
const
|
47
|
-
const
|
46
|
+
const finalReduce = this.imageCache.getFinalReduce(this.page.index, reduce);
|
47
|
+
const newImageURI = this.page.getURI(finalReduce, 0);
|
48
48
|
|
49
|
-
//
|
50
|
-
const
|
49
|
+
// Note: These must be computed _before_ we call .image()
|
50
|
+
const alreadyLoaded = this.imageCache.imageLoaded(this.page.index, finalReduce);
|
51
|
+
const nextBestLoadedReduce = this.imageCache.getBestLoadedReduce(this.page.index, reduce);
|
51
52
|
|
52
53
|
// Avoid removing/re-adding the image if it's already there
|
53
54
|
// This can be called quite a bit, so we need to be fast
|
54
|
-
if (this.$img?.
|
55
|
+
if (this.$img?.data('src') == newImageURI) {
|
55
56
|
return this;
|
56
57
|
}
|
57
58
|
|
58
|
-
this.$img
|
59
|
-
this.$img =
|
59
|
+
let $oldImg = this.$img;
|
60
|
+
this.$img = this.imageCache.image(this.page.index, finalReduce);
|
61
|
+
if ($oldImg) {
|
62
|
+
this.$img.insertAfter($oldImg);
|
63
|
+
} else {
|
64
|
+
this.$img.prependTo(this.$container);
|
65
|
+
}
|
60
66
|
|
61
|
-
const backgroundLayers = [];
|
62
67
|
if (!alreadyLoaded) {
|
63
68
|
this.$container.addClass('BRpageloading');
|
64
|
-
backgroundLayers.push(`url("${this.loadingImage}") center/20px no-repeat`);
|
65
|
-
}
|
66
|
-
if (nextBestLoadedReduce) {
|
67
|
-
backgroundLayers.push(`url("${this.page.getURI(nextBestLoadedReduce, 0)}") center/100% 100% no-repeat`);
|
68
69
|
}
|
69
70
|
|
70
|
-
if (!alreadyLoaded) {
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
71
|
+
if (!alreadyLoaded && nextBestLoadedReduce) {
|
72
|
+
// If we have a slightly lower quality image loaded, use that as the background
|
73
|
+
// while the higher res one loads
|
74
|
+
const nextBestUri = this.page.getURI(nextBestLoadedReduce, 0);
|
75
|
+
if ($oldImg) {
|
76
|
+
if ($oldImg.data('src') == nextBestUri) {
|
77
|
+
// Do nothing! It's already showing the right thing
|
78
|
+
} else {
|
79
|
+
// We have a different src, need to update the src
|
80
|
+
this.imageCache.image(this.page.index, nextBestLoadedReduce, $oldImg[0]);
|
81
|
+
}
|
82
|
+
} else {
|
83
|
+
// We don't have an old <img>, so we need to create a new one
|
84
|
+
$oldImg = this.imageCache.image(this.page.index, nextBestLoadedReduce);
|
85
|
+
$oldImg.prependTo(this.$container);
|
86
|
+
}
|
77
87
|
}
|
78
88
|
|
89
|
+
this.$img
|
90
|
+
.one('load', async (ev) => {
|
91
|
+
this.$container.removeClass('BRpageloading');
|
92
|
+
// `load` can fire a little early, so wait a spell before removing the old image
|
93
|
+
// to avoid flicker
|
94
|
+
await sleep(100);
|
95
|
+
$oldImg?.remove();
|
96
|
+
});
|
97
|
+
|
79
98
|
return this;
|
80
99
|
}
|
81
100
|
}
|
@@ -143,8 +143,16 @@ export const DEFAULT_OPTIONS = {
|
|
143
143
|
plugins: {
|
144
144
|
/** @type {import('../plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin['options']}*/
|
145
145
|
archiveAnalytics: null,
|
146
|
-
/** @type {import('../plugins/plugin.
|
146
|
+
/** @type {import('../plugins/plugin.autoplay.js').AutoplayPlugin['options']}*/
|
147
|
+
autoplay: null,
|
148
|
+
/** @type {import('../plugins/plugin.iiif.js').IiifPlugin['options']} */
|
149
|
+
iiif: null,
|
150
|
+
/** @type {import('../plugins/plugin.resume.js').ResumePlugin['options']} */
|
151
|
+
resume: null,
|
152
|
+
/** @type {import('../plugins/plugin.text_selection.js').TextSelectionPlugin['options']} */
|
147
153
|
textSelection: null,
|
154
|
+
/** @type {import('../plugins/tts/plugin.tts.js').TtsPlugin['options']} */
|
155
|
+
tts: null,
|
148
156
|
},
|
149
157
|
|
150
158
|
/**
|
@@ -186,16 +194,29 @@ export const DEFAULT_OPTIONS = {
|
|
186
194
|
/** @type {import('../plugins/plugin.chapters.js').TocEntry[]} */
|
187
195
|
table_of_contents: null,
|
188
196
|
|
189
|
-
/**
|
197
|
+
/**
|
198
|
+
* Advanced methods for page rendering.
|
199
|
+
* All option functions have their `this` object set to the BookReader instance.
|
200
|
+
**/
|
201
|
+
|
190
202
|
/** @type {() => number} */
|
191
203
|
getNumLeafs: null,
|
192
204
|
/** @type {(index: number) => number} */
|
193
205
|
getPageWidth: null,
|
194
206
|
/** @type {(index: number) => number} */
|
195
207
|
getPageHeight: null,
|
196
|
-
/** @type {(index: number, reduce: number, rotate: number) =>
|
208
|
+
/** @type {(index: number, reduce: number, rotate: number) => string} */
|
197
209
|
getPageURI: null,
|
198
210
|
|
211
|
+
/**
|
212
|
+
* @type {(img: HTMLImageElement, uri: string) => Promise<void>}
|
213
|
+
* Render the page URI into the image element. Perform any necessary preloading,
|
214
|
+
* authentication, etc.
|
215
|
+
*/
|
216
|
+
renderPageURI(img, uri) {
|
217
|
+
img.src = uri;
|
218
|
+
},
|
219
|
+
|
199
220
|
/**
|
200
221
|
* @type {(index: number) => 'L' | 'R'}
|
201
222
|
* Return which side, left or right, that a given page should be displayed on
|
package/src/BookReader/utils.js
CHANGED
@@ -288,3 +288,13 @@ export function promisifyEvent(target, eventType) {
|
|
288
288
|
export function escapeRegExp(string) {
|
289
289
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
290
290
|
}
|
291
|
+
|
292
|
+
/**
|
293
|
+
* @param {number | 'fast' | 'slow' | string} speed
|
294
|
+
* Parsing of the jquery animation speed; see https://api.jquery.com/animate/
|
295
|
+
*/
|
296
|
+
export function parseAnimationSpeed(speed) {
|
297
|
+
if (speed === 'slow') return 600;
|
298
|
+
if (speed === 'fast') return 200;
|
299
|
+
return parseInt(speed, 10);
|
300
|
+
}
|
package/src/BookReader.js
CHANGED
@@ -68,6 +68,14 @@ BookReader.constModeThumb = 3;
|
|
68
68
|
BookReader.PLUGINS = {
|
69
69
|
/** @type {typeof import('./plugins/plugin.archive_analytics.js').ArchiveAnalyticsPlugin | null}*/
|
70
70
|
archiveAnalytics: null,
|
71
|
+
/** @type {typeof import('./plugins/plugin.autoplay.js').AutoplayPlugin | null}*/
|
72
|
+
autoplay: null,
|
73
|
+
/** @type {typeof import('./plugins/plugin.resume.js').ResumePlugin | null}*/
|
74
|
+
resume: null,
|
75
|
+
/** @type {typeof import('./plugins/plugin.text_selection.js').TextSelectionPlugin | null}*/
|
76
|
+
textSelection: null,
|
77
|
+
/** @type {typeof import('./plugins/tts/plugin.tts.js').TtsPlugin | null}*/
|
78
|
+
tts: null,
|
71
79
|
};
|
72
80
|
|
73
81
|
/**
|
@@ -108,6 +116,38 @@ BookReader.prototype.setup = function(options) {
|
|
108
116
|
// Store the options used to setup bookreader
|
109
117
|
this.options = options;
|
110
118
|
|
119
|
+
// Construct the usual plugins first to get type hints
|
120
|
+
this._plugins = {
|
121
|
+
archiveAnalytics: BookReader.PLUGINS.archiveAnalytics ? new BookReader.PLUGINS.archiveAnalytics(this) : null,
|
122
|
+
autoplay: BookReader.PLUGINS.autoplay ? new BookReader.PLUGINS.autoplay(this) : null,
|
123
|
+
resume: BookReader.PLUGINS.resume ? new BookReader.PLUGINS.resume(this) : null,
|
124
|
+
textSelection: BookReader.PLUGINS.textSelection ? new BookReader.PLUGINS.textSelection(this) : null,
|
125
|
+
tts: BookReader.PLUGINS.tts ? new BookReader.PLUGINS.tts(this) : null,
|
126
|
+
};
|
127
|
+
|
128
|
+
// Delete anything that's null
|
129
|
+
for (const [pluginName, plugin] of Object.entries(this._plugins)) {
|
130
|
+
if (!plugin) delete this._plugins[pluginName];
|
131
|
+
}
|
132
|
+
|
133
|
+
// Now construct the rest of the plugins
|
134
|
+
for (const [pluginName, PluginClass] of Object.entries(BookReader.PLUGINS)) {
|
135
|
+
if (this._plugins[pluginName] || !PluginClass) continue;
|
136
|
+
this._plugins[pluginName] = new PluginClass(this);
|
137
|
+
}
|
138
|
+
|
139
|
+
// And call setup on them
|
140
|
+
for (const [pluginName, plugin] of Object.entries(this._plugins)) {
|
141
|
+
try {
|
142
|
+
plugin.setup(this.options.plugins?.[pluginName] ?? {});
|
143
|
+
// Write the options back; this way the plugin is the source of truth,
|
144
|
+
// and BR just contains a reference to it.
|
145
|
+
this.options.plugins[pluginName] = plugin.options;
|
146
|
+
} catch (e) {
|
147
|
+
console.error(`Error setting up plugin ${pluginName}`, e);
|
148
|
+
}
|
149
|
+
}
|
150
|
+
|
111
151
|
/** @type {number} @deprecated some past iterations set this */
|
112
152
|
this.numLeafs = undefined;
|
113
153
|
|
@@ -166,11 +206,7 @@ BookReader.prototype.setup = function(options) {
|
|
166
206
|
this.displayedIndices = [];
|
167
207
|
|
168
208
|
this.animating = false;
|
169
|
-
this.flipSpeed =
|
170
|
-
'fast': 200,
|
171
|
-
'slow': 600,
|
172
|
-
}[options.flipSpeed] || 400;
|
173
|
-
this.flipDelay = options.flipDelay;
|
209
|
+
this.flipSpeed = utils.parseAnimationSpeed(options.flipSpeed) || 400;
|
174
210
|
|
175
211
|
/**
|
176
212
|
* Represents the first displayed index
|
@@ -256,30 +292,11 @@ BookReader.prototype.setup = function(options) {
|
|
256
292
|
'_modes.modeThumb': this._modes.modeThumb,
|
257
293
|
};
|
258
294
|
|
259
|
-
// Construct the usual suspects first to get type hints
|
260
|
-
this._plugins = {
|
261
|
-
archiveAnalytics: BookReader.PLUGINS.archiveAnalytics ? new BookReader.PLUGINS.archiveAnalytics(this) : null,
|
262
|
-
};
|
263
|
-
|
264
|
-
// Now construct the rest of the plugins
|
265
|
-
for (const [pluginName, PluginClass] of Object.entries(BookReader.PLUGINS)) {
|
266
|
-
if (this._plugins[pluginName] || !PluginClass) continue;
|
267
|
-
this._plugins[pluginName] = new PluginClass(this);
|
268
|
-
}
|
269
|
-
|
270
|
-
// And call setup on them
|
271
|
-
for (const [pluginName, plugin] of Object.entries(this._plugins)) {
|
272
|
-
try {
|
273
|
-
plugin.setup(this.options.plugins?.[pluginName] ?? {});
|
274
|
-
} catch (e) {
|
275
|
-
console.error(`Error setting up plugin ${pluginName}`, e);
|
276
|
-
}
|
277
|
-
}
|
278
|
-
|
279
295
|
/** Image cache for general image fetching */
|
280
296
|
this.imageCache = new ImageCache(this.book, {
|
281
297
|
useSrcSet: this.options.useSrcSet,
|
282
298
|
reduceSet: this.reduceSet,
|
299
|
+
renderPageURI: options.renderPageURI.bind(this),
|
283
300
|
});
|
284
301
|
|
285
302
|
/**
|
@@ -388,9 +405,9 @@ BookReader.prototype.initParams = function() {
|
|
388
405
|
}
|
389
406
|
|
390
407
|
// Check for Resume plugin
|
391
|
-
if (this.options.
|
408
|
+
if (this._plugins.resume?.options.enabled) {
|
392
409
|
// Check cookies
|
393
|
-
const val = this.getResumeValue();
|
410
|
+
const val = this._plugins.resume.getResumeValue();
|
394
411
|
if (val !== null) {
|
395
412
|
// If page index different from default
|
396
413
|
if (params.index !== val) {
|
@@ -577,7 +594,12 @@ BookReader.prototype.init = function() {
|
|
577
594
|
this.initToolbar(this.mode, this.ui); // Build inside of toolbar div
|
578
595
|
}
|
579
596
|
if (this.options.showNavbar) { // default navigation
|
580
|
-
this.initNavbar();
|
597
|
+
const $navBar = this.initNavbar();
|
598
|
+
|
599
|
+
// extend navbar with plugins
|
600
|
+
for (const plugin of Object.values(this._plugins)) {
|
601
|
+
plugin.extendNavBar($navBar);
|
602
|
+
}
|
581
603
|
}
|
582
604
|
|
583
605
|
// Switch navbar controls on mobile/desktop
|
@@ -824,11 +846,17 @@ BookReader.prototype.drawLeafs = function() {
|
|
824
846
|
* @param {PageIndex} index
|
825
847
|
*/
|
826
848
|
BookReader.prototype._createPageContainer = function(index) {
|
827
|
-
|
849
|
+
const pageContainer = new PageContainer(this.book.getPage(index, false), {
|
828
850
|
isProtected: this.protected,
|
829
851
|
imageCache: this.imageCache,
|
830
|
-
loadingImage: this.imagesBaseURL + 'loading.gif',
|
831
852
|
});
|
853
|
+
|
854
|
+
// Call plugin handlers
|
855
|
+
for (const plugin of Object.values(this._plugins)) {
|
856
|
+
plugin._configurePageContainer(pageContainer);
|
857
|
+
}
|
858
|
+
|
859
|
+
return pageContainer;
|
832
860
|
};
|
833
861
|
|
834
862
|
BookReader.prototype.bindGestures = function(jElement) {
|
@@ -877,7 +905,7 @@ BookReader.prototype.zoom = function(direction) {
|
|
877
905
|
} else {
|
878
906
|
this.activeMode.zoom('out');
|
879
907
|
}
|
880
|
-
this.
|
908
|
+
this._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
|
881
909
|
return;
|
882
910
|
};
|
883
911
|
|
@@ -1108,7 +1136,7 @@ BookReader.prototype.switchMode = function(
|
|
1108
1136
|
const eventName = mode + 'PageViewSelected';
|
1109
1137
|
this.trigger(BookReader.eventNames[eventName]);
|
1110
1138
|
|
1111
|
-
this.
|
1139
|
+
this._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
|
1112
1140
|
};
|
1113
1141
|
|
1114
1142
|
BookReader.prototype.updateBrClasses = function() {
|
@@ -1180,7 +1208,7 @@ BookReader.prototype.enterFullscreen = async function(bindKeyboardControls = tru
|
|
1180
1208
|
}
|
1181
1209
|
this.jumpToIndex(currentIndex);
|
1182
1210
|
|
1183
|
-
this.
|
1211
|
+
this._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
|
1184
1212
|
// Add "?view=theater"
|
1185
1213
|
this.trigger(BookReader.eventNames.fragmentChange);
|
1186
1214
|
// trigger event here, so that animations,
|
@@ -1226,7 +1254,7 @@ BookReader.prototype.exitFullScreen = async function () {
|
|
1226
1254
|
await this.activeMode.mode1UpLit.updateComplete;
|
1227
1255
|
}
|
1228
1256
|
|
1229
|
-
this.
|
1257
|
+
this._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
|
1230
1258
|
// Remove "?view=theater"
|
1231
1259
|
this.trigger(BookReader.eventNames.fragmentChange);
|
1232
1260
|
this.refs.$br.removeClass('BRfullscreenAnimation');
|
@@ -1325,10 +1353,19 @@ BookReader.prototype.leftmost = function() {
|
|
1325
1353
|
}
|
1326
1354
|
};
|
1327
1355
|
|
1328
|
-
|
1356
|
+
/**
|
1357
|
+
* @param {object} options
|
1358
|
+
* @param {boolean} [options.triggerStop = true]
|
1359
|
+
* @param {number | 'fast' | 'slow'} [options.flipSpeed]
|
1360
|
+
*/
|
1361
|
+
BookReader.prototype.next = function({
|
1362
|
+
triggerStop = true,
|
1363
|
+
flipSpeed = null,
|
1364
|
+
} = {}) {
|
1329
1365
|
if (this.constMode2up == this.mode) {
|
1330
1366
|
if (triggerStop) this.trigger(BookReader.eventNames.stop);
|
1331
|
-
|
1367
|
+
flipSpeed = utils.parseAnimationSpeed(flipSpeed) || this.flipSpeed;
|
1368
|
+
this._modes.mode2Up.mode2UpLit.flipAnimation('next', {flipSpeed});
|
1332
1369
|
} else {
|
1333
1370
|
if (this.firstIndex < this.book.getNumLeafs() - 1) {
|
1334
1371
|
this.jumpToIndex(this.firstIndex + 1);
|
@@ -1336,13 +1373,22 @@ BookReader.prototype.next = function({triggerStop = true} = {}) {
|
|
1336
1373
|
}
|
1337
1374
|
};
|
1338
1375
|
|
1339
|
-
|
1376
|
+
/**
|
1377
|
+
* @param {object} options
|
1378
|
+
* @param {boolean} [options.triggerStop = true]
|
1379
|
+
* @param {number | 'fast' | 'slow'} [options.flipSpeed]
|
1380
|
+
*/
|
1381
|
+
BookReader.prototype.prev = function({
|
1382
|
+
triggerStop = true,
|
1383
|
+
flipSpeed = null,
|
1384
|
+
} = {}) {
|
1340
1385
|
const isOnFrontPage = this.firstIndex < 1;
|
1341
1386
|
if (isOnFrontPage) return;
|
1342
1387
|
|
1343
1388
|
if (this.constMode2up == this.mode) {
|
1344
1389
|
if (triggerStop) this.trigger(BookReader.eventNames.stop);
|
1345
|
-
|
1390
|
+
flipSpeed = utils.parseAnimationSpeed(flipSpeed) || this.flipSpeed;
|
1391
|
+
this._modes.mode2Up.mode2UpLit.flipAnimation('prev', {flipSpeed});
|
1346
1392
|
} else {
|
1347
1393
|
if (this.firstIndex >= 1) {
|
1348
1394
|
this.jumpToIndex(this.firstIndex - 1);
|
@@ -1527,6 +1573,11 @@ BookReader.prototype.bindNavigationHandlers = function() {
|
|
1527
1573
|
self.$('.BRnavCntl').animate({opacity:.75},250);
|
1528
1574
|
}
|
1529
1575
|
});
|
1576
|
+
|
1577
|
+
// Call _bindNavigationHandlers on the plugins
|
1578
|
+
for (const plugin of Object.values(this._plugins)) {
|
1579
|
+
plugin._bindNavigationHandlers();
|
1580
|
+
}
|
1530
1581
|
};
|
1531
1582
|
|
1532
1583
|
/**************************/
|
package/src/BookReaderPlugin.js
CHANGED
@@ -25,4 +25,20 @@ export class BookReaderPlugin {
|
|
25
25
|
|
26
26
|
/** @abstract */
|
27
27
|
init() {}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* @abstract
|
31
|
+
* @protected
|
32
|
+
* @param {import ("./BookReader/PageContainer.js").PageContainer} pageContainer
|
33
|
+
*/
|
34
|
+
_configurePageContainer(pageContainer) {
|
35
|
+
}
|
36
|
+
|
37
|
+
/** @abstract @protected */
|
38
|
+
_bindNavigationHandlers() {}
|
39
|
+
|
40
|
+
/**
|
41
|
+
* @param {JQuery<HTMLElement>} $navBar
|
42
|
+
*/
|
43
|
+
extendNavBar($navBar) {}
|
28
44
|
}
|
package/src/css/_BRpages.scss
CHANGED
@@ -60,9 +60,28 @@
|
|
60
60
|
left: 0;
|
61
61
|
z-index: 1;
|
62
62
|
}
|
63
|
-
&.BRpageloading
|
63
|
+
&.BRpageloading {
|
64
64
|
// Don't show the alt text while loading
|
65
|
-
|
65
|
+
img {
|
66
|
+
color: transparent;
|
67
|
+
}
|
68
|
+
|
69
|
+
// src can be set async, so hide the image if it's not set
|
70
|
+
img:not([src]) {
|
71
|
+
display: none;
|
72
|
+
}
|
73
|
+
|
74
|
+
&::after {
|
75
|
+
display: block;
|
76
|
+
content: "";
|
77
|
+
width: 20px;
|
78
|
+
height: 20px;
|
79
|
+
position: absolute;
|
80
|
+
left: 50%;
|
81
|
+
top: 50%;
|
82
|
+
transform: translate(-50%, -50%);
|
83
|
+
background: url("images/loading.gif") center/20px no-repeat;
|
84
|
+
}
|
66
85
|
}
|
67
86
|
&.BRemptypage {
|
68
87
|
background: transparent;
|