@internetarchive/bookreader 5.0.0-24-sortingstate-final → 5.0.0-27

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 5.0.0-27
2
+ Dev: eslint fix for $.browser @homewardgamer
3
+ Fix: cache search inside requests @iisa
4
+ # 5.0.0-26
5
+ Fix: read aloud play/pause button @nsharma123
6
+ Dev: strict keyboard shortcuts @mc2
7
+ Dev: update IA demo page @iisa
8
+
1
9
  # 5.0.0-24
2
10
  Fix: book-nav side panel zoom out @mc2
3
11
  Dev: refactor zoom code @mc2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-24-sortingstate-final",
3
+ "version": "5.0.0-27",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -463,6 +463,7 @@ class IABookmarks extends LitElement {
463
463
  return html`
464
464
  <button
465
465
  class="ia-button primary"
466
+ tabindex="-1"
466
467
  ?disabled=${this.shouldEnableAddBookmarkButton}
467
468
  @click=${this.addBookmark}>
468
469
  Add bookmark
@@ -7,16 +7,8 @@ import volumesIcon from '../assets/icon_volumes.js';
7
7
 
8
8
  import './volumes.js';
9
9
 
10
- const sortType = {
11
- title_asc: 'title_asc',
12
- title_desc: 'title_desc',
13
- default: 'default'
14
- };
15
10
  export default class VolumesProvider {
16
11
 
17
- /**
18
- * @param {import('../../BookReader').default} bookreader
19
- */
20
12
  constructor(baseHost, bookreader, optionChange) {
21
13
  this.optionChange = optionChange;
22
14
  this.component = document.createElement("viewable-files");
@@ -25,9 +17,6 @@ export default class VolumesProvider {
25
17
  this.viewableFiles = Object.keys(files).map(item => files[item]);
26
18
  this.volumeCount = Object.keys(files).length;
27
19
 
28
- /** @type {import('../../BookReader').default} */
29
- this.bookreader = bookreader;
30
-
31
20
  this.component.subPrefix = bookreader.options.subPrefix || "";
32
21
  this.component.hostUrl = baseHost;
33
22
  this.component.viewableFiles = this.viewableFiles;
@@ -36,30 +25,20 @@ export default class VolumesProvider {
36
25
  this.label = `Viewable files (${this.volumeCount})`;
37
26
  this.icon = html`${volumesIcon}`;
38
27
 
39
- this.sortOrderBy = sortType.default;
40
-
41
- // get sort state from query param
42
- if (this.bookreader.urlPlugin) {
43
- this.bookreader.urlPlugin.pullFromAddressBar();
44
-
45
- const urlSortValue = this.bookreader.urlPlugin.getUrlParam('sort');
46
- if (urlSortValue === sortType.title_asc || urlSortValue === sortType.title_desc) {
47
- this.sortOrderBy = urlSortValue;
48
- }
49
- }
50
- this.sortVolumes(this.sortOrderBy);
28
+ this.sortOrderBy = "orig_sort";
29
+ this.sortVolumes("orig_sort");
51
30
  }
52
31
 
53
32
  get sortButton() {
54
33
  const sortIcons = {
55
- default: html`
34
+ orig_sort: html`
56
35
  <button class="sort-by neutral-icon" aria-label="Sort volumes in initial order" @click=${() => this.sortVolumes("title_asc")}>${sortNeutralIcon}</button>
57
36
  `,
58
37
  title_asc: html`
59
38
  <button class="sort-by asc-icon" aria-label="Sort volumes in ascending order" @click=${() => this.sortVolumes("title_desc")}>${sortAscIcon}</button>
60
39
  `,
61
40
  title_desc: html`
62
- <button class="sort-by desc-icon" aria-label="Sort volumes in descending order" @click=${() => this.sortVolumes("default")}>${sortDescIcon}</button>
41
+ <button class="sort-by desc-icon" aria-label="Sort volumes in descending order" @click=${() => this.sortVolumes("orig_sort")}>${sortDescIcon}</button>
63
42
  `,
64
43
  };
65
44
 
@@ -67,37 +46,28 @@ export default class VolumesProvider {
67
46
  }
68
47
 
69
48
  /**
70
- * @param {'default' | 'title_asc' | 'title_desc'} sortByType
49
+ * @param {'orig_sort' | 'title_asc' | 'title_desc'} sortByType
71
50
  */
72
51
  sortVolumes(sortByType) {
73
52
  let sortedFiles = [];
74
53
 
75
54
  const files = this.viewableFiles;
76
55
  sortedFiles = files.sort((a, b) => {
77
- if (sortByType === sortType.title_asc) return a.title.localeCompare(b.title);
78
- else if (sortByType === sortType.title_desc) return b.title.localeCompare(a.title);
79
- else return a.orig_sort - b.orig_sort;
56
+ if (sortByType === 'orig_sort') return a.orig_sort - b.orig_sort;
57
+ else if (sortByType === 'title_asc') return a.title.localeCompare(b.title);
58
+ else return b.title.localeCompare(a.title);
80
59
  });
81
60
 
82
61
  this.sortOrderBy = sortByType;
83
62
  this.component.viewableFiles = [...sortedFiles];
84
63
  this.actionButton = this.sortButton;
85
-
86
- if (this.bookreader.urlPlugin) {
87
- if (this.sortOrderBy !== sortType.default) {
88
- this.bookreader.urlPlugin.setUrlParam('sort', sortByType);
89
- } else {
90
- this.bookreader.urlPlugin.removeUrlParam('sort');
91
- }
92
- }
93
-
94
64
  this.optionChange(this.bookreader);
95
65
 
96
66
  this.multipleFilesClicked(sortByType);
97
67
  }
98
68
 
99
69
  /**
100
- * @param {'default' | 'title_asc' | 'title_desc'} orderBy
70
+ * @param {'orig_sort' | 'title_asc' | 'title_desc'} orderBy
101
71
  */
102
72
  multipleFilesClicked(orderBy) {
103
73
  if (!window.archive_analytics) {
package/src/BookReader.js CHANGED
@@ -264,6 +264,15 @@ BookReader.prototype.setup = function(options) {
264
264
  useSrcSet: this.options.useSrcSet,
265
265
  reduceSet: this.reduceSet,
266
266
  });
267
+
268
+ /**
269
+ * Flag if BookReader has "focus" for keyboard shortcuts
270
+ * Initially true, set to false when:
271
+ * - BookReader scrolled out of view
272
+ * Set to true when:
273
+ * - BookReader scrolled into view
274
+ */
275
+ this.hasKeyFocus = true;
267
276
  };
268
277
 
269
278
  /**
@@ -662,87 +671,116 @@ BookReader.prototype.resize = function() {
662
671
  };
663
672
 
664
673
  /**
665
- * Binds keyboard event listeners
674
+ * Binds keyboard and keyboard focus event listeners
666
675
  */
667
- BookReader.prototype.setupKeyListeners = function() {
668
- var self = this;
676
+ BookReader.prototype.setupKeyListeners = function () {
669
677
 
670
- var KEY_PGUP = 33;
671
- var KEY_PGDOWN = 34;
672
- var KEY_END = 35;
673
- var KEY_HOME = 36;
674
-
675
- var KEY_LEFT = 37;
676
- var KEY_UP = 38;
677
- var KEY_RIGHT = 39;
678
- var KEY_DOWN = 40;
679
- // The minus(-) and equal(=) keys have different mappings for different browsers
680
- var KEY_MINUS = 189; // Chrome
681
- var KEY_MINUS_F = 173; // Firefox
682
- var KEY_NUMPAD_SUBTRACT = 109;
683
- var KEY_EQUAL = 187; // Chrome
684
- var KEY_EQUAL_F = 61; // Firefox
685
- var KEY_NUMPAD_ADD = 107;
686
-
687
- // We use document here instead of window to avoid a bug in jQuery on IE7
688
- $(document).on("keydown", function(e) {
689
-
690
- // Keyboard navigation
691
- switch (e.keyCode) {
692
- case KEY_PGUP:
693
- case KEY_UP:
694
- // In 1up mode page scrolling is handled by browser
695
- if (!utils.isInputActive() && self.constMode2up == self.mode) {
696
- e.preventDefault();
697
- self.prev();
698
- }
678
+ // Keyboard focus by BookReader in viewport
679
+ //
680
+ // Intersection observer and callback sets BookReader keyboard
681
+ // "focus" flag off when the BookReader is not in the viewport.
682
+ if (window.IntersectionObserver) {
683
+ const observer = new IntersectionObserver((entries) => {
684
+ entries.forEach((entry) => {
685
+ if (entry.intersectionRatio === 0) {
686
+ this.hasKeyFocus = false;
687
+ } else {
688
+ this.hasKeyFocus = true;
689
+ }
690
+ });
691
+ }, {
692
+ root: null,
693
+ rootMargin: '0px',
694
+ threshold: [0, 0.05, 1],
695
+ });
696
+ observer.observe(this.refs.$br[0]);
697
+ }
698
+
699
+ // Keyboard listeners
700
+ document.addEventListener('keydown', (e) => {
701
+
702
+ // Ignore if BookReader "focus" flag not set
703
+ if (!this.hasKeyFocus) {
704
+ return;
705
+ }
706
+
707
+ // Ignore if modifiers are active.
708
+ if (e.getModifierState('Control') ||
709
+ e.getModifierState('Alt') ||
710
+ e.getModifierState('Meta') ||
711
+ e.getModifierState('Win') /* hack for IE */) {
712
+ return;
713
+ }
714
+
715
+ // Ignore in input elements
716
+ if (utils.isInputActive()) {
717
+ return;
718
+ }
719
+
720
+ // KeyboardEvent code values:
721
+ // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values
722
+ switch (e.key) {
723
+
724
+ // Page navigation
725
+ case "Home":
726
+ e.preventDefault();
727
+ this.first();
699
728
  break;
700
- case KEY_DOWN:
701
- case KEY_PGDOWN:
702
- if (!utils.isInputActive() && self.constMode2up == self.mode) {
703
- e.preventDefault();
704
- self.next();
705
- }
729
+ case "End":
730
+ e.preventDefault();
731
+ this.last();
706
732
  break;
707
- case KEY_END:
708
- if (!utils.isInputActive()) {
733
+ case "ArrowDown":
734
+ case "PageDown":
735
+ case "Down": // hack for IE and old Gecko
736
+ // In 1up and thumb mode page scrolling handled by browser
737
+ if (this.constMode2up === this.mode) {
709
738
  e.preventDefault();
710
- self.last();
739
+ this.next();
711
740
  }
712
741
  break;
713
- case KEY_HOME:
714
- if (!utils.isInputActive()) {
742
+ case "ArrowUp":
743
+ case "PageUp":
744
+ case "Up": // hack for IE and old Gecko
745
+ // In 1up and thumb mode page scrolling handled by browser
746
+ if (this.constMode2up === this.mode) {
715
747
  e.preventDefault();
716
- self.first();
748
+ this.prev();
717
749
  }
718
750
  break;
719
- case KEY_LEFT:
720
- if (!utils.isInputActive() && self.constModeThumb != self.mode) {
751
+ case "ArrowLeft":
752
+ case "Left": // hack for IE and old Gecko
753
+ // No y-scrolling in thumb mode
754
+ if (this.constModeThumb != this.mode) {
721
755
  e.preventDefault();
722
- self.left();
756
+ this.left();
723
757
  }
724
758
  break;
725
- case KEY_RIGHT:
726
- if (!utils.isInputActive() && self.constModeThumb != self.mode) {
759
+ case "ArrowRight":
760
+ case "Right": // hack for IE and old Gecko
761
+ // No y-scrolling in thumb mode
762
+ if (this.constModeThumb != this.mode) {
727
763
  e.preventDefault();
728
- self.right();
764
+ this.right();
729
765
  }
730
766
  break;
731
- case KEY_MINUS:
732
- case KEY_MINUS_F:
733
- case KEY_NUMPAD_SUBTRACT:
734
- if (!utils.isInputActive()) {
735
- e.preventDefault();
736
- self.zoom(-1);
737
- }
767
+ // Zoom
768
+ case '-':
769
+ case 'Subtract':
770
+ e.preventDefault();
771
+ this.zoom(-1);
738
772
  break;
739
- case KEY_EQUAL:
740
- case KEY_EQUAL_F:
741
- case KEY_NUMPAD_ADD:
742
- if (!utils.isInputActive()) {
743
- e.preventDefault();
744
- self.zoom(+1);
745
- }
773
+ case '+':
774
+ case '=':
775
+ case 'Add':
776
+ e.preventDefault();
777
+ this.zoom(1);
778
+ break;
779
+ // Fullscreen
780
+ case 'F':
781
+ case 'f':
782
+ e.preventDefault();
783
+ this.toggleFullscreen();
746
784
  break;
747
785
  }
748
786
  });
@@ -50,7 +50,7 @@ BookReader.prototype.init = (function(super_) {
50
50
  this.bind(BookReader.eventNames.PostInit, () => {
51
51
  const { updateWindowTitle, urlMode } = this.options;
52
52
  if (updateWindowTitle) {
53
- document.title = this.shortTitle(this.bookTitle, 50);
53
+ document.title = this.shortTitle(50);
54
54
  }
55
55
  if (urlMode === 'hash') {
56
56
  this.urlStartLocationPolling();
@@ -86,7 +86,7 @@ BookReader.prototype.urlStartLocationPolling = function() {
86
86
  this.oldLocationHash = this.urlReadFragment();
87
87
 
88
88
  if (this.locationPollId) {
89
- clearInterval(this.locationPollId);
89
+ clearInterval(this.locationPollID);
90
90
  this.locationPollId = null;
91
91
  }
92
92
 
@@ -196,209 +196,3 @@ BookReader.prototype.urlReadFragment = function() {
196
196
  BookReader.prototype.urlReadHashFragment = function() {
197
197
  return window.location.hash.substr(1);
198
198
  };
199
- export class UrlPlugin {
200
- constructor(options = {}) {
201
- this.bookReaderOptions = options;
202
-
203
- // the canonical order of elements is important in the path and query string
204
- this.urlSchema = [
205
- { name: 'page', position: 'path', default: 'n0' },
206
- { name: 'mode', position: 'path', default: '2up' },
207
- { name: 'search', position: 'path', deprecated_for: 'q' },
208
- { name: 'q', position: 'query_param' },
209
- { name: 'sort', position: 'query_param' },
210
- { name: 'view', position: 'query_param' },
211
- { name: 'admin', position: 'query_param' },
212
- ];
213
-
214
- this.urlState = {};
215
- this.urlMode = this.bookReaderOptions.urlMode || 'hash';
216
- this.urlHistoryBasePath = this.bookReaderOptions.urlHistoryBasePath || '/';
217
- this.urlLocationPollId = null;
218
- this.oldLocationHash = null;
219
- this.oldUserHash = null;
220
- }
221
-
222
- /**
223
- * Parse JSON object URL state to string format
224
- * Arrange path names in an order that it is positioned on the urlSchema
225
- * @param {Object} urlState
226
- * @returns {string}
227
- */
228
- urlStateToUrlString(urlState) {
229
- const searchParams = new URLSearchParams();
230
- const pathParams = {};
231
-
232
- Object.keys(urlState).forEach(key => {
233
- let schema = this.urlSchema.find(schema => schema.name === key);
234
- if (schema?.deprecated_for) {
235
- schema = this.urlSchema.find(schemaKey => schemaKey.name === schema.deprecated_for);
236
- }
237
- if (schema?.position == 'path') {
238
- pathParams[schema?.name] = urlState[key];
239
- } else {
240
- searchParams.append(schema?.name || key, urlState[key]);
241
- }
242
- });
243
-
244
- const strPathParams = this.urlSchema
245
- .filter(s => s.position == 'path')
246
- .map(schema => pathParams[schema.name] ? `${schema.name}/${pathParams[schema.name]}` : '')
247
- .join('/');
248
-
249
- const strStrippedTrailingSlash = `${strPathParams.replace(/\/$/, '')}`;
250
- const concatenatedPath = `${strStrippedTrailingSlash}?${searchParams.toString()}`;
251
- return searchParams.toString() ? concatenatedPath : `${strStrippedTrailingSlash}`;
252
- }
253
-
254
- /**
255
- * Parse string URL and add it in the current urlState
256
- * Example:
257
- * /page/n7/mode/2up => {page: 'n7', mode: '2up'}
258
- * /page/n7/mode/2up/search/hello => {page: 'n7', mode: '2up', q: 'hello'}
259
- * @param {string} urlString
260
- * @returns {object}
261
- */
262
- urlStringToUrlState(urlString) {
263
- const urlState = {};
264
-
265
- // Fetch searchParams from given {str}
266
- // Note: whole URL path is needed for URL parsing
267
- const urlPath = new URL(urlString, 'http://example.com');
268
- const urlSearchParamsObj = Object.fromEntries(urlPath.searchParams.entries());
269
- const urlStrSplitSlashObj = Object.fromEntries(urlPath.pathname
270
- .match(/[^\\/]+\/[^\\/]+/g)
271
- .map(x => x.split('/'))
272
- );
273
- const doesKeyExists = (_object, _key) => {
274
- return Object.keys(_object).some(value => value == _key);
275
- };
276
-
277
- // Add path objects to urlState
278
- this.urlSchema
279
- .filter(schema => schema.position == 'path')
280
- .forEach(schema => {
281
- if (!urlStrSplitSlashObj[schema.name] && schema.default) {
282
- return urlState[schema.name] = schema.default;
283
- }
284
- const hasPropertyKey = doesKeyExists(urlStrSplitSlashObj, schema.name);
285
- const hasDeprecatedKey = doesKeyExists(schema, 'deprecated_for') && hasPropertyKey;
286
-
287
- if (hasDeprecatedKey) {
288
- urlState[schema.deprecated_for] = urlStrSplitSlashObj[schema.name];
289
- return;
290
- }
291
-
292
- if (hasPropertyKey) {
293
- urlState[schema.name] = urlStrSplitSlashObj[schema.name];
294
- return;
295
- }
296
- });
297
-
298
- // Add searchParams to urlState
299
- Object.entries(urlSearchParamsObj).forEach(([key, value]) => {
300
- urlState[key] = value;
301
- });
302
-
303
- return urlState;
304
- }
305
-
306
- /**
307
- * Add or update key-value to the urlState
308
- * @param {string} key
309
- * @param {string} val
310
- */
311
- setUrlParam(key, value) {
312
- this.urlState[key] = value;
313
-
314
- this.pushToAddressBar();
315
- }
316
-
317
- /**
318
- * Delete key-value to the urlState
319
- * @param {string} key
320
- */
321
- removeUrlParam(key) {
322
- delete this.urlState[key];
323
-
324
- this.pushToAddressBar();
325
- }
326
-
327
- /**
328
- * Get key-value from the urlState
329
- * @param {string} key
330
- * @return {string}
331
- */
332
- getUrlParam(key) {
333
- return this.urlState[key];
334
- }
335
-
336
- /**
337
- * Push URL params to addressbar
338
- */
339
- pushToAddressBar() {
340
- const urlStrPath = this.urlStateToUrlString(this.urlState);
341
- if (this.urlMode == 'history') {
342
- if (window.history && window.history.replaceState) {
343
- const newUrlPath = `${this.urlHistoryBasePath}/${urlStrPath}`;
344
- window.history.replaceState({}, null, newUrlPath);
345
- }
346
- } else {
347
- window.location.replace('#' + urlStrPath);
348
- }
349
- this.oldLocationHash = urlStrPath;
350
- }
351
-
352
- /**
353
- * Get the url and check if it has changed
354
- * If it was changeed, update the urlState
355
- */
356
- listenForHashChanges() {
357
- this.oldLocationHash = window.location.hash.substr(1);
358
- if (this.urlLocationPollId) {
359
- clearInterval(this.urlLocationPollId);
360
- this.urlLocationPollId = null;
361
- }
362
-
363
- // check if the URL changes
364
- const updateHash = () => {
365
- const newFragment = window.location.hash.substr(1);
366
- const hasFragmentChange = newFragment != this.oldLocationHash;
367
-
368
- if (!hasFragmentChange) { return; }
369
-
370
- this.urlState = this.urlStringToUrlState(newFragment);
371
- };
372
- this.urlLocationPollId = setInterval(updateHash, 500);
373
- }
374
-
375
- /**
376
- * Will read either the hash or URL and return the bookreader fragment
377
- */
378
- pullFromAddressBar (location = window.location) {
379
- const path = this.urlMode === 'history'
380
- ? (location.pathname.substr(this.urlHistoryBasePath.length) + location.search)
381
- : location.hash.substr(1);
382
- this.urlState = this.urlStringToUrlState(path);
383
- }
384
- }
385
-
386
- export class BookreaderUrlPlugin extends BookReader {
387
- init() {
388
- if (this.options.enableUrlPlugin) {
389
- this.urlPlugin = new UrlPlugin(this.options);
390
- this.bind(BookReader.eventNames.PostInit, () => {
391
- const { urlMode } = this.options;
392
-
393
- if (urlMode === 'hash') {
394
- this.urlPlugin.listenForHashChanges();
395
- }
396
- });
397
- }
398
-
399
- super.init();
400
- }
401
- }
402
-
403
- window.BookReader = BookreaderUrlPlugin;
404
- export default BookreaderUrlPlugin;
@@ -213,6 +213,7 @@ BookReader.prototype.search = function(term = '', overrides = {}) {
213
213
  return $.ajax({
214
214
  url: url,
215
215
  dataType: 'jsonp',
216
+ cache: true,
216
217
  beforeSend,
217
218
  jsonpCallback: 'BRSearchInProgress'
218
219
  }).then(processSearchResults);
@@ -23,7 +23,7 @@ export default class FestivalTTSEngine extends AbstractTTSEngine {
23
23
  // $.browsers is sometimes undefined on some Android browsers :/
24
24
  // Likely related to when $.browser was moved to npm
25
25
  /** @type {'mp3' | 'ogg'} format of audio to get */
26
- this.audioFormat = $.browser?.mozilla ? 'ogg' : 'mp3';
26
+ this.audioFormat = $.browser?.mozilla ? 'ogg' : 'mp3'; //eslint-disable-line no-jquery/no-browser
27
27
  }
28
28
 
29
29
  /** @override */