Skip to content

Commit bc95911

Browse files
committed
Update BR mustache + add first BR unit tests
1 parent 0f17ddb commit bc95911

File tree

5 files changed

+199
-26
lines changed

5 files changed

+199
-26
lines changed

dev-server/documents/html/bookreader.mustache

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@
1717
</p>
1818

1919
<!-- JS dependencies -->
20-
<script src="http://localhost:8000/BookReader/jquery-3.js"></script>
20+
<!-- IA uses the @next version of BR -->
21+
<script src="https://unpkg.com/@internetarchive/bookreader@next/BookReader/jquery-3.js"></script>
2122

2223
<!-- BookReader and any plugins -->
23-
<link rel="stylesheet" href="http://localhost:8000/BookReader/BookReader.css" />
24-
<script src="http://localhost:8000/BookReader/BookReader.js"></script>
25-
<script src="http://localhost:8000/BookReader/plugins/plugin.text_selection.js"></script>
24+
<link rel="stylesheet" href="https://unpkg.com/@internetarchive/bookreader@next/BookReader/BookReader.css" />
25+
<script src="https://unpkg.com/@internetarchive/bookreader@next/BookReader/BookReader.js"></script>
26+
<script src="https://unpkg.com/@internetarchive/bookreader@next/BookReader/plugins/plugin.text_selection.js"></script>
2627

2728
<!-- BookReader wrapper web component -->
2829
<script type="module"
29-
src="http://localhost:8000/BookReader/ia-bookreader-bundle.js"></script>
30+
src="https://unpkg.com/@internetarchive/bookreader@next/BookReader/ia-bookreader-bundle.js"></script>
3031

3132
<style>
3233
html {

src/annotator/anchoring/BookReader.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,12 @@ export async function describe(
7575

7676
const pageContainer: HTMLElement = textLayer.closest('.BRpagecontainer')!;
7777
const pageIndex = parseFloat(pageContainer.dataset.index!);
78-
const pageLabel = pageContainer.dataset.label!;
78+
const pageNum = pageContainer.dataset.pageNum!;
7979

8080
const pageSelector: PageSelector = {
8181
type: 'PageSelector',
8282
index: pageIndex,
83-
label: pageLabel || `n${pageIndex}`,
83+
label: pageNum || `n${pageIndex}`,
8484
};
8585

8686
return [quote, pageSelector];
@@ -153,7 +153,7 @@ async function pollUntilTruthy<T>(
153153
fn: () => T,
154154
{ timeout = 1000 },
155155
): Promise<T | undefined> {
156-
return new Promise((resolve, reject) => {
156+
return new Promise(resolve => {
157157
const start = Date.now();
158158
const interval = setInterval(() => {
159159
const val = fn();

src/annotator/integrations/BookReader.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import debounce from 'lodash.debounce';
2-
import { TinyEmitter } from 'tiny-emitter';
32

3+
import { EventEmitter } from '../../shared/event-emitter';
44
import type { BookReader } from '../../types/BookReader';
55
import type {
66
Anchor,
77
AnnotationData,
88
AnnotationTool,
99
Annotator,
1010
Integration,
11+
IntegrationEvents,
1112
SidebarLayout,
1213
} from '../../types/annotator';
13-
import { PageSelector, Selector } from '../../types/api';
14+
import type { PageSelector, Selector } from '../../types/api';
1415
import { anchor, canDescribe, describe } from '../anchoring/BookReader';
1516
import { TextRange } from '../anchoring/text-range';
1617

@@ -30,7 +31,10 @@ export function isBookReader() {
3031
/**
3132
* Integration that works with PDF.js
3233
*/
33-
export class BookReaderIntegration extends TinyEmitter implements Integration {
34+
export class BookReaderIntegration
35+
extends EventEmitter<IntegrationEvents>
36+
implements Integration
37+
{
3438
private _annotator: Annotator;
3539
private _br: BookReader;
3640
private _debouncedUpdate: () => void;
@@ -118,7 +122,7 @@ export class BookReaderIntegration extends TinyEmitter implements Integration {
118122
}
119123

120124
// This method (re-)anchors annotations when pages are rendered and destroyed.
121-
_update = async () => {
125+
_update = () => {
122126
const refreshAnnotations: AnnotationData[] = [];
123127

124128
// For pages that are no longer visible, we want to
@@ -132,22 +136,24 @@ export class BookReaderIntegration extends TinyEmitter implements Integration {
132136
continue;
133137
}
134138

139+
// If it's a placeholder and its page is now visible, we want to re-anchor
135140
const placeholder = anchor.highlights[0].closest(
136-
'.BRannotationPlaceholder',
141+
'.BRhypothesisPlaceholder',
137142
);
138-
// If it's a placeholder and its page is now visible, we want to re-anchor
139143
if (placeholder) {
140144
const pageSelector = anchor.target.selector?.find(
141145
s => s.type === 'PageSelector',
142146
) as PageSelector;
143147
if (!pageSelector) {
144-
throw new Error('No page selector found');
148+
// Legacy annotation without a page selector; skip?
149+
console.warn('No page selector found');
150+
continue;
145151
}
146152

147-
const pageContainer = this._br.refs.$br.find(
153+
const pageContainer = this.contentContainer().querySelector(
148154
`.BRpagecontainer[data-index="${pageSelector.index}"]`,
149155
);
150-
if (pageContainer.length) {
156+
if (pageContainer) {
151157
delete anchor.region;
152158
placeholder.remove();
153159
anchor.highlights.splice(0, anchor.highlights.length);
@@ -159,7 +165,8 @@ export class BookReaderIntegration extends TinyEmitter implements Integration {
159165
}
160166
}
161167

162-
// Now it's not a placeholder; if it's page has been removed, then we need to re-anchor (potentially creating a placeholder)
168+
// Ok, not a placeholder; if its page has been removed, then we need to
169+
// re-anchor (potentially creating a placeholder)
163170
const notInDom = anchor.highlights.some(
164171
highlight => !document.body.contains(highlight),
165172
);
@@ -181,7 +188,7 @@ export class BookReaderIntegration extends TinyEmitter implements Integration {
181188
* Return the scrollable element which contains the document content.
182189
*/
183190
contentContainer(): HTMLElement {
184-
return this._br.refs.$br.find('.BRcontainer')?.[0] as HTMLElement;
191+
return this._br.refs.$br[0] as HTMLElement;
185192
}
186193

187194
sideBySideActive() {
@@ -209,7 +216,7 @@ export class BookReaderIntegration extends TinyEmitter implements Integration {
209216
throw new Error('No page selector found');
210217
}
211218

212-
if (this._br.activeMode.name == 'thumb') {
219+
if (this._br.activeMode.name === 'thumb') {
213220
this._br.switchMode('2up', { suppressFragmentChange: true });
214221
await delay(50);
215222
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { BookReaderIntegration, isBookReader } from '../BookReader';
2+
3+
describe('annotator/integrations/BookReader', () => {
4+
describe('isBookReader', () => {
5+
beforeEach(() => {
6+
delete window.br;
7+
});
8+
9+
it('returns true in BookReader', () => {
10+
window.br = {};
11+
assert.isTrue(isBookReader());
12+
});
13+
14+
it('returns false in other applications', () => {
15+
assert.isFalse(isBookReader());
16+
});
17+
});
18+
19+
describe('BookReaderIntegration', () => {
20+
/** @type {HTMLDivElement} */
21+
let brRoot;
22+
/** @type {BookReaderIntegration} */
23+
let brIntegration;
24+
/** @type {import('../../../types/annotator').Annotator} */
25+
let fakeAnnotator;
26+
/** @type {import('../../../types/annotator').Anchor} */
27+
let fakeAnchor;
28+
29+
beforeEach(() => {
30+
brRoot = document.createElement('div');
31+
fakeAnchor = {
32+
highlights: [],
33+
target: {
34+
selector: [
35+
{
36+
type: 'TextQuoteSelector',
37+
prefix: 'prefix',
38+
suffix: 'suffix',
39+
exact: 'exact',
40+
},
41+
{
42+
type: 'PageSelector',
43+
index: 14,
44+
label: 'x',
45+
},
46+
],
47+
},
48+
};
49+
fakeAnnotator = {
50+
anchor: sinon.stub(),
51+
anchors: [],
52+
anchoring: null,
53+
};
54+
55+
window.br = {
56+
options: {
57+
bookTitle: 'Dummy Title',
58+
bookUri: 'dummy-uri',
59+
},
60+
refs: {
61+
$br: {
62+
0: brRoot,
63+
},
64+
},
65+
activeMode: {
66+
name: '1up',
67+
},
68+
on: sinon.stub(),
69+
off: sinon.stub(),
70+
jumpToIndex: sinon.stub(),
71+
switchMode: sinon.stub(),
72+
plugins: {
73+
textSelection: {
74+
options: { enabled: true },
75+
},
76+
},
77+
};
78+
79+
brIntegration = new BookReaderIntegration(fakeAnnotator);
80+
});
81+
82+
afterEach(() => {
83+
delete window.br;
84+
});
85+
86+
it('re-anchors placeholders if page now visible', () => {
87+
const [placeholderHighlight, anchor] = createPlaceholderHighlight(
88+
brRoot,
89+
fakeAnchor,
90+
fakeAnnotator,
91+
);
92+
brIntegration._update();
93+
94+
// Page is not attached, so placeholder should remain
95+
assert.isNotNull(placeholderHighlight.parentNode);
96+
assert.lengthOf(anchor.highlights, 1);
97+
assert.equal(anchor.highlights[0], placeholderHighlight);
98+
assert.notCalled(fakeAnnotator.anchor);
99+
100+
// Attach the page
101+
createFakePage(brRoot);
102+
brIntegration._update();
103+
104+
// Now the page is visible, so placeholder should be removed
105+
assert.isNull(placeholderHighlight.parentNode);
106+
assert.lengthOf(anchor.highlights, 0);
107+
assert.calledOnce(fakeAnnotator.anchor);
108+
assert.calledWith(fakeAnnotator.anchor, anchor.annotation);
109+
});
110+
111+
it('re-anchors annotations if their page is removed', () => {
112+
const [highlight, anchor] = createNonPlaceholderHighlight(
113+
brRoot,
114+
fakeAnchor,
115+
fakeAnnotator,
116+
);
117+
118+
// We haven't created the page, so this is like if the page has been removed
119+
brIntegration._update();
120+
121+
// The highlight should have been removed
122+
assert.isNull(highlight.parentNode);
123+
assert.lengthOf(anchor.highlights, 0);
124+
});
125+
126+
it('switches mode when anchoring in thumb mode ', async () => {
127+
window.br.activeMode.name = 'thumb';
128+
129+
await brIntegration.scrollToAnchor(fakeAnchor);
130+
131+
assert.calledOnce(window.br.switchMode);
132+
assert.calledOnce(window.br.jumpToIndex);
133+
});
134+
});
135+
});
136+
137+
function createFakePage(brRoot) {
138+
const fakePage = document.createElement('div');
139+
fakePage.classList.add('BRpagecontainer');
140+
fakePage.setAttribute('data-index', '14');
141+
brRoot.appendChild(fakePage);
142+
return fakePage;
143+
}
144+
145+
function createPlaceholderHighlight(brRoot, fakeAnchor, fakeAnnotator) {
146+
const placeholderHighlight = document.createElement('div');
147+
placeholderHighlight.classList.add('BRhypothesisPlaceholder');
148+
brRoot.appendChild(placeholderHighlight);
149+
const anchor = {
150+
...fakeAnchor,
151+
highlights: [placeholderHighlight],
152+
};
153+
fakeAnnotator.anchors = [anchor];
154+
return [placeholderHighlight, anchor];
155+
}
156+
157+
function createNonPlaceholderHighlight(brRoot, fakeAnchor, fakeAnnotator) {
158+
const highlight = document.createElement('div');
159+
const innerText = document.createElement('div');
160+
highlight.appendChild(innerText);
161+
brRoot.appendChild(highlight);
162+
const anchor = {
163+
...fakeAnchor,
164+
highlights: [highlight],
165+
};
166+
fakeAnnotator.anchors = [anchor];
167+
return [highlight, anchor];
168+
}

src/types/BookReader.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
/**
2-
* BookReader doesn't quite have types, so create a stub here
3-
*
4-
* Note that the definitions here are not complete, they only include properties
5-
* that the client uses.
2+
* Stub BookReader types. Note that the definitions here are not complete, they
3+
* only include properties that the client uses.
64
*/
75

86
export type BookReader = {
@@ -28,7 +26,7 @@ export type BookReader = {
2826
options?: { suppressFragmentChange: boolean },
2927
): void;
3028

31-
_plugins: {
29+
plugins: {
3230
textSelection:
3331
| undefined
3432
| {
@@ -40,5 +38,4 @@ export type BookReader = {
4038
type JQuery<T> = {
4139
[index: number]: T;
4240
length: number;
43-
find: (selector: string) => JQuery<T>;
4441
};

0 commit comments

Comments
 (0)