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

Sign up to get free protection for your applications and to get access to all the features.
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 */