@internetarchive/bookreader 5.0.0-22 → 5.0.0-24-sortingstate-1

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 (127) hide show
  1. package/.nvmrc +1 -0
  2. package/BookReader/BookReader.js +32148 -2
  3. package/BookReader/BookReader.js.map +1 -1
  4. package/BookReader/bookreader-component-bundle.js +10917 -941
  5. package/BookReader/bookreader-component-bundle.js.map +1 -1
  6. package/BookReader/icons/1up.svg +12 -1
  7. package/BookReader/icons/2up.svg +15 -1
  8. package/BookReader/icons/advance.svg +26 -3
  9. package/BookReader/icons/chevron-right.svg +1 -1
  10. package/BookReader/icons/close-circle-dark.svg +1 -1
  11. package/BookReader/icons/close-circle.svg +1 -1
  12. package/BookReader/icons/fullscreen.svg +17 -1
  13. package/BookReader/icons/fullscreen_exit.svg +17 -1
  14. package/BookReader/icons/hamburger.svg +15 -1
  15. package/BookReader/icons/left-arrow.svg +12 -1
  16. package/BookReader/icons/magnify-minus.svg +16 -1
  17. package/BookReader/icons/magnify-plus.svg +17 -1
  18. package/BookReader/icons/magnify.svg +15 -1
  19. package/BookReader/icons/pause.svg +23 -1
  20. package/BookReader/icons/play.svg +22 -1
  21. package/BookReader/icons/playback-speed.svg +34 -1
  22. package/BookReader/icons/read-aloud.svg +22 -1
  23. package/BookReader/icons/review.svg +22 -3
  24. package/BookReader/icons/thumbnails.svg +17 -1
  25. package/BookReader/icons/voice.svg +1 -1
  26. package/BookReader/icons/volume-full.svg +22 -1
  27. package/BookReader/images/BRicons.svg +94 -5
  28. package/BookReader/images/books_graphic.svg +177 -1
  29. package/BookReader/images/icon_book.svg +12 -1
  30. package/BookReader/images/icon_bookmark.svg +12 -1
  31. package/BookReader/images/icon_gear.svg +14 -1
  32. package/BookReader/images/icon_hamburger.svg +20 -1
  33. package/BookReader/images/icon_home.svg +21 -1
  34. package/BookReader/images/icon_info.svg +11 -1
  35. package/BookReader/images/icon_one_page.svg +8 -1
  36. package/BookReader/images/icon_pause.svg +1 -1
  37. package/BookReader/images/icon_play.svg +1 -1
  38. package/BookReader/images/icon_playback-rate.svg +15 -1
  39. package/BookReader/images/icon_search_button.svg +8 -1
  40. package/BookReader/images/icon_share.svg +9 -1
  41. package/BookReader/images/icon_skip-ahead.svg +6 -1
  42. package/BookReader/images/icon_skip-back.svg +13 -2
  43. package/BookReader/images/icon_speaker.svg +18 -1
  44. package/BookReader/images/icon_speaker_open.svg +10 -1
  45. package/BookReader/images/icon_thumbnails.svg +12 -1
  46. package/BookReader/images/icon_toc.svg +5 -1
  47. package/BookReader/images/icon_two_pages.svg +9 -1
  48. package/BookReader/images/marker_chap-off.svg +11 -1
  49. package/BookReader/images/marker_chap-on.svg +11 -1
  50. package/BookReader/images/marker_srch-on.svg +11 -1
  51. package/BookReader/jquery-1.10.1.js +108 -2
  52. package/BookReader/plugins/plugin.archive_analytics.js +170 -1
  53. package/BookReader/plugins/plugin.archive_analytics.js.map +1 -1
  54. package/BookReader/plugins/plugin.autoplay.js +163 -1
  55. package/BookReader/plugins/plugin.autoplay.js.map +1 -1
  56. package/BookReader/plugins/plugin.chapters.js +333 -1
  57. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  58. package/BookReader/plugins/plugin.iframe.js +72 -1
  59. package/BookReader/plugins/plugin.iframe.js.map +1 -1
  60. package/BookReader/plugins/plugin.mobile_nav.js +332 -1
  61. package/BookReader/plugins/plugin.mobile_nav.js.map +1 -1
  62. package/BookReader/plugins/plugin.resume.js +241 -1
  63. package/BookReader/plugins/plugin.resume.js.map +1 -1
  64. package/BookReader/plugins/plugin.search.js +1263 -1
  65. package/BookReader/plugins/plugin.search.js.map +1 -1
  66. package/BookReader/plugins/plugin.text_selection.js +839 -1
  67. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  68. package/BookReader/plugins/plugin.tts.js +9114 -2
  69. package/BookReader/plugins/plugin.tts.js.map +1 -1
  70. package/BookReader/plugins/plugin.url.js +750 -1
  71. package/BookReader/plugins/plugin.url.js.map +1 -1
  72. package/BookReader/plugins/plugin.vendor-fullscreen.js +326 -1
  73. package/BookReader/plugins/plugin.vendor-fullscreen.js.map +1 -1
  74. package/BookReader/webcomponents-bundle.js +411 -2
  75. package/BookReader/webcomponents-bundle.js.map +1 -1
  76. package/CHANGELOG.md +9 -0
  77. package/package.json +2 -6
  78. package/src/BookNavigator/bookmarks/bookmarks-provider.js +1 -0
  79. package/src/BookNavigator/bookmarks/ia-bookmarks.js +3 -0
  80. package/src/BookNavigator/visual-adjustments/visual-adjustments-provider.js +1 -1
  81. package/src/BookNavigator/volumes/volumes-provider.js +40 -9
  82. package/src/BookReader/Mode1Up.js +10 -2
  83. package/src/BookReader/ModeThumb.js +13 -6
  84. package/src/BookReader/options.js +4 -0
  85. package/src/BookReader.js +8 -22
  86. package/src/ItemNavigator/ItemNavigator.js +1 -0
  87. package/src/plugins/plugin.url.js +209 -2
  88. package/tests/{BookReader → jest/BookReader}/BookModel.test.js +0 -0
  89. package/tests/{BookReader → jest/BookReader}/BookReaderPublicFunctions.test.js +0 -0
  90. package/tests/{BookReader → jest/BookReader}/DebugConsole.test.js +0 -0
  91. package/tests/{BookReader → jest/BookReader}/ImageCache.test.js +0 -0
  92. package/tests/{BookReader → jest/BookReader}/Mode1UpLit.test.js +0 -0
  93. package/tests/{BookReader → jest/BookReader}/Mode2Up.test.js +0 -0
  94. package/tests/{BookReader → jest/BookReader}/ModeSmoothZoom.test.js +0 -0
  95. package/tests/jest/BookReader/ModeThumb.test.js +71 -0
  96. package/tests/{BookReader → jest/BookReader}/Navbar/Navbar.test.js +0 -0
  97. package/tests/{BookReader → jest/BookReader}/PageContainer.test.js +0 -0
  98. package/tests/{BookReader → jest/BookReader}/ReduceSet.test.js +0 -0
  99. package/tests/{BookReader → jest/BookReader}/Toolbar/Toolbar.test.js +0 -0
  100. package/tests/{BookReader → jest/BookReader}/utils/HTMLDimensionsCacher.test.js +0 -0
  101. package/tests/{BookReader → jest/BookReader}/utils/classes.test.js +0 -0
  102. package/tests/{BookReader → jest/BookReader}/utils.test.js +0 -0
  103. package/tests/{BookReader.options.test.js → jest/BookReader.options.test.js} +0 -0
  104. package/tests/{BookReader.test.js → jest/BookReader.test.js} +0 -0
  105. package/tests/{plugins → jest/plugins}/plugin.archive_analytics.test.js +0 -0
  106. package/tests/{plugins → jest/plugins}/plugin.autoplay.test.js +0 -0
  107. package/tests/{plugins → jest/plugins}/plugin.chapters.test.js +0 -0
  108. package/tests/{plugins → jest/plugins}/plugin.iframe.test.js +0 -0
  109. package/tests/{plugins → jest/plugins}/plugin.mobile_nav.test.js +0 -0
  110. package/tests/{plugins → jest/plugins}/plugin.resume.test.js +0 -0
  111. package/tests/{plugins → jest/plugins}/plugin.text_selection.test.js +0 -0
  112. package/tests/jest/plugins/plugin.url.test.js +306 -0
  113. package/tests/{plugins → jest/plugins}/plugin.vendor-fullscreen.test.js +0 -0
  114. package/tests/{plugins → jest/plugins}/search/plugin.search.test.js +0 -0
  115. package/tests/{plugins → jest/plugins}/search/plugin.search.view.test.js +0 -0
  116. package/tests/{plugins → jest/plugins}/tts/AbstractTTSEngine.test.js +0 -0
  117. package/tests/{plugins → jest/plugins}/tts/FestivalTTSEngine.test.js +0 -0
  118. package/tests/{plugins → jest/plugins}/tts/PageChunk.test.js +0 -0
  119. package/tests/{plugins → jest/plugins}/tts/PageChunkIterator.test.js +0 -0
  120. package/tests/{plugins → jest/plugins}/tts/WebTTSEngine.test.js +0 -0
  121. package/tests/{plugins → jest/plugins}/tts/utils.test.js +0 -0
  122. package/tests/{util → jest/util}/browserSniffing.test.js +0 -0
  123. package/tests/{util → jest/util}/docCookies.test.js +0 -0
  124. package/tests/{util → jest/util}/strings.test.js +0 -0
  125. package/tests/{utils.js → jest/utils.js} +0 -0
  126. package/tests/karma/BookNavigator/volumes/volumes-provider.test.js +6 -6
  127. package/tests/plugins/plugin.url.test.js +0 -147
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 5.0.0-24
2
+ Fix: book-nav side panel zoom out @mc2
3
+ Dev: refactor zoom code @mc2
4
+
5
+ # 5.0.0-23
6
+ Fix: Darken scrollbars in Safari @pezvi
7
+ Fix: Bookmarks service calls when reader is logged in @mc2
8
+ Dev: Move jest tests into separate directory @cdrini
9
+
1
10
  # 5.0.0-22
2
11
  - Dev: remove deprecated embed nav view, use standard default @iisa
3
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-22",
3
+ "version": "5.0.0-24-sortingstate-1",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -94,11 +94,7 @@
94
94
  ],
95
95
  "roots": [
96
96
  "<rootDir>/src/",
97
- "<rootDir>/tests/"
98
- ],
99
- "testPathIgnorePatterns": [
100
- "<rootDir>/tests/e2e/",
101
- "<rootDir>/tests/karma/"
97
+ "<rootDir>/tests/jest/"
102
98
  ],
103
99
  "coverageDirectory": "<rootDir>/coverage-jest"
104
100
  },
@@ -15,6 +15,7 @@ export default class BookmarksProvider {
15
15
  this.component = document.createElement('ia-bookmarks');
16
16
  this.component.bookreader = bookreader;
17
17
  this.component.options = boundOptions;
18
+ this.component.displayMode = this.component.options.displayMode;
18
19
 
19
20
  this.bindEvents();
20
21
 
@@ -121,6 +121,9 @@ class IABookmarks extends LitElement {
121
121
 
122
122
  setup() {
123
123
  this.api.identifier = this.bookreader.bookId;
124
+ if (this.displayMode === 'login') {
125
+ return;
126
+ }
124
127
  this.fetchBookmarks()
125
128
  .then(() => this.initializeBookmarks())
126
129
  .catch((err) => this.displayMode = 'login');
@@ -60,7 +60,7 @@ export default class {
60
60
  }
61
61
 
62
62
  onZoomOut() {
63
- this.bookreader.zoom();
63
+ this.bookreader.zoom(-1);
64
64
  }
65
65
 
66
66
  onAdjustmentChange(event) {
@@ -7,8 +7,16 @@ 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
+ };
10
15
  export default class VolumesProvider {
11
16
 
17
+ /**
18
+ * @param {import('../../BookReader').default} bookreader
19
+ */
12
20
  constructor(baseHost, bookreader, optionChange) {
13
21
  this.optionChange = optionChange;
14
22
  this.component = document.createElement("viewable-files");
@@ -17,6 +25,9 @@ export default class VolumesProvider {
17
25
  this.viewableFiles = Object.keys(files).map(item => files[item]);
18
26
  this.volumeCount = Object.keys(files).length;
19
27
 
28
+ /** @type {import('../../BookReader').default} */
29
+ this.bookreader = bookreader;
30
+
20
31
  this.component.subPrefix = bookreader.options.subPrefix || "";
21
32
  this.component.hostUrl = baseHost;
22
33
  this.component.viewableFiles = this.viewableFiles;
@@ -25,20 +36,28 @@ export default class VolumesProvider {
25
36
  this.label = `Viewable files (${this.volumeCount})`;
26
37
  this.icon = html`${volumesIcon}`;
27
38
 
28
- this.sortOrderBy = "orig_sort";
29
- this.sortVolumes("orig_sort");
39
+ // get sort state from query param
40
+ this.bookreader.urlPlugin.pullFromAddressBar(location.pathname + location.search);
41
+ const urlSortValue = this.bookreader.urlPlugin.getUrlParam('sort');
42
+ console.log('urlSortValue: ', urlSortValue);
43
+ if (urlSortValue === sortType.title_asc || urlSortValue === sortType.title_desc) {
44
+ this.sortOrderBy = urlSortValue;
45
+ } else {
46
+ this.sortOrderBy = sortType.default;
47
+ }
48
+ this.sortVolumes(this.sortOrderBy);
30
49
  }
31
50
 
32
51
  get sortButton() {
33
52
  const sortIcons = {
34
- orig_sort: html`
53
+ default: html`
35
54
  <button class="sort-by neutral-icon" aria-label="Sort volumes in initial order" @click=${() => this.sortVolumes("title_asc")}>${sortNeutralIcon}</button>
36
55
  `,
37
56
  title_asc: html`
38
57
  <button class="sort-by asc-icon" aria-label="Sort volumes in ascending order" @click=${() => this.sortVolumes("title_desc")}>${sortAscIcon}</button>
39
58
  `,
40
59
  title_desc: html`
41
- <button class="sort-by desc-icon" aria-label="Sort volumes in descending order" @click=${() => this.sortVolumes("orig_sort")}>${sortDescIcon}</button>
60
+ <button class="sort-by desc-icon" aria-label="Sort volumes in descending order" @click=${() => this.sortVolumes("default")}>${sortDescIcon}</button>
42
61
  `,
43
62
  };
44
63
 
@@ -46,28 +65,40 @@ export default class VolumesProvider {
46
65
  }
47
66
 
48
67
  /**
49
- * @param {'orig_sort' | 'title_asc' | 'title_desc'} sortByType
68
+ * @param {'default' | 'title_asc' | 'title_desc'} sortByType
50
69
  */
51
70
  sortVolumes(sortByType) {
52
71
  let sortedFiles = [];
53
72
 
54
73
  const files = this.viewableFiles;
55
74
  sortedFiles = files.sort((a, b) => {
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);
75
+ if (sortByType === sortType.title_asc) return a.title.localeCompare(b.title);
76
+ else if (sortByType === sortType.title_desc) return b.title.localeCompare(a.title);
77
+ else return a.orig_sort - b.orig_sort;
59
78
  });
60
79
 
61
80
  this.sortOrderBy = sortByType;
62
81
  this.component.viewableFiles = [...sortedFiles];
63
82
  this.actionButton = this.sortButton;
83
+
84
+ if (this.sortOrderBy !== sortType.default) {
85
+ this.bookreader.urlPlugin.setUrlParam('sort', sortByType);
86
+ } else {
87
+ this.bookreader.urlPlugin.removeUrlParam('sort');
88
+ }
89
+
90
+ const urlSchema = this.bookreader.urlPlugin.urlSchema;
91
+ const urlState = this.bookreader.urlPlugin.urlState;
92
+ this.bookreader.urlPlugin.urlStateToUrlString(urlSchema, urlState);
93
+ this.bookreader.urlPlugin.pushToAddressBar();
94
+
64
95
  this.optionChange(this.bookreader);
65
96
 
66
97
  this.multipleFilesClicked(sortByType);
67
98
  }
68
99
 
69
100
  /**
70
- * @param {'orig_sort' | 'title_asc' | 'title_desc'} orderBy
101
+ * @param {'default' | 'title_asc' | 'title_desc'} orderBy
71
102
  */
72
103
  multipleFilesClicked(orderBy) {
73
104
  if (!window.archive_analytics) {
@@ -79,8 +79,16 @@ export class Mode1Up {
79
79
  * @param {'in' | 'out'} direction
80
80
  */
81
81
  zoom(direction) {
82
- if (direction == 'in') this.mode1UpLit.zoomIn();
83
- else this.mode1UpLit.zoomOut();
82
+ switch (direction) {
83
+ case 'in':
84
+ this.mode1UpLit.zoomIn();
85
+ break;
86
+ case 'out':
87
+ this.mode1UpLit.zoomOut();
88
+ break;
89
+ default:
90
+ console.error(`Unsupported direction: ${direction}`);
91
+ }
84
92
  }
85
93
 
86
94
  /**
@@ -235,20 +235,27 @@ export class ModeThumb {
235
235
  }
236
236
 
237
237
  /**
238
- * @param {1 | -1} direction
238
+ * @param {'in' | 'out'} direction
239
239
  */
240
240
  zoom(direction) {
241
241
  const oldColumns = this.br.thumbColumns;
242
242
  switch (direction) {
243
- case -1:
244
- this.br.thumbColumns += 1;
245
- break;
246
- case 1:
243
+ case 'in':
247
244
  this.br.thumbColumns -= 1;
248
245
  break;
246
+ case 'out':
247
+ this.br.thumbColumns += 1;
248
+ break;
249
+ default:
250
+ console.error(`Unsupported direction: ${direction}`);
249
251
  }
250
252
 
251
- this.br.thumbColumns = clamp(this.br.thumbColumns, 2, 8);
253
+ // Limit zoom in/out columns
254
+ this.br.thumbColumns = clamp(
255
+ this.br.thumbColumns,
256
+ this.br.options.thumbMinZoomColumns,
257
+ this.br.options.thumbMaxZoomColumns
258
+ );
252
259
 
253
260
  if (this.br.thumbColumns != oldColumns) {
254
261
  this.br.displayedRows = []; /* force a gallery redraw */
@@ -25,6 +25,10 @@ export const DEFAULT_OPTIONS = {
25
25
  thumbMaxLoading: 4,
26
26
  /** spacing between thumbnails */
27
27
  thumbPadding: 10,
28
+ /** min zoom in columns */
29
+ thumbMinZoomColumns: 2,
30
+ /** max zoom out columns */
31
+ thumbMaxZoomColumns: 8,
28
32
 
29
33
  /** @type {number | 'fast' | 'slow'} speed for flip animation */
30
34
  flipSpeed: 'fast',
package/src/BookReader.js CHANGED
@@ -63,6 +63,10 @@ if (location.toString().indexOf('_debugShowConsole=true') != -1) {
63
63
  */
64
64
  export default function BookReader(overrides = {}) {
65
65
  const options = jQuery.extend(true, {}, BookReader.defaultOptions, overrides, BookReader.optionOverrides);
66
+
67
+ /** @type {import('./plugins/plugin.url').UrlPlugin | null} */
68
+ this.urlPlugin = null;
69
+
66
70
  this.setup(options);
67
71
  }
68
72
 
@@ -829,29 +833,11 @@ BookReader.prototype.drawLeafsThrottled = utils.throttle(
829
833
  * @param {number} direction Pass 1 to zoom in, anything else to zoom out
830
834
  */
831
835
  BookReader.prototype.zoom = function(direction) {
832
- switch (this.mode) {
833
- case this.constMode1up:
834
- if (direction == 1) {
835
- // XXX other cases
836
- this.zoom1up('in');
837
- } else {
838
- this.zoom1up('out');
839
- }
840
- break;
841
- case this.constMode2up:
842
- if (direction == 1) {
843
- // XXX other cases
844
- this.zoom2up('in');
845
- } else {
846
- this.zoom2up('out');
847
- }
848
- break;
849
- case this.constModeThumb:
850
- // XXX update zoomThumb for named directions
851
- this.zoomThumb(direction);
852
- break;
836
+ if (direction == 1) {
837
+ this.activeMode.zoom('in');
838
+ } else {
839
+ this.activeMode.zoom('out');
853
840
  }
854
-
855
841
  this.textSelectionPlugin?.stopPageFlip(this.refs.$brContainer);
856
842
  return;
857
843
  };
@@ -267,6 +267,7 @@ export default class ItemNavigator extends LitElement {
267
267
  #frame {
268
268
  position: relative;
269
269
  overflow: hidden;
270
+ color-scheme: dark;
270
271
  }
271
272
 
272
273
  #frame.fullscreen,
@@ -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(50);
53
+ document.title = this.shortTitle(this.bookTitle, 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,3 +196,210 @@ 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
+ this.urlSchema = [
204
+ { name: 'page', position: 'path', default: 'n0' },
205
+ { name: 'mode', position: 'path', default: '2up' },
206
+ { name: 'search', position: 'path', deprecated_for: 'q' },
207
+ { name: 'q', position: 'query_param' },
208
+ { name: 'sort', position: 'query_param' },
209
+ { name: 'view', position: 'query_param' },
210
+ { name: 'admin', position: 'query_param' },
211
+ ];
212
+
213
+ this.urlState = {};
214
+ this.urlMode = 'hash';
215
+ this.urlHistoryBasePath = '/';
216
+ this.urlLocationPollId = null;
217
+ this.oldLocationHash = null;
218
+ this.oldUserHash = null;
219
+ }
220
+
221
+ /**
222
+ * Parse JSON object URL state to string format
223
+ * Arrange path names in an order that it is positioned on the urlSchema
224
+ * @param {object} urlState
225
+ * @returns {string}
226
+ */
227
+ urlStateToUrlString(urlSchema, urlState) {
228
+ const searchParams = new URLSearchParams();
229
+ const pathParams = {};
230
+
231
+ Object.keys(urlState).forEach(key => {
232
+ let schema = urlSchema.find(schema => schema.name === key);
233
+ if (schema?.deprecated_for) {
234
+ schema = urlSchema.find(schemaKey => schemaKey.name === schema.deprecated_for);
235
+ }
236
+ if (schema?.position == 'path') {
237
+ pathParams[schema?.name] = urlState[key];
238
+ } else {
239
+ searchParams.append(schema?.name || key, urlState[key]);
240
+ }
241
+ });
242
+
243
+ const strPathParams = urlSchema
244
+ .filter(s => s.position == 'path')
245
+ .map(schema => pathParams[schema.name] ? `${schema.name}/${pathParams[schema.name]}` : '')
246
+ .join('/');
247
+
248
+ const strStrippedTrailingSlash = `${strPathParams.replace(/\/$/, '')}`;
249
+ const concatenatedPath = `/${strStrippedTrailingSlash}?${searchParams.toString()}`;
250
+ return searchParams.toString() ? concatenatedPath : `/${strStrippedTrailingSlash}`;
251
+ }
252
+
253
+ /**
254
+ * Parse string URL and add it in the current urlState
255
+ * Example:
256
+ * /page/n7/mode/2up => {page: 'n7', mode: '2up'}
257
+ * /page/n7/mode/2up/search/hello => {page: 'n7', mode: '2up', q: 'hello'}
258
+ * @param {array} urlSchema
259
+ * @param {string} str
260
+ * @returns {object}
261
+ */
262
+ urlStringToUrlState(urlSchema, str) {
263
+ const urlState = {};
264
+
265
+ // Fetch searchParams from given {str}
266
+ // Note: whole URL path is needed for URLSearchParams
267
+ const urlPath = new URL(str, '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
+ urlSchema
278
+ .filter(schema => schema.position == 'path')
279
+ .forEach(schema => {
280
+ if (!urlStrSplitSlashObj[schema.name] && schema.default) {
281
+ return urlState[schema.name] = schema.default;
282
+ }
283
+ const hasPropertyKey = doesKeyExists(urlStrSplitSlashObj, schema.name);
284
+ const hasDeprecatedKey = doesKeyExists(schema, 'deprecated_for') && hasPropertyKey;
285
+
286
+ if (hasDeprecatedKey)
287
+ return urlState[schema.deprecated_for] = urlStrSplitSlashObj[schema.name];
288
+
289
+ if (hasPropertyKey)
290
+ return urlState[schema.name] = urlStrSplitSlashObj[schema.name];
291
+ });
292
+
293
+ // Add searchParams to urlState
294
+ // Check if Object value is a Boolean and convert value to Boolean
295
+ // Otherwise, return Object value
296
+ const isBoolean = value => value === 'true' || (value === 'false' ? false : value);
297
+ Object.entries(urlSearchParamsObj).forEach(([key, value]) => {
298
+ urlState[key] = isBoolean(value);
299
+ });
300
+
301
+ return urlState;
302
+ }
303
+
304
+ /**
305
+ * Add or update key-value to the urlState
306
+ * @param {string} key
307
+ * @param {string} val
308
+ */
309
+ setUrlParam(key, value) {
310
+ this.urlState[key] = value;
311
+
312
+ this.pushToAddressBar();
313
+ }
314
+
315
+ /**
316
+ * Delete key-value to the urlState
317
+ * @param {string} key
318
+ */
319
+ removeUrlParam(key) {
320
+ delete this.urlState[key];
321
+
322
+ this.pushToAddressBar();
323
+ }
324
+
325
+ /**
326
+ * Get key-value from the urlState
327
+ * @param {string} key
328
+ * @return {string}
329
+ */
330
+ getUrlParam(key) {
331
+ return this.urlState[key];
332
+ }
333
+
334
+ /**
335
+ * Push URL params to addressbar
336
+ */
337
+ pushToAddressBar() {
338
+ const urlStrPath = this.urlStateToUrlString(this.urlSchema, this.urlState);
339
+ if (this.urlMode == 'history') {
340
+ if (window.history && window.history.replaceState) {
341
+ const newUrlPath = `${this.urlHistoryBasePath}${urlStrPath}`;
342
+ window.history.replaceState({}, null, newUrlPath);
343
+ }
344
+ } else {
345
+ window.location.replace('#' + urlStrPath);
346
+ }
347
+ this.oldLocationHash = urlStrPath;
348
+ }
349
+
350
+ /**
351
+ * Get the url and check if it has changed
352
+ * If it was changeed, update the urlState
353
+ */
354
+ listenForHashChanges() {
355
+ this.oldLocationHash = window.location.hash.substr(1);
356
+ if (this.urlLocationPollId) {
357
+ clearInterval(this.urlLocationPollId);
358
+ this.urlLocationPollId = null;
359
+ }
360
+
361
+ // check if the URL changes
362
+ const updateHash = () => {
363
+ const newFragment = window.location.hash.substr(1);
364
+ const hasFragmentChange = newFragment != this.oldLocationHash;
365
+
366
+ if (!hasFragmentChange) { return; }
367
+
368
+ this.urlState = this.urlStringToUrlState(newFragment);
369
+ };
370
+ this.urlLocationPollId = setInterval(updateHash, 500);
371
+ }
372
+
373
+ /**
374
+ * Will read either the hash or URL and return the bookreader fragment
375
+ * @param {string} location
376
+ * @return {string}
377
+ */
378
+ pullFromAddressBar (location) {
379
+ const path = this.urlMode === 'history'
380
+ ? location.substr(this.urlHistoryBasePath.length)
381
+ : location.substr(1);
382
+ this.urlState = this.urlStringToUrlState(this.urlSchema, path);
383
+ }
384
+ }
385
+ export class BookreaderUrlPlugin extends BookReader {
386
+
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
+
404
+ window.BookReader = BookreaderUrlPlugin;
405
+ export default BookreaderUrlPlugin;
@@ -0,0 +1,71 @@
1
+
2
+ import sinon from 'sinon';
3
+ import BookReader from '@/src/BookReader.js';
4
+ /** @typedef {import('@/src/BookReader/options.js').BookReaderOptions} BookReaderOptions */
5
+
6
+ beforeAll(() => {
7
+ global.alert = jest.fn();
8
+ });
9
+ afterEach(() => {
10
+ jest.restoreAllMocks();
11
+ sinon.restore();
12
+ });
13
+
14
+ /** @type {BookReaderOptions['data']} */
15
+ const SAMPLE_DATA = [
16
+ [
17
+ { width: 123, height: 123, uri: 'https://archive.org/image0.jpg', pageNum: '1' },
18
+ ],
19
+ [
20
+ { width: 123, height: 123, uri: 'https://archive.org/image1.jpg', pageNum: '2' },
21
+ { width: 123, height: 123, uri: 'https://archive.org/image2.jpg', pageNum: '3' },
22
+ ],
23
+ [
24
+ { width: 123, height: 123, uri: 'https://archive.org/image3.jpg', pageNum: '4' },
25
+ { width: 123, height: 123, uri: 'https://archive.org/image4.jpg', pageNum: '5' },
26
+ ],
27
+ [
28
+ { width: 123, height: 123, uri: 'https://archive.org/image5.jpg', pageNum: '6' },
29
+ ],
30
+ ];
31
+
32
+ describe('zoom', () => {
33
+ const br = new BookReader({ data: SAMPLE_DATA });
34
+ br.init();
35
+
36
+ test('initializes with default columns', () => {
37
+ expect(br.thumbColumns).toBe(br.options.thumbColumns);
38
+ });
39
+
40
+ test('removes column and redraws zooming in', () => {
41
+ const prepare = sinon.spy(br._modes.modeThumb, 'prepare');
42
+ const startColumns = br.thumbColumns;
43
+ br._modes.modeThumb.zoom('in');
44
+ expect(br.thumbColumns).toBe(startColumns - 1);
45
+ expect(prepare.callCount).toBe(1);
46
+ });
47
+
48
+ test('adds column and redraws zooming out', () => {
49
+ const prepare = sinon.spy(br._modes.modeThumb, 'prepare');
50
+ const startColumns = br.thumbColumns;
51
+ br._modes.modeThumb.zoom('out');
52
+ expect(br.thumbColumns).toBe(startColumns + 1);
53
+ expect(prepare.callCount).toBe(1);
54
+ });
55
+
56
+ test('keeps columns and no redraw at zooming in limit', () => {
57
+ const prepare = sinon.spy(br._modes.modeThumb, 'prepare');
58
+ br.thumbColumns = br.options.thumbMinZoomColumns;
59
+ br._modes.modeThumb.zoom('in');
60
+ expect(br.thumbColumns).toBe(br.options.thumbMinZoomColumns);
61
+ expect(prepare.callCount).toBe(0);
62
+ });
63
+
64
+ test('keeps columns and no redraw at zooming out limit', () => {
65
+ const prepare = sinon.spy(br._modes.modeThumb, 'prepare');
66
+ br.thumbColumns = br.options.thumbMaxZoomColumns;
67
+ br._modes.modeThumb.zoom('out');
68
+ expect(br.thumbColumns).toBe(br.options.thumbMaxZoomColumns);
69
+ expect(prepare.callCount).toBe(0);
70
+ });
71
+ });