Commit 7303742a authored by Robert Knight's avatar Robert Knight

Retrieve VitalSource book ID and metadata from <mosaic-book> element

When the "book_as_single_document" flag is enabled, retrieve the book ID
and title from the `<mosaic-book>` element in the container frame, and
use it to populate the URI and metadata for annotations.
parent 893f0498
......@@ -98,4 +98,24 @@ export class MosaicBookElement extends HTMLElement {
this.prevButton.disabled = index === 0;
this.nextButton.disabled = index === this.chapterURLs.length - 1;
}
getBookInfo() {
const book = this.getAttribute('book');
if (book === 'little-women') {
return {
format: 'epub',
isbn: '9780451532084',
title: 'Little Women',
};
} else if (book === 'test-pdf') {
return {
format: 'pbk',
isbn: 'TEST-PDF',
title: 'Test PDF',
};
} else {
throw new Error('Unknown book ID');
}
}
}
......@@ -191,12 +191,24 @@ describe('annotator/integrations/vitalsource', () => {
});
});
class FakeMosaicBookElement {
getBookInfo() {
return {
format: 'epub',
title: 'Test book title',
isbn: 'TEST-BOOK-ID',
};
}
}
describe('VitalSourceContentIntegration', () => {
let integrations;
let fakeBookElement;
function createIntegration() {
const integration = new VitalSourceContentIntegration(document.body, {
features: featureFlags,
bookElement: fakeBookElement,
});
integrations.push(integration);
return integration;
......@@ -204,6 +216,7 @@ describe('annotator/integrations/vitalsource', () => {
beforeEach(() => {
integrations = [];
fakeBookElement = new FakeMosaicBookElement();
});
afterEach(() => {
......@@ -265,11 +278,26 @@ describe('annotator/integrations/vitalsource', () => {
});
describe('#getMetadata', () => {
it('returns book metadata', async () => {
const integration = createIntegration();
const metadata = await integration.getMetadata();
assert.equal(metadata.title, document.title);
assert.deepEqual(metadata.link, []);
context('when "book_as_single_document" flag is off', () => {
it('returns metadata for current page/chapter', async () => {
const integration = createIntegration();
const metadata = await integration.getMetadata();
assert.equal(metadata.title, document.title);
assert.deepEqual(metadata.link, []);
});
});
context('when "book_as_single_document" flag is on', () => {
beforeEach(() => {
featureFlags.update({ book_as_single_document: true });
});
it('returns book metadata', async () => {
const integration = createIntegration();
const metadata = await integration.getMetadata();
assert.equal(metadata.title, 'Test book title');
assert.deepEqual(metadata.link, []);
});
});
});
......@@ -305,7 +333,7 @@ describe('annotator/integrations/vitalsource', () => {
const uri = await integration.uri();
assert.equal(
uri,
'https://bookshelf.vitalsource.com/reader/books/1234'
'https://bookshelf.vitalsource.com/reader/books/TEST-BOOK-ID'
);
});
});
......
......@@ -22,11 +22,41 @@ import type { InjectConfig } from '../hypothesis-injector';
// smaller and it feels unreadable or too-zoomed-out
const MIN_CONTENT_WIDTH = 480;
/**
* Book metadata exposed by the VitalSource viewer.
*/
type BookInfo = {
/**
* Indicates the book type. "epub" means the book was created from an EPUB
* and the content is XHTML. "pbk" means the book was created from a PDF and
* has a fixed layout.
*/
format: 'epub' | 'pbk';
/**
* VitalSource book ID ("vbid"). This identifier is _usually_ the book's
* ISBN, hence the field name. However this value is not always a valid ISBN.
*/
isbn: string;
title: string;
};
/**
* `<mosaic-book>` custom element in the VitalSource container frame.
*
* This element has various extra methods that can be used to fetch book
* metadata, get information about the current location and navigate the book.
*/
type MosaicBookElement = HTMLElement & {
/** Returns metadata about the currently loaded book. */
getBookInfo(): BookInfo;
};
/**
* Return the custom DOM element that contains the book content iframe.
*/
function findBookElement(document_ = document) {
return document_.querySelector('mosaic-book');
function findBookElement(document_ = document): MosaicBookElement | null {
return document_.querySelector('mosaic-book') as MosaicBookElement | null;
}
/**
......@@ -206,6 +236,7 @@ export class VitalSourceContentIntegration
extends TinyEmitter
implements Integration
{
private _bookElement: MosaicBookElement;
private _features: IFeatureFlags;
private _htmlIntegration: HTMLIntegration;
private _listeners: ListenerCollection;
......@@ -214,12 +245,25 @@ export class VitalSourceContentIntegration
constructor(
/* istanbul ignore next - defaults are overridden in tests */
container: HTMLElement = document.body,
options: { features: IFeatureFlags }
options: {
features: IFeatureFlags;
// Test seam
bookElement?: MosaicBookElement;
}
) {
super();
this._features = options.features;
const bookElement =
options.bookElement ?? findBookElement(window.parent.document);
if (!bookElement) {
throw new Error(
'Failed to find <mosaic-book> element in container frame'
);
}
this._bookElement = bookElement;
// If the book_as_single_document flag changed, this will change the
// document URI returned by this integration.
this._features.on('flagsChanged', () => {
......@@ -351,6 +395,14 @@ export class VitalSourceContentIntegration
}
async getMetadata() {
if (this._bookIsSingleDocument()) {
const bookInfo = this._bookElement.getBookInfo();
return {
title: bookInfo.title,
link: [],
};
}
// Return minimal metadata which includes only the information we really
// want to include.
return {
......@@ -361,27 +413,26 @@ export class VitalSourceContentIntegration
async uri() {
if (this._bookIsSingleDocument()) {
// Dummy book ID that will in future be replaced with a real value
// retrieved via the `<mosaic-book>` element's API.
const bookId = '1234';
const bookInfo = this._bookElement.getBookInfo();
const bookId = bookInfo.isbn;
return `https://bookshelf.vitalsource.com/reader/books/${bookId}`;
} else {
// An example of a typical URL for the chapter content in the Bookshelf reader is:
//
// https://jigsaw.vitalsource.com/books/9781848317703/epub/OPS/xhtml/chapter_001.html#cfi=/6/10%5B;vnd.vst.idref=chap001%5D!/4
//
// Where "9781848317703" is the VitalSource book ID ("vbid"), "chapter_001.html"
// is the location of the HTML page for the current chapter within the book
// and the `#cfi` fragment identifies the scroll location.
//
// Note that this URL is typically different than what is displayed in the
// iframe's `src` attribute.
// Strip off search parameters and fragments.
const uri = new URL(document.location.href);
uri.search = '';
return uri.toString();
}
// An example of a typical URL for the chapter content in the Bookshelf reader is:
//
// https://jigsaw.vitalsource.com/books/9781848317703/epub/OPS/xhtml/chapter_001.html#cfi=/6/10%5B;vnd.vst.idref=chap001%5D!/4
//
// Where "9781848317703" is the VitalSource book ID ("vbid"), "chapter_001.html"
// is the location of the HTML page for the current chapter within the book
// and the `#cfi` fragment identifies the scroll location.
//
// Note that this URL is typically different than what is displayed in the
// iframe's `src` attribute.
// Strip off search parameters and fragments.
const uri = new URL(document.location.href);
uri.search = '';
return uri.toString();
}
async scrollToAnchor(anchor: 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