@internetarchive/bookreader 5.0.0-44-a7 → 5.0.0-46-no-right-click

Sign up to get free protection for your applications and to get access to all the features.
@@ -126,6 +126,11 @@
126
126
  const placeholder = document.querySelector('#placeholder');
127
127
  placeholder.innerHTML = 'Dependencies are complete, bookreader has loaded';
128
128
  });
129
+
130
+ // analytics stub
131
+ window.archive_analytics = {
132
+ send_event_no_sampling: (category, action, label) => console.log('~~~ NO SAMPLE EVENT CALLED: ', { category, action, label }),
133
+ }
129
134
  </script>
130
135
 
131
136
  <!-- IA fetch demo -->
package/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 5.0.0-45
2
+ # 5.0.0-44
3
+ Fix: dynamic `q=<term>` url parameter @iisa
4
+ Dev: dependency updates @renovate
5
+
1
6
  # 5.0.0-43
2
7
  Fix: search results panel display asserted page numbers @cdrini
3
8
  Dev: dependency updates @renovate
@@ -46,12 +51,12 @@ Dev: Re-enable testcafe tests in GH action @iisa
46
51
  Fix: Search results bar clears and closes properly @iisa
47
52
 
48
53
  # 5.0.0-35
49
- Fix: global syle leak specify colorbox styles @iisa
54
+ Fix: global style leak specify colorbox styles @iisa
50
55
  Fix: br menu reinits with shared ro load @iisa
51
56
  Fix: url plugin does not rewrite with multiple slashes @iisa
52
57
 
53
58
  # 5.0.0-34
54
- Dev: udpate test dependencies @cdrini
59
+ Dev: update test dependencies @cdrini
55
60
  Fix: Update hyphen stitching regex to include dangling "¬" @cdrini
56
61
  Fix: pop open multiple files menu at proper width @iisa
57
62
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/bookreader",
3
- "version": "5.0.0-44-a7",
3
+ "version": "5.0.0-46-no-right-click",
4
4
  "description": "The Internet Archive BookReader.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,7 +46,7 @@
46
46
  "@babel/plugin-proposal-class-properties": "7.16.7",
47
47
  "@babel/plugin-proposal-decorators": "7.17.9",
48
48
  "@babel/preset-env": "7.16.11",
49
- "@open-wc/testing-helpers": "^2.1.2",
49
+ "@open-wc/testing-helpers": "^2.1.3",
50
50
  "@types/jest": "^27.5.1",
51
51
  "@webcomponents/webcomponentsjs": "^2.6.0",
52
52
  "babel-loader": "8.2.5",
@@ -443,6 +443,29 @@ export class BookNavigator extends LitElement {
443
443
  this.downloadableTypes = downloadURLs;
444
444
  this.bookIsRestricted = isRestricted;
445
445
  });
446
+ window.addEventListener('contextmenu', (e) => this.manageContextMenuVisibility(e), { capture: true });
447
+ }
448
+
449
+ /** Display an element's context menu */
450
+ manageContextMenuVisibility(e) {
451
+ if (window.archive_analytics) {
452
+ window.archive_analytics?.send_event_no_sampling(
453
+ 'BookReader',
454
+ `contextmenu-${this.bookIsRestricted ? 'restricted' : 'unrestricted'}`,
455
+ e.target.classList.value
456
+ );
457
+ }
458
+ if (!this.bookIsRestricted) {
459
+ return;
460
+ }
461
+
462
+ const imagePane = e.target.classList.value.match(/BRscreen|BRpageimage/g);
463
+ if (!imagePane) {
464
+ return;
465
+ }
466
+
467
+ e.preventDefault();
468
+ return false;
446
469
  }
447
470
 
448
471
  loadSharedObserver() {
@@ -843,6 +843,13 @@ export class Mode2Up {
843
843
  this.pageContainers[this.br.twoPage.currentIndexR].$container.animate({width: '0px'}, speed, 'easeInSine', () => {
844
844
  this.br.$('BRgutter').css({left: `${gutter - this.br.twoPage.bookSpineDivWidth * 0.5}px`});
845
845
  $(this.br.leafEdgeTmp).animate({left: `${gutter - newWidthL - leafEdgeTmpW}px`}, speed, 'easeOutSine');
846
+
847
+ // Ensure the new left leaf is right-positioned before animating its width.
848
+ // Otherwise, it animates in the wrong direction.
849
+ this.pageContainers[newIndexL].$container.css({
850
+ right: `${$twoPageViewEl.prop('clientWidth') - gutter}px`,
851
+ left: ''
852
+ });
846
853
  this.pageContainers[newIndexL].$container.animate({width: `${newWidthL}px`}, speed, 'easeOutSine', () => {
847
854
  this.pageContainers[newIndexR].$container.css('zIndex', 2);
848
855
 
File without changes
@@ -13,8 +13,9 @@ import VolumesProvider from '@/src/BookNavigator/volumes/volumes-provider.js';
13
13
  import { ModalManager } from '@internetarchive/modal-manager';
14
14
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
15
15
  import '@/src/BookNavigator/book-navigator.js';
16
+ import { sleep } from '@/src/BookReader/utils';
16
17
 
17
- const promise0 = () => new Promise(res => setTimeout(res, 0));
18
+ const promise0 = () => new Promise((res) => setTimeout(res, 0));
18
19
 
19
20
  const container = (sharedObserver = null) => {
20
21
  const itemStub = {
@@ -22,7 +23,7 @@ const container = (sharedObserver = null) => {
22
23
  identifier: 'foo',
23
24
  creator: 'bar',
24
25
  title: 'baz',
25
- }
26
+ },
26
27
  };
27
28
  const modalMgr = new ModalManager();
28
29
  return html`
@@ -32,18 +33,18 @@ const container = (sharedObserver = null) => {
32
33
  .sharedObserver=${sharedObserver || new SharedResizeObserver()}
33
34
  .modal=${modalMgr}
34
35
  >
35
- <div slot="main">
36
- <div id="BookReader"></div>
37
- <p class="visible-in-reader">now showing</p>
36
+ <div slot='main'>
37
+ <div id='BookReader'></div>
38
+ <p class='visible-in-reader'>now showing</p>
38
39
  <\div>
39
40
  </book-navigator>
40
41
  `;
41
42
  };
42
43
 
43
44
  window.ResizeObserver = class ResizeObserver {
44
- observe = sinon.fake()
45
- unobserve = sinon.fake()
46
- disconnect = sinon.fake()
45
+ observe = sinon.fake();
46
+ unobserve = sinon.fake();
47
+ disconnect = sinon.fake();
47
48
  };
48
49
 
49
50
  afterEach(() => {
@@ -52,11 +53,11 @@ afterEach(() => {
52
53
  const body = document.querySelector('body');
53
54
  body.innerHTML = '';
54
55
  sinon.restore();
56
+ window.archive_analytics = null;
55
57
  });
56
58
 
57
-
58
59
  describe('<book-navigator>', () => {
59
- describe("How it loads", () => {
60
+ describe('How it loads', () => {
60
61
  describe('Attaches BookReader listeners before `br.init` is called', () => {
61
62
  test('binds global event listeners', async () => {
62
63
  const el = fixtureSync(container());
@@ -71,7 +72,7 @@ describe('<book-navigator>', () => {
71
72
  jumpToIndex: sinon.fake(),
72
73
  options: { enableMultipleBooks: false }, // for multipleBooks
73
74
  el: '#BookReader',
74
- refs: {}
75
+ refs: {},
75
76
  };
76
77
 
77
78
  const sharedObserver = new SharedResizeObserver();
@@ -85,9 +86,11 @@ describe('<book-navigator>', () => {
85
86
 
86
87
  expect(brStub.resize.callCount).toEqual(0);
87
88
 
88
- window.dispatchEvent(new CustomEvent('BookReader:PostInit', {
89
- detail: { props: brStub }
90
- }));
89
+ window.dispatchEvent(
90
+ new CustomEvent('BookReader:PostInit', {
91
+ detail: { props: brStub },
92
+ })
93
+ );
91
94
  await elementUpdated(el);
92
95
 
93
96
  expect(el.emitLoadingStatusUpdate.callCount).toEqual(1);
@@ -120,13 +123,15 @@ describe('<book-navigator>', () => {
120
123
  const itemImage = fixtureSync(el.itemImage);
121
124
  expect(itemImage).toBeInstanceOf(HTMLImageElement);
122
125
  expect(itemImage.getAttribute('class')).toEqual('cover-img');
123
- expect(itemImage.getAttribute('src')).toEqual('https://https://foo.archive.org/services/img/foo');
126
+ expect(itemImage.getAttribute('src')).toEqual(
127
+ 'https://https://foo.archive.org/services/img/foo'
128
+ );
124
129
  });
125
130
  });
126
131
  describe('Menu/Layer Provider', () => {
127
132
  describe('Connecting with a provider:', () => {
128
133
  // loads Providers with base shared resources
129
- test('We load 3 Sub Menus by default', async() => {
134
+ test('We load 3 Sub Menus by default', async () => {
130
135
  const el = fixtureSync(container());
131
136
  const $brContainer = document.createElement('div');
132
137
  const brStub = {
@@ -135,8 +140,8 @@ describe('<book-navigator>', () => {
135
140
  jumpToIndex: sinon.fake(),
136
141
  options: {},
137
142
  refs: {
138
- $brContainer
139
- }
143
+ $brContainer,
144
+ },
140
145
  };
141
146
  el.bookreader = brStub;
142
147
  await el.elementUpdated;
@@ -151,10 +156,12 @@ describe('<book-navigator>', () => {
151
156
  expect(el.menuProviders.share).toBeInstanceOf(SharingProvider);
152
157
 
153
158
  expect(defaultMenus).toContain('visualAdjustments');
154
- expect(el.menuProviders.visualAdjustments).toBeInstanceOf(VisualAdjustmentsProvider);
159
+ expect(el.menuProviders.visualAdjustments).toBeInstanceOf(
160
+ VisualAdjustmentsProvider
161
+ );
155
162
  });
156
163
  describe('Loading Sub Menus By Plugin Flags', () => {
157
- test('Search: uses `enableSearch` flag', async() => {
164
+ test('Search: uses `enableSearch` flag', async () => {
158
165
  const el = fixtureSync(container());
159
166
  const $brContainer = document.createElement('div');
160
167
  const brStub = {
@@ -163,8 +170,8 @@ describe('<book-navigator>', () => {
163
170
  jumpToIndex: sinon.fake(),
164
171
  options: { enableSearch: true },
165
172
  refs: {
166
- $brContainer
167
- }
173
+ $brContainer,
174
+ },
168
175
  };
169
176
  el.bookreader = brStub;
170
177
  await el.elementUpdated;
@@ -176,9 +183,9 @@ describe('<book-navigator>', () => {
176
183
  expect(el.menuProviders.search).toBeInstanceOf(SearchProvider);
177
184
 
178
185
  // also adds a menu shortcut
179
- expect(el.menuShortcuts.find(m => m.id === 'search')).toBeDefined();
186
+ expect(el.menuShortcuts.find((m) => m.id === 'search')).toBeDefined();
180
187
  });
181
- test('Volumes/Multiple Books: uses `enableMultipleBooks` flag', async() => {
188
+ test('Volumes/Multiple Books: uses `enableMultipleBooks` flag', async () => {
182
189
  const el = fixtureSync(container());
183
190
  const $brContainer = document.createElement('div');
184
191
  const brStub = {
@@ -189,13 +196,13 @@ describe('<book-navigator>', () => {
189
196
  enableMultipleBooks: true,
190
197
  multipleBooksList: {
191
198
  by_subprefix: {
192
- fooSubprefix: 'beep'
193
- }
194
- }
199
+ fooSubprefix: 'beep',
200
+ },
201
+ },
195
202
  },
196
203
  refs: {
197
- $brContainer
198
- }
204
+ $brContainer,
205
+ },
199
206
  };
200
207
  el.bookreader = brStub;
201
208
  await el.elementUpdated;
@@ -207,7 +214,9 @@ describe('<book-navigator>', () => {
207
214
  expect(el.menuProviders.volumes).toBeInstanceOf(VolumesProvider);
208
215
 
209
216
  // also adds a menu shortcut
210
- expect(el.menuShortcuts.find(m => m.id === 'volumes')).toBeDefined();
217
+ expect(
218
+ el.menuShortcuts.find((m) => m.id === 'volumes')
219
+ ).toBeDefined();
211
220
  });
212
221
  });
213
222
  test('keeps track of base shared resources for providers in: `baseProviderConfig`', () => {
@@ -315,7 +324,9 @@ describe('<book-navigator>', () => {
315
324
  console.log();
316
325
  sidePanelConfig = e.detail;
317
326
  });
318
- const toggleSearchMenuEvent = new Event('BookReader:ToggleSearchMenu');
327
+ const toggleSearchMenuEvent = new Event(
328
+ 'BookReader:ToggleSearchMenu'
329
+ );
319
330
  window.dispatchEvent(toggleSearchMenuEvent);
320
331
 
321
332
  await elementUpdated(el);
@@ -327,15 +338,15 @@ describe('<book-navigator>', () => {
327
338
  });
328
339
  });
329
340
 
330
- describe('Resizing',() => {
341
+ describe('Resizing', () => {
331
342
  test('keeps track of `brWidth` and `brHeight`', async () => {
332
343
  const el = fixtureSync(container());
333
344
  const brStub = {
334
345
  resize: sinon.fake(),
335
346
  options: {},
336
347
  refs: {
337
- $brContainer: document.createElement('div')
338
- }
348
+ $brContainer: document.createElement('div'),
349
+ },
339
350
  };
340
351
  el.bookreader = brStub;
341
352
  await elementUpdated(el);
@@ -345,9 +356,9 @@ describe('<book-navigator>', () => {
345
356
  const mockResizeEvent = {
346
357
  contentRect: {
347
358
  height: 500,
348
- width: 900
359
+ width: 900,
349
360
  },
350
- target: el.mainBRContainer
361
+ target: el.mainBRContainer,
351
362
  };
352
363
  el.handleResize(mockResizeEvent);
353
364
 
@@ -364,8 +375,8 @@ describe('<book-navigator>', () => {
364
375
  resize: sinon.fake(),
365
376
  options: {},
366
377
  refs: {
367
- $brContainer: document.createElement('div')
368
- }
378
+ $brContainer: document.createElement('div'),
379
+ },
369
380
  };
370
381
 
371
382
  el.bookreader = brStub;
@@ -470,7 +481,7 @@ describe('<book-navigator>', () => {
470
481
 
471
482
  expect(el.closeFullscreen.callCount).toEqual(1);
472
483
  });
473
- test('removes Fullscreen shortcut when leaving fullscreen', async() => {
484
+ test('removes Fullscreen shortcut when leaving fullscreen', async () => {
474
485
  const el = fixtureSync(container());
475
486
  const brStub = {
476
487
  isFullscreen: () => false,
@@ -489,7 +500,7 @@ describe('<book-navigator>', () => {
489
500
  expect(el.menuShortcuts.length).toEqual(0);
490
501
  expect(el.emitMenuShortcutsUpdated.callCount).toEqual(1);
491
502
  });
492
- test('Event: Listens for `BookReader:FullscreenToggled', async() => {
503
+ test('Event: Listens for `BookReader:FullscreenToggled', async () => {
493
504
  const el = fixtureSync(container());
494
505
  const brStub = {
495
506
  isFullscreen: () => true,
@@ -513,4 +524,83 @@ describe('<book-navigator>', () => {
513
524
  expect(el.manageFullScreenBehavior.callCount).toEqual(1);
514
525
  });
515
526
  });
527
+ describe('Handles Restricted Books', () => {
528
+ describe('contextMenu is prevented when book is restricted', () => {
529
+ it('watches on `div.BRscreen`', async () => {
530
+ window.archive_analytics = { send_event_no_sampling: sinon.fake() };
531
+
532
+ const el = fixtureSync(container());
533
+ const brStub = {
534
+ options: { restricted: true },
535
+ };
536
+
537
+ el.bookreader = brStub;
538
+ el.bookIsRestricted = true;
539
+
540
+ const elSpy = sinon.spy(el.manageContextMenuVisibility);
541
+ await el.elementUpdated;
542
+
543
+ expect(window.archive_analytics.send_event_no_sampling.called).toEqual(
544
+ false
545
+ );
546
+ expect(elSpy.called).toEqual(false);
547
+
548
+ const body = document.querySelector('body');
549
+
550
+ const divBRscreen = document.createElement('div');
551
+ divBRscreen.classList.add('BRscreen');
552
+ body.appendChild(divBRscreen);
553
+
554
+ const contextMenuEvent = new Event('contextmenu', { bubbles: true });
555
+
556
+ // Set spy on contextMenuEvent to check if `preventDefault` is called
557
+ const preventDefaultSpy = sinon.spy(contextMenuEvent, 'preventDefault');
558
+ expect(preventDefaultSpy.called).toEqual(false);
559
+
560
+ divBRscreen.dispatchEvent(contextMenuEvent);
561
+
562
+ // analytics fires
563
+ expect(window.archive_analytics.send_event_no_sampling.called).toEqual(
564
+ true
565
+ );
566
+ // we prevent default
567
+ expect(preventDefaultSpy.called).toEqual(true);
568
+ });
569
+ it('watches on `img.BRpageimage`', async () => {
570
+ window.archive_analytics = { send_event_no_sampling: sinon.fake() };
571
+
572
+ const el = fixtureSync(container());
573
+ const brStub = {
574
+ options: { restricted: true },
575
+ };
576
+
577
+ el.bookreader = brStub;
578
+ el.bookIsRestricted = true;
579
+
580
+ await el.elementUpdated;
581
+
582
+ expect(window.archive_analytics.send_event_no_sampling.called).toEqual(
583
+ false
584
+ );
585
+
586
+ const body = document.querySelector('body');
587
+ // const element stub for img.BRpageimage
588
+ const imgBRpageimage = document.createElement('img');
589
+ imgBRpageimage.classList.add('BRpageimage');
590
+ body.appendChild(imgBRpageimage);
591
+ const contextMenuEvent = new Event('contextmenu', { bubbles: true });
592
+
593
+ // Set spy on contextMenuEvent to check if `preventDefault` is called
594
+ const preventDefaultSpy = sinon.spy(contextMenuEvent, 'preventDefault');
595
+ expect(preventDefaultSpy.called).toEqual(false);
596
+
597
+ imgBRpageimage.dispatchEvent(contextMenuEvent);
598
+
599
+ // analytics fires
600
+ expect(window.archive_analytics.send_event_no_sampling.called).toEqual(true);
601
+ // we prevent default
602
+ expect(preventDefaultSpy.called).toEqual(true);
603
+ });
604
+ });
605
+ });
516
606
  });
@@ -128,5 +128,40 @@ describe('Search Provider', () => {
128
128
  expect(urlPluginMock.pullFromAddressBar.callCount).toEqual(2);
129
129
  expect(urlPluginMock.removeUrlParam.callCount).toEqual(2);
130
130
  });
131
+ it('updateSearchInUrl', async () => {
132
+ let fieldToSet;
133
+ let valueOfFieldToSet;
134
+ let setUrlParamCalled = false;
135
+ const urlPluginMock = {
136
+ pullFromAddressBar: sinon.fake(),
137
+ removeUrlParam: sinon.fake(),
138
+ setUrlParam: (field, val) => {
139
+ fieldToSet = field;
140
+ valueOfFieldToSet = val;
141
+ setUrlParamCalled = true;
142
+ }
143
+ };
144
+ const provider = new searchProvider({
145
+ onProviderChange: sinon.fake(),
146
+ bookreader: {
147
+ leafNumToIndex: sinon.fake(),
148
+ _searchPluginGoToResult: sinon.fake(),
149
+ urlPlugin: urlPluginMock,
150
+ search: sinon.fake()
151
+ }
152
+ });
153
+
154
+ const searchInitiatedEvent = new CustomEvent('bookSearchInitiated', { detail: { query: 'foobar' } });
155
+ // set initial seachState with a query
156
+ provider.onBookSearchInitiated(searchInitiatedEvent);
157
+ await provider.updateComplete;
158
+ // checking this fn:
159
+ provider.updateSearchInUrl();
160
+ await provider.updateComplete;
161
+
162
+ expect(fieldToSet).toEqual('q');
163
+ expect(valueOfFieldToSet).toEqual('foobar');
164
+ expect(setUrlParamCalled).toBe(true);
165
+ });
131
166
  });
132
167
  });
@@ -236,4 +236,17 @@ describe('<ia-book-search-results>', () => {
236
236
 
237
237
  expect(response).toBeDefined();
238
238
  });
239
+ it('cancels search when input is cleared', async () => {
240
+ const el = await fixture(container(results));
241
+
242
+ el.cancelSearch = sinon.fake();
243
+ await el.updateComplete;
244
+
245
+ const searchInput = el.shadowRoot.querySelector('[name="query"]');
246
+
247
+ searchInput.value = '';
248
+ searchInput.dispatchEvent(new Event('search'));
249
+
250
+ expect(el.cancelSearch.callCount).toEqual(1);
251
+ });
239
252
  });
@@ -47,6 +47,29 @@ describe('zoom', () => {
47
47
  });
48
48
  });
49
49
 
50
+ describe('page flip directions', () => {
51
+ test('animates the left page in the correct direction', () => {
52
+ const br = new BookReader({ data: SAMPLE_DATA });
53
+ br.init();
54
+
55
+ const fake = sinon.fake();
56
+ const fakeAnimWithCB = sinon.fake.yields();
57
+ const fakeAnim = sinon.fake((...args) =>
58
+ typeof args[args.length - 1] === 'function' ? fakeAnimWithCB(...args) : fake
59
+ );
60
+ sinon.replace(jQuery.prototype, 'animate', fakeAnim);
61
+
62
+ const fakeCSS = sinon.spy(jQuery.prototype, 'css');
63
+
64
+ br.next();
65
+
66
+ expect(fakeAnimWithCB.callCount).toBe(2);
67
+ // Find the call to .css() immediately preceding the second animation with a callback (i.e., the left page animation)
68
+ const preSecondAnimCssCallIndex = fakeCSS.getCalls().findIndex(call => call.calledAfter(fakeAnimWithCB.getCall(1))) - 1;
69
+ expect(fakeCSS.getCall(preSecondAnimCssCallIndex).args[0].left).toBe('');
70
+ });
71
+ });
72
+
50
73
  describe('prefetch', () => {
51
74
  test('loads nearby pages', () => {
52
75
  const br = new BookReader({ data: SAMPLE_DATA });