@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.
@@ -1,5 +1,7 @@
1
1
  // @ts-check
2
- import Hammer from "hammerjs";
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, center: { x: number, y: 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 interfere with Hammer,
52
- // so block them.
53
- this.mode.$container.addEventListener('gesturestart', this._preventEvent);
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._preventEvent);
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.hammer.on("pinchstart", this._pinchStart);
59
- this.hammer.on("pinchmove", this._pinchMove);
60
- this.hammer.on("pinchend", this._pinchEnd);
61
- this.hammer.on("pinchcancel", this._pinchCancel);
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._preventEvent);
95
+ this.mode.$container.removeEventListener('gesturestart', this._pinchStart);
72
96
  this.mode.$container.removeEventListener('gesturechange', this._preventEvent);
73
- this.mode.$container.removeEventListener('gestureend', this._preventEvent);
97
+ this.mode.$container.removeEventListener('gestureend', this._pinchEnd);
98
+
99
+ this.touchesMonitor?.detach?.();
74
100
 
75
101
  // The pinch listeners
76
- this.hammer.off("pinchstart", this._pinchStart);
77
- this.hammer.off("pinchmove", this._pinchMove);
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, center: { x: number, y: number }}} e */
141
+ /** @param {{ scale: number, clientX: number, clientY: number }}} e */
101
142
  _pinchMove = async (e) => {
102
- this.lastEvent = e;
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.mode.scaleCenter = { x: 0.5, y: 0.5 };
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
- _pinchCancel = async () => {
137
- // iOS fires pinchcancel ~randomly; it looks like it sometimes
138
- // thinks the pinch becomes a pan, at which point it cancels?
139
- await this._pinchEnd();
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.mode.scaleCenter = {
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.mode.scaleCenter.x;
198
- const YPOS = this.mode.scaleCenter.y;
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 { 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
+ });
@@ -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
- });