@internetarchive/bookreader 5.0.0-25 → 5.0.0-28

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,185 @@
1
+ export class UrlPlugin {
2
+ constructor(options = {}) {
3
+ this.bookReaderOptions = options;
4
+
5
+ // the canonical order of elements is important in the path and query string
6
+ this.urlSchema = [
7
+ { name: 'page', position: 'path', default: 'n0' },
8
+ { name: 'mode', position: 'path', default: '2up' },
9
+ { name: 'search', position: 'path', deprecated_for: 'q' },
10
+ { name: 'q', position: 'query_param' },
11
+ { name: 'sort', position: 'query_param' },
12
+ { name: 'view', position: 'query_param' },
13
+ { name: 'admin', position: 'query_param' },
14
+ ];
15
+
16
+ this.urlState = {};
17
+ this.urlMode = this.bookReaderOptions.urlMode || 'hash';
18
+ this.urlHistoryBasePath = this.bookReaderOptions.urlHistoryBasePath || '/';
19
+ this.urlLocationPollId = null;
20
+ this.oldLocationHash = null;
21
+ this.oldUserHash = null;
22
+ }
23
+
24
+ /**
25
+ * Parse JSON object URL state to string format
26
+ * Arrange path names in an order that it is positioned on the urlSchema
27
+ * @param {Object} urlState
28
+ * @returns {string}
29
+ */
30
+ urlStateToUrlString(urlState) {
31
+ const searchParams = new URLSearchParams();
32
+ const pathParams = {};
33
+
34
+ Object.keys(urlState).forEach(key => {
35
+ let schema = this.urlSchema.find(schema => schema.name === key);
36
+ if (schema?.deprecated_for) {
37
+ schema = this.urlSchema.find(schemaKey => schemaKey.name === schema.deprecated_for);
38
+ }
39
+ if (schema?.position == 'path') {
40
+ pathParams[schema?.name] = urlState[key];
41
+ } else {
42
+ searchParams.append(schema?.name || key, urlState[key]);
43
+ }
44
+ });
45
+
46
+ const strPathParams = this.urlSchema
47
+ .filter(s => s.position == 'path')
48
+ .map(schema => pathParams[schema.name] ? `${schema.name}/${pathParams[schema.name]}` : '')
49
+ .join('/');
50
+
51
+ const strStrippedTrailingSlash = `${strPathParams.replace(/\/$/, '')}`;
52
+ const concatenatedPath = `${strStrippedTrailingSlash}?${searchParams.toString()}`;
53
+ return searchParams.toString() ? concatenatedPath : `${strStrippedTrailingSlash}`;
54
+ }
55
+
56
+ /**
57
+ * Parse string URL and add it in the current urlState
58
+ * Example:
59
+ * /page/n7/mode/2up => {page: 'n7', mode: '2up'}
60
+ * /page/n7/mode/2up/search/hello => {page: 'n7', mode: '2up', q: 'hello'}
61
+ * @param {string} urlString
62
+ * @returns {object}
63
+ */
64
+ urlStringToUrlState(urlString) {
65
+ const urlState = {};
66
+
67
+ // Fetch searchParams from given {str}
68
+ // Note: whole URL path is needed for URL parsing
69
+ const urlPath = new URL(urlString, 'http://example.com');
70
+ const urlSearchParamsObj = Object.fromEntries(urlPath.searchParams.entries());
71
+ const splitUrlMatches = urlPath.pathname.match(/[^\\/]+\/[^\\/]+/g);
72
+ const urlStrSplitSlashObj = splitUrlMatches ? Object.fromEntries(splitUrlMatches.map(x => x.split('/'))) : {};
73
+
74
+ const doesKeyExists = (_object, _key) => {
75
+ return Object.keys(_object).some(value => value == _key);
76
+ };
77
+
78
+ // Add path objects to urlState
79
+ this.urlSchema
80
+ .filter(schema => schema.position == 'path')
81
+ .forEach(schema => {
82
+ if (!urlStrSplitSlashObj[schema.name] && schema.default) {
83
+ return urlState[schema.name] = schema.default;
84
+ }
85
+ const hasPropertyKey = doesKeyExists(urlStrSplitSlashObj, schema.name);
86
+ const hasDeprecatedKey = doesKeyExists(schema, 'deprecated_for') && hasPropertyKey;
87
+
88
+ if (hasDeprecatedKey) {
89
+ urlState[schema.deprecated_for] = urlStrSplitSlashObj[schema.name];
90
+ return;
91
+ }
92
+
93
+ if (hasPropertyKey) {
94
+ urlState[schema.name] = urlStrSplitSlashObj[schema.name];
95
+ return;
96
+ }
97
+ });
98
+
99
+ // Add searchParams to urlState
100
+ Object.entries(urlSearchParamsObj).forEach(([key, value]) => {
101
+ urlState[key] = value;
102
+ });
103
+
104
+ return urlState;
105
+ }
106
+
107
+ /**
108
+ * Add or update key-value to the urlState
109
+ * @param {string} key
110
+ * @param {string} val
111
+ */
112
+ setUrlParam(key, value) {
113
+ this.urlState[key] = value;
114
+
115
+ this.pushToAddressBar();
116
+ }
117
+
118
+ /**
119
+ * Delete key-value to the urlState
120
+ * @param {string} key
121
+ */
122
+ removeUrlParam(key) {
123
+ delete this.urlState[key];
124
+
125
+ this.pushToAddressBar();
126
+ }
127
+
128
+ /**
129
+ * Get key-value from the urlState
130
+ * @param {string} key
131
+ * @return {string}
132
+ */
133
+ getUrlParam(key) {
134
+ return this.urlState[key];
135
+ }
136
+
137
+ /**
138
+ * Push URL params to addressbar
139
+ */
140
+ pushToAddressBar() {
141
+ const urlStrPath = this.urlStateToUrlString(this.urlState);
142
+ if (this.urlMode == 'history') {
143
+ if (window.history && window.history.replaceState) {
144
+ const newUrlPath = `${this.urlHistoryBasePath}${urlStrPath}`;
145
+ window.history.replaceState({}, null, newUrlPath);
146
+ }
147
+ } else {
148
+ window.location.replace('#' + urlStrPath);
149
+ }
150
+ this.oldLocationHash = urlStrPath;
151
+ }
152
+
153
+ /**
154
+ * Get the url and check if it has changed
155
+ * If it was changeed, update the urlState
156
+ */
157
+ listenForHashChanges() {
158
+ this.oldLocationHash = window.location.hash.substr(1);
159
+ if (this.urlLocationPollId) {
160
+ clearInterval(this.urlLocationPollId);
161
+ this.urlLocationPollId = null;
162
+ }
163
+
164
+ // check if the URL changes
165
+ const updateHash = () => {
166
+ const newFragment = window.location.hash.substr(1);
167
+ const hasFragmentChange = newFragment != this.oldLocationHash;
168
+
169
+ if (!hasFragmentChange) { return; }
170
+
171
+ this.urlState = this.urlStringToUrlState(newFragment);
172
+ };
173
+ this.urlLocationPollId = setInterval(updateHash, 500);
174
+ }
175
+
176
+ /**
177
+ * Will read either the hash or URL and return the bookreader fragment
178
+ */
179
+ pullFromAddressBar (location = window.location) {
180
+ const path = this.urlMode === 'history'
181
+ ? (location.pathname.substr(this.urlHistoryBasePath.length) + location.search)
182
+ : location.hash.substr(1);
183
+ this.urlState = this.urlStringToUrlState(path);
184
+ }
185
+ }
@@ -1,4 +1,7 @@
1
1
  /* global BookReader */
2
+
3
+ import { UrlPlugin } from "./UrlPlugin";
4
+
2
5
  /**
3
6
  * Plugin for URL management in BookReader
4
7
  * Note read more about the url "fragment" here:
@@ -50,7 +53,7 @@ BookReader.prototype.init = (function(super_) {
50
53
  this.bind(BookReader.eventNames.PostInit, () => {
51
54
  const { updateWindowTitle, urlMode } = this.options;
52
55
  if (updateWindowTitle) {
53
- document.title = this.shortTitle(50);
56
+ document.title = this.shortTitle(this.bookTitle, 50);
54
57
  }
55
58
  if (urlMode === 'hash') {
56
59
  this.urlStartLocationPolling();
@@ -86,7 +89,7 @@ BookReader.prototype.urlStartLocationPolling = function() {
86
89
  this.oldLocationHash = this.urlReadFragment();
87
90
 
88
91
  if (this.locationPollId) {
89
- clearInterval(this.locationPollID);
92
+ clearInterval(this.locationPollId);
90
93
  this.locationPollId = null;
91
94
  }
92
95
 
@@ -196,3 +199,22 @@ BookReader.prototype.urlReadFragment = function() {
196
199
  BookReader.prototype.urlReadHashFragment = function() {
197
200
  return window.location.hash.substr(1);
198
201
  };
202
+ export class BookreaderUrlPlugin extends BookReader {
203
+ init() {
204
+ if (this.options.enableUrlPlugin) {
205
+ this.urlPlugin = new UrlPlugin(this.options);
206
+ this.bind(BookReader.eventNames.PostInit, () => {
207
+ const { urlMode } = this.options;
208
+
209
+ if (urlMode === 'hash') {
210
+ this.urlPlugin.listenForHashChanges();
211
+ }
212
+ });
213
+ }
214
+
215
+ super.init();
216
+ }
217
+ }
218
+
219
+ window.BookReader = BookreaderUrlPlugin;
220
+ export default BookreaderUrlPlugin;
@@ -0,0 +1,190 @@
1
+ import BookReader from '@/src/BookReader.js';
2
+ import * as utils from '@/src/BookReader/utils.js';
3
+
4
+ let br;
5
+ beforeAll(() => {
6
+ document.body.innerHTML = '<div id="BookReader">';
7
+ br = new BookReader();
8
+ });
9
+
10
+ afterEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ /**
15
+ * Only run init() once. Otherwise multiple EventListeners will be added.
16
+ */
17
+ test('Initialzation enables IntersectionObserver and defaults', () => {
18
+ const observe = jest.fn();
19
+ window.IntersectionObserver = jest.fn(() => ({
20
+ observe
21
+ }));
22
+ br.init();
23
+ expect(br.hasKeyFocus).toBe(true);
24
+ expect(observe).toHaveBeenCalledTimes(1);
25
+ });
26
+
27
+ describe('Keyboard shortcuts turned off', () => {
28
+
29
+ test('Focus flag disables', () => {
30
+ br.next = jest.fn();
31
+ br.hasKeyFocus = false;
32
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'ArrowDown'});
33
+ keyEvent.preventDefault = jest.fn();
34
+ document.dispatchEvent(keyEvent);
35
+ expect(br.next).toHaveBeenCalledTimes(0);
36
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(0);
37
+ // Must reset for following tests
38
+ br.hasKeyFocus = true;
39
+ });
40
+
41
+ test('Input active disables', () => {
42
+ // eslint-disable-next-line no-import-assign
43
+ utils.isInputActive = jest.fn(() => true);
44
+ br.next = jest.fn();
45
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'ArrowDown'});
46
+ keyEvent.preventDefault = jest.fn();
47
+ document.dispatchEvent(keyEvent);
48
+ expect(br.next).toHaveBeenCalledTimes(0);
49
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(0);
50
+ // Must reset for following tests
51
+ utils.isInputActive.mockReset();
52
+ });
53
+
54
+ });
55
+
56
+ describe('Keyboard shortcuts', () => {
57
+
58
+ test('Home key', () => {
59
+ br.first = jest.fn();
60
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'Home'});
61
+ keyEvent.preventDefault = jest.fn();
62
+ document.dispatchEvent(keyEvent);
63
+ expect(br.first).toHaveBeenCalledTimes(1);
64
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
65
+ });
66
+
67
+ test('End key', () => {
68
+ br.last = jest.fn();
69
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'End'});
70
+ keyEvent.preventDefault = jest.fn();
71
+ document.dispatchEvent(keyEvent);
72
+ expect(br.last).toHaveBeenCalledTimes(1);
73
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ test('ArrowDown key', () => {
77
+ br.mode = br.constMode2up;
78
+ br.next = jest.fn();
79
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'ArrowDown'});
80
+ keyEvent.preventDefault = jest.fn();
81
+ document.dispatchEvent(keyEvent);
82
+ expect(br.next).toHaveBeenCalledTimes(1);
83
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
84
+ });
85
+
86
+ test('PageDown key', () => {
87
+ br.mode = br.constMode2up;
88
+ br.next = jest.fn();
89
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'PageDown'});
90
+ keyEvent.preventDefault = jest.fn();
91
+ document.dispatchEvent(keyEvent);
92
+ expect(br.next).toHaveBeenCalledTimes(1);
93
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
94
+ });
95
+
96
+ test('ArrowUp key', () => {
97
+ br.mode = br.constMode2up;
98
+ br.prev = jest.fn();
99
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'ArrowUp'});
100
+ keyEvent.preventDefault = jest.fn();
101
+ document.dispatchEvent(keyEvent);
102
+ expect(br.prev).toHaveBeenCalledTimes(1);
103
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
104
+ });
105
+
106
+ test('PageUp key', () => {
107
+ br.mode = br.constMode2up;
108
+ br.prev = jest.fn();
109
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'PageUp'});
110
+ keyEvent.preventDefault = jest.fn();
111
+ document.dispatchEvent(keyEvent);
112
+ expect(br.prev).toHaveBeenCalledTimes(1);
113
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
114
+ });
115
+
116
+ test('ArrowLeft key', () => {
117
+ br.mode = br.constMode2up;
118
+ br.left = jest.fn();
119
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'ArrowLeft'});
120
+ keyEvent.preventDefault = jest.fn();
121
+ document.dispatchEvent(keyEvent);
122
+ expect(br.left).toHaveBeenCalledTimes(1);
123
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
124
+ });
125
+
126
+ test('ArrowRight key', () => {
127
+ br.mode = br.constMode2up;
128
+ br.right = jest.fn();
129
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'ArrowRight'});
130
+ keyEvent.preventDefault = jest.fn();
131
+ document.dispatchEvent(keyEvent);
132
+ expect(br.right).toHaveBeenCalledTimes(1);
133
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
134
+ });
135
+
136
+ test('Subtract key', () => {
137
+ br.zoom = jest.fn();
138
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'Subtract'});
139
+ keyEvent.preventDefault = jest.fn();
140
+ document.dispatchEvent(keyEvent);
141
+ expect(br.zoom).toHaveBeenCalledTimes(1);
142
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
143
+ });
144
+
145
+ test('- key', () => {
146
+ br.zoom = jest.fn();
147
+ const keyEvent = new KeyboardEvent('keydown', {'key': '-'});
148
+ keyEvent.preventDefault = jest.fn();
149
+ document.dispatchEvent(keyEvent);
150
+ expect(br.zoom).toHaveBeenCalledTimes(1);
151
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
152
+ });
153
+
154
+ test('Add key', () => {
155
+ br.zoom = jest.fn();
156
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'Add'});
157
+ keyEvent.preventDefault = jest.fn();
158
+ document.dispatchEvent(keyEvent);
159
+ expect(br.zoom).toHaveBeenCalledTimes(1);
160
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
161
+ });
162
+
163
+ test('+ key', () => {
164
+ br.zoom = jest.fn();
165
+ const keyEvent = new KeyboardEvent('keydown', {'key': '+'});
166
+ keyEvent.preventDefault = jest.fn();
167
+ document.dispatchEvent(keyEvent);
168
+ expect(br.zoom).toHaveBeenCalledTimes(1);
169
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
170
+ });
171
+
172
+ test('= key', () => {
173
+ br.zoom = jest.fn();
174
+ const keyEvent = new KeyboardEvent('keydown', {'key': '='});
175
+ keyEvent.preventDefault = jest.fn();
176
+ document.dispatchEvent(keyEvent);
177
+ expect(br.zoom).toHaveBeenCalledTimes(1);
178
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
179
+ });
180
+
181
+ test('F key', () => {
182
+ br.toggleFullscreen = jest.fn();
183
+ const keyEvent = new KeyboardEvent('keydown', {'key': 'F'});
184
+ keyEvent.preventDefault = jest.fn();
185
+ document.dispatchEvent(keyEvent);
186
+ expect(br.toggleFullscreen).toHaveBeenCalledTimes(1);
187
+ expect(keyEvent.preventDefault).toHaveBeenCalledTimes(1);
188
+ });
189
+
190
+ });
@@ -1,7 +1,7 @@
1
1
 
2
2
  import BookReader from '@/src/BookReader.js';
3
3
  import '@/src/plugins/plugin.resume.js';
4
- import '@/src/plugins/plugin.url.js';
4
+ import '@/src/plugins/url/plugin.url.js';
5
5
 
6
6
  let br;
7
7
  beforeAll(() => {
@@ -0,0 +1,152 @@
1
+ import { UrlPlugin } from '@/src/plugins/url/UrlPlugin';
2
+
3
+ afterEach(() => {
4
+ jest.clearAllMocks();
5
+ });
6
+
7
+ describe('UrlPlugin tests', () => {
8
+ const urlPlugin = new UrlPlugin();
9
+
10
+ describe('urlStateToUrlString tests', () => {
11
+ test('urlStateToUrlString with known states in schema', () => {
12
+ const urlState = { page: 'n7', mode: '1up', search: 'foo' };
13
+ const urlStateWithQueries = { page: 'n7', mode: '1up', q: 'hello', view: 'theater', sort: 'title_asc' };
14
+
15
+ const expectedUrlFromState = 'page/n7/mode/1up?q=foo';
16
+ const expectedUrlFromStateWithQueries = 'page/n7/mode/1up?q=hello&view=theater&sort=title_asc';
17
+
18
+ expect(urlPlugin.urlStateToUrlString(urlState)).toBe(expectedUrlFromState);
19
+ expect(urlPlugin.urlStateToUrlString(urlStateWithQueries)).toBe(expectedUrlFromStateWithQueries);
20
+ });
21
+
22
+ test('urlStateToUrlString with unknown states in schema', () => {
23
+ const urlState = { page: 'n7', mode: '1up' };
24
+ const urlStateWithQueries = { page: 'n7', mode: '1up', q: 'hello', viewer: 'theater', sortBy: 'title_asc' };
25
+
26
+ const expectedUrlFromState = 'page/n7/mode/1up';
27
+ const expectedUrlFromStateWithQueries = 'page/n7/mode/1up?q=hello&viewer=theater&sortBy=title_asc';
28
+
29
+ expect(urlPlugin.urlStateToUrlString(urlState)).toBe(expectedUrlFromState);
30
+ expect(urlPlugin.urlStateToUrlString(urlStateWithQueries)).toBe(expectedUrlFromStateWithQueries);
31
+ });
32
+
33
+ test('urlStateToUrlString with boolean value', () => {
34
+ const urlState = { page: 'n7', mode: '1up', search: 'foo', view: 'theater', wrapper: 'false' };
35
+ const expectedUrlFromState = 'page/n7/mode/1up?q=foo&view=theater&wrapper=false';
36
+
37
+ expect(urlPlugin.urlStateToUrlString(urlState)).toBe(expectedUrlFromState);
38
+ });
39
+ });
40
+
41
+ describe('urlStringToUrlState tests', () => {
42
+ test('urlStringToUrlState without query string', () => {
43
+ const url = '/page/n7/mode/2up';
44
+ const url1 = '/page/n7/mode/1up';
45
+
46
+ expect(urlPlugin.urlStringToUrlState(url)).toEqual({page: 'n7', mode: '2up'});
47
+ expect(urlPlugin.urlStringToUrlState(url1)).toEqual({page: 'n7', mode: '1up'});
48
+ });
49
+
50
+ test('urlStringToUrlState with deprecated_for', () => {
51
+ const url = '/page/n7/mode/2up/search/hello';
52
+
53
+ expect(urlPlugin.urlStringToUrlState(url)).toEqual({page: 'n7', mode: '2up', q: 'hello'});
54
+ });
55
+
56
+ test('urlStringToUrlState with query string', () => {
57
+ const url = '/page/n7/mode/2up/search/hello?view=theather&foo=bar&sort=title_asc';
58
+ const url1 = '/mode/2up?ref=ol&ui=embed&wrapper=false&view=theater';
59
+
60
+ expect(urlPlugin.urlStringToUrlState(url)).toEqual(
61
+ {page: 'n7', mode: '2up', q: 'hello', view: 'theather', foo: 'bar', sort: 'title_asc'}
62
+ );
63
+ expect(urlPlugin.urlStringToUrlState(url1)).toEqual(
64
+ {page: 'n0', mode: '2up', ref: 'ol', ui: 'embed', wrapper: 'false', view: 'theater'}
65
+ );
66
+ });
67
+
68
+ test('urlStringToUrlState compare search and ?q', () => {
69
+ const url = '/page/n7/mode/2up/search/hello';
70
+ urlPlugin.urlState = { q: 'hello' };
71
+
72
+ expect(urlPlugin.urlStringToUrlState(url)).toEqual({page: 'n7', mode: '2up', q: 'hello'});
73
+ });
74
+ });
75
+
76
+ describe('url plugin helper functions', () => {
77
+ test('setUrlParam', () => {
78
+ urlPlugin.urlState = {};
79
+ urlPlugin.setUrlParam('page', '20');
80
+ urlPlugin.setUrlParam('mode', '2up');
81
+
82
+ expect(urlPlugin.urlState).toEqual({page: '20', mode: '2up'});
83
+ });
84
+
85
+ test('removeUrlParam', () => {
86
+ urlPlugin.setUrlParam('page', '20');
87
+ urlPlugin.setUrlParam('mode', '2up');
88
+ urlPlugin.removeUrlParam('mode');
89
+
90
+ expect(urlPlugin.urlState).toEqual({page: '20'});
91
+ });
92
+
93
+ test('getUrlParam', () => {
94
+ urlPlugin.setUrlParam('page', '20');
95
+ urlPlugin.setUrlParam('mode', '2up');
96
+ expect(urlPlugin.getUrlParam('page')).toEqual('20');
97
+ expect(urlPlugin.getUrlParam('mode')).toEqual('2up');
98
+ });
99
+ });
100
+
101
+ describe('pullFromAddressBar and pushToAddressBar - hash mode', () => {
102
+ test('url without mode state value - use default', () => {
103
+ urlPlugin.urlState = {};
104
+ urlPlugin.urlMode = 'hash';
105
+
106
+ urlPlugin.pullFromAddressBar({ pathname: '', search: '', hash: '#page/12' });
107
+ expect(urlPlugin.urlState).toEqual({page: '12', mode: '2up'});
108
+
109
+ urlPlugin.pushToAddressBar();
110
+ expect(window.location.hash).toEqual('#page/12/mode/2up');
111
+ });
112
+
113
+ test('url with query param', () => {
114
+ urlPlugin.urlState = {};
115
+ urlPlugin.urlMode = 'hash';
116
+
117
+ urlPlugin.pullFromAddressBar({ pathname: '', search: '', hash: '#page/12?q=hello&view=theater' });
118
+ expect(urlPlugin.urlState).toEqual({page: '12', mode: '2up', q: 'hello', view: 'theater'});
119
+
120
+ urlPlugin.pushToAddressBar();
121
+ expect(window.location.hash).toEqual('#page/12/mode/2up?q=hello&view=theater');
122
+ });
123
+ });
124
+
125
+ describe('pullFromAddressBar and pushToAddressBar - history mode', () => {
126
+ test('url without mode state value - use default', () => {
127
+ urlPlugin.urlState = {};
128
+ urlPlugin.urlHistoryBasePath = '/details/foo/';
129
+ urlPlugin.urlMode = 'history';
130
+
131
+ urlPlugin.pullFromAddressBar({ pathname: '/details/foo/page/12', search: '', hash: '' });
132
+ expect(urlPlugin.urlState).toEqual({page: '12', mode: '2up'});
133
+
134
+ urlPlugin.pushToAddressBar();
135
+ expect(window.location.pathname).toEqual('/details/foo/page/12/mode/2up');
136
+ });
137
+
138
+ test('url with query param', () => {
139
+ urlPlugin.urlState = {};
140
+ urlPlugin.urlHistoryBasePath = '/details/foo/';
141
+ urlPlugin.urlMode = 'history';
142
+
143
+ urlPlugin.pullFromAddressBar({ pathname: '/details/foo/page/12', search: '?q=hello&view=theater', hash: '' });
144
+ expect(urlPlugin.urlState).toEqual({page: '12', mode: '2up', q: 'hello', view: 'theater'});
145
+
146
+ urlPlugin.pushToAddressBar();
147
+ const locationUrl = `${window.location.pathname}${window.location.search}`;
148
+ expect(locationUrl).toEqual('/details/foo/page/12/mode/2up?q=hello&view=theater');
149
+ });
150
+ });
151
+
152
+ });
@@ -1,6 +1,6 @@
1
-
2
1
  import BookReader from '@/src/BookReader.js';
3
- import '@/src/plugins/plugin.url.js';
2
+ import '@/src/plugins/url/plugin.url.js';
3
+ import sinon from 'sinon';
4
4
 
5
5
  let br;
6
6
  beforeAll(() => {
@@ -10,6 +10,7 @@ beforeAll(() => {
10
10
 
11
11
  afterEach(() => {
12
12
  jest.clearAllMocks();
13
+ sinon.restore();
13
14
  });
14
15
 
15
16
  describe('Plugin: URL controller', () => {
@@ -64,7 +64,7 @@ describe('Volumes Provider', () => {
64
64
  const baseHost = "https://archive.org";
65
65
  const provider = new volumesProvider(baseHost, brOptions, onSortClick);
66
66
 
67
- expect(provider.sortOrderBy).to.equal("orig_sort");
67
+ expect(provider.sortOrderBy).to.equal("default");
68
68
 
69
69
  provider.sortVolumes("title_asc");
70
70
  expect(provider.sortOrderBy).to.equal("title_asc");
@@ -74,8 +74,8 @@ describe('Volumes Provider', () => {
74
74
  expect(provider.sortOrderBy).to.equal("title_desc");
75
75
  expect(provider.sortButton.getHTML()).includes("sort-by desc-icon");
76
76
 
77
- provider.sortVolumes("orig_sort");
78
- expect(provider.sortOrderBy).to.equal("orig_sort");
77
+ provider.sortVolumes("default");
78
+ expect(provider.sortOrderBy).to.equal("default");
79
79
  expect(provider.sortButton.getHTML()).includes("sort-by neutral-icon");
80
80
  });
81
81
 
@@ -88,9 +88,9 @@ describe('Volumes Provider', () => {
88
88
  const files = Object.keys(parsedFiles).map(item => parsedFiles[item]).sort((a, b) => a.orig_sort - b.orig_sort);
89
89
  const origSortTitles = files.map(item => item.title);
90
90
 
91
- provider.sortVolumes("orig_sort");
91
+ provider.sortVolumes("default");
92
92
 
93
- expect(provider.sortOrderBy).to.equal("orig_sort");
93
+ expect(provider.sortOrderBy).to.equal("default");
94
94
  expect(provider.actionButton).to.exist;
95
95
 
96
96
  const providerFileTitles = provider.viewableFiles.map(item => item.title);
@@ -144,7 +144,7 @@ describe('Volumes Provider', () => {
144
144
  const baseHost = "https://archive.org";
145
145
  const provider = new volumesProvider(baseHost, brOptions, onSortClick);
146
146
 
147
- provider.sortOrderBy = 'orig_sort';
147
+ provider.sortOrderBy = 'default';
148
148
  const origSortButton = await fixture(provider.sortButton);
149
149
  expect(origSortButton.classList.contains('neutral-icon')).to.be.true;
150
150
 
package/webpack.config.js CHANGED
@@ -51,7 +51,7 @@ module.exports = [
51
51
  'plugins/plugin.search.js': { import: './src/plugins/search/plugin.search.js', dependOn: 'BookReader.js' },
52
52
  'plugins/plugin.text_selection.js': { import: './src/plugins/plugin.text_selection.js', dependOn: 'BookReader.js' },
53
53
  'plugins/plugin.tts.js': { import: './src/plugins/tts/plugin.tts.js', dependOn: 'BookReader.js' },
54
- 'plugins/plugin.url.js': { import: './src/plugins/plugin.url.js', dependOn: 'BookReader.js' },
54
+ 'plugins/plugin.url.js': { import: './src/plugins/url/plugin.url.js', dependOn: 'BookReader.js' },
55
55
  'plugins/plugin.vendor-fullscreen.js': { import: './src/plugins/plugin.vendor-fullscreen.js', dependOn: 'BookReader.js' },
56
56
  'bookreader-component-bundle.js': { import: './src/BookReaderComponent/BookReaderComponent.js', dependOn: 'BookReader.js' }
57
57
  },