@internetarchive/bookreader 5.0.0-106 → 5.0.0-107

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 (33) hide show
  1. package/BookReader/BookReader.js +1 -1
  2. package/BookReader/BookReader.js.map +1 -1
  3. package/BookReader/ia-bookreader-bundle.js +54 -60
  4. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  5. package/BookReader/plugins/plugin.chapters.js +1 -4
  6. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  7. package/package.json +12 -10
  8. package/src/BookReader.js +1 -1
  9. package/src/css/icon_checkmark.js +9 -0
  10. package/src/css/sharedStyles.js +15 -0
  11. package/src/{BookNavigator → ia-bookreader}/downloads/downloads.js +1 -1
  12. package/src/ia-bookreader/ia-bookreader.js +513 -71
  13. package/src/{BookNavigator → ia-bookreader}/viewable-files.js +4 -1
  14. package/src/{BookNavigator → ia-bookreader}/visual-adjustments/visual-adjustments-provider.js +1 -2
  15. package/src/{BookNavigator → ia-bookreader}/visual-adjustments/visual-adjustments.js +4 -14
  16. package/src/{BookNavigator → plugins}/bookmarks/bookmark-button.js +1 -1
  17. package/src/{BookNavigator → plugins}/bookmarks/bookmark-edit.js +41 -28
  18. package/src/{BookNavigator → plugins}/bookmarks/bookmarks-list.js +46 -46
  19. package/src/{BookNavigator → plugins}/bookmarks/bookmarks-loginCTA.js +1 -1
  20. package/src/{BookNavigator → plugins}/bookmarks/bookmarks-provider.js +1 -1
  21. package/src/{BookNavigator → plugins}/bookmarks/ia-bookmarks.js +2 -2
  22. package/src/{BookNavigator → plugins}/search/search-results.js +14 -20
  23. package/src/util/lit.js +10 -0
  24. package/src/BookNavigator/assets/ia-logo.js +0 -17
  25. package/src/BookNavigator/assets/icon_checkmark.js +0 -6
  26. package/src/BookNavigator/assets/icon_close.js +0 -3
  27. package/src/BookNavigator/book-navigator.js +0 -620
  28. /package/src/{BookNavigator/assets → css}/button-base.js +0 -0
  29. /package/src/{BookNavigator → ia-bookreader}/downloads/downloads-provider.js +0 -0
  30. /package/src/{BookNavigator → ia-bookreader}/sharing.js +0 -0
  31. /package/src/{BookNavigator/assets → plugins/bookmarks}/bookmark-colors.js +0 -0
  32. /package/src/{BookNavigator → plugins/bookmarks}/delete-modal-actions.js +0 -0
  33. /package/src/{BookNavigator → plugins}/search/search-provider.js +0 -0
@@ -1,15 +1,26 @@
1
+ // @ts-check
1
2
  /**
2
- * BookReaderTemplate to load BookNavigator components
3
+ * Wrapping web component for Internet Archive's BookReader. Currently operates
4
+ * more as a shell ; requires BookReader to be instantiated independently in the
5
+ * main slot.
3
6
  */
4
7
 
5
8
  import { LitElement, html, css } from 'lit';
6
9
 
7
10
  import '@internetarchive/ia-item-navigator';
8
- import '../BookNavigator/book-navigator.js';
9
- // eslint-disable-next-line no-unused-vars
10
- import { ModalManager } from '@internetarchive/modal-manager';
11
11
  import '@internetarchive/modal-manager';
12
12
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
13
+ import '@internetarchive/icon-ia-logo';
14
+ import SearchProvider from '../plugins/search/search-provider.js';
15
+ import DownloadProvider from './downloads/downloads-provider.js';
16
+ import VisualAdjustmentProvider from './visual-adjustments/visual-adjustments-provider.js';
17
+ import BookmarksProvider from '../plugins/bookmarks/bookmarks-provider.js';
18
+ import SharingProvider from './sharing.js';
19
+ import ViewableFilesProvider from './viewable-files.js';
20
+ import { sortBy } from '../BookReader/utils.js';
21
+ /** @typedef {import('@/src/BookReader.js').default} BookReader */
22
+ /** @typedef {import('@internetarchive/modal-manager').ModalManager} ModalManager */
23
+
13
24
 
14
25
  export class IaBookReader extends LitElement {
15
26
  static get properties() {
@@ -23,12 +34,28 @@ export class IaBookReader extends LitElement {
23
34
  loaded: { type: Boolean },
24
35
  menuShortcuts: { type: Array },
25
36
  menuContents: { type: Array },
37
+ bookReaderLoaded: { type: Boolean },
38
+ bookreader: { type: Object },
39
+ bookIsRestricted: { type: Boolean },
40
+ downloadableTypes: { type: Array },
41
+ isAdmin: { type: Boolean },
42
+ lendingInitialized: { type: Boolean },
43
+ lendingStatus: { type: Object },
44
+ menuProviders: { type: Object },
45
+ fullscreenBranding: { type: Object },
26
46
  };
27
47
  }
28
48
 
49
+ /** @type {import('@internetarchive/ia-item-navigator').ItemNavigator} */
50
+ get itemNav() {
51
+ return this.shadowRoot.querySelector('iaux-item-navigator');
52
+ }
53
+
29
54
  constructor() {
30
55
  super();
56
+ /** The IA metadata item */
31
57
  this.item = undefined;
58
+ /** @type {BookReader} */
32
59
  this.bookreader = undefined;
33
60
  this.baseHost = 'archive.org';
34
61
  this.fullscreen = false;
@@ -38,12 +65,62 @@ export class IaBookReader extends LitElement {
38
65
  /** @type {SharedResizeObserver} */
39
66
  this.sharedObserver = undefined;
40
67
  this.loaded = false;
68
+ /** @type {Array<{ id: string, icon: string | import('lit').TemplateResult }>} */
41
69
  this.menuShortcuts = [];
42
70
  this.menuContents = [];
43
71
  this.openMenuName = '';
72
+
73
+ this.bookReaderCannotLoad = false;
74
+ this.bookReaderLoaded = false;
75
+ this.bookIsRestricted = false;
76
+ this.downloadableTypes = [];
77
+ this.isAdmin = false;
78
+ this.lendingInitialized = false;
79
+ this.lendingStatus = {};
80
+ this.menuProviders = {
81
+ /** @type {BookmarksProvider} */
82
+ bookmarks: null,
83
+ /** @type {SearchProvider} */
84
+ search: null,
85
+ /** @type {DownloadProvider} */
86
+ downloads: null,
87
+ /** @type {VisualAdjustmentProvider} */
88
+ visualAdjustments: null,
89
+ /** @type {SharingProvider} */
90
+ share: null,
91
+ /** @type {ViewableFilesProvider} */
92
+ volumes: null,
93
+ };
94
+ this.signedIn = false;
95
+ this.fullscreenBranding = html`<ia-icon-ia-logo aria-hidden="true"></ia-icon-ia-logo>`;
96
+ // Untracked properties
97
+ this._sharedObserverHandler = { handleResize: this.handleResize.bind(this) };
98
+ this._brWidth = 0;
99
+ this._brHeight = 0;
100
+ this.shortcutOrder = [
101
+ /**
102
+ * sets exit FS button (`this.fullscreenBranding`)
103
+ * when `br.options.enableFSLogoShortcut`
104
+ */
105
+ 'fullscreen',
106
+ 'volumes',
107
+ 'chapters',
108
+ 'search',
109
+ 'translate',
110
+ 'bookmarks',
111
+ 'downloads',
112
+ 'visualAdjustments',
113
+ 'share',
114
+ 'experiments',
115
+ ];
116
+ }
117
+
118
+ disconnectedCallback() {
119
+ super.disconnectedCallback();
120
+ this.unloadSharedObserver();
44
121
  }
45
122
 
46
- updated() {
123
+ firstUpdated() {
47
124
  if (!this.modal) {
48
125
  this.setModalManager();
49
126
  }
@@ -51,50 +128,242 @@ export class IaBookReader extends LitElement {
51
128
  if (!this.sharedObserver) {
52
129
  this.sharedObserver = new SharedResizeObserver();
53
130
  }
131
+
132
+ this._bindEventListeners();
133
+ this.loaded = true;
54
134
  }
55
135
 
56
- get itemNav() {
57
- return this.shadowRoot.querySelector('iaux-item-navigator');
136
+ /**
137
+ * @param {import('lit').PropertyValues} changed
138
+ */
139
+ updated(changed) {
140
+ if (!this.bookreader || !this.item || !this.bookReaderLoaded) {
141
+ return;
142
+ }
143
+
144
+ const reload = changed.has('loaded') && this.loaded;
145
+ if (reload
146
+ || changed.has('item')
147
+ || changed.has('bookreader')
148
+ || changed.has('signedIn')
149
+ || changed.has('isAdmin')
150
+ || changed.has('modal')) {
151
+ this.initializeBookSubmenus();
152
+ }
153
+
154
+ if (changed.has('sharedObserver') && this.bookreader) {
155
+ this.loadSharedObserver();
156
+ this.initializeBookSubmenus();
157
+ }
158
+
159
+ if (changed.has('downloadableTypes')) {
160
+ this.initializeBookSubmenus();
161
+ }
162
+ }
163
+
164
+ loadSharedObserver() {
165
+ this.unloadSharedObserver();
166
+ this.sharedObserver?.addObserver({
167
+ target: this.mainBRContainer,
168
+ handler: this._sharedObserverHandler,
169
+ });
170
+ }
171
+
172
+ unloadSharedObserver() {
173
+ this.sharedObserver?.removeObserver({
174
+ target: this.mainBRContainer,
175
+ handler: this._sharedObserverHandler,
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Uses resize observer to fire BookReader's `resize` functionality
181
+ * We do not want to trigger resize IF:
182
+ * - book animation is happening
183
+ * - book is in fullscreen (fullscreen is handled separately)
184
+ *
185
+ * @param { target: HTMLElement, contentRect: DOMRectReadOnly } entry
186
+ */
187
+ handleResize({ contentRect, target }) {
188
+ const startBrWidth = this._brWidth;
189
+ const startBrHeight = this._brHeight;
190
+ const { animating } = this.bookreader;
191
+
192
+ if (target === this.mainBRContainer) {
193
+ this._brWidth = contentRect.width;
194
+ this._brHeight = contentRect.height;
195
+ }
196
+
197
+ if (!startBrWidth && this._brWidth) {
198
+ // loading up, let's update side menus
199
+ this.initializeBookSubmenus();
200
+ }
201
+
202
+ const widthChange = startBrWidth !== this._brWidth;
203
+ const heightChange = startBrHeight !== this._brHeight;
204
+
205
+ if (!animating && (widthChange || heightChange)) {
206
+ this.bookreader?.resize();
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Manages Fullscreen behavior
212
+ * This makes sure that controls are _always_ in view
213
+ * We need this to accommodate LOAN BAR during fullscreen
214
+ */
215
+ manageFullScreenBehavior() {
216
+ if (!this.bookreader.options.enableFSLogoShortcut) {
217
+ return;
218
+ }
219
+
220
+ this.fullscreen = this.bookreader.isFullscreen();
221
+ this.dispatchEvent(new CustomEvent('fullscreenStateUpdated', { detail: { fullscreen: this.fullscreen }}));
222
+ if (this.fullscreen) {
223
+ this.addFullscreenShortcut();
224
+ } else {
225
+ this.deleteFullscreenShortcut();
226
+ }
58
227
  }
59
228
 
60
229
  /** Creates modal DOM & attaches to `<body>` */
61
230
  setModalManager() {
231
+ /** @type {ModalManager} */
62
232
  let modalManager = document.querySelector('modal-manager');
63
233
  if (!modalManager) {
64
- modalManager = document.createElement(
65
- 'modal-manager',
66
- );
234
+ modalManager = /** @type {ModalManager} */(document.createElement('modal-manager'));
67
235
  document.body.appendChild(modalManager);
68
236
  }
69
237
 
70
238
  this.modal = modalManager;
71
239
  }
72
240
 
73
- manageFullscreen(e) {
74
- const { detail } = e;
75
- const fullscreen = !!detail.isFullScreen;
76
- this.fullscreen = fullscreen;
77
- this.dispatchEvent(new CustomEvent('fullscreenStateUpdated', { detail: { fullscreen }}));
241
+ /**
242
+ * Instantiates books submenus & their update callbacks
243
+ *
244
+ * NOTE: we are doing our best to scope bookreader's instance.
245
+ * If your submenu provider uses a bookreader instance to read, manually
246
+ * manipulate BookReader, please update ia-bookreader's instance of it
247
+ * to keep it in sync.
248
+ */
249
+ initializeBookSubmenus() {
250
+ const providers = {
251
+ visualAdjustments: new VisualAdjustmentProvider({
252
+ ...this.baseProviderConfig,
253
+ /** Update menu contents */
254
+ onProviderChange: () => {
255
+ this.updateMenuContents();
256
+ },
257
+ }),
258
+ };
259
+
260
+ if (this.baseProviderConfig.item) {
261
+ // Share options currently rely on IA item metadata
262
+ providers.share = new SharingProvider(this.baseProviderConfig);
263
+ }
264
+
265
+ if (this.shouldShowDownloadsMenu()) {
266
+ providers.downloads = new DownloadProvider(this.baseProviderConfig);
267
+ }
268
+
269
+ // Note plugins will never be null-ish in runtime, but some of the unit tests
270
+ // stub BR with a nullish value there.
271
+ if (this.bookreader.options.plugins?.search?.enabled) {
272
+ providers.search = new SearchProvider({
273
+ ...this.baseProviderConfig,
274
+ /**
275
+ * Search specific menu updates
276
+ * @param {BookReader} brInstance
277
+ * @param {Partial<{ searchCanceled: boolean, openMenu: boolean }>} searchUpdates
278
+ */
279
+ onProviderChange: (brInstance = null, searchUpdates = {}) => {
280
+ if (brInstance) {
281
+ /** @type {BookReader} refresh br instance reference */
282
+ this.bookreader = brInstance;
283
+ }
284
+
285
+ this.updateMenuContents();
286
+
287
+ if (searchUpdates.openMenu === false) {
288
+ return;
289
+ }
78
290
 
291
+ if (this.isWideEnoughToOpenMenu && !searchUpdates?.searchCanceled) {
292
+ /* open side search menu */
293
+ setTimeout(() => {
294
+ this.updateSideMenu('search', 'open');
295
+ }, 0);
296
+ }
297
+ },
298
+ });
299
+ }
300
+
301
+ if (this.bookreader.options.enableBookmarks) {
302
+ providers.bookmarks = new BookmarksProvider({
303
+ ...this.baseProviderConfig,
304
+ onProviderChange: (bookmarks) => {
305
+ const method = Object.keys(bookmarks).length ? 'add' : 'remove';
306
+ this[`${method}MenuShortcut`]('bookmarks');
307
+ this.updateMenuContents();
308
+ },
309
+ });
310
+ }
311
+
312
+ // add shortcut for volumes if multipleBooksList exists
313
+ if (this.bookreader.options.enableMultipleBooks) {
314
+ providers.volumes = new ViewableFilesProvider({
315
+ ...this.baseProviderConfig,
316
+ onProviderChange: (brInstance = null, volumesUpdates = {}) => {
317
+ if (brInstance) {
318
+ /* refresh br instance reference */
319
+ this.bookreader = brInstance;
320
+ }
321
+ this.updateMenuContents();
322
+ if (this.isWideEnoughToOpenMenu) {
323
+ /* open side search menu */
324
+ setTimeout(() => {
325
+ this.updateSideMenu('volumes', 'open');
326
+ });
327
+ }
328
+ },
329
+ });
330
+ }
331
+
332
+ Object.assign(this.menuProviders, providers);
333
+ this.addMenuShortcut('search');
334
+ this.addMenuShortcut('volumes');
335
+ this.updateMenuContents();
79
336
  }
80
337
 
81
- loadingStateUpdated(e) {
82
- const { loaded } = e.detail;
83
- this.loaded = loaded || null;
84
- this.dispatchEvent(new CustomEvent('loadingStateUpdated', { detail: { loaded }}));
338
+ /** gets element that houses the bookreader in light dom */
339
+ get mainBRContainer() {
340
+ return document.querySelector(this.bookreader?.el);
85
341
  }
86
342
 
87
- setMenuShortcuts(e) {
88
- this.menuShortcuts = [...e.detail];
343
+ get baseProviderConfig() {
344
+ return {
345
+ baseHost: this.baseHost,
346
+ modal: this.modal,
347
+ sharedObserver: this.sharedObserver,
348
+ bookreader: this.bookreader,
349
+ item: this.item,
350
+ signedIn: this.signedIn,
351
+ isAdmin: this.isAdmin,
352
+ /** @type {function(BookReader, object): void} */
353
+ onProviderChange: () => {},
354
+ };
89
355
  }
90
356
 
91
- setMenuContents(e) {
92
- const updatedContents = [...e.detail];
93
- this.menuContents = updatedContents;
357
+ get isWideEnoughToOpenMenu() {
358
+ return this._brWidth >= 640;
94
359
  }
95
360
 
96
- manageSideMenuEvents(e) {
97
- const { menuId, action } = e.detail;
361
+ /**
362
+ * Open side menu
363
+ * @param {string} menuId
364
+ * @param {('open'|'close'|'toggle')} action
365
+ */
366
+ updateSideMenu(menuId = '', action = 'open') {
98
367
  if (!menuId) {
99
368
  return;
100
369
  }
@@ -108,45 +377,207 @@ export class IaBookReader extends LitElement {
108
377
  }
109
378
  }
110
379
 
380
+ /** Fullscreen Shortcut */
381
+ addFullscreenShortcut() {
382
+ this.menuShortcuts.push({
383
+ icon: this.fullscreenShortcut,
384
+ id: 'fullscreen',
385
+ });
386
+ this._sortMenuShortcuts();
387
+ }
388
+
389
+ deleteFullscreenShortcut() {
390
+ this.menuShortcuts = this.menuShortcuts
391
+ .filter(s => s.id !== 'fullscreen');
392
+ this._sortMenuShortcuts();
393
+ }
394
+
395
+ get fullscreenShortcut() {
396
+ return html`
397
+ <button
398
+ @click=${() => this.bookreader.exitFullScreen()}
399
+ title="Exit fullscreen view"
400
+ >${this.fullscreenBranding}</button>
401
+ `;
402
+ }
403
+ /** End Fullscreen Shortcut */
404
+
405
+ /**
406
+ * Sets order of menu and emits custom event when done
407
+ */
408
+ updateMenuContents() {
409
+ const availableMenus = sortBy(
410
+ Object.entries(this.menuProviders)
411
+ .filter(([id, menu]) => !!menu)
412
+ .filter(([id, menu]) => {
413
+ return id === 'downloads' ? this.shouldShowDownloadsMenu() : true;
414
+ }),
415
+ ([id, menu]) => {
416
+ const index = this.shortcutOrder.indexOf(id);
417
+ return index === -1 ? this.shortcutOrder.length : index;
418
+ },
419
+ ).map(([id, menu]) => menu);
420
+
421
+ if (this.shouldShowDownloadsMenu()) {
422
+ this.menuProviders.downloads?.update(this.downloadableTypes);
423
+ }
424
+
425
+ this.menuContents = availableMenus;
426
+ }
427
+
428
+ /**
429
+ * Confirms if we should show the downloads menu
430
+ * @returns {boolean}
431
+ */
432
+ shouldShowDownloadsMenu() {
433
+ if (!this.downloadableTypes.length) { return false; }
434
+ if (this.bookIsRestricted === false) { return true; }
435
+ if (this.isAdmin) { return true; }
436
+ const { user_loan_record = {} } = this.lendingStatus;
437
+ const hasNoLoanRecord = Array.isArray(user_loan_record); /* (bc PHP assoc. arrays) */
438
+
439
+ if (hasNoLoanRecord) { return false; }
440
+
441
+ const hasValidLoan = user_loan_record.type && (user_loan_record.type !== 'SESSION_LOAN');
442
+ return hasValidLoan;
443
+ }
444
+
445
+ /**
446
+ * Adds a provider object to the menuShortcuts array property if it isn't
447
+ * already added, then sorts menuShortcuts based on shortcutOrder.
448
+ *
449
+ * @param {string} menuId - a string matching the id property of a provider
450
+ */
451
+ addMenuShortcut(menuId) {
452
+ if (this.menuShortcuts.find((m) => m.id === menuId)) {
453
+ // menu is already there
454
+ return;
455
+ }
456
+
457
+ if (!this.menuProviders[menuId]) {
458
+ // no provider for this menu
459
+ return;
460
+ }
461
+
462
+ this.menuShortcuts.push(this.menuProviders[menuId]);
463
+ this._sortMenuShortcuts();
464
+ }
465
+
466
+ /**
467
+ * Sorts the menuShortcuts property by comparing each provider's id to
468
+ * the id in each iteration over the shortcutOrder array.
469
+ */
470
+ _sortMenuShortcuts() {
471
+ this.menuShortcuts = sortBy(
472
+ this.menuShortcuts,
473
+ (shortcut) => {
474
+ const index = this.shortcutOrder.indexOf(shortcut.id);
475
+ return index === -1 ? this.shortcutOrder.length : index;
476
+ },
477
+ );
478
+ }
479
+
480
+ /**
481
+ * Core bookreader event handler registry
482
+ *
483
+ * NOTE: we are trying to keep bookreader's instance in scope
484
+ * Please update ia-bookreader's instance reference of it to keep it current
485
+ */
486
+ _bindEventListeners() {
487
+ window.addEventListener('BookReader:PostInit', /** @param {CustomEvent} e */ (e) => {
488
+ this.bookreader = e.detail.props;
489
+ this.bookreader.shell = this;
490
+ this.bookReaderLoaded = true;
491
+ this.bookReaderCannotLoad = false;
492
+ this.loaded = true;
493
+ this.loadSharedObserver();
494
+ setTimeout(() => this.bookreader.resize(), 0);
495
+ });
496
+ window.addEventListener('BookReader:fullscreenToggled', /** @param {CustomEvent} event */ (event) => {
497
+ const brInstance = event.detail.props;
498
+ if (brInstance) {
499
+ this.bookreader = brInstance;
500
+ }
501
+ this.manageFullScreenBehavior();
502
+ }, { passive: true });
503
+ window.addEventListener('BookReader:ToggleSearchMenu', /** @param {CustomEvent} event */ (event) => {
504
+ this.updateSideMenu('search', 'toggle');
505
+ });
506
+ window.addEventListener('LendingFlow:PostInit', /** @param {CustomEvent} detail */ ({ detail }) => {
507
+ const {
508
+ downloadTypesAvailable, lendingStatus, isAdmin, previewType,
509
+ } = detail;
510
+ this.lendingInitialized = true;
511
+ this.downloadableTypes = downloadTypesAvailable;
512
+ this.lendingStatus = lendingStatus;
513
+ this.isAdmin = isAdmin;
514
+ this.bookReaderCannotLoad = previewType === 'singlePagePreview';
515
+ this.loaded = true;
516
+ });
517
+ window.addEventListener('BRJSIA:PostInit', /** @param {CustomEvent} detail */ ({ detail }) => {
518
+ const { isRestricted, downloadURLs } = detail;
519
+ this.bookReaderLoaded = true;
520
+ this.downloadableTypes = downloadURLs;
521
+ this.bookIsRestricted = isRestricted;
522
+ });
523
+ window.addEventListener('contextmenu', (e) => this._manageContextMenuVisibility(e), { capture: true });
524
+ }
525
+
526
+ /**
527
+ * @param {PointerEvent} e
528
+ **/
529
+ _manageContextMenuVisibility(e) {
530
+ const target = /** @type {HTMLElement} */(e.target);
531
+
532
+ window['archive_analytics']?.send_event(
533
+ 'BookReader',
534
+ `contextmenu-${this.bookIsRestricted ? 'restricted' : 'unrestricted'}`,
535
+ target?.classList?.value,
536
+ );
537
+ if (!this.bookIsRestricted) {
538
+ return;
539
+ }
540
+
541
+ const imagePane = target.classList.value.match(/BRscreen|BRpageimage/g);
542
+ if (!imagePane) {
543
+ return;
544
+ }
545
+
546
+ e.preventDefault();
547
+ return false;
548
+ }
549
+
550
+ get itemImage() {
551
+ const identifier = this.item?.metadata.identifier;
552
+ const url = `https://${this.baseHost}/services/img/${identifier}`;
553
+ return html`<img class="cover-img" src=${url} alt="cover image for ${identifier}">`;
554
+ }
555
+
556
+ get placeholder() {
557
+ return html`<div class="placeholder">${this.itemImage}</div>`;
558
+ }
559
+
111
560
  render() {
112
561
  return html`
113
- <div class="main-component">
114
- <iaux-item-navigator
115
- ?viewportInFullscreen=${this.fullscreen}
116
- .basehost=${this.baseHost}
117
- .item=${this.item}
118
- .modal=${this.modal}
119
- .loaded=${this.loaded}
120
- .sharedObserver=${this.sharedObserver}
121
- ?signedIn=${this.signedIn}
122
- .menuShortcuts=${this.menuShortcuts}
123
- .menuContents=${this.menuContents}
124
- .openMenu=${this.openMenuName}
125
- >
126
- <div slot="header">
127
- <slot name="header"></slot>
128
- </div>
129
- <div slot="main">
130
- <book-navigator
131
- .modal=${this.modal}
132
- .baseHost=${this.baseHost}
133
- .itemMD=${this.item}
134
- ?signedIn=${this.signedIn}
135
- ?sideMenuOpen=${this.menuOpened}
136
- .sharedObserver=${this.sharedObserver}
137
- @ViewportInFullScreen=${this.manageFullscreen}
138
- @loadingStateUpdated=${this.loadingStateUpdated}
139
- @updateSideMenu=${this.manageSideMenuEvents}
140
- @menuUpdated=${this.setMenuContents}
141
- @menuShortcutsUpdated=${this.setMenuShortcuts}
142
- >
143
- <div slot="main">
144
- <slot name="main"></slot>
145
- </div>
146
- </book-navigator>
147
- </div>
148
- </iaux-item-navigator>
149
- </div>
562
+ <iaux-item-navigator
563
+ ?viewportInFullscreen=${this.fullscreen}
564
+ .basehost=${this.baseHost}
565
+ .item=${this.item}
566
+ .modal=${this.modal}
567
+ .loaded=${this.loaded}
568
+ .sharedObserver=${this.sharedObserver}
569
+ ?signedIn=${this.signedIn}
570
+ .menuShortcuts=${this.menuShortcuts}
571
+ .menuContents=${this.menuContents}
572
+ .openMenu=${this.openMenuName}
573
+ >
574
+ <div slot="header">
575
+ <slot name="header"></slot>
576
+ </div>
577
+ <div slot="main">
578
+ ${this.bookReaderCannotLoad ? this.placeholder : html`<slot name="main"></slot>`}
579
+ </div>
580
+ </iaux-item-navigator>
150
581
  `;
151
582
  }
152
583
 
@@ -176,12 +607,6 @@ export class IaBookReader extends LitElement {
176
607
  min-height: unset;
177
608
  }
178
609
 
179
- .main-component {
180
- height: 100%;
181
- width: 100%;
182
- min-height: inherit;
183
- }
184
-
185
610
  div[slot="header"],
186
611
  div[slot="main"] {
187
612
  display: flex;
@@ -193,11 +618,28 @@ export class IaBookReader extends LitElement {
193
618
  flex: 1;
194
619
  }
195
620
 
621
+ slot,
622
+ slot > * {
623
+ display: block;
624
+ height: inherit;
625
+ width: inherit;
626
+ }
627
+ .placeholder {
628
+ display: flex;
629
+ align-items: center;
630
+ justify-content: center;
631
+ flex-direction: column;
632
+ margin: 5%;
633
+ }
634
+ .cover-img {
635
+ max-height: 300px;
636
+ }
637
+
196
638
  iaux-item-navigator {
197
- min-height: var(--br-height, inherit);
198
- height: var(--br-height, inherit);
199
639
  display: block;
200
640
  width: 100%;
641
+ min-height: var(--br-height, inherit);
642
+ height: var(--br-height, 100%);
201
643
  color: var(--primaryTextColor);
202
644
  --menuButtonLabelDisplay: block;
203
645
  --menuWidth: 320px;
@@ -13,7 +13,10 @@ const sortTypes = {
13
13
  };
14
14
  export default class ViewableFilesProvider {
15
15
  /**
16
- * @param {import('../BookReader').default} bookreader
16
+ * @param {object} options
17
+ * @param {import('../BookReader').default} options.bookreader
18
+ * @param {string} options.baseHost
19
+ * @param {function} options.onProviderChange
17
20
  */
18
21
  constructor({ baseHost, bookreader, onProviderChange }) {
19
22
  /** @type {import('../BookReader').default} */
@@ -29,8 +29,7 @@ const visualAdjustmentOptions = [{
29
29
  }];
30
30
 
31
31
  export default class VisualAdjustmentsProvider {
32
- constructor(options) {
33
- const { onProviderChange, bookreader } = options;
32
+ constructor({ onProviderChange, bookreader }) {
34
33
  this.onProviderChange = onProviderChange;
35
34
  this.bookContainer = bookreader.refs.$brContainer;
36
35
  this.bookreader = bookreader;