@internetarchive/bookreader 5.0.0-71 → 5.0.0-73

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. package/.github/workflows/node.js.yml +16 -16
  2. package/.github/workflows/npm-publish.yml +6 -6
  3. package/BookReader/BookReader.css +9 -6
  4. package/BookReader/BookReader.js +1 -1
  5. package/BookReader/BookReader.js.map +1 -1
  6. package/BookReader/ia-bookreader-bundle.js +9 -9
  7. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  8. package/BookReader/icons/1up.svg +1 -1
  9. package/BookReader/icons/2up.svg +1 -1
  10. package/BookReader/icons/advance.svg +1 -1
  11. package/BookReader/icons/close-circle-dark.svg +1 -1
  12. package/BookReader/icons/close-circle.svg +1 -1
  13. package/BookReader/icons/fullscreen.svg +1 -1
  14. package/BookReader/icons/fullscreen_exit.svg +1 -1
  15. package/BookReader/icons/hamburger.svg +1 -1
  16. package/BookReader/icons/left-arrow.svg +1 -1
  17. package/BookReader/icons/magnify-minus.svg +1 -1
  18. package/BookReader/icons/magnify-plus.svg +1 -1
  19. package/BookReader/icons/magnify.svg +1 -1
  20. package/BookReader/icons/pause.svg +1 -1
  21. package/BookReader/icons/play.svg +1 -1
  22. package/BookReader/icons/playback-speed.svg +1 -1
  23. package/BookReader/icons/read-aloud.svg +1 -1
  24. package/BookReader/icons/review.svg +1 -1
  25. package/BookReader/icons/thumbnails.svg +1 -1
  26. package/BookReader/images/BRicons.svg +2 -2
  27. package/BookReader/images/books_graphic.svg +1 -1
  28. package/BookReader/images/icon_book.svg +1 -1
  29. package/BookReader/images/icon_gear.svg +1 -1
  30. package/BookReader/images/icon_info.svg +1 -1
  31. package/BookReader/images/icon_one_page.svg +1 -1
  32. package/BookReader/images/icon_search_button.svg +1 -1
  33. package/BookReader/images/icon_share.svg +1 -1
  34. package/BookReader/images/icon_speaker.svg +1 -1
  35. package/BookReader/images/icon_speaker_open.svg +1 -1
  36. package/BookReader/images/icon_thumbnails.svg +1 -1
  37. package/BookReader/images/icon_toc.svg +1 -1
  38. package/BookReader/images/icon_two_pages.svg +1 -1
  39. package/BookReader/images/marker_chap-off.svg +1 -1
  40. package/BookReader/images/marker_chap-on.svg +1 -1
  41. package/BookReader/images/marker_srch-on.svg +1 -1
  42. package/BookReader/plugins/plugin.archive_analytics.js +1 -1
  43. package/BookReader/plugins/plugin.autoplay.js +1 -1
  44. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  45. package/BookReader/plugins/plugin.chapters.js +2 -2
  46. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  47. package/BookReader/plugins/plugin.resume.js +1 -1
  48. package/BookReader/plugins/plugin.search.js +1 -1
  49. package/BookReader/plugins/plugin.search.js.map +1 -1
  50. package/BookReader/plugins/plugin.text_selection.js +1 -1
  51. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  52. package/BookReader/plugins/plugin.tts.js +1 -1
  53. package/BookReader/plugins/plugin.tts.js.map +1 -1
  54. package/BookReader/plugins/plugin.url.js +1 -1
  55. package/BookReader/plugins/plugin.url.js.map +1 -1
  56. package/BookReader/plugins/plugin.vendor-fullscreen.js +1 -1
  57. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  58. package/BookReaderDemo/BookReaderDemo.css +3 -0
  59. package/CHANGELOG.md +11 -0
  60. package/netlify.toml +1 -1
  61. package/package.json +14 -15
  62. package/scripts/preversion.js +0 -4
  63. package/src/BookReader/options.js +3 -0
  64. package/src/BookReader/utils/SelectionObserver.js +3 -1
  65. package/src/css/_BRsearch.scss +4 -6
  66. package/src/plugins/plugin.chapters.js +36 -16
  67. package/src/plugins/tts/AbstractTTSEngine.js +5 -2
  68. package/src/plugins/tts/plugin.tts.js +1 -1
  69. package/src/plugins/tts/utils.js +15 -0
  70. package/tests/jest/BookReader/utils/SelectionObserver.test.js +14 -0
  71. package/tests/jest/plugins/plugin.chapters.test.js +51 -3
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 5.0.0-73
2
+ - Feature: Add `table_of_contents` option to work with chapters plugin @cdrini
3
+ - Fix: Simple demos not working @xonx4l
4
+ - Dev: Update test/build dependencies @cdrini
5
+
6
+ # 5.0.0-72
7
+ - Fix: Play/pause button out of sync with ReadAloud @sbwhitt
8
+ - Fix: BookReader not loading in sandboxed iframe @cdrini
9
+ - Dev: Fix noisy sentry error firing on any selection @cdrini
10
+ - Dev: Update to Node 20 @cdrini
11
+
1
12
  # 5.0.0-71
2
13
  - Dev: update jest monorepo @renovate
3
14
  - Fix: Share & Mutliple View menu panel refactor @iisa
package/netlify.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [build.environment]
2
2
  # Keep in sync with CI in .github/workflows/node.js.yml
3
- NODE_VERSION = "16"
3
+ NODE_VERSION = "20"
4
4
 
5
5
  [[headers]]
6
6
  # Define which paths this specific [[headers]] block will cover.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-71",
3
+ "version": "5.0.0-73",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,18 +40,18 @@
40
40
  "lit": "^2.5.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@babel/core": "7.22.9",
44
- "@babel/eslint-parser": "7.22.9",
43
+ "@babel/core": "7.23.3",
44
+ "@babel/eslint-parser": "7.23.3",
45
45
  "@babel/plugin-proposal-class-properties": "7.18.6",
46
- "@babel/plugin-proposal-decorators": "7.22.7",
47
- "@babel/preset-env": "7.22.9",
48
- "@open-wc/testing-helpers": "^2.3.0",
49
- "@types/jest": "29.5.5",
46
+ "@babel/plugin-proposal-decorators": "7.23.3",
47
+ "@babel/preset-env": "7.23.3",
48
+ "@open-wc/testing-helpers": "3.0.0",
49
+ "@types/jest": "29.5.10",
50
50
  "@webcomponents/webcomponentsjs": "^2.6.0",
51
51
  "babel-loader": "9.1.3",
52
52
  "codecov": "^3.8.3",
53
53
  "concurrently": "7.6.0",
54
- "core-js": "3.27.1",
54
+ "core-js": "3.33.3",
55
55
  "cpx2": "4.2.3",
56
56
  "eslint": "^7.32.0",
57
57
  "eslint-plugin-no-jquery": "^2.7.0",
@@ -67,21 +67,20 @@
67
67
  "jquery-ui-touch-punch": "0.2.3",
68
68
  "jquery.browser": "0.1.0",
69
69
  "live-server": "1.2.2",
70
- "node-fetch": "3.3.2",
71
- "regenerator-runtime": "0.13.11",
72
- "sass": "1.64.2",
73
- "sinon": "^15.1.0",
70
+ "regenerator-runtime": "0.14.0",
71
+ "sass": "1.69.5",
72
+ "sinon": "17.0.1",
74
73
  "soundmanager2": "2.97.20170602",
75
- "svgo": "3.0.2",
74
+ "svgo": "3.0.4",
76
75
  "testcafe": "2.6.2",
77
76
  "testcafe-browser-provider-browserstack": "^1.13.2-alpha.1",
78
- "webpack": "5.88.2",
77
+ "webpack": "5.89.0",
79
78
  "webpack-cli": "5.1.4"
80
79
  },
81
80
  "jest": {
82
81
  "testEnvironment": "jsdom",
83
82
  "transformIgnorePatterns": [
84
- "node_modules/(?!(lit-html|lit-element|lit|@lit|@internetarchive|@open-wc)/)"
83
+ "node_modules/(?!(sinon|lit-html|lit-element|lit|@lit|@internetarchive|@open-wc)/)"
85
84
  ],
86
85
  "moduleNameMapper": {
87
86
  "^@/(.*)$": "<rootDir>/$1"
@@ -2,10 +2,6 @@ const { version: OLD_VERSION } = require('../package.json');
2
2
  const OLD_RELEASE_URL = `https://api.github.com/repos/internetarchive/bookreader/releases/tags/v${OLD_VERSION}`;
3
3
 
4
4
  async function main() {
5
- // Need this because fetch is ESM-only, and we're on Node 16. Someday we should
6
- // be able to move this up to the top without renaming this file to a .mjs or whatever
7
- const {default: fetch} = await import('node-fetch');
8
-
9
5
  const {created_at} = await fetch(OLD_RELEASE_URL).then(r => r.json());
10
6
  const today = new Date().toISOString().slice(0, -5);
11
7
  const searchUrl = 'https://github.com/internetarchive/bookreader/pulls?' + new URLSearchParams({
@@ -180,6 +180,9 @@ export const DEFAULT_OPTIONS = {
180
180
  */
181
181
  data: [],
182
182
 
183
+ /** @type {import('../plugins/plugin.chapters.js').TocEntry[]} */
184
+ table_of_contents: null,
185
+
183
186
  /** Advanced methods for page rendering */
184
187
  /** @type {() => number} */
185
188
  getNumLeafs: null,
@@ -30,8 +30,10 @@ export class SelectionObserver {
30
30
  const sel = window.getSelection();
31
31
 
32
32
  if (!this.selecting && sel.toString()) {
33
+ const target = $(sel.anchorNode).closest(this.selector)[0];
34
+ if (!target) return;
35
+ this.target = target;
33
36
  this.selecting = true;
34
- this.target = $(sel.anchorNode).closest(this.selector)[0];
35
37
  this.handler('started', this.target);
36
38
  }
37
39
 
@@ -9,8 +9,7 @@
9
9
  display: none;
10
10
  position: absolute;
11
11
  bottom: calc(100% + 5px);
12
- left: 50%;
13
- transform: translateX(-50%);
12
+ left: -14px;
14
13
  width: 350px;
15
14
  max-width: 100vw;
16
15
  padding: 12px 14px;
@@ -34,9 +33,7 @@
34
33
  position: absolute;
35
34
  content: "";
36
35
  bottom: -9px;
37
- left: 50%;
38
- margin-left: -1px;
39
- transform: translateX(-50%);
36
+ left: 0;
40
37
  width: 30px;
41
38
  height: 10px;
42
39
  clip-path: polygon(0 0, 100% 0, 50% 100%);
@@ -131,9 +128,10 @@
131
128
  main {
132
129
  @include ellipsis-lines(4);
133
130
  margin-bottom: 6px;
131
+ &:before { content: "“"; }
132
+ &:after { content: "”"; }
134
133
  }
135
134
  footer {
136
- text-align: center;
137
135
  font-size: 0.85em;
138
136
  opacity: .8;
139
137
  }
@@ -28,11 +28,28 @@ BookReader.prototype.init = (function(super_) {
28
28
  })(BookReader.prototype.init);
29
29
 
30
30
  BookReader.prototype._chapterInit = async function() {
31
- const olEdition = await this.getOpenLibraryRecord(this.options.olHost, this.options.bookId);
32
- if (olEdition?.table_of_contents?.length) {
33
- this._tocEntries = olEdition.table_of_contents.map(rawTOCEntry => (
34
- Object.assign({}, rawTOCEntry, {pageIndex: this.book.getPageIndex(rawTOCEntry.pagenum)})
35
- ));
31
+ let rawTableOfContents = null;
32
+ // Prefer IA TOC for now, until we update the second half to check for
33
+ // `openlibrary_edition` on the IA metadata instead of making a bunch of
34
+ // requests to OL.
35
+ if (this.options.table_of_contents?.length) {
36
+ rawTableOfContents = this.options.table_of_contents;
37
+ } else {
38
+ const olEdition = await this.getOpenLibraryRecord(this.options.olHost, this.options.bookId);
39
+ if (olEdition?.table_of_contents?.length) {
40
+ rawTableOfContents = olEdition.table_of_contents;
41
+ }
42
+ }
43
+
44
+ if (rawTableOfContents) {
45
+ this._tocEntries = rawTableOfContents
46
+ .map(rawTOCEntry => (Object.assign({}, rawTOCEntry, {
47
+ pageIndex: (
48
+ typeof(rawTOCEntry.leaf) == 'number' ? this.book.leafNumToIndex(rawTOCEntry.leaf) :
49
+ rawTOCEntry.pagenum ? this.book.getPageIndex(rawTOCEntry.pagenum) :
50
+ undefined
51
+ ),
52
+ })));
36
53
  this._chaptersRender(this._tocEntries);
37
54
  this.bind(BookReader.eventNames.pageChanged, () => this._chaptersUpdateCurrent());
38
55
  }
@@ -60,19 +77,18 @@ BookReader.prototype._chaptersRender = function() {
60
77
  />`,
61
78
  };
62
79
  shell.updateMenuContents();
63
- for (const tocEntry of this._tocEntries) {
64
- this._chaptersRenderMarker(tocEntry);
65
- }
80
+ this._tocEntries.forEach((tocEntry, i) => this._chaptersRenderMarker(tocEntry, i));
66
81
  };
67
82
 
68
83
  /**
69
84
  * @typedef {Object} TocEntry
70
85
  * Table of contents entry as defined by Open Library, with some extra values for internal use
71
- * @property {string} pagenum
72
- * @property {number} level
73
- * @property {string} label
74
- * @property {string} title
75
- * @property {number} pageIndex - Added
86
+ * @property {number} [level]
87
+ * @property {string} [label]
88
+ * @property {string} [title]
89
+ * @property {PageString} [pagenum]
90
+ * @property {LeafNum} [leaf]
91
+ * @property {number} [pageIndex] - Added
76
92
  *
77
93
  * @example {
78
94
  * "pagenum": "17",
@@ -84,21 +100,25 @@ BookReader.prototype._chaptersRender = function() {
84
100
 
85
101
  /**
86
102
  * @param {TocEntry} tocEntry
103
+ * @param {number} entryIndex
87
104
  */
88
- BookReader.prototype._chaptersRenderMarker = function(tocEntry) {
105
+ BookReader.prototype._chaptersRenderMarker = function(tocEntry, entryIndex) {
89
106
  if (tocEntry.pageIndex == undefined) return;
90
107
 
91
108
  //creates a string with non-void tocEntry.label and tocEntry.title
92
109
  const chapterStr = [tocEntry.label, tocEntry.title]
93
110
  .filter(x => x)
94
- .join(' ');
111
+ .join(' ') || `Chapter ${entryIndex + 1}`;
95
112
 
96
113
  const percentThrough = BookReader.util.cssPercentage(tocEntry.pageIndex, this.book.getNumLeafs() - 1);
97
114
  $(`<div></div>`)
98
115
  .append(
99
116
  $('<div />')
100
117
  .text(chapterStr)
101
- .append($('<div class="BRchapterPage" />').text(`Page ${tocEntry.pagenum}`))
118
+ .append(
119
+ $('<div class="BRchapterPage" />')
120
+ .text(this.book.getPageName(tocEntry.pageIndex))
121
+ )
102
122
  )
103
123
  .addClass('BRchapter')
104
124
  .css({ left: percentThrough })
@@ -1,4 +1,5 @@
1
1
  import PageChunkIterator from './PageChunkIterator.js';
2
+ import { hasLocalStorage } from './utils.js';
2
3
  /** @typedef {import('./utils.js').ISO6391} ISO6391 */
3
4
  /** @typedef {import('./PageChunk.js')} PageChunk */
4
5
 
@@ -80,6 +81,7 @@ export default class AbstractTTSEngine {
80
81
  */
81
82
  start(leafIndex, numLeafs) {
82
83
  this.playing = true;
84
+ this.paused = false;
83
85
  this.opts.onLoadingStart();
84
86
 
85
87
  this._chunkIterator = new PageChunkIterator(numLeafs, leafIndex, {
@@ -95,6 +97,7 @@ export default class AbstractTTSEngine {
95
97
  stop() {
96
98
  if (this.activeSound) this.activeSound.stop();
97
99
  this.playing = false;
100
+ this.paused = true;
98
101
  this._chunkIterator = null;
99
102
  this.activeSound = null;
100
103
  this.events.trigger('stop');
@@ -143,7 +146,7 @@ export default class AbstractTTSEngine {
143
146
  this.events.off('voiceschanged', this.updateBestVoice);
144
147
  this.voice = this.getVoices().find(voice => voice.voiceURI === voiceURI);
145
148
  // if the current book has a language set, store the selected voice with the book language as a suffix
146
- if (this.opts.bookLanguage) {
149
+ if (this.opts.bookLanguage && hasLocalStorage()) {
147
150
  localStorage.setItem(`BRtts-voice-${this.opts.bookLanguage}`, this.voice.voiceURI);
148
151
  }
149
152
  if (this.activeSound) this.activeSound.setVoice(this.voice);
@@ -247,7 +250,7 @@ export default class AbstractTTSEngine {
247
250
  * @return {SpeechSynthesisVoice | undefined}
248
251
  */
249
252
  static getMatchingStoredVoice(voices, bookLanguage) {
250
- const storedVoice = localStorage.getItem(`BRtts-voice-${bookLanguage}`);
253
+ const storedVoice = hasLocalStorage() && localStorage.getItem(`BRtts-voice-${bookLanguage}`);
251
254
  return (storedVoice ? voices.find(v => v.voiceURI === storedVoice) : undefined);
252
255
  }
253
256
 
@@ -251,7 +251,7 @@ BookReader.prototype.ttsPlayPause = function() {
251
251
  this.ttsToggle();
252
252
  } else {
253
253
  this.ttsEngine.togglePlayPause();
254
- this.ttsUpdateState(this.ttsEngine.paused);
254
+ this.ttsUpdateState();
255
255
  }
256
256
  };
257
257
 
@@ -64,3 +64,18 @@ function searchForISO6391(language, columnsToSearch) {
64
64
  }
65
65
  return null;
66
66
  }
67
+
68
+ /**
69
+ * Checks whether the current browser supports localStorage or
70
+ * if the current context has access to it.
71
+ * @return {boolean}
72
+ */
73
+ export function hasLocalStorage() {
74
+ try {
75
+ return !!window.localStorage;
76
+ } catch (e) {
77
+ // Will throw in sandboxed iframe
78
+ // DOMException: Window.localStorage getter: Forbidden in a sandboxed document without the 'allow-same-origin' flag.
79
+ return false;
80
+ }
81
+ }
@@ -40,4 +40,18 @@ describe("SelectionObserver", () => {
40
40
  observer._onSelectionChange();
41
41
  expect(handler.callCount).toBe(2);
42
42
  });
43
+
44
+ test('Only fires when selection started in selector', () => {
45
+ const handler = sinon.spy();
46
+ const observer = new SelectionObserver(".text-layer", handler);
47
+ const target = document.createElement("div");
48
+ target.classList.add("text-layer");
49
+
50
+ // stub window.getSelection
51
+ const getSelectionStub = sinon.stub(window, "getSelection");
52
+ getSelectionStub.returns({ toString: () => "test", anchorNode: document.body });
53
+ observer._onSelectionChange();
54
+ expect(handler.callCount).toBe(0);
55
+ expect(observer.selecting).toBe(false);
56
+ });
43
57
  });
@@ -4,7 +4,9 @@ import BookReader from "@/src/BookReader.js";
4
4
  import "@/src/plugins/plugin.chapters.js";
5
5
  import { BookModel } from "@/src/BookReader/BookModel";
6
6
  import { deepCopy } from "../utils";
7
+ /** @typedef {import('@/src/plugins/plugin.chapters').TocEntry} TocEntry */
7
8
 
9
+ /** @type {TocEntry[]} */
8
10
  const SAMPLE_TOC = [{
9
11
  "pagenum": "3",
10
12
  "level": 1,
@@ -34,16 +36,15 @@ const SAMPLE_TOC = [{
34
36
  "pageIndex": 40,
35
37
  }];
36
38
 
39
+ /** @type {TocEntry[]} */
37
40
  const SAMPLE_TOC_UNDEF = [
38
41
  {
39
- "pagenum": "undefined",
40
42
  "level": 1,
41
43
  "label": "CHAPTER I",
42
44
  "type": { "key": "/type/toc_item" },
43
45
  "title": "THE COUNTRY AND THE MISSION 1",
44
46
  },
45
47
  {
46
- "pagenum": "undefined",
47
48
  "level": 1,
48
49
  "label": "CHAPTER II",
49
50
  "type": { "key": "/type/toc_item" },
@@ -51,6 +52,18 @@ const SAMPLE_TOC_UNDEF = [
51
52
  },
52
53
  ];
53
54
 
55
+ /** @type {TocEntry[]} */
56
+ const SAMPLE_TOC_OPTION = [
57
+ {
58
+ "level": 1,
59
+ "title": "THE COUNTRY AND THE MISSION 1",
60
+ },
61
+ {
62
+ "level": 1,
63
+ "title": "THE COUNTRY AND THE MISSION 2",
64
+ },
65
+ ];
66
+
54
67
  afterEach(() => {
55
68
  sinon.restore();
56
69
  });
@@ -92,7 +105,7 @@ describe("BRChaptersPlugin", () => {
92
105
  },
93
106
  getOpenLibraryRecord: async () => ({
94
107
  "title": "The Adventures of Sherlock Holmes",
95
- "table_of_contents": deepCopy(SAMPLE_TOC_UNDEF),
108
+ "table_of_contents": deepCopy(SAMPLE_TOC_OPTION),
96
109
  "ocaid": "adventureofsherl0000unse",
97
110
  }),
98
111
  _chaptersRender: sinon.stub(),
@@ -100,6 +113,41 @@ describe("BRChaptersPlugin", () => {
100
113
  await BookReader.prototype._chapterInit.call(fakeBR);
101
114
  expect(fakeBR._chaptersRender.callCount).toBe(1);
102
115
  });
116
+
117
+ test("does not fetch open library record if table of contents in options", async () => {
118
+ const fakeBR = {
119
+ options: {
120
+ table_of_contents: deepCopy(SAMPLE_TOC_UNDEF),
121
+ },
122
+ bind: sinon.stub(),
123
+ getOpenLibraryRecord: sinon.stub(),
124
+ _chaptersRender: sinon.stub(),
125
+ };
126
+ await BookReader.prototype._chapterInit.call(fakeBR);
127
+ expect(fakeBR.getOpenLibraryRecord.callCount).toBe(0);
128
+ expect(fakeBR._chaptersRender.callCount).toBe(1);
129
+ });
130
+
131
+ test("converts leafs and pagenums to page index", async () => {
132
+ const table_of_contents = deepCopy(SAMPLE_TOC_UNDEF);
133
+ table_of_contents[0].leaf = 0;
134
+ table_of_contents[1].pagenum = '17';
135
+ const fakeBR = {
136
+ options: {
137
+ table_of_contents,
138
+ },
139
+ bind: sinon.stub(),
140
+ book: {
141
+ leafNumToIndex: (leaf) => leaf + 1,
142
+ getPageIndex: (str) => parseFloat(str),
143
+ },
144
+ _chaptersRender: sinon.stub(),
145
+ };
146
+ await BookReader.prototype._chapterInit.call(fakeBR);
147
+ expect(fakeBR._chaptersRender.callCount).toBe(1);
148
+ expect(fakeBR._tocEntries[0].pageIndex).toBe(1);
149
+ expect(fakeBR._tocEntries[1].pageIndex).toBe(17);
150
+ });
103
151
  });
104
152
 
105
153
  describe('_chaptersRender', () => {