@internetarchive/bookreader 5.0.0-64 → 5.0.0-66
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.
- package/BookReader/BookReader.css +2 -1
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.js.LICENSE.txt +0 -6
- package/BookReader/BookReader.js.map +1 -1
- package/BookReader/ia-bookreader-bundle.js +2 -2
- package/BookReader/ia-bookreader-bundle.js.map +1 -1
- package/BookReader/plugins/plugin.search.js +1 -1
- package/BookReader/plugins/plugin.search.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReader/plugins/plugin.url.js +1 -1
- package/BookReader/plugins/plugin.url.js.map +1 -1
- package/CHANGELOG.md +8 -0
- package/package.json +14 -14
- 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/BookReader.js +0 -5
- package/src/util/browserSniffing.js +22 -0
- package/tests/jest/BookReader/ModeSmoothZoom.test.js +75 -32
- package/src/BookReader/DebugConsole.js +0 -54
- package/tests/jest/BookReader/DebugConsole.test.js +0 -25
|
@@ -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
|
}
|
package/src/BookReader.js
CHANGED
|
@@ -33,7 +33,6 @@ import { DEFAULT_OPTIONS, OptionsParseError } from './BookReader/options.js';
|
|
|
33
33
|
/** @typedef {import('./BookReader/options.js').ReductionFactor} ReductionFactor */
|
|
34
34
|
/** @typedef {import('./BookReader/BookModel.js').PageIndex} PageIndex */
|
|
35
35
|
import { EVENTS } from './BookReader/events.js';
|
|
36
|
-
import { DebugConsole } from './BookReader/DebugConsole.js';
|
|
37
36
|
import { Toolbar } from './BookReader/Toolbar/Toolbar.js';
|
|
38
37
|
import { BookModel } from './BookReader/BookModel.js';
|
|
39
38
|
import { Mode1Up } from './BookReader/Mode1Up.js';
|
|
@@ -43,10 +42,6 @@ import { ImageCache } from './BookReader/ImageCache.js';
|
|
|
43
42
|
import { PageContainer } from './BookReader/PageContainer.js';
|
|
44
43
|
import { NAMED_REDUCE_SETS } from './BookReader/ReduceSet';
|
|
45
44
|
|
|
46
|
-
if (location.toString().indexOf('_debugShowConsole=true') != -1) {
|
|
47
|
-
$(() => new DebugConsole().init());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
45
|
/**
|
|
51
46
|
* BookReader
|
|
52
47
|
* @param {BookReaderOptions} options
|
|
@@ -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
|
+
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Displays a console on the document for debugging devices where remote
|
|
3
|
-
* debugging is not feasible, and forwards all console.log's to be displayed
|
|
4
|
-
* on screen.
|
|
5
|
-
*/
|
|
6
|
-
export class DebugConsole {
|
|
7
|
-
constructor() {
|
|
8
|
-
/** How many times we've seen the same line in a row */
|
|
9
|
-
this.currentRun = 0;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
init() {
|
|
13
|
-
this.$log = $(`<div id="_debugLog" style="width: 100%; height: 300px; overflow: auto" />`);
|
|
14
|
-
$(document.body).prepend(this.$log);
|
|
15
|
-
|
|
16
|
-
this.$form = $(`
|
|
17
|
-
<form>
|
|
18
|
-
<input style="width:100%; font-family: monospace;" id="_debugLogInput">
|
|
19
|
-
</form>`);
|
|
20
|
-
this.$log.append(this.$form);
|
|
21
|
-
|
|
22
|
-
this.$form.on("submit", ev => {
|
|
23
|
-
ev.preventDefault();
|
|
24
|
-
const result = eval(this.$form.find('input').val());
|
|
25
|
-
this.logToScreen([result]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const _realLog = console.log.bind(console);
|
|
29
|
-
console.log = (...args) => {
|
|
30
|
-
_realLog(...args);
|
|
31
|
-
this.logToScreen(args);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
window.onerror = (...args) => this.logToScreen(args);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Log the provided array onto the on screen console
|
|
39
|
-
* @param {Array} args
|
|
40
|
-
*/
|
|
41
|
-
logToScreen(args) {
|
|
42
|
-
const html = args.map(JSON.stringify).join(',');
|
|
43
|
-
const $lastEntry = this.$log.children('.log-entry:last-child');
|
|
44
|
-
if ($lastEntry.find('.entry-code').html() == html) {
|
|
45
|
-
$lastEntry.find('.count').text(`(${++this.currentRun})`);
|
|
46
|
-
} else {
|
|
47
|
-
this.currentRun = 1;
|
|
48
|
-
this.$log.append($(`
|
|
49
|
-
<div class="log-entry">
|
|
50
|
-
<code class="count"></code> <code class="entry-code">${html}</code>
|
|
51
|
-
</div>`));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import sinon from 'sinon';
|
|
2
|
-
import { DebugConsole } from '@/src/BookReader/DebugConsole.js';
|
|
3
|
-
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
sinon.stub(console, 'log');
|
|
6
|
-
});
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
sinon.restore();
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test('hijacks console.log', () => {
|
|
12
|
-
const _realLog = console.log;
|
|
13
|
-
expect(_realLog).toBe(console.log);
|
|
14
|
-
new DebugConsole().init();
|
|
15
|
-
expect(_realLog).not.toBe(console.log);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('logging the same thing twice does not create more entries', () => {
|
|
19
|
-
const dc = new DebugConsole();
|
|
20
|
-
dc.init();
|
|
21
|
-
dc.logToScreen(['hello']);
|
|
22
|
-
dc.logToScreen(['hello']);
|
|
23
|
-
expect(dc.$log.children('.log-entry')).toHaveLength(1);
|
|
24
|
-
expect(dc.$log.find('.count').text()).toBe('(2)');
|
|
25
|
-
});
|