@internetarchive/bookreader 5.0.0-65 → 5.0.0-67

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. package/BookReader/BookReader.css +16 -28
  2. package/BookReader/BookReader.js +1 -1
  3. package/BookReader/BookReader.js.LICENSE.txt +0 -6
  4. package/BookReader/BookReader.js.map +1 -1
  5. package/BookReader/ia-bookreader-bundle.js +102 -114
  6. package/BookReader/ia-bookreader-bundle.js.map +1 -1
  7. package/BookReader/plugins/plugin.chapters.js +24 -1
  8. package/BookReader/plugins/plugin.chapters.js.map +1 -1
  9. package/BookReader/plugins/plugin.search.js +1 -1
  10. package/BookReader/plugins/plugin.search.js.map +1 -1
  11. package/BookReader/plugins/plugin.text_selection.js +1 -1
  12. package/BookReader/plugins/plugin.text_selection.js.map +1 -1
  13. package/BookReader/plugins/plugin.tts.js +1 -1
  14. package/BookReader/plugins/plugin.tts.js.map +1 -1
  15. package/BookReaderDemo/demo-internetarchive.html +41 -3
  16. package/BookReaderDemo/toggle_controls.html +1 -1
  17. package/CHANGELOG.md +8 -0
  18. package/index.html +1 -1
  19. package/package.json +5 -4
  20. package/src/BookNavigator/book-navigator.js +4 -3
  21. package/src/BookReader/BookModel.js +12 -0
  22. package/src/BookReader/Mode1Up.js +1 -1
  23. package/src/BookReader/Mode1UpLit.js +0 -3
  24. package/src/BookReader/Mode2UpLit.js +0 -4
  25. package/src/BookReader/ModeSmoothZoom.js +153 -52
  26. package/src/BookReader/Navbar/Navbar.js +2 -2
  27. package/src/css/_BRnav.scss +5 -2
  28. package/src/css/_BRsearch.scss +6 -2
  29. package/src/css/_MobileNav.scss +0 -26
  30. package/src/css/_controls.scss +3 -2
  31. package/src/plugins/plugin.chapters.js +201 -169
  32. package/src/plugins/tts/plugin.tts.js +1 -1
  33. package/src/util/browserSniffing.js +22 -0
  34. package/tests/jest/BookReader/ModeSmoothZoom.test.js +75 -32
  35. package/tests/jest/plugins/plugin.chapters.test.js +93 -76
@@ -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
  }
@@ -28,3 +28,25 @@ export function isFirefox(userAgent = navigator.userAgent) {
28
28
  export function isSafari(userAgent = navigator.userAgent) {
29
29
  return /safari/i.test(userAgent) && !/chrome|chromium/i.test(userAgent);
30
30
  }
31
+
32
+ /**
33
+ * Checks whether the current browser is iOS (and hence iOS webkit)
34
+ * @return {boolean}
35
+ */
36
+ export function isIOS() {
37
+ // We can't just check the userAgent because as of iOS 13,
38
+ // the userAgent is the same as desktop Safari because
39
+ // they wanted iPad's to be served the same version of websites
40
+ // as desktops.
41
+ return 'ongesturestart' in window && navigator.maxTouchPoints > 0;
42
+ }
43
+
44
+ /**
45
+ * Checks whether the current browser is Samsung Internet
46
+ * https://stackoverflow.com/a/40684162/2317712
47
+ * @param {string} [userAgent]
48
+ * @return {boolean}
49
+ */
50
+ export function isSamsungInternet(userAgent = navigator.userAgent) {
51
+ return /SamsungBrowser/i.test(userAgent);
52
+ }
@@ -1,6 +1,7 @@
1
1
  import sinon from 'sinon';
2
- import { EventTargetSpy } from '../utils.js';
3
- import { ModeSmoothZoom } from '@/src/BookReader/ModeSmoothZoom.js';
2
+ import interact from 'interactjs';
3
+ import { EventTargetSpy, afterEventLoop } from '../utils.js';
4
+ import { ModeSmoothZoom, TouchesMonitor } from '@/src/BookReader/ModeSmoothZoom.js';
4
5
  /** @typedef {import('@/src/BookReader/ModeSmoothZoom.js').SmoothZoomable} SmoothZoomable */
5
6
 
6
7
  /**
@@ -22,33 +23,32 @@ function dummy_mode(overrides = {}) {
22
23
  };
23
24
  }
24
25
 
25
- afterEach(() => sinon.restore());
26
+ afterEach(() => {
27
+ sinon.restore();
28
+ try {
29
+ interact.removeDocument(document);
30
+ } catch (e) {}
31
+ });
26
32
 
27
33
  describe('ModeSmoothZoom', () => {
28
- test('preventsDefault on iOS-only gesture events', () => {
34
+ test('handle iOS-only gesture events', () => {
29
35
  const mode = dummy_mode();
30
36
  const msz = new ModeSmoothZoom(mode);
37
+ sinon.stub(msz, '_pinchStart');
38
+ sinon.stub(msz, '_pinchMove');
39
+ sinon.stub(msz, '_pinchEnd');
40
+
31
41
  msz.attach();
32
- for (const event_name of ['gesturestart', 'gesturechange', 'gestureend']) {
33
- const ev = new Event(event_name, {});
34
- const prevDefaultSpy = sinon.spy(ev, 'preventDefault');
35
- mode.$container.dispatchEvent(ev);
36
- expect(prevDefaultSpy.callCount).toBe(1);
37
- }
38
- });
39
42
 
40
- test('pinchCancel alias for pinchEnd', () => {
41
- const mode = dummy_mode();
42
- const msz = new ModeSmoothZoom(mode);
43
- const pinchEndSpy = sinon.spy(msz, '_pinchEnd');
44
- msz._pinchStart();
45
- msz._pinchCancel();
46
- expect(pinchEndSpy.callCount).toBe(1);
43
+ const gesturestart = new Event('gesturestart', {});
44
+ mode.$container.dispatchEvent(gesturestart);
45
+ expect(msz._pinchStart.callCount).toBe(1);
47
46
  });
48
47
 
49
48
  test('sets will-change', async () => {
50
49
  const mode = dummy_mode();
51
50
  const msz = new ModeSmoothZoom(mode);
51
+ msz.attach();
52
52
  expect(mode.$visibleWorld.style.willChange).toBeFalsy();
53
53
  msz._pinchStart();
54
54
  expect(mode.$visibleWorld.style.willChange).toBe('transform');
@@ -59,6 +59,7 @@ describe('ModeSmoothZoom', () => {
59
59
  test('pinch move updates scale', () => {
60
60
  const mode = dummy_mode();
61
61
  const msz = new ModeSmoothZoom(mode);
62
+ msz.attach();
62
63
  // disable buffering
63
64
  msz.bufferFn = (callback) => callback();
64
65
  msz._pinchStart();
@@ -79,48 +80,47 @@ describe('ModeSmoothZoom', () => {
79
80
  }
80
81
  });
81
82
  const msz = new ModeSmoothZoom(mode);
82
- expect(mode.scaleCenter).toEqual({ x: 0.5, y: 0.5 });
83
+ expect(msz.scaleCenter).toEqual({ x: 0.5, y: 0.5 });
83
84
  msz.updateScaleCenter({ clientX: 85, clientY: 110 });
84
- expect(mode.scaleCenter).toEqual({ x: 0.4, y: 0.6 });
85
+ expect(msz.scaleCenter).toEqual({ x: 0.4, y: 0.6 });
85
86
  });
86
87
 
87
- test('detaches all listeners', () => {
88
+ test('detaches all listeners', async () => {
88
89
  const mode = dummy_mode();
89
90
  const msz = new ModeSmoothZoom(mode);
91
+
92
+ const documentEventSpy = EventTargetSpy.wrap(document);
90
93
  const containerEventSpy = EventTargetSpy.wrap(mode.$container);
91
94
  const visibleWorldSpy = EventTargetSpy.wrap(mode.$visibleWorld);
92
- const hammerEventSpy = new EventTargetSpy();
93
- msz.hammer.on = hammerEventSpy.addEventListener.bind(hammerEventSpy);
94
- msz.hammer.off = hammerEventSpy.removeEventListener.bind(hammerEventSpy);
95
95
 
96
96
  msz.attach();
97
+ await afterEventLoop();
98
+ expect(documentEventSpy._totalListenerCount).toBeGreaterThan(0);
97
99
  expect(containerEventSpy._totalListenerCount).toBeGreaterThan(0);
98
- expect(hammerEventSpy._totalListenerCount).toBeGreaterThan(0);
99
100
 
100
101
  msz.detach();
102
+ expect(documentEventSpy._totalListenerCount).toBe(0);
101
103
  expect(containerEventSpy._totalListenerCount).toBe(0);
102
104
  expect(visibleWorldSpy._totalListenerCount).toBe(0);
103
- expect(hammerEventSpy._totalListenerCount).toBe(0);
104
105
  });
105
106
 
106
107
  test('attach can be called twice without double attachments', () => {
107
108
  const mode = dummy_mode();
108
109
  const msz = new ModeSmoothZoom(mode);
110
+
111
+ const documentEventSpy = EventTargetSpy.wrap(document);
109
112
  const containerEventSpy = EventTargetSpy.wrap(mode.$container);
110
113
  const visibleWorldSpy = EventTargetSpy.wrap(mode.$visibleWorld);
111
- const hammerEventSpy = new EventTargetSpy();
112
- msz.hammer.on = hammerEventSpy.addEventListener.bind(hammerEventSpy);
113
- msz.hammer.off = hammerEventSpy.removeEventListener.bind(hammerEventSpy);
114
- msz.attach();
115
114
 
115
+ msz.attach();
116
+ const documentListenersCount = documentEventSpy._totalListenerCount;
116
117
  const containerListenersCount = containerEventSpy._totalListenerCount;
117
118
  const visibleWorldListenersCount = visibleWorldSpy._totalListenerCount;
118
- const hammerListenersCount = hammerEventSpy._totalListenerCount;
119
119
 
120
120
  msz.attach();
121
+ expect(documentEventSpy._totalListenerCount).toBe(documentListenersCount);
121
122
  expect(containerEventSpy._totalListenerCount).toBe(containerListenersCount);
122
123
  expect(visibleWorldSpy._totalListenerCount).toBe(visibleWorldListenersCount);
123
- expect(hammerEventSpy._totalListenerCount).toBe(hammerListenersCount);
124
124
  });
125
125
 
126
126
  describe('_handleCtrlWheel', () => {
@@ -173,3 +173,46 @@ describe('ModeSmoothZoom', () => {
173
173
  });
174
174
  });
175
175
  });
176
+
177
+
178
+ describe("TouchesMonitor", () => {
179
+ /** @type {HTMLElement} */
180
+ let container;
181
+ /** @type {TouchesMonitor} */
182
+ let monitor;
183
+
184
+ beforeEach(() => {
185
+ container = document.createElement("div");
186
+ monitor = new TouchesMonitor(container);
187
+ });
188
+
189
+ afterEach(() => {
190
+ monitor.detach();
191
+ });
192
+
193
+ test("should start with 0 touches", () => {
194
+ expect(monitor.touches).toBe(0);
195
+ });
196
+
197
+ test("should update touch count on touch events", () => {
198
+ monitor.attach();
199
+ container.dispatchEvent(new TouchEvent("touchstart", { touches: [{}] }));
200
+ expect(monitor.touches).toBe(1);
201
+
202
+ container.dispatchEvent(new TouchEvent("touchstart", { touches: [{}, {}] }));
203
+ expect(monitor.touches).toBe(2);
204
+
205
+ container.dispatchEvent(new TouchEvent("touchend", { touches: [{}] }));
206
+ expect(monitor.touches).toBe(1);
207
+
208
+ container.dispatchEvent(new TouchEvent("touchend", { touches: [] }));
209
+ });
210
+
211
+ test("should detach all listeners", () => {
212
+ const spy = EventTargetSpy.wrap(container);
213
+ monitor.attach();
214
+ expect(spy._totalListenerCount).toBeGreaterThan(0);
215
+ monitor.detach();
216
+ expect(spy._totalListenerCount).toBe(0);
217
+ });
218
+ });