@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.
Files changed (102) hide show
  1. package/BookReader/BookReader.css +2 -1
  2. package/BookReader/BookReader.js +1 -1
  3. package/BookReader/BookReader.js.map +1 -1
  4. package/BookReader/ia-bookreader-bundle.js +14 -14
  5. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  6. package/BookReader/images/hypothesis.ico +0 -0
  7. package/BookReader/jquery-3.js +1 -1
  8. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  9. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  10. package/BookReader/plugins/plugin.autoplay.js +1 -1
  11. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  12. package/BookReader/plugins/plugin.chapters.js +2 -2
  13. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  14. package/BookReader/plugins/plugin.experiments.js +3 -0
  15. package/BookReader/plugins/plugin.experiments.js.LICENSE.txt +1 -0
  16. package/BookReader/plugins/plugin.experiments.js.map +1 -0
  17. package/BookReader/plugins/plugin.iframe.js +1 -1
  18. package/BookReader/plugins/plugin.iiif.js +1 -1
  19. package/BookReader/plugins/plugin.iiif.js.map +1 -1
  20. package/BookReader/plugins/plugin.resume.js +1 -1
  21. package/BookReader/plugins/plugin.resume.js.map +1 -1
  22. package/BookReader/plugins/plugin.search.js +1 -1
  23. package/BookReader/plugins/plugin.search.js.map +1 -1
  24. package/BookReader/plugins/plugin.text_selection.js +1 -1
  25. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  26. package/BookReader/plugins/plugin.tts.js +1 -1
  27. package/BookReader/plugins/plugin.tts.js.map +1 -1
  28. package/BookReader/plugins/plugin.url.js +1 -1
  29. package/BookReader/plugins/plugin.url.js.map +1 -1
  30. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  31. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  32. package/BookReaderDemo/IADemoBr.js +1 -24
  33. package/BookReaderDemo/demo-internetarchive.html +1 -0
  34. package/CHANGELOG.md +14 -0
  35. package/package.json +8 -4
  36. package/scripts/postversion.js +3 -2
  37. package/scripts/preversion.js +3 -1
  38. package/scripts/version.js +4 -3
  39. package/src/BookNavigator/book-navigator.js +38 -12
  40. package/src/BookNavigator/downloads/downloads-provider.js +2 -2
  41. package/src/BookNavigator/search/search-provider.js +5 -5
  42. package/src/BookNavigator/search/search-results.js +1 -1
  43. package/src/BookNavigator/sharing.js +2 -2
  44. package/src/BookNavigator/viewable-files.js +2 -2
  45. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +3 -3
  46. package/src/BookNavigator/visual-adjustments/visual-adjustments.js +2 -2
  47. package/src/BookReader/BookModel.js +13 -3
  48. package/src/BookReader/ImageCache.js +2 -2
  49. package/src/BookReader/Mode1Up.js +2 -0
  50. package/src/BookReader/Mode1UpLit.js +5 -5
  51. package/src/BookReader/Mode2Up.js +2 -0
  52. package/src/BookReader/Mode2UpLit.js +5 -5
  53. package/src/BookReader/ModeCoordinateSpace.js +1 -1
  54. package/src/BookReader/ModeThumb.js +2 -0
  55. package/src/BookReader/PageContainer.js +4 -1
  56. package/src/BookReader/Toolbar/Toolbar.js +1 -1
  57. package/src/BookReader/options.js +5 -0
  58. package/src/BookReader/utils/HTMLDimensionsCacher.js +1 -1
  59. package/src/BookReader/utils.js +13 -0
  60. package/src/BookReader.js +57 -31
  61. package/src/assets/images/hypothesis.ico +0 -0
  62. package/src/css/_TextSelection.scss +3 -1
  63. package/src/plugins/plugin.autoplay.js +3 -3
  64. package/src/plugins/plugin.chapters.js +2 -2
  65. package/src/plugins/plugin.experiments.js +294 -0
  66. package/src/plugins/plugin.iiif.js +1 -1
  67. package/src/plugins/plugin.text_selection.js +112 -1
  68. package/src/plugins/search/plugin.search.js +2 -2
  69. package/src/plugins/search/view.js +5 -5
  70. package/src/plugins/tts/plugin.tts.js +3 -3
  71. package/src/plugins/url/plugin.url.js +2 -2
  72. package/tests/e2e/autoplay.test.js +1 -1
  73. package/tests/e2e/base.test.js +4 -4
  74. package/tests/e2e/helpers/base.js +2 -2
  75. package/tests/e2e/models/BookReader.js +1 -1
  76. package/tests/e2e/rightToLeft.test.js +4 -4
  77. package/tests/e2e/viewmode.test.js +2 -2
  78. package/tests/jest/BookNavigator/book-navigator.test.js +0 -13
  79. package/tests/jest/BookNavigator/downloads/downloads-provider.test.js +1 -1
  80. package/tests/jest/BookNavigator/downloads/downloads.test.js +1 -1
  81. package/tests/jest/BookNavigator/search/search-provider.test.js +5 -5
  82. package/tests/jest/BookReader/BookReaderPublicFunctions.test.js +1 -1
  83. package/tests/jest/BookReader/Mode2Up.test.js +1 -1
  84. package/tests/jest/BookReader/ModeCoordinateSpace.test.js +1 -1
  85. package/tests/jest/BookReader/PageContainer.test.js +14 -3
  86. package/tests/jest/BookReader/utils/HTMLDimensionsCacher.test.js +1 -1
  87. package/tests/jest/BookReader/utils/ScrollClassAdder.test.js +1 -1
  88. package/tests/jest/BookReader/utils/SelectionObserver.test.js +1 -1
  89. package/tests/jest/BookReader.test.js +10 -10
  90. package/tests/jest/plugins/plugin.autoplay.test.js +6 -6
  91. package/tests/jest/plugins/plugin.chapters.test.js +2 -2
  92. package/tests/jest/plugins/plugin.resume.test.js +13 -13
  93. package/tests/jest/plugins/plugin.text_selection.test.js +155 -24
  94. package/tests/jest/plugins/search/plugin.search.test.js +7 -7
  95. package/tests/jest/plugins/search/plugin.search.view.test.js +8 -8
  96. package/tests/jest/plugins/search/utils.js +1 -1
  97. package/tests/jest/plugins/tts/PageChunkIterator.test.js +2 -2
  98. package/tests/jest/plugins/url/UrlPlugin.test.js +1 -1
  99. package/webpack.config.js +8 -3
  100. /package/{.eslintrc.js → .eslintrc.cjs} +0 -0
  101. /package/{.testcaferc.js → .testcaferc.cjs} +0 -0
  102. /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._plugins = {
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._plugins)) {
149
- if (!plugin) delete this._plugins[pluginName];
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._plugins[pluginName] || !PluginClass) continue;
155
- this._plugins[pluginName] = new PluginClass(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._plugins)) {
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._plugins.search?.options.enabled) {
170
+ if (this.plugins.search?.options.enabled) {
171
171
  // Expose the search method for convenience / backward compat
172
- this.search = this._plugins.search.search.bind(this._plugins.search);
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._plugins.resume?.options.enabled) {
429
+ if (this.plugins.resume?.options.enabled) {
430
430
  // Check cookies
431
- const val = this._plugins.resume.getResumeValue();
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._plugins.search?.options.enabled) {
466
- const sp = this._plugins.search;
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._plugins)) {
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._plugins.search?.options.initialSearchTerm) {
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._plugins)) {
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.bind = function(name, callback) {
705
+ BookReader.prototype.on = function(name, callback) {
706
706
  $(document).on('BookReader:' + name, callback);
707
707
  };
708
708
 
709
- BookReader.prototype.unbind = function(name, callback) {
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._plugins)) {
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._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
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._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
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._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
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._plugins.textSelection?.stopPageFlip(this.refs.$brContainer);
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._plugins.search.options.initialSearchTerm && !suppressFragmentChange) {
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._plugins)) {
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._plugins.search?.enabled && 'undefined' != typeof(params.search)) {
1657
- if (this._plugins.search.searchTerm !== params.search) {
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._plugins.search?.enabled) {
1862
- params.search = this._plugins.search.searchTerm;
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
- .BRpagecontainer:not(.BRpagecontainer--hasSelection) .BRtextLayer {
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
+ }
@@ -1,5 +1,5 @@
1
1
  // @ts-check
2
- import { BookReaderPlugin } from '../BookReaderPlugin';
2
+ import { BookReaderPlugin } from '../BookReaderPlugin.js';
3
3
 
4
4
  const BookReader = /** @type {typeof import('../BookReader').default} */(window.BookReader);
5
5