@internetarchive/bookreader 5.0.0-61 → 5.0.0-63
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 +63 -14
- package/BookReader/BookReader.js +1 -1
- package/BookReader/BookReader.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.text_selection.js +1 -1
- package/BookReader/plugins/plugin.text_selection.js.map +1 -1
- package/BookReader/plugins/plugin.tts.js +1 -1
- package/BookReader/plugins/plugin.tts.js.map +1 -1
- package/BookReaderDemo/BookReaderDemo.css +2 -0
- package/BookReaderDemo/demo-internetarchive.html +91 -0
- package/CHANGELOG.md +9 -0
- package/package.json +6 -6
- package/src/BookReader/BookModel.js +2 -2
- package/src/BookReader/Mode2UpLit.js +3 -4
- package/src/BookReader/ModeThumb.js +1 -1
- package/src/BookReader/PageContainer.js +12 -0
- package/src/BookReader/events.js +0 -2
- package/src/BookReader/utils/SelectionObserver.js +43 -0
- package/src/BookReader.js +0 -264
- package/src/css/_BRpages.scss +2 -0
- package/src/css/_TextSelection.scss +84 -23
- package/src/plugins/plugin.text_selection.js +437 -112
- package/src/plugins/search/view.js +0 -9
- package/tests/jest/BookReader/BookModel.test.js +15 -0
- package/tests/jest/BookReader/utils/SelectionObserver.test.js +43 -0
- package/tests/jest/plugins/plugin.text_selection.test.js +166 -44
- package/src/BookReader/utils/SelectionStartedObserver.js +0 -46
- package/tests/jest/BookReader/utils/SelectionStartedObserver.test.js +0 -73
|
@@ -60,6 +60,21 @@ describe('getMedianPageSizeInches', () => {
|
|
|
60
60
|
expect(bm.getMedianPageSizeInches()).toEqual({ width: 300, height: 2300 });
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test('does not lexicographic sort for median', () => {
|
|
64
|
+
const sizes = [
|
|
65
|
+
{width: 100, height: 100},
|
|
66
|
+
{width: 20, height: 20},
|
|
67
|
+
{width: 30, height: 30},
|
|
68
|
+
];
|
|
69
|
+
const data = deepCopy(SAMPLE_DATA);
|
|
70
|
+
delete data[2];
|
|
71
|
+
Object.assign(data[0][0], sizes[0]);
|
|
72
|
+
Object.assign(data[1][0], sizes[1]);
|
|
73
|
+
Object.assign(data[1][1], sizes[2]);
|
|
74
|
+
const bm = new BookModel({ data, options: {ppi: 1} });
|
|
75
|
+
expect(bm.getMedianPageSizeInches()).toEqual({ width: 30, height: 30 });
|
|
76
|
+
});
|
|
77
|
+
|
|
63
78
|
test('caches result', () => {
|
|
64
79
|
const bm = new BookModel({ data: SAMPLE_DATA });
|
|
65
80
|
const firstResult = bm.getMedianPageSizeInches();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import sinon from "sinon";
|
|
3
|
+
import { SelectionObserver } from "@/src/BookReader/utils/SelectionObserver";
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
sinon.restore();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("SelectionObserver", () => {
|
|
10
|
+
test("_onSelectionChange", () => {
|
|
11
|
+
const handler = sinon.spy();
|
|
12
|
+
const observer = new SelectionObserver(".text-layer", handler);
|
|
13
|
+
const target = document.createElement("div");
|
|
14
|
+
target.classList.add("text-layer");
|
|
15
|
+
|
|
16
|
+
// stub window.getSelection
|
|
17
|
+
const getSelectionStub = sinon.stub(window, "getSelection");
|
|
18
|
+
getSelectionStub.returns({ toString: () => "test", anchorNode: target });
|
|
19
|
+
observer._onSelectionChange();
|
|
20
|
+
expect(handler.callCount).toBe(1);
|
|
21
|
+
expect(handler.calledWith("started", target)).toBe(true);
|
|
22
|
+
expect(observer.selecting).toBe(true);
|
|
23
|
+
|
|
24
|
+
// Calling it again does not call the handler again
|
|
25
|
+
observer._onSelectionChange();
|
|
26
|
+
expect(handler.callCount).toBe(1);
|
|
27
|
+
|
|
28
|
+
// Until the selection is cleared
|
|
29
|
+
getSelectionStub.returns({ toString: () => "", anchorNode: null });
|
|
30
|
+
expect(observer.selecting).toBe(true);
|
|
31
|
+
expect(handler.callCount).toBe(1);
|
|
32
|
+
|
|
33
|
+
observer._onSelectionChange();
|
|
34
|
+
expect(handler.callCount).toBe(2);
|
|
35
|
+
expect(handler.calledWith("cleared", target)).toBe(true);
|
|
36
|
+
|
|
37
|
+
// Calling it again does not call the handler again
|
|
38
|
+
sinon.restore();
|
|
39
|
+
sinon.stub(window, "getSelection").returns({ toString: () => "" });
|
|
40
|
+
observer._onSelectionChange();
|
|
41
|
+
expect(handler.callCount).toBe(2);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -1,21 +1,88 @@
|
|
|
1
1
|
import sinon from 'sinon';
|
|
2
2
|
|
|
3
3
|
import '@/src/BookReader.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BookreaderWithTextSelection,
|
|
6
|
+
Cache,
|
|
7
|
+
genMap,
|
|
8
|
+
lookAroundWindow,
|
|
9
|
+
zip,
|
|
10
|
+
} from '@/src/plugins/plugin.text_selection.js';
|
|
5
11
|
|
|
6
12
|
|
|
7
13
|
/** @type {BookReader} */
|
|
8
14
|
|
|
9
15
|
// djvu.xml book infos copied from https://ia803103.us.archive.org/14/items/goodytwoshoes00newyiala/goodytwoshoes00newyiala_djvu.xml
|
|
10
|
-
const FAKE_XML_1WORD =
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const FAKE_XML_1WORD = `
|
|
17
|
+
<OBJECT data="file://localhost//tmp/derive/goodytwoshoes00newyiala//goodytwoshoes00newyiala.djvu" height="3192" type="image/x.djvu" usemap="goodytwoshoes00newyiala_0001.djvu" width="2454">
|
|
18
|
+
<PARAGRAPH>
|
|
19
|
+
<LINE>
|
|
20
|
+
<WORD coords="1216,2768,1256,2640">test</WORD>
|
|
21
|
+
</LINE>
|
|
22
|
+
</PARAGRAPH>
|
|
23
|
+
</OBJECT>`;
|
|
24
|
+
const FAKE_XML_MULT_WORDS = `
|
|
25
|
+
<OBJECT data="file://localhost//tmp/derive/goodytwoshoes00newyiala//goodytwoshoes00newyiala.djvu" height="3192" type="image/x.djvu" usemap="goodytwoshoes00newyiala_0001.djvu" width="2454">
|
|
26
|
+
<PARAGRAPH>
|
|
27
|
+
<LINE>
|
|
28
|
+
<WORD coords="1216,2768,1256,2640">test1</WORD>
|
|
29
|
+
<WORD coords="1400,2768,1500,2640">test2</WORD>
|
|
30
|
+
<WORD coords="1600,2768,1700,2640">test3</WORD>
|
|
31
|
+
</LINE>
|
|
32
|
+
</PARAGRAPH>
|
|
33
|
+
</OBJECT>`;
|
|
34
|
+
const FAKE_XML_MULT_LINES = `
|
|
35
|
+
<OBJECT data="file://localhost//tmp/derive/goodytwoshoes00newyiala//goodytwoshoes00newyiala.djvu" height="3192" type="image/x.djvu" usemap="goodytwoshoes00newyiala_0001.djvu" width="2454">
|
|
36
|
+
<PARAGRAPH>
|
|
37
|
+
<LINE>
|
|
38
|
+
<WORD coords="119,2050,230,2014" x-confidence="29">way </WORD>
|
|
39
|
+
<WORD coords="230,2038,320,2002" x-confidence="30">can </WORD>
|
|
40
|
+
<WORD coords="320,2039,433,2002" x-confidence="28">false </WORD>
|
|
41
|
+
<WORD coords="433,2051,658,2003" x-confidence="29">judgment </WORD>
|
|
42
|
+
<WORD coords="658,2039,728,2002" x-confidence="30">be </WORD>
|
|
43
|
+
<WORD coords="658,2039,728,2002" x-confidence="30">-</WORD>
|
|
44
|
+
<WORD coords="728,2039,939,2001" x-confidence="29">formed. </WORD>
|
|
45
|
+
<WORD coords="939,2039,1087,2001" x-confidence="29">There </WORD>
|
|
46
|
+
<WORD coords="1087,2039,1187,2002" x-confidence="29">still </WORD>
|
|
47
|
+
<WORD coords="1187,2038,1370,2003" x-confidence="29">remains </WORD>
|
|
48
|
+
<WORD coords="1370,2037,1433,2014" x-confidence="28">an-</WORD>
|
|
49
|
+
</LINE>
|
|
50
|
+
<LINE>
|
|
51
|
+
<WORD coords="244,2099,370,2063" x-confidence="29">other mode </WORD>
|
|
52
|
+
<WORD coords="370,2100,427,2064" x-confidence="29">in </WORD>
|
|
53
|
+
<WORD coords="427,2100,566,2063" x-confidence="29">which </WORD>
|
|
54
|
+
<WORD coords="566,2100,670,2063" x-confidence="29">false </WORD>
|
|
55
|
+
<WORD coords="670,2112,907,2063" x-confidence="29">judgments </WORD>
|
|
56
|
+
<WORD coords="907,2112,1006,2064" x-confidence="29">may </WORD>
|
|
57
|
+
<WORD coords="1006,2100,1071,2063" x-confidence="29">be </WORD>
|
|
58
|
+
<WORD coords="1071,2100,1266,2062" x-confidence="29">formed. </WORD>
|
|
59
|
+
<WORD coords="1266,2110,1435,2062" x-confidence="29">Suppose</WORD>
|
|
60
|
+
</LINE>
|
|
61
|
+
<LINE>
|
|
62
|
+
<WORD coords="118,2160,217,2123" x-confidence="29">that </WORD>
|
|
63
|
+
<WORD coords="217,2160,289,2124" x-confidence="29">we </WORD>
|
|
64
|
+
<WORD coords="289,2160,400,2124" x-confidence="29">have </WORD>
|
|
65
|
+
<WORD coords="400,2160,456,2124" x-confidence="30">in </WORD>
|
|
66
|
+
<WORD coords="456,2161,542,2136" x-confidence="29">our </WORD>
|
|
67
|
+
<WORD coords="542,2161,660,2124" x-confidence="29">souls </WORD>
|
|
68
|
+
<WORD coords="660,2160,700,2136" x-confidence="29">a </WORD>
|
|
69
|
+
<WORD coords="700,2160,847,2129" x-confidence="28">waxen </WORD>
|
|
70
|
+
<WORD coords="847,2160,983,2123" x-confidence="29">tablet </WORD>
|
|
71
|
+
<WORD coords="983,2160,1045,2124" x-confidence="29">of </WORD>
|
|
72
|
+
<WORD coords="1045,2160,1211,2124" x-confidence="29">various </WORD>
|
|
73
|
+
<WORD coords="1211,2171,1398,2122" x-confidence="29">qualities </WORD>
|
|
74
|
+
<WORD coords="1398,2157,1434,2122" x-confidence="29">lastWord</WORD>
|
|
75
|
+
</LINE>
|
|
76
|
+
</PARAGRAPH>
|
|
77
|
+
</OBJECT>`;
|
|
78
|
+
const FAKE_XML_5COORDS = `
|
|
79
|
+
<OBJECT data="file://localhost//tmp/derive/goodytwoshoes00newyiala//goodytwoshoes00newyiala.djvu" height="3192" type="image/x.djvu" usemap="goodytwoshoes00newyiala_0001.djvu" width="2454">
|
|
80
|
+
<PARAGRAPH>
|
|
81
|
+
<LINE>
|
|
82
|
+
<WORD coords="1216,2768,1256,2640,2690">test</WORD>
|
|
83
|
+
</LINE>
|
|
84
|
+
</PARAGRAPH>
|
|
85
|
+
</OBJECT>`;
|
|
19
86
|
const FAKE_XML_EMPTY = '';
|
|
20
87
|
|
|
21
88
|
describe("Generic tests", () => {
|
|
@@ -44,7 +111,7 @@ describe("Generic tests", () => {
|
|
|
44
111
|
|
|
45
112
|
afterEach(() => {
|
|
46
113
|
sinon.restore();
|
|
47
|
-
$('.
|
|
114
|
+
$('.BRtextLayer').remove();
|
|
48
115
|
});
|
|
49
116
|
|
|
50
117
|
test("_createPageContainer overridden function still creates a BRpagecontainer element", () => {
|
|
@@ -76,8 +143,8 @@ describe("Generic tests", () => {
|
|
|
76
143
|
.returns($(new DOMParser().parseFromString(FAKE_XML_1WORD, "text/xml")));
|
|
77
144
|
const pageIndex = br.data.length - 1;
|
|
78
145
|
await br.textSelectionPlugin.createTextLayer({ $container, page: { index: pageIndex, width: 100, height: 100 }});
|
|
79
|
-
expect($container.find(".
|
|
80
|
-
expect($container.find("
|
|
146
|
+
expect($container.find(".BRtextLayer").length).toBe(1);
|
|
147
|
+
expect($container.find("p").length).toBe(1);
|
|
81
148
|
});
|
|
82
149
|
|
|
83
150
|
test("createTextLayer will not create text layer if there are too many words", async () => {
|
|
@@ -86,50 +153,76 @@ describe("Generic tests", () => {
|
|
|
86
153
|
sinon.stub(br.textSelectionPlugin, "getPageText")
|
|
87
154
|
.returns($(new DOMParser().parseFromString(xml, "text/xml")));
|
|
88
155
|
await br.textSelectionPlugin.createTextLayer({ $container, page: { index: 0, width: 100, height: 100 }});
|
|
89
|
-
expect($container.find(".
|
|
90
|
-
expect($container.find("
|
|
156
|
+
expect($container.find(".BRtextLayer").length).toBe(0);
|
|
157
|
+
expect($container.find("p").length).toBe(0);
|
|
91
158
|
expect($container.find(".BRwordElement").length).toBe(0);
|
|
92
159
|
});
|
|
93
160
|
|
|
94
|
-
test("createTextLayer creates
|
|
161
|
+
test("createTextLayer creates text layer with paragraph with 1 word element", async () => {
|
|
95
162
|
const $container = br.refs.$brContainer;
|
|
96
163
|
sinon.stub(br.textSelectionPlugin, "getPageText")
|
|
97
164
|
.returns($(new DOMParser().parseFromString(FAKE_XML_1WORD, "text/xml")));
|
|
98
165
|
await br.textSelectionPlugin.createTextLayer({ $container, page: { index: 1, width: 100, height: 100 }});
|
|
99
|
-
expect($container.find(".
|
|
100
|
-
expect($container.find("
|
|
166
|
+
expect($container.find(".BRtextLayer").length).toBe(1);
|
|
167
|
+
expect($container.find("p").length).toBe(1);
|
|
101
168
|
expect($container.find(".BRwordElement").length).toBe(1);
|
|
102
169
|
expect($container.find(".BRwordElement").text()).toBe("test");
|
|
103
170
|
});
|
|
104
171
|
|
|
105
|
-
test("createTextLayer creates
|
|
172
|
+
test("createTextLayer creates text layer with paragraph with multiple word elements", async () => {
|
|
106
173
|
const $container = br.refs.$brContainer;
|
|
107
174
|
sinon.stub(br.textSelectionPlugin, "getPageText")
|
|
108
175
|
.returns($(new DOMParser().parseFromString(FAKE_XML_MULT_WORDS, "text/xml")));
|
|
109
176
|
await br.textSelectionPlugin.createTextLayer({ $container, page: { index: 2, width: 100, height: 100 }});
|
|
110
|
-
expect($container.find(".
|
|
111
|
-
expect($container.find("
|
|
112
|
-
expect($container.find(".BRwordElement").length).toBe(
|
|
113
|
-
expect($container.find(".
|
|
177
|
+
expect($container.find(".BRtextLayer").length).toBe(1);
|
|
178
|
+
expect($container.find("p").length).toBe(1);
|
|
179
|
+
expect($container.find(".BRwordElement").length).toBe(3);
|
|
180
|
+
expect($container.find(".BRspace").length).toBe(2);
|
|
114
181
|
});
|
|
115
182
|
|
|
116
|
-
test("createTextLayer creates
|
|
183
|
+
test("createTextLayer creates text layer with paragraph with word with 5 params coordinates", async () => {
|
|
117
184
|
const $container = br.refs.$brContainer;
|
|
118
185
|
sinon.stub(br.textSelectionPlugin, "getPageText")
|
|
119
186
|
.returns($(new DOMParser().parseFromString(FAKE_XML_5COORDS, "text/xml")));
|
|
120
187
|
await br.textSelectionPlugin.createTextLayer({ $container, page: { index: 3, width: 100, height: 100 }});
|
|
121
|
-
expect($container.find(".
|
|
122
|
-
expect($container.find("
|
|
188
|
+
expect($container.find(".BRtextLayer").length).toBe(1);
|
|
189
|
+
expect($container.find("p").length).toBe(1);
|
|
123
190
|
expect($container.find(".BRwordElement").length).toBe(1);
|
|
124
191
|
});
|
|
125
192
|
|
|
193
|
+
test("createTextLayer handles multiple lines", async () => {
|
|
194
|
+
const $container = br.refs.$brContainer;
|
|
195
|
+
sinon.stub(br.textSelectionPlugin, "getPageText")
|
|
196
|
+
.returns($(new DOMParser().parseFromString(FAKE_XML_MULT_LINES, "text/xml")));
|
|
197
|
+
await br.textSelectionPlugin.createTextLayer({ $container, page: { index: 3, width: 100, height: 100 }});
|
|
198
|
+
expect($container.find(".BRtextLayer").length).toBe(1);
|
|
199
|
+
expect($container.find("p").length).toBe(1);
|
|
200
|
+
expect($container.find(".BRlineElement").length).toBe(3);
|
|
201
|
+
// Adds space at end of line; except last line/hyphens
|
|
202
|
+
expect($container.find("p").text()).toMatch(/another/);
|
|
203
|
+
expect($container.find("p").text()).toMatch(/Suppose /);
|
|
204
|
+
expect($container.find("p").text()).toMatch(/lastWord$/);
|
|
205
|
+
expect($container.find("p > br").length).toBe(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("createTextLayer repairs trailing hyphens", async () => {
|
|
209
|
+
const $container = br.refs.$brContainer;
|
|
210
|
+
sinon.stub(br.textSelectionPlugin, "getPageText")
|
|
211
|
+
.returns($(new DOMParser().parseFromString(FAKE_XML_MULT_LINES, "text/xml")));
|
|
212
|
+
await br.textSelectionPlugin.createTextLayer({ $container, page: { index: 3, width: 100, height: 100 }});
|
|
213
|
+
|
|
214
|
+
expect($container.find(".BRwordElement--hyphen").length).toBe(1);
|
|
215
|
+
expect($container.find(".BRwordElement--hyphen").closest(".BRlineElement").text().endsWith(' ')).toBe(false);
|
|
216
|
+
expect($container.find(".BRwordElement--hyphen").closest(".BRlineElement").text().endsWith('-')).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
126
219
|
test("createTextLayer can handle empty xml", async () => {
|
|
127
220
|
const $container = br.refs.$brContainer;
|
|
128
221
|
sinon.stub(br.textSelectionPlugin, "getPageText")
|
|
129
222
|
.returns($(new DOMParser().parseFromString(FAKE_XML_EMPTY, "text/xml")));
|
|
130
223
|
await br.textSelectionPlugin.createTextLayer({ $container, page: { index: 4, width: 100, height: 100 }});
|
|
131
|
-
expect($container.find(".
|
|
132
|
-
expect($container.find("
|
|
224
|
+
expect($container.find(".BRtextLayer").length).toBe(1);
|
|
225
|
+
expect($container.find("p").length).toBe(0);
|
|
133
226
|
expect($container.find(".BRwordElement").length).toBe(0);
|
|
134
227
|
});
|
|
135
228
|
|
|
@@ -150,22 +243,6 @@ describe("Generic tests", () => {
|
|
|
150
243
|
});
|
|
151
244
|
});
|
|
152
245
|
|
|
153
|
-
describe("textSelectionPlugin constructor", () => {
|
|
154
|
-
test("textSelectionPlugin constructor with firefox browser", () => {
|
|
155
|
-
const tsp = new TextSelectionPlugin({}, {}, true);
|
|
156
|
-
expect(tsp.djvuPagesPromise).toBe(null);
|
|
157
|
-
expect(tsp.svgParagraphElement).toBe("g");
|
|
158
|
-
expect(tsp.svgWordElement).toBe("text");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("textSelectionPlugin constructor not on firefox browser", () => {
|
|
162
|
-
const tsp = new TextSelectionPlugin({}, {}, false);
|
|
163
|
-
expect(tsp.djvuPagesPromise).toBe(null);
|
|
164
|
-
expect(tsp.svgParagraphElement).toBe("text");
|
|
165
|
-
expect(tsp.svgWordElement).toBe("tspan");
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
246
|
describe("Cache", () => {
|
|
170
247
|
test('Adding works', () => {
|
|
171
248
|
const c = new Cache(10);
|
|
@@ -193,3 +270,48 @@ describe("Cache", () => {
|
|
|
193
270
|
expect(c.entries).toEqual([12, 10]);
|
|
194
271
|
});
|
|
195
272
|
});
|
|
273
|
+
|
|
274
|
+
describe('genMap', () => {
|
|
275
|
+
test('handles empty', () => {
|
|
276
|
+
expect(Array.from(genMap([], x => x ** 2))).toEqual([]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('handles non-empty', () => {
|
|
280
|
+
expect(Array.from(genMap([1,2,3], x => x ** 2))).toEqual([1,4,9]);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('lookAroundWindow', () => {
|
|
285
|
+
test('handles empty', () => {
|
|
286
|
+
expect(Array.from(lookAroundWindow([]))).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('handles smaller than window', () => {
|
|
290
|
+
expect(Array.from(lookAroundWindow([1]))).toEqual([[undefined, 1, undefined]]);
|
|
291
|
+
expect(Array.from(lookAroundWindow([1, 2]))).toEqual([[undefined, 1, 2], [1, 2, undefined]]);
|
|
292
|
+
expect(Array.from(lookAroundWindow([1, 2, 3]))).toEqual([[undefined, 1, 2], [1, 2, 3], [2, 3, undefined]]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('handles larger than window', () => {
|
|
296
|
+
expect(Array.from(lookAroundWindow([1, 2, 3, 4]))).toEqual([
|
|
297
|
+
[undefined, 1, 2],
|
|
298
|
+
[1, 2, 3],
|
|
299
|
+
[2, 3, 4],
|
|
300
|
+
[3, 4, undefined],
|
|
301
|
+
]);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe('zip', () => {
|
|
306
|
+
test('handles empty', () => {
|
|
307
|
+
expect(Array.from(zip([], []))).toEqual([]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('uneven throws error', () => {
|
|
311
|
+
expect(() => Array.from(zip([1], [2, 3]))).toThrow();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('handles even', () => {
|
|
315
|
+
expect(Array.from(zip([1, 2], [3, 4]))).toEqual([[1, 3], [2, 4]]);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
export class SelectionStartedObserver {
|
|
3
|
-
loggedForSelection = false;
|
|
4
|
-
startedInSelector = false;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* @param {string} selector
|
|
8
|
-
* @param {function(): any} handler
|
|
9
|
-
*/
|
|
10
|
-
constructor(selector, handler) {
|
|
11
|
-
this.selector = selector;
|
|
12
|
-
this.handler = handler;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
attach() {
|
|
16
|
-
// We can't just use select start, because Chrome fires that willy
|
|
17
|
-
// nilly even when a user slightly long presses.
|
|
18
|
-
document.addEventListener("selectstart", this._onSelectStart);
|
|
19
|
-
// This has to be on document :/
|
|
20
|
-
document.addEventListener("selectionchange", this._onSelectionChange);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
detach() {
|
|
24
|
-
document.removeEventListener("selectstart", this._onSelectStart);
|
|
25
|
-
document.removeEventListener("selectionchange", this._onSelectionChange);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @param {Event} ev
|
|
30
|
-
*/
|
|
31
|
-
_onSelectStart = (ev) => {
|
|
32
|
-
this.loggedForSelection = false;
|
|
33
|
-
// Use jQuery because ev.target could be a Node (eg TextNode), which
|
|
34
|
-
// doesn't have .closest on it.
|
|
35
|
-
this.startedInSelector = $(ev.target).closest(this.selector).length > 0;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
_onSelectionChange = () => {
|
|
39
|
-
if (this.loggedForSelection || !this.startedInSelector) return;
|
|
40
|
-
const sel = window.getSelection();
|
|
41
|
-
if (sel.toString()) {
|
|
42
|
-
this.loggedForSelection = true;
|
|
43
|
-
this.handler();
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import sinon from "sinon";
|
|
3
|
-
import { SelectionStartedObserver } from "@/src/BookReader/utils/SelectionStartedObserver";
|
|
4
|
-
|
|
5
|
-
afterEach(() => {
|
|
6
|
-
sinon.restore();
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
describe("SelectionStartedObserver", () => {
|
|
10
|
-
describe("_onSelectStart", () => {
|
|
11
|
-
test("sets matches selector correctly", () => {
|
|
12
|
-
const observer = new SelectionStartedObserver(".text-layer", () => {});
|
|
13
|
-
const ev = new Event("selectstart", {});
|
|
14
|
-
const target = document.createElement("div");
|
|
15
|
-
Object.defineProperty(ev, "target", { get: () => target });
|
|
16
|
-
observer._onSelectStart(ev);
|
|
17
|
-
expect(observer.startedInSelector).toBe(false);
|
|
18
|
-
target.classList.add("text-layer");
|
|
19
|
-
observer._onSelectStart(ev);
|
|
20
|
-
expect(observer.startedInSelector).toBe(true);
|
|
21
|
-
expect(observer.loggedForSelection).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test("resets loggedForSelction", () => {
|
|
25
|
-
const observer = new SelectionStartedObserver(".text-layer", () => {});
|
|
26
|
-
const ev = new Event("selectstart", {});
|
|
27
|
-
const target = document.createElement("div");
|
|
28
|
-
Object.defineProperty(ev, "target", { get: () => target });
|
|
29
|
-
target.classList.add("text-layer");
|
|
30
|
-
observer._onSelectStart(ev);
|
|
31
|
-
expect(observer.loggedForSelection).toBe(false);
|
|
32
|
-
observer.loggedForSelection = true;
|
|
33
|
-
observer._onSelectStart(ev);
|
|
34
|
-
expect(observer.loggedForSelection).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("_onSelectionChange", () => {
|
|
39
|
-
const handler = sinon.spy();
|
|
40
|
-
const observer = new SelectionStartedObserver(".text-layer", handler);
|
|
41
|
-
const ev = new Event("selectstart", {});
|
|
42
|
-
const target = document.createElement("div");
|
|
43
|
-
target.classList.add("text-layer");
|
|
44
|
-
Object.defineProperty(ev, "target", { get: () => target });
|
|
45
|
-
observer._onSelectStart(ev);
|
|
46
|
-
|
|
47
|
-
// stub window.getSelection
|
|
48
|
-
sinon.stub(window, "getSelection").returns({ toString: () => "test" });
|
|
49
|
-
observer._onSelectionChange();
|
|
50
|
-
expect(handler.callCount).toBe(1);
|
|
51
|
-
expect(observer.loggedForSelection).toBe(true);
|
|
52
|
-
|
|
53
|
-
// Calling it again does not call the handler again
|
|
54
|
-
observer._onSelectionChange();
|
|
55
|
-
expect(handler.callCount).toBe(1);
|
|
56
|
-
|
|
57
|
-
// Until the selection is cleared
|
|
58
|
-
observer._onSelectStart(ev);
|
|
59
|
-
expect(observer.loggedForSelection).toBe(false);
|
|
60
|
-
expect(handler.callCount).toBe(1);
|
|
61
|
-
|
|
62
|
-
observer._onSelectionChange();
|
|
63
|
-
expect(handler.callCount).toBe(2);
|
|
64
|
-
|
|
65
|
-
observer._onSelectStart(ev);
|
|
66
|
-
|
|
67
|
-
// Calling it again does not call the handler again
|
|
68
|
-
sinon.restore();
|
|
69
|
-
sinon.stub(window, "getSelection").returns({ toString: () => "" });
|
|
70
|
-
observer._onSelectionChange();
|
|
71
|
-
expect(handler.callCount).toBe(2);
|
|
72
|
-
});
|
|
73
|
-
});
|