@internetarchive/bookreader 5.0.0-65 → 5.0.0-66
Sign up to get free protection for your applications and to get access to all the features.
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.LICENSE.txt +0 -6
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/CHANGELOG.md +3 -0
- package/package.json +2 -2
- package/src/BookReader/Mode1Up.js +1 -1
- package/src/BookReader/Mode1UpLit.js +0 -3
- package/src/BookReader/Mode2UpLit.js +0 -4
- package/src/BookReader/ModeSmoothZoom.js +153 -52
- package/src/util/browserSniffing.js +22 -0
- package/tests/jest/BookReader/ModeSmoothZoom.test.js +75 -32
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@internetarchive/bookreader",
|
3
|
-
"version": "5.0.0-
|
3
|
+
"version": "5.0.0-66",
|
4
4
|
"description": "The Internet Archive BookReader.",
|
5
5
|
"repository": {
|
6
6
|
"type": "git",
|
@@ -57,8 +57,8 @@
|
|
57
57
|
"eslint": "^7.32.0",
|
58
58
|
"eslint-plugin-no-jquery": "^2.7.0",
|
59
59
|
"eslint-plugin-testcafe": "^0.2.1",
|
60
|
-
"hammerjs": "^2.0.8",
|
61
60
|
"http-server": "14.1.1",
|
61
|
+
"interactjs": "^1.10.18",
|
62
62
|
"iso-language-codes": "1.1.0",
|
63
63
|
"jest": "29.6.2",
|
64
64
|
"jest-environment-jsdom": "^29.4.3",
|
@@ -51,7 +51,7 @@ export class Mode1Up {
|
|
51
51
|
new DragScrollable(this.mode1UpLit, {
|
52
52
|
preventDefault: true,
|
53
53
|
dragSelector: '.br-mode-1up__visible-world',
|
54
|
-
// Only handle mouse events; let browser/
|
54
|
+
// Only handle mouse events; let browser/interact.js handle touch
|
55
55
|
dragstart: 'mousedown',
|
56
56
|
dragcontinue: 'mousemove',
|
57
57
|
dragend: 'mouseup',
|
@@ -47,9 +47,6 @@ export class Mode1UpLit extends LitElement {
|
|
47
47
|
|
48
48
|
@property({ type: Number })
|
49
49
|
scale = 1;
|
50
|
-
/** Position (in unit-less, [0, 1] coordinates) in client to scale around */
|
51
|
-
@property({ type: Object })
|
52
|
-
scaleCenter = { x: 0.5, y: 0.5 };
|
53
50
|
|
54
51
|
/************** VIRTUAL-SCROLLING PROPERTIES **************/
|
55
52
|
|
@@ -41,10 +41,6 @@ export class Mode2UpLit extends LitElement {
|
|
41
41
|
|
42
42
|
initialScale = 1;
|
43
43
|
|
44
|
-
/** Position (in unit-less, [0, 1] coordinates) in client to scale around */
|
45
|
-
@property({ type: Object })
|
46
|
-
scaleCenter = { x: 0.5, y: 0.5 };
|
47
|
-
|
48
44
|
/** @type {import('./options').AutoFitValues} */
|
49
45
|
@property({ type: String })
|
50
46
|
autoFit = 'auto';
|
@@ -1,5 +1,7 @@
|
|
1
1
|
// @ts-check
|
2
|
-
import
|
2
|
+
import interact from 'interactjs';
|
3
|
+
import { isIOS, isSamsungInternet } from '../util/browserSniffing.js';
|
4
|
+
import { sleep } from './utils.js';
|
3
5
|
/** @typedef {import('./utils/HTMLDimensionsCacher.js').HTMLDimensionsCacher} HTMLDimensionsCacher */
|
4
6
|
|
5
7
|
/**
|
@@ -8,7 +10,6 @@ import Hammer from "hammerjs";
|
|
8
10
|
* @property {HTMLElement} $visibleWorld
|
9
11
|
* @property {import("./options.js").AutoFitValues} autoFit
|
10
12
|
* @property {number} scale
|
11
|
-
* @property {{ x: number, y: number }} scaleCenter
|
12
13
|
* @property {HTMLDimensionsCacher} htmlDimensionsCacher
|
13
14
|
* @property {function(): void} [attachScrollListeners]
|
14
15
|
* @property {function(): void} [detachScrollListeners]
|
@@ -16,31 +17,27 @@ import Hammer from "hammerjs";
|
|
16
17
|
|
17
18
|
/** Manages pinch-zoom, ctrl-wheel, and trackpad pinch smooth zooming. */
|
18
19
|
export class ModeSmoothZoom {
|
20
|
+
/** Position (in unit-less, [0, 1] coordinates) in client to scale around */
|
21
|
+
scaleCenter = { x: 0.5, y: 0.5 };
|
22
|
+
|
19
23
|
/** @param {SmoothZoomable} mode */
|
20
24
|
constructor(mode) {
|
21
25
|
/** @type {SmoothZoomable} */
|
22
26
|
this.mode = mode;
|
23
27
|
|
28
|
+
/** Whether a pinch is currently happening */
|
29
|
+
this.pinching = false;
|
24
30
|
/** Non-null when a scale has been enqueued/is being processed by the buffer function */
|
25
31
|
this.pinchMoveFrame = null;
|
26
32
|
/** Promise for the current/enqueued pinch move frame. Resolves when it is complete. */
|
27
33
|
this.pinchMoveFramePromise = Promise.resolve();
|
28
34
|
this.oldScale = 1;
|
29
|
-
/** @type {{ scale: number,
|
35
|
+
/** @type {{ scale: number, clientX: number, clientY: number }}} */
|
30
36
|
this.lastEvent = null;
|
31
37
|
this.attached = false;
|
32
38
|
|
33
39
|
/** @type {function(function(): void): any} */
|
34
40
|
this.bufferFn = window.requestAnimationFrame.bind(window);
|
35
|
-
|
36
|
-
// Hammer.js by default set userSelect to None; we don't want that!
|
37
|
-
// TODO: Is there any way to do this not globally on Hammer?
|
38
|
-
delete Hammer.defaults.cssProps.userSelect;
|
39
|
-
this.hammer = new Hammer.Manager(this.mode.$container, {
|
40
|
-
touchAction: "pan-x pan-y",
|
41
|
-
});
|
42
|
-
|
43
|
-
this.hammer.add(new Hammer.Pinch());
|
44
41
|
}
|
45
42
|
|
46
43
|
attach() {
|
@@ -48,17 +45,44 @@ export class ModeSmoothZoom {
|
|
48
45
|
|
49
46
|
this.attachCtrlZoom();
|
50
47
|
|
51
|
-
// GestureEvents work only on Safari; they
|
52
|
-
//
|
53
|
-
|
48
|
+
// GestureEvents work only on Safari; they're too glitchy to use
|
49
|
+
// fully, but they can sometimes help error correct when interact
|
50
|
+
// misses an end/start event on Safari due to Safari bugs.
|
51
|
+
this.mode.$container.addEventListener('gesturestart', this._pinchStart);
|
54
52
|
this.mode.$container.addEventListener('gesturechange', this._preventEvent);
|
55
|
-
this.mode.$container.addEventListener('gestureend', this.
|
53
|
+
this.mode.$container.addEventListener('gestureend', this._pinchEnd);
|
54
|
+
|
55
|
+
if (isIOS()) {
|
56
|
+
this.touchesMonitor = new TouchesMonitor(this.mode.$container);
|
57
|
+
this.touchesMonitor.attach();
|
58
|
+
}
|
59
|
+
|
60
|
+
this.mode.$container.style.touchAction = "pan-x pan-y";
|
56
61
|
|
57
62
|
// The pinch listeners
|
58
|
-
this.
|
59
|
-
this.
|
60
|
-
|
61
|
-
|
63
|
+
this.interact = interact(this.mode.$container);
|
64
|
+
this.interact.gesturable({
|
65
|
+
listeners: {
|
66
|
+
start: this._pinchStart,
|
67
|
+
end: this._pinchEnd,
|
68
|
+
}
|
69
|
+
});
|
70
|
+
if (isSamsungInternet()) {
|
71
|
+
// Samsung internet pinch-zoom will not work unless we disable
|
72
|
+
// all touch actions. So use interact.js' built-in drag support
|
73
|
+
// to handle moving on that browser.
|
74
|
+
this.mode.$container.style.touchAction = "none";
|
75
|
+
this.interact
|
76
|
+
.draggable({
|
77
|
+
inertia: {
|
78
|
+
resistance: 2,
|
79
|
+
minSpeed: 100,
|
80
|
+
allowResume: true,
|
81
|
+
},
|
82
|
+
listeners: { move: this._dragMove }
|
83
|
+
});
|
84
|
+
}
|
85
|
+
|
62
86
|
|
63
87
|
this.attached = true;
|
64
88
|
}
|
@@ -68,15 +92,15 @@ export class ModeSmoothZoom {
|
|
68
92
|
|
69
93
|
// GestureEvents work only on Safari; they interfere with Hammer,
|
70
94
|
// so block them.
|
71
|
-
this.mode.$container.removeEventListener('gesturestart', this.
|
95
|
+
this.mode.$container.removeEventListener('gesturestart', this._pinchStart);
|
72
96
|
this.mode.$container.removeEventListener('gesturechange', this._preventEvent);
|
73
|
-
this.mode.$container.removeEventListener('gestureend', this.
|
97
|
+
this.mode.$container.removeEventListener('gestureend', this._pinchEnd);
|
98
|
+
|
99
|
+
this.touchesMonitor?.detach?.();
|
74
100
|
|
75
101
|
// The pinch listeners
|
76
|
-
this.
|
77
|
-
|
78
|
-
this.hammer.off("pinchend", this._pinchEnd);
|
79
|
-
this.hammer.off("pinchcancel", this._pinchCancel);
|
102
|
+
this.interact.unset();
|
103
|
+
interact.removeDocument(document);
|
80
104
|
|
81
105
|
this.attached = false;
|
82
106
|
}
|
@@ -87,7 +111,16 @@ export class ModeSmoothZoom {
|
|
87
111
|
return false;
|
88
112
|
}
|
89
113
|
|
90
|
-
_pinchStart = () => {
|
114
|
+
_pinchStart = async () => {
|
115
|
+
// Safari calls gesturestart twice!
|
116
|
+
if (this.pinching) return;
|
117
|
+
if (isIOS()) {
|
118
|
+
// Safari sometimes causes a pinch to trigger when there's only one touch!
|
119
|
+
await sleep(0); // touches monitor can receive the touch event late
|
120
|
+
if (this.touchesMonitor.touches < 2) return;
|
121
|
+
}
|
122
|
+
this.pinching = true;
|
123
|
+
|
91
124
|
// Do this in case the pinchend hasn't fired yet.
|
92
125
|
this.oldScale = 1;
|
93
126
|
this.mode.$visibleWorld.classList.add("BRsmooth-zooming");
|
@@ -95,37 +128,44 @@ export class ModeSmoothZoom {
|
|
95
128
|
this.mode.autoFit = "none";
|
96
129
|
this.detachCtrlZoom();
|
97
130
|
this.mode.detachScrollListeners?.();
|
131
|
+
|
132
|
+
this.interact.gesturable({
|
133
|
+
listeners: {
|
134
|
+
start: this._pinchStart,
|
135
|
+
move: this._pinchMove,
|
136
|
+
end: this._pinchEnd,
|
137
|
+
}
|
138
|
+
});
|
98
139
|
}
|
99
140
|
|
100
|
-
/** @param {{ scale: number,
|
141
|
+
/** @param {{ scale: number, clientX: number, clientY: number }}} e */
|
101
142
|
_pinchMove = async (e) => {
|
102
|
-
this.
|
143
|
+
if (!this.pinching) return;
|
144
|
+
this.lastEvent = {
|
145
|
+
scale: e.scale,
|
146
|
+
clientX: e.clientX,
|
147
|
+
clientY: e.clientY,
|
148
|
+
};
|
103
149
|
if (!this.pinchMoveFrame) {
|
104
|
-
let pinchMoveFramePromiseRes = null;
|
105
|
-
this.pinchMoveFramePromise = new Promise(
|
106
|
-
(res) => (pinchMoveFramePromiseRes = res)
|
107
|
-
);
|
108
|
-
|
109
150
|
// Buffer these events; only update the scale when request animation fires
|
110
|
-
this.pinchMoveFrame = this.bufferFn(
|
111
|
-
this.updateScaleCenter({
|
112
|
-
clientX: this.lastEvent.center.x,
|
113
|
-
clientY: this.lastEvent.center.y,
|
114
|
-
});
|
115
|
-
this.mode.scale *= this.lastEvent.scale / this.oldScale;
|
116
|
-
this.oldScale = this.lastEvent.scale;
|
117
|
-
this.pinchMoveFrame = null;
|
118
|
-
pinchMoveFramePromiseRes();
|
119
|
-
});
|
151
|
+
this.pinchMoveFrame = this.bufferFn(this._drawPinchZoomFrame);
|
120
152
|
}
|
121
153
|
}
|
122
154
|
|
123
155
|
_pinchEnd = async () => {
|
156
|
+
if (!this.pinching) return;
|
157
|
+
this.pinching = false;
|
158
|
+
this.interact.gesturable({
|
159
|
+
listeners: {
|
160
|
+
start: this._pinchStart,
|
161
|
+
end: this._pinchEnd,
|
162
|
+
}
|
163
|
+
});
|
124
164
|
// Want this to happen after the pinchMoveFrame,
|
125
165
|
// if one is in progress; otherwise setting oldScale
|
126
166
|
// messes up the transform.
|
127
167
|
await this.pinchMoveFramePromise;
|
128
|
-
this.
|
168
|
+
this.scaleCenter = { x: 0.5, y: 0.5 };
|
129
169
|
this.oldScale = 1;
|
130
170
|
this.mode.$visibleWorld.classList.remove("BRsmooth-zooming");
|
131
171
|
this.mode.$visibleWorld.style.willChange = "auto";
|
@@ -133,10 +173,42 @@ export class ModeSmoothZoom {
|
|
133
173
|
this.mode.attachScrollListeners?.();
|
134
174
|
}
|
135
175
|
|
136
|
-
|
137
|
-
//
|
138
|
-
//
|
139
|
-
|
176
|
+
_drawPinchZoomFrame = async () => {
|
177
|
+
// Because of the buffering/various timing locks,
|
178
|
+
// this can be called after the pinch has ended, which
|
179
|
+
// results in a janky zoom after the pinch.
|
180
|
+
if (!this.pinching) {
|
181
|
+
this.pinchMoveFrame = null;
|
182
|
+
return;
|
183
|
+
}
|
184
|
+
|
185
|
+
this.mode.$container.style.overflow = "hidden";
|
186
|
+
this.pinchMoveFramePromiseRes = null;
|
187
|
+
this.pinchMoveFramePromise = new Promise(
|
188
|
+
(res) => (this.pinchMoveFramePromiseRes = res)
|
189
|
+
);
|
190
|
+
this.updateScaleCenter({
|
191
|
+
clientX: this.lastEvent.clientX,
|
192
|
+
clientY: this.lastEvent.clientY,
|
193
|
+
});
|
194
|
+
const curScale = this.mode.scale;
|
195
|
+
const newScale = curScale * this.lastEvent.scale / this.oldScale;
|
196
|
+
|
197
|
+
if (curScale != newScale) {
|
198
|
+
this.mode.scale = newScale;
|
199
|
+
await this.pinchMoveFramePromise;
|
200
|
+
}
|
201
|
+
this.mode.$container.style.overflow = "auto";
|
202
|
+
this.oldScale = this.lastEvent.scale;
|
203
|
+
this.pinchMoveFrame = null;
|
204
|
+
}
|
205
|
+
|
206
|
+
_dragMove = async (e) => {
|
207
|
+
if (this.pinching) {
|
208
|
+
await this._pinchEnd();
|
209
|
+
}
|
210
|
+
this.mode.$container.scrollTop -= e.dy;
|
211
|
+
this.mode.$container.scrollLeft -= e.dx;
|
140
212
|
}
|
141
213
|
|
142
214
|
/** @private */
|
@@ -174,7 +246,7 @@ export class ModeSmoothZoom {
|
|
174
246
|
*/
|
175
247
|
updateScaleCenter({ clientX, clientY }) {
|
176
248
|
const bc = this.mode.htmlDimensionsCacher.boundingClientRect;
|
177
|
-
this.
|
249
|
+
this.scaleCenter = {
|
178
250
|
x: (clientX - bc.left) / this.mode.htmlDimensionsCacher.clientWidth,
|
179
251
|
y: (clientY - bc.top) / this.mode.htmlDimensionsCacher.clientHeight,
|
180
252
|
};
|
@@ -194,8 +266,8 @@ export class ModeSmoothZoom {
|
|
194
266
|
const F = newScale / oldScale;
|
195
267
|
|
196
268
|
// Where in the viewport the zoom is centered on
|
197
|
-
const XPOS = this.
|
198
|
-
const YPOS = this.
|
269
|
+
const XPOS = this.scaleCenter.x;
|
270
|
+
const YPOS = this.scaleCenter.y;
|
199
271
|
const oldCenter = {
|
200
272
|
x: L + XPOS * W,
|
201
273
|
y: T + YPOS * H,
|
@@ -207,5 +279,34 @@ export class ModeSmoothZoom {
|
|
207
279
|
|
208
280
|
container.scrollTop = newCenter.y - YPOS * H;
|
209
281
|
container.scrollLeft = newCenter.x - XPOS * W;
|
282
|
+
this.pinchMoveFramePromiseRes?.();
|
283
|
+
}
|
284
|
+
}
|
285
|
+
|
286
|
+
export class TouchesMonitor {
|
287
|
+
/**
|
288
|
+
* @param {HTMLElement} container
|
289
|
+
*/
|
290
|
+
constructor(container) {
|
291
|
+
/** @type {HTMLElement} */
|
292
|
+
this.container = container;
|
293
|
+
this.touches = 0;
|
294
|
+
}
|
295
|
+
|
296
|
+
attach() {
|
297
|
+
this.container.addEventListener("touchstart", this._updateTouchCount);
|
298
|
+
this.container.addEventListener("touchend", this._updateTouchCount);
|
299
|
+
}
|
300
|
+
|
301
|
+
detach() {
|
302
|
+
this.container.removeEventListener("touchstart", this._updateTouchCount);
|
303
|
+
this.container.removeEventListener("touchend", this._updateTouchCount);
|
304
|
+
}
|
305
|
+
|
306
|
+
/**
|
307
|
+
* @param {TouchEvent} ev
|
308
|
+
*/
|
309
|
+
_updateTouchCount = (ev) => {
|
310
|
+
this.touches = ev.touches.length;
|
210
311
|
}
|
211
312
|
}
|
@@ -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
|
3
|
-
import {
|
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(() =>
|
26
|
+
afterEach(() => {
|
27
|
+
sinon.restore();
|
28
|
+
try {
|
29
|
+
interact.removeDocument(document);
|
30
|
+
} catch (e) {}
|
31
|
+
});
|
26
32
|
|
27
33
|
describe('ModeSmoothZoom', () => {
|
28
|
-
test('
|
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
|
-
|
41
|
-
|
42
|
-
|
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(
|
83
|
+
expect(msz.scaleCenter).toEqual({ x: 0.5, y: 0.5 });
|
83
84
|
msz.updateScaleCenter({ clientX: 85, clientY: 110 });
|
84
|
-
expect(
|
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
|
+
});
|