@internetarchive/bookreader 5.0.0-61 → 5.0.0-62
Sign up to get free protection for your applications and to get access to all the features.
- 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 +5 -0
- package/package.json +2 -2
- package/src/BookReader/BookModel.js +2 -2
- 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
|
-
});
|