Commit c82457bf authored by Robert Knight's avatar Robert Knight

Pass information about current document segment to sidebar

Enable the sidebar to know which segment of a document (eg. which chapter in an
EPUB) is loaded in a guest frame. The sidebar will then be able to use this to
know which annotations associated with the document it should send to the guest,
as well as whether to trigger a segment/chapter navigation when scrolling to an
annotation.
parent 0cf562a1
...@@ -311,14 +311,16 @@ export class Guest { ...@@ -311,14 +311,16 @@ export class Guest {
* Retrieve metadata for the current document. * Retrieve metadata for the current document.
*/ */
async getDocumentInfo() { async getDocumentInfo() {
const [uri, metadata] = await Promise.all([ const [uri, metadata, segmentInfo] = await Promise.all([
this._integration.uri(), this._integration.uri(),
this._integration.getMetadata(), this._integration.getMetadata(),
this._integration.segmentInfo?.(),
]); ]);
return { return {
uri: normalizeURI(uri), uri: normalizeURI(uri),
metadata, metadata,
segmentInfo,
}; };
} }
......
...@@ -400,6 +400,17 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -400,6 +400,17 @@ describe('annotator/integrations/vitalsource', () => {
}); });
}); });
describe('#segmentInfo', () => {
it('returns metadata for current page/chapter', async () => {
const integration = createIntegration();
const segment = await integration.segmentInfo();
assert.deepEqual(segment, {
cfi: '/2',
url: '/pages/chapter_02.xhtml',
});
});
});
describe('#uri', () => { describe('#uri', () => {
beforeEach(() => { beforeEach(() => {
const bookURI = const bookURI =
......
...@@ -12,6 +12,7 @@ import type { ...@@ -12,6 +12,7 @@ import type {
Anchor, Anchor,
FeatureFlags as IFeatureFlags, FeatureFlags as IFeatureFlags,
Integration, Integration,
SegmentInfo,
SidebarLayout, SidebarLayout,
} from '../../types/annotator'; } from '../../types/annotator';
import type { Selector } from '../../types/api'; import type { Selector } from '../../types/api';
...@@ -508,6 +509,14 @@ export class VitalSourceContentIntegration ...@@ -508,6 +509,14 @@ export class VitalSourceContentIntegration
}; };
} }
async segmentInfo(): Promise<SegmentInfo> {
const pageInfo = await this._bookElement.getCurrentPage();
return {
cfi: pageInfo.cfi,
url: pageInfo.absoluteURL,
};
}
async uri() { async uri() {
if (this._bookIsSingleDocument()) { if (this._bookIsSingleDocument()) {
const bookInfo = this._bookElement.getBookInfo(); const bookInfo = this._bookElement.getBookInfo();
......
...@@ -1382,6 +1382,30 @@ describe('Guest', () => { ...@@ -1382,6 +1382,30 @@ describe('Guest', () => {
title: 'Test title', title: 'Test title',
documentFingerprint: 'test-fingerprint', documentFingerprint: 'test-fingerprint',
}, },
segmentInfo: undefined,
});
});
it('sends segment info to sidebar when available', async () => {
fakeIntegration.uri.resolves('https://bookstore.com/books/1234');
fakeIntegration.getMetadata.resolves({ title: 'A little book' });
fakeIntegration.segmentInfo = sinon.stub().resolves({
cfi: '/2',
url: '/chapters/02.xhtml',
});
createGuest();
await delay(0);
assert.calledWith(sidebarRPC().call, 'documentInfoChanged', {
uri: 'https://bookstore.com/books/1234',
metadata: {
title: 'A little book',
},
segmentInfo: {
cfi: '/2',
url: '/chapters/02.xhtml',
},
}); });
}); });
...@@ -1398,6 +1422,7 @@ describe('Guest', () => { ...@@ -1398,6 +1422,7 @@ describe('Guest', () => {
metadata: { metadata: {
title: 'Page 1', title: 'Page 1',
}, },
segmentInfo: undefined,
}); });
sidebarRPCCall.resetHistory(); sidebarRPCCall.resetHistory();
...@@ -1412,6 +1437,7 @@ describe('Guest', () => { ...@@ -1412,6 +1437,7 @@ describe('Guest', () => {
metadata: { metadata: {
title: 'Page 2', title: 'Page 2',
}, },
segmentInfo: undefined,
}); });
}); });
......
...@@ -12,7 +12,11 @@ import { isReply, isPublic } from '../helpers/annotation-metadata'; ...@@ -12,7 +12,11 @@ import { isReply, isPublic } from '../helpers/annotation-metadata';
import { watch } from '../util/watch'; import { watch } from '../util/watch';
import type { Message } from '../../shared/messaging'; import type { Message } from '../../shared/messaging';
import type { AnnotationData, DocumentMetadata } from '../../types/annotator'; import type {
AnnotationData,
DocumentMetadata,
SegmentInfo,
} from '../../types/annotator';
import type { Annotation } from '../../types/api'; import type { Annotation } from '../../types/api';
import type { import type {
SidebarToHostEvent, SidebarToHostEvent,
...@@ -27,6 +31,7 @@ import type { AnnotationsService } from './annotations'; ...@@ -27,6 +31,7 @@ import type { AnnotationsService } from './annotations';
type DocumentInfo = { type DocumentInfo = {
uri: string; uri: string;
metadata: DocumentMetadata; metadata: DocumentMetadata;
segmentInfo?: SegmentInfo;
}; };
/** /**
...@@ -274,6 +279,7 @@ export class FrameSyncService { ...@@ -274,6 +279,7 @@ export class FrameSyncService {
id: sourceId, id: sourceId,
metadata: info.metadata, metadata: info.metadata,
uri: info.uri, uri: info.uri,
segment: info.segmentInfo,
}); });
}); });
......
...@@ -35,6 +35,19 @@ const fixtures = { ...@@ -35,6 +35,19 @@ const fixtures = {
link: [], link: [],
}, },
}, },
// Argument to the `documentInfoChanged` call made by a guest displaying an EPUB
// document.
epubDocumentInfo: {
uri: testAnnotation.uri,
metadata: {
title: 'Test book',
},
segmentInfo: {
cfi: '/2',
url: '/chapters/02.xhtml',
},
},
}; };
describe('FrameSyncService', () => { describe('FrameSyncService', () => {
...@@ -579,25 +592,29 @@ describe('FrameSyncService', () => { ...@@ -579,25 +592,29 @@ describe('FrameSyncService', () => {
frameSync.connect(); frameSync.connect();
}); });
it('adds guest frame details to the store', async () => { [fixtures.htmlDocumentInfo, fixtures.epubDocumentInfo].forEach(
const frameInfo = fixtures.htmlDocumentInfo; frameInfo => {
const frameId = 'test-frame'; it('adds guest frame details to the store', async () => {
const frameId = 'test-frame';
await connectGuest(frameId); await connectGuest(frameId);
emitGuestEvent('documentInfoChanged', frameInfo); emitGuestEvent('documentInfoChanged', frameInfo);
assert.deepEqual(fakeStore.frames(), [ assert.deepEqual(fakeStore.frames(), [
{ {
id: frameId, id: frameId,
metadata: frameInfo.metadata, metadata: frameInfo.metadata,
uri: frameInfo.uri, uri: frameInfo.uri,
segment: frameInfo.segmentInfo,
// This would be false in the real application initially, but in these // This would be false in the real application initially, but in these
// tests we pretend that the fetch completed immediately. // tests we pretend that the fetch completed immediately.
isAnnotationFetchComplete: true, isAnnotationFetchComplete: true,
}, },
]); ]);
}); });
}
);
it("synchronizes highlight visibility in the guest with the sidebar's controls", async () => { it("synchronizes highlight visibility in the guest with the sidebar's controls", async () => {
let channel; let channel;
......
...@@ -10,6 +10,7 @@ import { createStoreModule, makeAction } from '../create-store'; ...@@ -10,6 +10,7 @@ import { createStoreModule, makeAction } from '../create-store';
/** /**
* @typedef {import('../../../types/annotator').ContentInfoConfig} ContentInfoConfig * @typedef {import('../../../types/annotator').ContentInfoConfig} ContentInfoConfig
* @typedef {import('../../../types/annotator').DocumentMetadata} DocumentMetadata * @typedef {import('../../../types/annotator').DocumentMetadata} DocumentMetadata
* @typedef {import('../../../types/annotator').SegmentInfo} SegmentInfo
*/ */
/** /**
...@@ -19,6 +20,9 @@ import { createStoreModule, makeAction } from '../create-store'; ...@@ -19,6 +20,9 @@ import { createStoreModule, makeAction } from '../create-store';
* @prop {DocumentMetadata} metadata - Metadata about the document currently loaded in this frame * @prop {DocumentMetadata} metadata - Metadata about the document currently loaded in this frame
* @prop {string} uri - Current primary URI of the document being displayed * @prop {string} uri - Current primary URI of the document being displayed
* @prop {boolean} [isAnnotationFetchComplete] * @prop {boolean} [isAnnotationFetchComplete]
* @prop {SegmentInfo} [segment] - Information about the section of a document
* that is currently loaded. This is for content such as EPUBs, where the
* content displayed in a guest frame is only part of the whole document.
*/ */
const initialState = { const initialState = {
......
...@@ -49,6 +49,19 @@ export type DocumentMetadata = { ...@@ -49,6 +49,19 @@ export type DocumentMetadata = {
documentFingerprint?: string; documentFingerprint?: string;
}; };
/**
* Identifies a loadable chunk or segment of a document.
*
* Some document viewers do not load the whole document at once. For example
* an EPUB reader will load one Content Document from the publication at a time.
*/
export type SegmentInfo = {
/** Canonical Fragment Identifier for an EPUB Content Document */
cfi?: string;
/** Relative or absolute URL of the segment. */
url?: string;
};
/** /**
* A subset of annotation data allowing the representation of an annotation in * A subset of annotation data allowing the representation of an annotation in
* the document. * the document.
...@@ -168,14 +181,25 @@ export type IntegrationBase = { ...@@ -168,14 +181,25 @@ export type IntegrationBase = {
* false otherwise. * false otherwise.
*/ */
fitSideBySide(layout: SidebarLayout): boolean; fitSideBySide(layout: SidebarLayout): boolean;
/** Return the metadata of the currently loaded document, such as title, PDF fingerprint, etc. */ /** Return the metadata of the currently loaded document, such as title, PDF fingerprint, etc. */
getMetadata(): Promise<DocumentMetadata>; getMetadata(): Promise<DocumentMetadata>;
/**
* Return information about which section of the document is currently loaded.
*
* This is used for content such as EPUBs, where typically one Content Document
* (typically one chapter) is loaded at a time.
*/
segmentInfo?(): Promise<SegmentInfo>;
/** /**
* Return the URL of the currently loaded document. * Return the URL of the currently loaded document.
* *
* This may be different than the current URL (`location.href`) in a PDF for example. * This may be different than the current URL (`location.href`) in a PDF for example.
*/ */
uri(): Promise<string>; uri(): Promise<string>;
/** /**
* Scroll to an anchor. * Scroll to an anchor.
* *
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment