@internetarchive/bookreader 5.0.0-66 → 5.0.0-67

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,11 @@
1
1
  /* global BookReader */
2
+ import { css, html, LitElement, nothing } from "lit";
3
+ import { customElement, property } from 'lit/decorators.js';
4
+ import { ifDefined } from 'lit/directives/if-defined.js';
5
+ import { styleMap } from 'lit/directives/style-map.js';
6
+ import '@internetarchive/icon-toc/icon-toc';
7
+ /** @typedef {import('@/src/BookNavigator/book-navigator.js').BookNavigator} BookNavigator */
8
+
2
9
  /**
3
10
  * Plugin for chapter markers in BookReader. Fetches from openlibrary.org
4
11
  * Could be forked, or extended to alter behavior
@@ -10,153 +17,111 @@ jQuery.extend(BookReader.defaultOptions, {
10
17
  bookId: '',
11
18
  });
12
19
 
13
- /** @override Extend the constructor to add search properties */
14
- BookReader.prototype.setup = (function (super_) {
15
- return function (options) {
16
- super_.call(this, options);
17
-
18
- this.olHost = options.olHost;
19
- this.enableChaptersPlugin = options.enableChaptersPlugin;
20
- this.bookId = options.bookId;
21
- };
22
- })(BookReader.prototype.setup);
23
-
24
20
  /** @override Extend to call Open Library for TOC */
25
21
  BookReader.prototype.init = (function(super_) {
26
22
  return function() {
27
23
  super_.call(this);
28
- if (this.enableChaptersPlugin && this.ui !== 'embed') {
29
- this.getOpenLibraryRecord();
30
- }
31
- if (this.enableMobileNav) {
32
- this.bind(BookReader.eventNames.mobileNavOpen,
33
- () => {
34
- this.updateTOCState(this.firstIndex, this._tocEntries);
35
- if ($('table-contents-list').parent().hasClass('mm-opened')) {
36
- this.updateTOCState(this.firstIndex, this._tocEntries);
37
- }
38
- }
39
- );
40
- $(".BRmobileMenu__tableContents").on("click", () => {
41
- this.updateTOCState(this.firstIndex, this._tocEntries);
42
- });
24
+ if (this.options.enableChaptersPlugin && this.ui !== 'embed') {
25
+ this._chapterInit();
43
26
  }
44
27
  };
45
28
  })(BookReader.prototype.init);
46
29
 
47
- /**
48
- * Adds chapter marker to navigation scrubber
49
- *
50
- * @param {string} chapterTitle
51
- * @param {string} pageNumber
52
- * @param {number} pageIndex
53
- */
54
- BookReader.prototype.addChapter = function(chapterTitle, pageNumber, pageIndex) {
55
- const uiStringPage = 'Page'; // i18n
56
- const percentThrough = BookReader.util.cssPercentage(pageIndex, this.book.getNumLeafs() - 1);
57
- const jumpToChapter = (event) => {
58
- this.jumpToIndex($(event.delegateTarget).data('pageIndex'));
59
- $('.current-chapter').removeClass('current-chapter');
60
- $(event.delegateTarget).addClass('current-chapter');
61
- };
62
- const title = `${chapterTitle} | `;
63
- const pageStr = `${uiStringPage} ${pageNumber}`;
64
-
65
- //adding items to mobile table of contents
66
- const mobileChapter = $(`<li></li>`).append($(`<span class='BRTOCElementTitle'></span>`).text(title))
67
- .append($(`<span class='BRTOCElementPage'></span>`).text(pageStr));
68
- mobileChapter.addClass('BRtable-contents-el')
69
- .appendTo(this.$('.table-contents-list'))
70
- .data({ pageIndex });
71
-
72
- //adds .BRchapters to the slider only if pageIndex exists
73
- if (pageIndex != undefined) {
74
- $(`<div></div>`)
75
- .append($('<div />').text(title + pageStr))
76
- .addClass('BRchapter')
77
- .css({ left: percentThrough })
78
- .appendTo(this.$('.BRnavline'))
79
- .data({ pageIndex })
80
- .on("mouseenter", event => {
81
- // remove hover effect from other markers then turn on just for this
82
- const marker = event.currentTarget;
83
- const tooltip = marker.querySelector('div');
84
- const tooltipOffset = tooltip.getBoundingClientRect();
85
- const targetOffset = marker.getBoundingClientRect();
86
- const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
87
- if (tooltipOffset.x - boxSizeAdjust < 0) {
88
- tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
89
- }
90
- this.$('.BRsearch,.BRchapter').removeClass('front');
91
- $(event.target).addClass('front');
92
- })
93
- .on("mouseleave", event => $(event.target).removeClass('front'))
94
- .on('click', jumpToChapter);
95
-
96
- //adding clickable properties to mobile chapters
97
- mobileChapter.bind('click', jumpToChapter)
98
- .addClass('chapter-clickable')
99
- .attr("data-event-click-tracking","BRTOCPanel|GoToChapter");
30
+ BookReader.prototype._chapterInit = async function() {
31
+ const olEdition = await this.getOpenLibraryRecord(this.options.olHost, this.options.bookId);
32
+ if (olEdition?.table_of_contents?.length) {
33
+ this._tocEntries = olEdition.table_of_contents.map(rawTOCEntry => (
34
+ Object.assign({}, rawTOCEntry, {pageIndex: this.book.getPageIndex(rawTOCEntry.pagenum)})
35
+ ));
36
+ this._chaptersRender(this._tocEntries);
37
+ this.bind(BookReader.eventNames.pageChanged, () => this._chaptersUpdateCurrent());
100
38
  }
101
-
102
- };
103
-
104
- /*
105
- * Remove all chapters.
106
- */
107
- BookReader.prototype.removeChapters = function() {
108
- this.$('.BRnavpos .BRchapter').remove();
109
39
  };
110
40
 
111
41
  /**
112
42
  * Update the table of contents based on array of TOC entries.
113
- * @param {TocEntry[]} tocEntries
114
43
  */
115
- BookReader.prototype.updateTOC = function(tocEntries) {
116
- this.removeChapters();
117
- if (this.enableMobileNav && tocEntries.length > 0) {
118
- this.$(".BRmobileMenu__tableContents").show();
119
- }
120
- for (let i = 0; i < tocEntries.length; i++) {
121
- this.addChapterFromEntry(tocEntries[i]);
44
+ BookReader.prototype._chaptersRender = function() {
45
+ const shell = /** @type {BookNavigator} */(this.shell);
46
+ shell.menuProviders['chapters'] = {
47
+ id: 'chapters',
48
+ icon: html`<ia-icon-toc style="width: var(--iconWidth); height: var(--iconHeight);"></ia-icon-toc>`,
49
+ label: 'Table of Contents',
50
+ component: html`<br-chapters-panel
51
+ .contents="${this._tocEntries}"
52
+ .jumpToPage="${(pageIndex) => {
53
+ this._chaptersUpdateCurrent(pageIndex);
54
+ this.jumpToIndex(pageIndex);
55
+ }}"
56
+ @connected="${(e) => {
57
+ this._chaptersPanel = e.target;
58
+ this._chaptersUpdateCurrent();
59
+ }}"
60
+ />`,
61
+ };
62
+ shell.updateMenuContents();
63
+ for (const tocEntry of this._tocEntries) {
64
+ this._chaptersRenderMarker(tocEntry);
122
65
  }
123
- this._tocEntries = tocEntries;
124
- $('.table-contents-list').children().each((i, el) => {
125
- tocEntries[i].mobileHTML = el;
126
- });
127
66
  };
128
67
 
129
68
  /**
130
69
  * @typedef {Object} TocEntry
131
- * Table of contents entry as defined -- format is defined by Open Library
70
+ * Table of contents entry as defined by Open Library, with some extra values for internal use
132
71
  * @property {string} pagenum
133
72
  * @property {number} level
134
73
  * @property {string} label
135
- * @property {{type: '/type/toc_item'}} type
136
74
  * @property {string} title
137
- * @property {HTMLElement} mobileHTML
138
- * @property {number} pageIndex
139
-
75
+ * @property {number} pageIndex - Added
140
76
  *
141
77
  * @example {
142
78
  * "pagenum": "17",
143
79
  * "level": 1,
144
80
  * "label": "CHAPTER I",
145
- * "type": {"key": "/type/toc_item"},
146
81
  * "title": "THE COUNTRY AND THE MISSION"
147
82
  * }
148
83
  */
149
84
 
150
85
  /**
151
- * @param {TocEntry} tocEntryObject
86
+ * @param {TocEntry} tocEntry
152
87
  */
153
- BookReader.prototype.addChapterFromEntry = function(tocEntryObject) {
154
- tocEntryObject.pageIndex = this.book.getPageIndex(tocEntryObject['pagenum']);
155
- //creates a string with non-void tocEntryObject.label and tocEntryObject.title
156
- const chapterStr = [tocEntryObject.label, tocEntryObject.title]
88
+ BookReader.prototype._chaptersRenderMarker = function(tocEntry) {
89
+ if (tocEntry.pageIndex == undefined) return;
90
+
91
+ //creates a string with non-void tocEntry.label and tocEntry.title
92
+ const chapterStr = [tocEntry.label, tocEntry.title]
157
93
  .filter(x => x)
158
94
  .join(' ');
159
- this.addChapter(chapterStr, tocEntryObject['pagenum'], tocEntryObject.pageIndex);
95
+
96
+ const percentThrough = BookReader.util.cssPercentage(tocEntry.pageIndex, this.book.getNumLeafs() - 1);
97
+ $(`<div></div>`)
98
+ .append(
99
+ $('<div />')
100
+ .text(chapterStr)
101
+ .append($('<div class="BRchapterPage" />').text(`Page ${tocEntry.pagenum}`))
102
+ )
103
+ .addClass('BRchapter')
104
+ .css({ left: percentThrough })
105
+ .appendTo(this.$('.BRnavline'))
106
+ .on("mouseenter", event => {
107
+ // remove hover effect from other markers then turn on just for this
108
+ const marker = event.currentTarget;
109
+ const tooltip = marker.querySelector('div');
110
+ const tooltipOffset = tooltip.getBoundingClientRect();
111
+ const targetOffset = marker.getBoundingClientRect();
112
+ const boxSizeAdjust = parseInt(getComputedStyle(tooltip).paddingLeft) * 2;
113
+ if (tooltipOffset.x - boxSizeAdjust < 0) {
114
+ tooltip.style.setProperty('transform', `translateX(-${targetOffset.left - boxSizeAdjust}px)`);
115
+ }
116
+ this.$('.BRsearch,.BRchapter').removeClass('front');
117
+ $(event.target).addClass('front');
118
+ })
119
+ .on("mouseleave", event => $(event.target).removeClass('front'))
120
+ .on('click', () => {
121
+ this._chaptersUpdateCurrent(tocEntry.pageIndex);
122
+ this.jumpToIndex(tocEntry.pageIndex);
123
+ });
124
+
160
125
  this.$('.BRchapter, .BRsearch').each((i, el) => {
161
126
  const $el = $(el);
162
127
  $el
@@ -166,79 +131,146 @@ BookReader.prototype.addChapterFromEntry = function(tocEntryObject) {
166
131
  };
167
132
 
168
133
  /**
169
- * getOpenLibraryRecord
170
- *
171
- * The bookreader is designed to call openlibrary API and constructs the
172
- * "Return book" button using the response.
173
- *
174
134
  * This makes a call to OL API and calls the given callback function with the
175
135
  * response from the API.
136
+ *
137
+ * @param {string} olHost
138
+ * @param {string} ocaid
176
139
  */
177
- BookReader.prototype.getOpenLibraryRecord = async function () {
140
+ BookReader.prototype.getOpenLibraryRecord = async function (olHost, ocaid) {
178
141
  // Try looking up by ocaid first, then by source_record
179
- const baseURL = `${this.olHost}/query.json?type=/type/edition&*=`;
180
- const fetchUrlByBookId = `${baseURL}&ocaid=${this.bookId}`;
181
-
182
- /*
183
- * Update Chapter markers based on received record from Open Library.
184
- * Notes that Open Library record is used for extra metadata, and also for lending
185
- */
186
- const setUpChapterMarkers = (olObject) => {
187
- if (olObject && olObject.table_of_contents) {
188
- // XXX check here that TOC is valid
189
- this.updateTOC(olObject.table_of_contents);
190
- }
191
- };
142
+ const baseURL = `${olHost}/query.json?type=/type/edition&*=`;
143
+ const fetchUrlByBookId = `${baseURL}&ocaid=${ocaid}`;
192
144
 
193
145
  let data = await $.ajax({ url: fetchUrlByBookId, dataType: 'jsonp' });
194
146
 
195
147
  if (!data || !data.length) {
196
148
  // try sourceid
197
- data = await $.ajax({ url: `${baseURL}&source_records=ia:${this.bookId}`, dataType: 'jsonp' });
149
+ data = await $.ajax({ url: `${baseURL}&source_records=ia:${ocaid}`, dataType: 'jsonp' });
198
150
  }
199
151
 
200
- if (data && data.length > 0) {
201
- setUpChapterMarkers(data[0]);
202
- }
152
+ return data?.[0];
203
153
  };
204
154
 
205
- // Extend buildMobileDrawerElement with table of contents list
206
- BookReader.prototype.buildMobileDrawerElement = (function (super_) {
207
- return function () {
208
- const $el = super_.call(this);
209
- if (this.enableMobileNav && this.options.enableChaptersPlugin) {
210
- $el.find('.BRmobileMenu__moreInfoRow').after($(`
211
- <li class="BRmobileMenu__tableContents" data-event-click-tracking="BRSidebar|TOCPanel">
212
- <span>
213
- <span class="DrawerIconWrapper">
214
- <img class="DrawerIcon" src="${this.imagesBaseURL}icon_toc.svg" alt="toc-icon"/>
215
- </span>
216
- Table of Contents
217
- </span>
218
- <div>
219
- <ol class="table-contents-list">
220
- </ol>
221
- </div>
222
- </li>`).hide());
223
- }
224
- return $el;
225
- };
226
- })(BookReader.prototype.buildMobileDrawerElement);
227
-
228
155
  /**
229
- * highlights the current chapter based on current page
230
156
  * @private
231
- * @param {TocEntry[]} tocEntries
232
- * @param {number} tocEntries
157
+ * Highlights the current chapter based on current page
158
+ * @param {PageIndex} curIndex
233
159
  */
234
- BookReader.prototype.updateTOCState = function(currIndex, tocEntries) {
235
- //this function won't have any effects if called before OpenLibrary request is finished
236
- if (!tocEntries) {return;}
237
- $('.current-chapter').removeClass('current-chapter');
238
- const tocEntriesIndexed = tocEntries.filter((el) => el.pageIndex != undefined).reverse();
239
- const currChapter = tocEntriesIndexed[tocEntriesIndexed.findIndex(
240
- (el) => el.pageIndex <= currIndex)];
241
- if (currChapter != undefined) {
242
- $(currChapter.mobileHTML).addClass('current-chapter');
160
+ BookReader.prototype._chaptersUpdateCurrent = function(
161
+ curIndex = (this.mode == 2 ? Math.max(...this.displayedIndices) : this.firstIndex)
162
+ ) {
163
+ const tocEntriesIndexed = this._tocEntries.filter((el) => el.pageIndex != undefined).reverse();
164
+ const currChapter = tocEntriesIndexed[
165
+ // subtract one so that 2up shows the right label
166
+ tocEntriesIndexed.findIndex((chapter) => chapter.pageIndex <= curIndex)
167
+ ];
168
+ if (this._chaptersPanel) {
169
+ this._chaptersPanel.currentChapter = currChapter;
243
170
  }
244
171
  };
172
+
173
+ @customElement('br-chapters-panel')
174
+ export class BRChaptersPanel extends LitElement {
175
+ /** @type {TocEntry[]} */
176
+ @property({ type: Array })
177
+ contents = [];
178
+
179
+ /** @type {TocEntry?} */
180
+ @property({ type: Object })
181
+ currentChapter = {};
182
+
183
+ /** @type {(pageIndex: PageIndex) => void} */
184
+ jumpToPage = () => {};
185
+
186
+ /**
187
+ * @param {TocEntry[]} contents
188
+ */
189
+ constructor(contents) {
190
+ super();
191
+ this.contents = contents;
192
+ }
193
+
194
+ render() {
195
+ return html`
196
+ <ol>
197
+ ${this.contents.map(tocEntry => this.renderTOCEntry(tocEntry))}
198
+ </ol>
199
+ `;
200
+ }
201
+
202
+ /**
203
+ * @param {TocEntry} tocEntry
204
+ */
205
+ renderTOCEntry(tocEntry) {
206
+ const chapterTitle = [tocEntry.label, tocEntry.title]
207
+ .filter(x => x)
208
+ .join(' ');
209
+ const clickable = tocEntry.pageIndex != undefined;
210
+ // note the click-tracking won't work...
211
+ return html`
212
+ <li
213
+ class="
214
+ BRtable-contents-el
215
+ ${clickable ? 'clickable' : ''}
216
+ ${tocEntry == this.currentChapter ? 'current' : ''}
217
+ "
218
+ style="${styleMap({marginLeft: (tocEntry.level - 1) * 10 + 'px'})}"
219
+ data-event-click-tracking="${ifDefined(clickable ? "BRTOCPanel|GoToChapter" : undefined)}"
220
+ @click="${() => this.jumpToPage(tocEntry.pageIndex)}"
221
+ >
222
+ ${chapterTitle}
223
+ ${tocEntry.pagenum ? html`
224
+ <br />
225
+ <span class="BRTOCElementPage">Page ${tocEntry.pagenum}</span>
226
+ ` : nothing}
227
+ </li>`;
228
+ }
229
+
230
+ connectedCallback() {
231
+ super.connectedCallback();
232
+ this.dispatchEvent(new CustomEvent('connected'));
233
+ }
234
+
235
+ updated(changedProperties) {
236
+ if (changedProperties.has('currentChapter')) {
237
+ this.shadowRoot.querySelector('li.current')?.scrollIntoView({
238
+ block: 'nearest',
239
+ behavior: 'smooth',
240
+ });
241
+ }
242
+ }
243
+
244
+ static get styles() {
245
+ return css`
246
+ ol {
247
+ padding: 0;
248
+ margin: 0;
249
+ margin-right: 5px;
250
+ }
251
+ li {
252
+ padding: 10px;
253
+ overflow: hidden;
254
+ border-radius: 4px;
255
+ }
256
+ li.clickable {
257
+ font-weight: normal;
258
+ cursor: pointer;
259
+ transition: background-color 0.2s;
260
+ }
261
+
262
+ li.clickable:not(.current):hover {
263
+ background-color: rgba(255,255,255, 0.05);
264
+ }
265
+
266
+ li.current {
267
+ background-color: rgba(255,255,255,0.9);
268
+ color: #333;
269
+ }
270
+
271
+ .BRTOCElementPage {
272
+ font-size: 0.85em;
273
+ opacity: .8;
274
+ }`;
275
+ }
276
+ }
@@ -195,7 +195,7 @@ BookReader.prototype.initNavbar = (function (super_) {
195
195
  $(`<li>
196
196
  <button class="BRicon read js-tooltip" title="${tooltips.readAloud}">
197
197
  <div class="icon icon-read-aloud"></div>
198
- <span class="tooltip">${tooltips.readAloud}</span>
198
+ <span class="BRtooltip">${tooltips.readAloud}</span>
199
199
  </button>
200
200
  </li>`).insertBefore($el.find('.BRcontrols .BRicon.zoom_out').closest('li'));
201
201
  }
@@ -1,37 +1,36 @@
1
- import sinon from 'sinon';
1
+ import sinon from "sinon";
2
2
 
3
- import BookReader from '@/src/BookReader.js';
4
- import '@/src/plugins/plugin.mobile_nav.js';
5
- import '@/src/plugins/plugin.chapters.js';
3
+ import BookReader from "@/src/BookReader.js";
4
+ import "@/src/plugins/plugin.mobile_nav.js";
5
+ import "@/src/plugins/plugin.chapters.js";
6
+ import { BookModel } from "@/src/BookReader/BookModel";
7
+ import { deepCopy } from "../utils";
6
8
 
7
9
  const SAMPLE_TOC = [{
8
10
  "pagenum": "3",
9
11
  "level": 1,
10
12
  "label": "CHAPTER I",
11
- "type": {"key": "/type/toc_item"},
13
+ "type": { "key": "/type/toc_item" },
12
14
  "title": "THE COUNTRY AND THE MISSION 1",
13
15
  "pageIndex": 3,
14
- },{
16
+ }, {
15
17
  "pagenum": "17",
16
18
  "level": 1,
17
19
  "label": "CHAPTER II",
18
- "type": {"key": "/type/toc_item"},
20
+ "type": { "key": "/type/toc_item" },
19
21
  "title": "THE COUNTRY AND THE MISSION 2",
20
22
  "pageIndex": 17,
21
- },
22
- {
23
+ }, {
23
24
  "pagenum": "undefined",
24
25
  "level": 1,
25
26
  "label": "CHAPTER III",
26
- "type": {"key": "/type/toc_item"},
27
+ "type": { "key": "/type/toc_item" },
27
28
  "title": "THE COUNTRY AND THE MISSION 3",
28
- "pageIndex": undefined,
29
- },
30
- {
29
+ }, {
31
30
  "pagenum": "40",
32
31
  "level": 1,
33
32
  "label": "CHAPTER IV",
34
- "type": {"key": "/type/toc_item"},
33
+ "type": { "key": "/type/toc_item" },
35
34
  "title": "THE COUNTRY AND THE MISSION 4",
36
35
  "pageIndex": 40,
37
36
  }];
@@ -41,89 +40,107 @@ const SAMPLE_TOC_UNDEF = [
41
40
  "pagenum": "undefined",
42
41
  "level": 1,
43
42
  "label": "CHAPTER I",
44
- "type": {"key": "/type/toc_item"},
43
+ "type": { "key": "/type/toc_item" },
45
44
  "title": "THE COUNTRY AND THE MISSION 1",
46
- "pageIndex": undefined,
47
45
  },
48
46
  {
49
47
  "pagenum": "undefined",
50
48
  "level": 1,
51
49
  "label": "CHAPTER II",
52
- "type": {"key": "/type/toc_item"},
50
+ "type": { "key": "/type/toc_item" },
53
51
  "title": "THE COUNTRY AND THE MISSION 2",
54
- "pageIndex": undefined,
55
- }
52
+ },
56
53
  ];
57
54
 
58
- /** @type {BookReader} */
59
- let br;
60
- beforeEach(() => {
61
- $.ajax = jest.fn().mockImplementation((args) => {
62
- return Promise.resolve([{
63
- table_of_contents: SAMPLE_TOC,
64
- }]);
65
- });
66
- document.body.innerHTML = '<div id="BookReader">';
67
- br = new BookReader();
68
- });
69
-
70
55
  afterEach(() => {
71
- jest.clearAllMocks();
56
+ sinon.restore();
72
57
  });
73
58
 
74
- describe('Plugin: Menu Toggle', () => {
75
- test('has option flag', () => {
76
- expect(BookReader.defaultOptions.enableChaptersPlugin).toEqual(true);
77
- });
78
- test('has added BR property: olHost', () => {
79
- expect(br).toHaveProperty('olHost');
80
- expect(br.olHost).toBeTruthy();
81
- });
82
- test('has added BR property: bookId', () => {
83
- expect(br).toHaveProperty('bookId');
84
- expect(br.bookId).toBeFalsy();
85
- });
86
- test('fetches OL Book Record on init', () => {
87
- br.getOpenLibraryRecord = jest.fn();
88
- br.init();
89
- expect(br.getOpenLibraryRecord).toHaveBeenCalled();
90
- });
91
- test('Updates Table of Contents when available', () => {
92
- br.init();
93
- expect($.ajax).toHaveBeenCalled();
59
+ describe("BRChaptersPlugin", () => {
60
+ beforeEach(() => {
61
+ sinon.stub(BookModel.prototype, "getPageIndex").callsFake((str) =>
62
+ parseFloat(str)
63
+ );
94
64
  });
95
- });
96
65
 
97
- describe('updateTOCState', () => {
66
+ describe("_chaptersInit", () => {
67
+ test("does not render when no open library record", async () => {
68
+ const fakeBR = {
69
+ options: {},
70
+ getOpenLibraryRecord: async () => null,
71
+ _chaptersRender: sinon.stub(),
72
+ };
73
+ await BookReader.prototype._chapterInit.call(fakeBR);
74
+ expect(fakeBR._chaptersRender.callCount).toBe(0);
75
+ });
98
76
 
99
- beforeEach(() => {
100
- window.HTMLElement.prototype.scrollIntoView = sinon.stub();
101
- br.book.getPageIndex = (str) => parseFloat(str);
102
- br.init();
103
- });
77
+ test("does not render when open library record with no TOC", async () => {
78
+ const fakeBR = {
79
+ options: {},
80
+ getOpenLibraryRecord: async () => ({ key: "/books/OL1M" }),
81
+ _chaptersRender: sinon.stub(),
82
+ };
83
+ await BookReader.prototype._chapterInit.call(fakeBR);
84
+ expect(fakeBR._chaptersRender.callCount).toBe(0);
85
+ });
104
86
 
105
- test('Test page is one of TOC', () => {
106
- br.updateTOCState(20, SAMPLE_TOC);
107
- expect($('li.BRtable-contents-el')[1].classList.contains('current-chapter'));
87
+ test("renders if valid TOC on open library", async () => {
88
+ const fakeBR = {
89
+ options: {},
90
+ bind: sinon.stub(),
91
+ book: {
92
+ getPageIndex: (str) => parseFloat(str),
93
+ },
94
+ getOpenLibraryRecord: async () => ({
95
+ "title": "The Adventures of Sherlock Holmes",
96
+ "table_of_contents": deepCopy(SAMPLE_TOC_UNDEF),
97
+ "ocaid": "adventureofsherl0000unse",
98
+ }),
99
+ _chaptersRender: sinon.stub(),
100
+ };
101
+ await BookReader.prototype._chapterInit.call(fakeBR);
102
+ expect(fakeBR._chaptersRender.callCount).toBe(1);
103
+ });
108
104
  });
109
105
 
110
- test('Only one element has .current-chapter', () => {
111
- br.updateTOCState(9, SAMPLE_TOC);
112
- expect($('li.BRtable-contents-el.current-chapter').length).toBe(1);
106
+ describe('_chaptersRender', () => {
107
+ test('renders markers and panel', () => {
108
+ const fakeBR = {
109
+ _tocEntries: SAMPLE_TOC,
110
+ _chaptersRenderMarker: sinon.stub(),
111
+ shell: {
112
+ menuProviders: {},
113
+ updateMenuContents: sinon.stub(),
114
+ }
115
+ };
116
+ BookReader.prototype._chaptersRender.call(fakeBR);
117
+ expect(fakeBR.shell.menuProviders['chapters']).toBeTruthy();
118
+ expect(fakeBR.shell.updateMenuContents.callCount).toBe(1);
119
+ expect(fakeBR._chaptersRenderMarker.callCount).toBeGreaterThan(1);
120
+ });
113
121
  });
114
122
 
115
- test('No chapter has .current-chapter if current index is < than any chapter index', () => {
116
- br.updateTOCState(1, SAMPLE_TOC);
117
- expect($('li.BRtable-contents-el.current-chapter').length).toBe(0);
118
- });
123
+ describe('_chaptersUpdateCurrent', () => {
124
+ test('highlights the current chapter', () => {
125
+ const fakeBR = {
126
+ mode: 2,
127
+ firstIndex: 16,
128
+ displayedIndices: [16, 17],
129
+ _tocEntries: SAMPLE_TOC,
130
+ _chaptersPanel: {
131
+ currentChapter: null,
132
+ }
133
+ };
134
+ BookReader.prototype._chaptersUpdateCurrent.call(fakeBR);
135
+ expect(fakeBR._chaptersPanel.currentChapter).toEqual(SAMPLE_TOC[1]);
119
136
 
120
- test('if current index == chapter index ', () => {
121
- br.updateTOCState(17, SAMPLE_TOC);
122
- expect($('li.BRtable-contents-el')[1].classList.contains('current-chapter'));
123
- });
137
+ fakeBR.mode = 1;
138
+ BookReader.prototype._chaptersUpdateCurrent.call(fakeBR);
139
+ expect(fakeBR._chaptersPanel.currentChapter).toEqual(SAMPLE_TOC[0]);
124
140
 
125
- test('No chapter has .current-chapter if all chapter have undefined pageIndex ', () => {
126
- br.updateTOCState(10, SAMPLE_TOC_UNDEF);
127
- expect($('li.BRtable-contents-el.current-chapter').length).toBe(0);
141
+ fakeBR.firstIndex = 0;
142
+ BookReader.prototype._chaptersUpdateCurrent.call(fakeBR);
143
+ expect(fakeBR._chaptersPanel.currentChapter).toBeUndefined();
144
+ });
128
145
  });
129
146
  });