@internetarchive/bookreader 5.0.0-92 → 5.0.0-94
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 +2 -1
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +14 -14
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/images/hypothesis.ico +0 -0
- package/BookReader/jquery-3.js +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.chapters.js +2 -2
- package/BookReader/plugins/plugin.chapters.js.map +1 -1
- package/BookReader/plugins/plugin.experiments.js +3 -0
- package/BookReader/plugins/plugin.experiments.js.LICENSE.txt +1 -0
- package/BookReader/plugins/plugin.experiments.js.map +1 -0
- package/BookReader/plugins/plugin.iframe.js +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.search.js +1 -1
- package/BookReader/plugins/plugin.search.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/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
- package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
- package/BookReaderDemo/IADemoBr.js +1 -24
- package/BookReaderDemo/demo-internetarchive.html +1 -0
- package/CHANGELOG.md +14 -0
- package/package.json +8 -4
- package/scripts/postversion.js +3 -2
- package/scripts/preversion.js +3 -1
- package/scripts/version.js +4 -3
- package/src/BookNavigator/book-navigator.js +38 -12
- package/src/BookNavigator/downloads/downloads-provider.js +2 -2
- package/src/BookNavigator/search/search-provider.js +5 -5
- package/src/BookNavigator/search/search-results.js +1 -1
- package/src/BookNavigator/sharing.js +2 -2
- package/src/BookNavigator/viewable-files.js +2 -2
- package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +3 -3
- package/src/BookNavigator/visual-adjustments/visual-adjustments.js +2 -2
- package/src/BookReader/BookModel.js +13 -3
- package/src/BookReader/ImageCache.js +2 -2
- package/src/BookReader/Mode1Up.js +2 -0
- package/src/BookReader/Mode1UpLit.js +5 -5
- package/src/BookReader/Mode2Up.js +2 -0
- package/src/BookReader/Mode2UpLit.js +5 -5
- package/src/BookReader/ModeCoordinateSpace.js +1 -1
- package/src/BookReader/ModeThumb.js +2 -0
- package/src/BookReader/PageContainer.js +4 -1
- package/src/BookReader/Toolbar/Toolbar.js +1 -1
- package/src/BookReader/options.js +5 -0
- package/src/BookReader/utils/HTMLDimensionsCacher.js +1 -1
- package/src/BookReader/utils.js +13 -0
- package/src/BookReader.js +57 -31
- package/src/assets/images/hypothesis.ico +0 -0
- package/src/css/_TextSelection.scss +3 -1
- package/src/plugins/plugin.autoplay.js +3 -3
- package/src/plugins/plugin.chapters.js +2 -2
- package/src/plugins/plugin.experiments.js +294 -0
- package/src/plugins/plugin.iiif.js +1 -1
- package/src/plugins/plugin.text_selection.js +112 -1
- package/src/plugins/search/plugin.search.js +2 -2
- package/src/plugins/search/view.js +5 -5
- package/src/plugins/tts/plugin.tts.js +3 -3
- package/src/plugins/url/plugin.url.js +2 -2
- package/tests/e2e/autoplay.test.js +1 -1
- package/tests/e2e/base.test.js +4 -4
- package/tests/e2e/helpers/base.js +2 -2
- package/tests/e2e/models/BookReader.js +1 -1
- package/tests/e2e/rightToLeft.test.js +4 -4
- package/tests/e2e/viewmode.test.js +2 -2
- package/tests/jest/BookNavigator/book-navigator.test.js +0 -13
- package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +1 -1
- package/tests/jest/BookNavigator/downloads/downloads.test.js +1 -1
- package/tests/jest/BookNavigator/search/search-provider.test.js +5 -5
- package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +1 -1
- package/tests/jest/BookReader/Mode2Up.test.js +1 -1
- package/tests/jest/BookReader/ModeCoordinateSpace.test.js +1 -1
- package/tests/jest/BookReader/PageContainer.test.js +14 -3
- package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +1 -1
- package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +1 -1
- package/tests/jest/BookReader/utils/SelectionObserver.test.js +1 -1
- package/tests/jest/BookReader.test.js +10 -10
- package/tests/jest/plugins/plugin.autoplay.test.js +6 -6
- package/tests/jest/plugins/plugin.chapters.test.js +2 -2
- package/tests/jest/plugins/plugin.resume.test.js +13 -13
- package/tests/jest/plugins/plugin.text_selection.test.js +155 -24
- package/tests/jest/plugins/search/plugin.search.test.js +7 -7
- package/tests/jest/plugins/search/plugin.search.view.test.js +8 -8
- package/tests/jest/plugins/search/utils.js +1 -1
- package/tests/jest/plugins/tts/PageChunkIterator.test.js +2 -2
- package/tests/jest/plugins/url/UrlPlugin.test.js +1 -1
- package/webpack.config.js +8 -3
- /package/{.eslintrc.js → .eslintrc.cjs} +0 -0
- /package/{.testcaferc.js → .testcaferc.cjs} +0 -0
- /package/{babel.config.js → babel.config.cjs} +0 -0
package/src/BookReader.js
CHANGED
@@ -37,10 +37,10 @@ import { Toolbar } from './BookReader/Toolbar/Toolbar.js';
|
|
37
37
|
import { BookModel } from './BookReader/BookModel.js';
|
38
38
|
import { Mode1Up } from './BookReader/Mode1Up.js';
|
39
39
|
import { Mode2Up } from './BookReader/Mode2Up.js';
|
40
|
-
import { ModeThumb } from './BookReader/ModeThumb';
|
40
|
+
import { ModeThumb } from './BookReader/ModeThumb.js';
|
41
41
|
import { ImageCache } from './BookReader/ImageCache.js';
|
42
42
|
import { PageContainer } from './BookReader/PageContainer.js';
|
43
|
-
import { NAMED_REDUCE_SETS } from './BookReader/ReduceSet';
|
43
|
+
import { NAMED_REDUCE_SETS } from './BookReader/ReduceSet.js';
|
44
44
|
|
45
45
|
/**
|
46
46
|
* BookReader
|
@@ -134,7 +134,7 @@ BookReader.prototype.setup = function(options) {
|
|
134
134
|
this.bookPath = options.bookPath;
|
135
135
|
|
136
136
|
// Construct the usual plugins first to get type hints
|
137
|
-
this.
|
137
|
+
this.plugins = {
|
138
138
|
archiveAnalytics: BookReader.PLUGINS.archiveAnalytics ? new BookReader.PLUGINS.archiveAnalytics(this) : null,
|
139
139
|
autoplay: BookReader.PLUGINS.autoplay ? new BookReader.PLUGINS.autoplay(this) : null,
|
140
140
|
chapters: BookReader.PLUGINS.chapters ? new BookReader.PLUGINS.chapters(this) : null,
|
@@ -145,18 +145,18 @@ BookReader.prototype.setup = function(options) {
|
|
145
145
|
};
|
146
146
|
|
147
147
|
// Delete anything that's null
|
148
|
-
for (const [pluginName, plugin] of Object.entries(this.
|
149
|
-
if (!plugin) delete this.
|
148
|
+
for (const [pluginName, plugin] of Object.entries(this.plugins)) {
|
149
|
+
if (!plugin) delete this.plugins[pluginName];
|
150
150
|
}
|
151
151
|
|
152
152
|
// Now construct the rest of the plugins
|
153
153
|
for (const [pluginName, PluginClass] of Object.entries(BookReader.PLUGINS)) {
|
154
|
-
if (this.
|
155
|
-
this.
|
154
|
+
if (this.plugins[pluginName] || !PluginClass) continue;
|
155
|
+
this.plugins[pluginName] = new PluginClass(this);
|
156
156
|
}
|
157
157
|
|
158
158
|
// And call setup on them
|
159
|
-
for (const [pluginName, plugin] of Object.entries(this.
|
159
|
+
for (const [pluginName, plugin] of Object.entries(this.plugins)) {
|
160
160
|
try {
|
161
161
|
plugin.setup(this.options.plugins?.[pluginName] ?? {});
|
162
162
|
// Write the options back; this way the plugin is the source of truth,
|
@@ -167,9 +167,9 @@ BookReader.prototype.setup = function(options) {
|
|
167
167
|
}
|
168
168
|
}
|
169
169
|
|
170
|
-
if (this.
|
170
|
+
if (this.plugins.search?.options.enabled) {
|
171
171
|
// Expose the search method for convenience / backward compat
|
172
|
-
this.search = this.
|
172
|
+
this.search = this.plugins.search.search.bind(this.plugins.search);
|
173
173
|
}
|
174
174
|
|
175
175
|
/** @type {number} @deprecated some past iterations set this */
|
@@ -426,9 +426,9 @@ BookReader.prototype.initParams = function() {
|
|
426
426
|
}
|
427
427
|
|
428
428
|
// Check for Resume plugin
|
429
|
-
if (this.
|
429
|
+
if (this.plugins.resume?.options.enabled) {
|
430
430
|
// Check cookies
|
431
|
-
const val = this.
|
431
|
+
const val = this.plugins.resume.getResumeValue();
|
432
432
|
if (val !== null) {
|
433
433
|
// If page index different from default
|
434
434
|
if (params.index !== val) {
|
@@ -462,8 +462,8 @@ BookReader.prototype.initParams = function() {
|
|
462
462
|
}
|
463
463
|
|
464
464
|
// Check for Search plugin
|
465
|
-
if (this.
|
466
|
-
const sp = this.
|
465
|
+
if (this.plugins.search?.options.enabled) {
|
466
|
+
const sp = this.plugins.search;
|
467
467
|
// Go to first result only if no default or URL page
|
468
468
|
sp.options.goToFirstResult = !params.pageFound;
|
469
469
|
|
@@ -619,7 +619,7 @@ BookReader.prototype.init = function() {
|
|
619
619
|
const $navBar = this.initNavbar();
|
620
620
|
|
621
621
|
// extend navbar with plugins
|
622
|
-
for (const plugin of Object.values(this.
|
622
|
+
for (const plugin of Object.values(this.plugins)) {
|
623
623
|
plugin.extendNavBar($navBar);
|
624
624
|
}
|
625
625
|
}
|
@@ -660,7 +660,7 @@ BookReader.prototype.init = function() {
|
|
660
660
|
}
|
661
661
|
|
662
662
|
// If not searching, set to allow on-going fragment changes
|
663
|
-
if (!this.
|
663
|
+
if (!this.plugins.search?.options.initialSearchTerm) {
|
664
664
|
this.suppressFragmentChange = false;
|
665
665
|
}
|
666
666
|
|
@@ -669,7 +669,7 @@ BookReader.prototype.init = function() {
|
|
669
669
|
}
|
670
670
|
|
671
671
|
// Init plugins
|
672
|
-
for (const [pluginName, plugin] of Object.entries(this.
|
672
|
+
for (const [pluginName, plugin] of Object.entries(this.plugins)) {
|
673
673
|
try {
|
674
674
|
plugin.init();
|
675
675
|
}
|
@@ -702,14 +702,28 @@ BookReader.prototype.trigger = function(name, props = this) {
|
|
702
702
|
$(document).trigger(eventName, props);
|
703
703
|
};
|
704
704
|
|
705
|
-
BookReader.prototype.
|
705
|
+
BookReader.prototype.on = function(name, callback) {
|
706
706
|
$(document).on('BookReader:' + name, callback);
|
707
707
|
};
|
708
708
|
|
709
|
-
BookReader.prototype.
|
709
|
+
BookReader.prototype.off = function(name, callback) {
|
710
710
|
$(document).off('BookReader:' + name, callback);
|
711
711
|
};
|
712
712
|
|
713
|
+
/**
|
714
|
+
* @deprecated Use .on and .off instead
|
715
|
+
*/
|
716
|
+
BookReader.prototype.bind = function(name, callback) {
|
717
|
+
return this.on(name, callback);
|
718
|
+
};
|
719
|
+
|
720
|
+
/**
|
721
|
+
* @deprecated Use .on and .off instead
|
722
|
+
*/
|
723
|
+
BookReader.prototype.unbind = function(name, callback) {
|
724
|
+
return this.off(name, callback);
|
725
|
+
};
|
726
|
+
|
713
727
|
/**
|
714
728
|
* Resizes based on the container width and height
|
715
729
|
*/
|
@@ -874,7 +888,7 @@ BookReader.prototype._createPageContainer = function(index) {
|
|
874
888
|
});
|
875
889
|
|
876
890
|
// Call plugin handlers
|
877
|
-
for (const plugin of Object.values(this.
|
891
|
+
for (const plugin of Object.values(this.plugins)) {
|
878
892
|
plugin._configurePageContainer(pageContainer);
|
879
893
|
}
|
880
894
|
|
@@ -927,7 +941,7 @@ BookReader.prototype.zoom = function(direction) {
|
|
927
941
|
} else {
|
928
942
|
this.activeMode.zoom('out');
|
929
943
|
}
|
930
|
-
this.
|
944
|
+
this.plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
|
931
945
|
return;
|
932
946
|
};
|
933
947
|
|
@@ -1105,7 +1119,7 @@ BookReader.prototype.getPrevReadMode = function(mode) {
|
|
1105
1119
|
|
1106
1120
|
/**
|
1107
1121
|
* Switches the mode (eg 1up 2up thumb)
|
1108
|
-
* @param {number}
|
1122
|
+
* @param {number|'1up' | '2up' | 'thumb'}
|
1109
1123
|
* @param {object} [options]
|
1110
1124
|
* @param {boolean} [options.suppressFragmentChange = false]
|
1111
1125
|
* @param {boolean} [options.onInit = false] - this
|
@@ -1118,6 +1132,18 @@ BookReader.prototype.switchMode = function(
|
|
1118
1132
|
pageFound = false,
|
1119
1133
|
} = {},
|
1120
1134
|
) {
|
1135
|
+
if (typeof mode === 'string') {
|
1136
|
+
mode = {
|
1137
|
+
'1up': this.constMode1up,
|
1138
|
+
'2up': this.constMode2up,
|
1139
|
+
'thumb': this.constModeThumb,
|
1140
|
+
}[mode];
|
1141
|
+
}
|
1142
|
+
|
1143
|
+
if (!mode) {
|
1144
|
+
throw new Error(`Invalid mode: ${mode}`);
|
1145
|
+
}
|
1146
|
+
|
1121
1147
|
// Skip checks before init() complete
|
1122
1148
|
if (this.init.initComplete) {
|
1123
1149
|
if (mode === this.mode) {
|
@@ -1162,7 +1188,7 @@ BookReader.prototype.switchMode = function(
|
|
1162
1188
|
const eventName = mode + 'PageViewSelected';
|
1163
1189
|
this.trigger(BookReader.eventNames[eventName]);
|
1164
1190
|
|
1165
|
-
this.
|
1191
|
+
this.plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
|
1166
1192
|
};
|
1167
1193
|
|
1168
1194
|
BookReader.prototype.updateBrClasses = function() {
|
@@ -1234,7 +1260,7 @@ BookReader.prototype.enterFullscreen = async function(bindKeyboardControls = tru
|
|
1234
1260
|
}
|
1235
1261
|
this.jumpToIndex(currentIndex);
|
1236
1262
|
|
1237
|
-
this.
|
1263
|
+
this.plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
|
1238
1264
|
// Add "?view=theater"
|
1239
1265
|
this.trigger(BookReader.eventNames.fragmentChange);
|
1240
1266
|
// trigger event here, so that animations,
|
@@ -1280,7 +1306,7 @@ BookReader.prototype.exitFullScreen = async function () {
|
|
1280
1306
|
await this.activeMode.mode1UpLit.updateComplete;
|
1281
1307
|
}
|
1282
1308
|
|
1283
|
-
this.
|
1309
|
+
this.plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
|
1284
1310
|
// Remove "?view=theater"
|
1285
1311
|
this.trigger(BookReader.eventNames.fragmentChange);
|
1286
1312
|
this.refs.$br.removeClass('BRfullscreenAnimation');
|
@@ -1324,7 +1350,7 @@ BookReader.prototype.updateFirstIndex = function(
|
|
1324
1350
|
// If there's an initial search we stop suppressing global URL changes
|
1325
1351
|
// when local suppression ends
|
1326
1352
|
// This seems to correctly handle multiple calls during mode/1up
|
1327
|
-
if (this.
|
1353
|
+
if (this.plugins.search?.options.initialSearchTerm && !suppressFragmentChange) {
|
1328
1354
|
this.suppressFragmentChange = false;
|
1329
1355
|
}
|
1330
1356
|
|
@@ -1601,7 +1627,7 @@ BookReader.prototype.bindNavigationHandlers = function() {
|
|
1601
1627
|
});
|
1602
1628
|
|
1603
1629
|
// Call _bindNavigationHandlers on the plugins
|
1604
|
-
for (const plugin of Object.values(this.
|
1630
|
+
for (const plugin of Object.values(this.plugins)) {
|
1605
1631
|
plugin._bindNavigationHandlers();
|
1606
1632
|
}
|
1607
1633
|
};
|
@@ -1653,8 +1679,8 @@ BookReader.prototype.updateFromParams = function(params) {
|
|
1653
1679
|
// process /search
|
1654
1680
|
// @deprecated for urlMode 'history'
|
1655
1681
|
// Continues to work for urlMode 'hash'
|
1656
|
-
if (this.
|
1657
|
-
if (this.
|
1682
|
+
if (this.plugins.search?.enabled && 'undefined' != typeof(params.search)) {
|
1683
|
+
if (this.plugins.search.searchTerm !== params.search) {
|
1658
1684
|
this.$('.BRsearchInput').val(params.search);
|
1659
1685
|
}
|
1660
1686
|
}
|
@@ -1858,8 +1884,8 @@ BookReader.prototype.paramsFromCurrent = function() {
|
|
1858
1884
|
params.view = fullscreenView;
|
1859
1885
|
}
|
1860
1886
|
// Search
|
1861
|
-
if (this.
|
1862
|
-
params.search = this.
|
1887
|
+
if (this.plugins.search?.enabled) {
|
1888
|
+
params.search = this.plugins.search.searchTerm;
|
1863
1889
|
}
|
1864
1890
|
|
1865
1891
|
return params;
|
Binary file
|
@@ -8,6 +8,7 @@
|
|
8
8
|
// Make it so right-clicking on "blank" part of text layer sends events to the image (for saving)
|
9
9
|
pointer-events: none;
|
10
10
|
cursor: text;
|
11
|
+
mix-blend-mode: multiply;
|
11
12
|
}
|
12
13
|
|
13
14
|
.BRparagraphElement {
|
@@ -58,7 +59,8 @@
|
|
58
59
|
|
59
60
|
// Hide text layer for performance during zooming & scrolling
|
60
61
|
.BRsmooth-zooming, .BRscrolling-active {
|
61
|
-
|
62
|
+
// If the text layer has an annotation, don't hide it
|
63
|
+
.BRpagecontainer:not(.BRpagecontainer--hasSelection):not(:has(hypothesis-highlight)) .BRtextLayer {
|
62
64
|
display: none;
|
63
65
|
}
|
64
66
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
// @ts-check
|
2
|
-
import { EVENTS } from "../BookReader/events";
|
3
|
-
import { parseAnimationSpeed } from "../BookReader/utils";
|
4
|
-
import { BookReaderPlugin } from "../BookReaderPlugin";
|
2
|
+
import { EVENTS } from "../BookReader/events.js";
|
3
|
+
import { parseAnimationSpeed } from "../BookReader/utils.js";
|
4
|
+
import { BookReaderPlugin } from "../BookReaderPlugin.js";
|
5
5
|
|
6
6
|
/**
|
7
7
|
* Plugin which adds an autoplay feature. Useful for kiosk situations.
|
@@ -3,8 +3,8 @@ import { css, html, LitElement, nothing } from "lit";
|
|
3
3
|
import { customElement, property } from 'lit/decorators.js';
|
4
4
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
5
5
|
import { styleMap } from 'lit/directives/style-map.js';
|
6
|
-
import '@internetarchive/icon-toc/icon-toc';
|
7
|
-
import { BookReaderPlugin } from "../BookReaderPlugin";
|
6
|
+
import '@internetarchive/icon-toc/icon-toc.js';
|
7
|
+
import { BookReaderPlugin } from "../BookReaderPlugin.js";
|
8
8
|
import { applyVariables } from "../util/strings.js";
|
9
9
|
/** @typedef {import('@/src/BookReader/BookModel.js').PageIndex} PageIndex */
|
10
10
|
/** @typedef {import('@/src/BookReader/BookModel.js').PageString} PageString */
|
@@ -0,0 +1,294 @@
|
|
1
|
+
// @ts-check
|
2
|
+
import { css, html, LitElement } from 'lit';
|
3
|
+
import { BookReaderPlugin } from '../BookReaderPlugin.js';
|
4
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
5
|
+
import { sleep } from '../BookReader/utils.js';
|
6
|
+
|
7
|
+
// @ts-ignore
|
8
|
+
const BookReader = /** @type {typeof import('@/src/BookReader.js').default} */(window.BookReader);
|
9
|
+
|
10
|
+
class ExperimentModel {
|
11
|
+
/** @type {string} test */
|
12
|
+
name;
|
13
|
+
/** @type {string} */
|
14
|
+
title;
|
15
|
+
/** @type {string} */
|
16
|
+
description;
|
17
|
+
/** @type {string} */
|
18
|
+
icon;
|
19
|
+
/** @type {boolean} */
|
20
|
+
enabled;
|
21
|
+
/** @type {string} */
|
22
|
+
learnMore;
|
23
|
+
|
24
|
+
assetRoot = '/BookReader/';
|
25
|
+
|
26
|
+
enabledLoading = false;
|
27
|
+
|
28
|
+
/**
|
29
|
+
* @param {string} path
|
30
|
+
*/
|
31
|
+
buildAssetPath(path) {
|
32
|
+
return `${this.assetRoot}${path}`;
|
33
|
+
}
|
34
|
+
|
35
|
+
/**
|
36
|
+
* @param {object} param0
|
37
|
+
* @param {boolean} param0.manual Whether the experiment was enabled manually
|
38
|
+
*/
|
39
|
+
async enable({ manual }) { }
|
40
|
+
async disable() { }
|
41
|
+
}
|
42
|
+
|
43
|
+
export class ExperimentsPlugin extends BookReaderPlugin {
|
44
|
+
options = {
|
45
|
+
enabled: true,
|
46
|
+
|
47
|
+
/** Where the state of this plugin is saved in localStorage */
|
48
|
+
localStorageKey: 'BrExperiments',
|
49
|
+
}
|
50
|
+
|
51
|
+
/** @type {ExperimentModel[]} */
|
52
|
+
experiments = [
|
53
|
+
new class extends ExperimentModel {
|
54
|
+
name = 'hypothesis';
|
55
|
+
title = 'Hypothes.is';
|
56
|
+
description = 'Create public, collaborative, or fully private annotations on books and the web.';
|
57
|
+
learnMore = 'https://web.hypothes.is/about/';
|
58
|
+
icon = 'images/hypothesis.ico';
|
59
|
+
enabled = false;
|
60
|
+
|
61
|
+
async enable({ manual = false }) {
|
62
|
+
// Hypothesis configs ; see https://h.readthedocs.io/projects/client/en/latest/publishers/config.html
|
63
|
+
const configScript = document.createElement('script');
|
64
|
+
configScript.type = 'application/json';
|
65
|
+
configScript.className = 'js-hypothesis-config';
|
66
|
+
configScript.textContent = JSON.stringify({
|
67
|
+
// Open the sidebar if this is the first time enabling
|
68
|
+
openSidebar: manual,
|
69
|
+
assetRoot: this.buildAssetPath('hypothesis/'),
|
70
|
+
});
|
71
|
+
|
72
|
+
document.head.appendChild(configScript);
|
73
|
+
return importAsScript(this.buildAssetPath('hypothesis/build/boot.js'));
|
74
|
+
// For testing
|
75
|
+
// return importAsScript('http://localhost:3001/hypothesis');
|
76
|
+
}
|
77
|
+
|
78
|
+
async disable() {
|
79
|
+
// need to reload to remove the Hypothesis script
|
80
|
+
// Sleep so that the event loop can finish processing before the reload
|
81
|
+
sleep(0).then(() => {
|
82
|
+
window.location.reload();
|
83
|
+
});
|
84
|
+
}
|
85
|
+
}(),
|
86
|
+
]
|
87
|
+
|
88
|
+
/** @type {BrExperimentsPanel} */
|
89
|
+
_panel;
|
90
|
+
|
91
|
+
async init() {
|
92
|
+
if (!this.options.enabled) {
|
93
|
+
return;
|
94
|
+
}
|
95
|
+
|
96
|
+
for (const experiment of this.experiments) {
|
97
|
+
// TODO: imagesBaseURL should be replaced with assetRoot everywhere
|
98
|
+
experiment.assetRoot = this.br.options.imagesBaseURL.replace(/images\/$/, '');
|
99
|
+
experiment.icon = experiment.buildAssetPath(experiment.icon);
|
100
|
+
}
|
101
|
+
|
102
|
+
this._loadExperimentStates();
|
103
|
+
await Promise.resolve();
|
104
|
+
this._render();
|
105
|
+
}
|
106
|
+
|
107
|
+
_loadExperimentStates() {
|
108
|
+
const savedStates = JSON.parse(localStorage.getItem(this.options.localStorageKey) || '{}');
|
109
|
+
this.experiments.forEach(experiment => {
|
110
|
+
if (savedStates[experiment.name] !== undefined) {
|
111
|
+
experiment.enabled = savedStates[experiment.name];
|
112
|
+
if (experiment.enabled) {
|
113
|
+
experiment.enable({ manual: false });
|
114
|
+
}
|
115
|
+
}
|
116
|
+
});
|
117
|
+
}
|
118
|
+
|
119
|
+
_saveExperimentStates() {
|
120
|
+
const states = Object.fromEntries(
|
121
|
+
this.experiments.map(experiment => [experiment.name, experiment.enabled]),
|
122
|
+
);
|
123
|
+
localStorage.setItem(this.options.localStorageKey, JSON.stringify(states));
|
124
|
+
}
|
125
|
+
|
126
|
+
/**
|
127
|
+
* @param {ExperimentModel} experiment
|
128
|
+
* @param {boolean} enabled
|
129
|
+
*/
|
130
|
+
async _toggleExperiment(experiment, enabled) {
|
131
|
+
experiment.enabledLoading = true;
|
132
|
+
this.br.plugins.archiveAnalytics?.sendEvent(`BRExperiment-${experiment.name}`, enabled ? 'Enable' : 'Disable');
|
133
|
+
this._panel.requestUpdate();
|
134
|
+
|
135
|
+
if (enabled) {
|
136
|
+
await experiment.enable({ manual: true });
|
137
|
+
} else {
|
138
|
+
await experiment.disable();
|
139
|
+
}
|
140
|
+
experiment.enabledLoading = false;
|
141
|
+
experiment.enabled = enabled;
|
142
|
+
this._panel.requestUpdate();
|
143
|
+
}
|
144
|
+
|
145
|
+
_render() {
|
146
|
+
this.br.shell.menuProviders['experiments'] = {
|
147
|
+
id: 'experiments',
|
148
|
+
// https://icon-sets.iconify.design/hugeicons/?icon-filter=eco-lab-02&query=lab
|
149
|
+
icon: html`
|
150
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="34" viewBox="0 0 24 24"><path fill="currentColor" d="M10 5.75h1.25v2.5H9.5c-.41 0-.75.34-.75.75s.34.75.75.75h.25v1.96A5.72 5.72 0 0 0 6.25 17c0 3.17 2.58 5.75 5.75 5.75s5.75-2.58 5.75-5.75c0-2.33-1.39-4.4-3.5-5.29V9.75h.25c.41 0 .75-.34.75-.75s-.34-.75-.75-.75h-1.75v-1.5H14c1.52 0 2.75-1.23 2.75-2.75V3c0-.41-.34-.75-.75-.75h-2c-.579 0-1.115.178-1.558.483A2.75 2.75 0 0 0 10 1.25H8c-.41 0-.75.34-.75.75v1c0 1.52 1.23 2.75 2.75 2.75m2.75-.5V5c0-.69.56-1.25 1.25-1.25h1.25V4c0 .69-.56 1.25-1.25 1.25zm-1.5 6.98V9.75h1.5v2.48c0 .33.22.62.53.72c1.77.56 2.97 2.19 2.97 4.06A4.26 4.26 0 0 1 12 21.26a4.26 4.26 0 0 1-4.25-4.25c0-1.87 1.19-3.5 2.97-4.06c.32-.1.53-.39.53-.72m-2.5-9.48H10c.69 0 1.25.56 1.25 1.25v.25H10c-.69 0-1.25-.56-1.25-1.25z" color="currentColor"/></svg>
|
151
|
+
`,
|
152
|
+
label: 'Experiments',
|
153
|
+
component: html`<br-experiments-panel
|
154
|
+
.experiments="${this.experiments}"
|
155
|
+
@connected="${e => this._panel = e.target}"
|
156
|
+
@toggle="${async e => {
|
157
|
+
await this._toggleExperiment(e.detail.experiment, e.detail.enabled);
|
158
|
+
this._saveExperimentStates();
|
159
|
+
}}"
|
160
|
+
/>`,
|
161
|
+
};
|
162
|
+
this.br.shell.updateMenuContents();
|
163
|
+
}
|
164
|
+
}
|
165
|
+
BookReader?.registerPlugin('experiments', ExperimentsPlugin);
|
166
|
+
|
167
|
+
@customElement('br-experiments-panel')
|
168
|
+
export class BrExperimentsPanel extends LitElement {
|
169
|
+
|
170
|
+
/** @type {ExperimentModel[]} */
|
171
|
+
@property({ type: Array }) experiments = [];
|
172
|
+
|
173
|
+
render() {
|
174
|
+
return html`
|
175
|
+
<div style="padding: 10px">
|
176
|
+
${this.experiments.map(
|
177
|
+
experiment => html`
|
178
|
+
<br-experiment-toggle
|
179
|
+
.icon="${experiment.icon}"
|
180
|
+
.title="${experiment.title}"
|
181
|
+
.description="${experiment.description}"
|
182
|
+
.enabled="${experiment.enabled}"
|
183
|
+
.loading="${experiment.enabledLoading}"
|
184
|
+
.learnMore="${experiment.learnMore}"
|
185
|
+
@toggle="${e => this._dispatchToggle(experiment, e.detail.enabled)}"
|
186
|
+
></br-experiment-toggle>
|
187
|
+
`,
|
188
|
+
)}
|
189
|
+
</div>
|
190
|
+
`;
|
191
|
+
}
|
192
|
+
|
193
|
+
/**
|
194
|
+
* @param {ExperimentModel} experiment
|
195
|
+
* @param {boolean} enabled
|
196
|
+
*/
|
197
|
+
async _dispatchToggle(experiment, enabled) {
|
198
|
+
this.dispatchEvent(new CustomEvent('toggle', {
|
199
|
+
detail: { experiment, enabled },
|
200
|
+
}));
|
201
|
+
}
|
202
|
+
|
203
|
+
connectedCallback() {
|
204
|
+
super.connectedCallback();
|
205
|
+
this.dispatchEvent(new CustomEvent('connected'));
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
@customElement('br-experiment-toggle')
|
210
|
+
export class BrExperimentToggle extends LitElement {
|
211
|
+
@property({ type: String }) icon = '';
|
212
|
+
@property({ type: String }) title = '';
|
213
|
+
@property({ type: String }) description = '';
|
214
|
+
@property({ type: Boolean }) enabled = false;
|
215
|
+
@property({ type: Boolean }) loading = false;
|
216
|
+
@property({ type: String }) learnMore = '';
|
217
|
+
|
218
|
+
/**
|
219
|
+
* We want to disable the button immediately if loading, but only display
|
220
|
+
* the loading indicator after 200ms.
|
221
|
+
*/
|
222
|
+
@state() _longLoading = false;
|
223
|
+
|
224
|
+
/** @override */
|
225
|
+
update(changedProperties) {
|
226
|
+
super.update(changedProperties);
|
227
|
+
if (changedProperties.has('loading')) {
|
228
|
+
if (this.loading) {
|
229
|
+
sleep(500).then(() => {
|
230
|
+
if (this.loading) {
|
231
|
+
this._longLoading = true;
|
232
|
+
this.requestUpdate();
|
233
|
+
}
|
234
|
+
});
|
235
|
+
} else {
|
236
|
+
this._longLoading = false;
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}
|
240
|
+
|
241
|
+
render() {
|
242
|
+
return html`
|
243
|
+
<div class="experiment-card">
|
244
|
+
<div style="display: flex; align-items: center; gap: 10px;">
|
245
|
+
<img src="${this.icon}" style="width: 20px; height: 20px;" alt="" />
|
246
|
+
<div style="flex-grow: 1; font-weight: bold;">${this.title}</div>
|
247
|
+
</div>
|
248
|
+
<p style="opacity: 0.9">
|
249
|
+
${this.description}
|
250
|
+
<a href="${this.learnMore}" target="_blank">Learn more</a>.
|
251
|
+
</p>
|
252
|
+
<div style="display: flex">
|
253
|
+
<div style="flex-grow: 1;"></div>
|
254
|
+
<button @click="${this._dispatchToggle}" .disabled="${this.loading}">
|
255
|
+
${this._longLoading ? 'Loading…' : this.enabled ? 'Disable' : 'Enable'}
|
256
|
+
</button>
|
257
|
+
</div>
|
258
|
+
</div>
|
259
|
+
`;
|
260
|
+
}
|
261
|
+
|
262
|
+
static get styles() {
|
263
|
+
return css`
|
264
|
+
.experiment-card {
|
265
|
+
border-radius: 8px;
|
266
|
+
background-color: #fff2;
|
267
|
+
padding: 10px;
|
268
|
+
display: flex;
|
269
|
+
flex-direction: column;
|
270
|
+
}
|
271
|
+
|
272
|
+
.experiment-card p {
|
273
|
+
margin: 6px 0;
|
274
|
+
}
|
275
|
+
`;
|
276
|
+
}
|
277
|
+
|
278
|
+
_dispatchToggle() {
|
279
|
+
this.dispatchEvent(new CustomEvent('toggle', { detail: { enabled: !this.enabled } }));
|
280
|
+
}
|
281
|
+
}
|
282
|
+
|
283
|
+
/**
|
284
|
+
* @param {string} url
|
285
|
+
*/
|
286
|
+
async function importAsScript(url) {
|
287
|
+
return new Promise((resolve, reject) => {
|
288
|
+
const script = document.createElement('script');
|
289
|
+
script.src = url;
|
290
|
+
script.onload = () => resolve();
|
291
|
+
script.onerror = () => reject(new Error(`Failed to load script: ${url}`));
|
292
|
+
document.head.appendChild(script);
|
293
|
+
});
|
294
|
+
}
|