Commit eefac00b authored by Robert Knight's avatar Robert Knight

Enable side-by-side mode for iframe guests that fill the host page

To make side-by-side mode work for VitalSource EPUB books, the guest in
the VS content iframe needs to invoke the integration's `fitSideBySide`
method. This is done in a generic way by making guests in iframes call
this method, but only if they know that the iframe fills the host frame.
parent 1a15838c
......@@ -17,6 +17,7 @@ import { createIntegration } from './integrations';
import * as rangeUtil from './range-util';
import { SelectionObserver, selectedRange } from './selection-observer';
import { findClosestOffscreenAnchor } from './util/buckets';
import { frameFillsAncestor } from './util/frame';
import { normalizeURI } from './util/url';
/**
......@@ -187,7 +188,7 @@ export class Guest {
* @type {PortRPC<HostToGuestEvent, GuestToHostEvent>}
*/
this._hostRPC = new PortRPC();
this._connectHost();
this._connectHost(hostFrame);
/**
* Channel for guest-sidebar communication.
......@@ -301,7 +302,8 @@ export class Guest {
}
}
async _connectHost() {
/** @param {Window} hostFrame */
async _connectHost(hostFrame) {
this._hostRPC.on('clearSelection', () => {
if (selectedRange(document)) {
this._informHostOnNextSelectionClear = false;
......@@ -339,7 +341,7 @@ export class Guest {
'sidebarLayoutChanged',
/** @param {SidebarLayout} sidebarLayout */
sidebarLayout => {
if (this._frameIdentifier === null) {
if (frameFillsAncestor(window, hostFrame)) {
this.fitSideBySide(sidebarLayout);
}
}
......
......@@ -39,6 +39,7 @@ describe('Guest', () => {
let fakeBucketBarClient;
let fakeCreateIntegration;
let fakeFindClosestOffscreenAnchor;
let fakeFrameFillsAncestor;
let fakeIntegration;
let fakePortFinder;
let FakePortRPC;
......@@ -123,6 +124,8 @@ describe('Guest', () => {
fakeFindClosestOffscreenAnchor = sinon.stub();
fakeFrameFillsAncestor = sinon.stub().returns(true);
fakeIntegration = {
anchor: sinon.stub(),
canAnnotate: sinon.stub().returns(true),
......@@ -178,6 +181,9 @@ describe('Guest', () => {
'./util/buckets': {
findClosestOffscreenAnchor: fakeFindClosestOffscreenAnchor,
},
'./util/frame': {
frameFillsAncestor: fakeFrameFillsAncestor,
},
});
});
......@@ -189,18 +195,21 @@ describe('Guest', () => {
describe('events from host frame', () => {
describe('on "sidebarLayoutChanged" event', () => {
it('calls fitSideBySide if `Guest` is the main annotatable frame', () => {
it('calls fitSideBySide if guest frame fills host frame', () => {
createGuest();
const dummyLayout = {};
fakeFrameFillsAncestor.withArgs(window, hostFrame).returns(true);
emitHostEvent('sidebarLayoutChanged', dummyLayout);
assert.calledWith(fakeFrameFillsAncestor, window, hostFrame);
assert.calledWith(fakeIntegration.fitSideBySide, dummyLayout);
});
it('does not call fitSideBySide if `Guest` is not the main annotatable frame', () => {
it('does not call fitSideBySide if guest frame does not fill host frame', () => {
createGuest({ subFrameIdentifier: 'dummy' });
const dummyLayout = {};
fakeFrameFillsAncestor.withArgs(window, hostFrame).returns(false);
emitHostEvent('sidebarLayoutChanged', dummyLayout);
......
/**
* Test whether an iframe fills the viewport of an ancestor frame.
*
* @param {Window} frame
* @param {Window} ancestor
*/
export function frameFillsAncestor(frame, ancestor) {
if (frame === ancestor) {
return true;
}
if (frame.parent !== ancestor) {
// To keep things simple, we initially only support direct ancestors.
return false;
}
if (!frame.frameElement) {
// This is a cross-origin iframe. In this case we can't tell if it fills
// the parent frame or not.
return false;
}
const frameBox = frame.frameElement.getBoundingClientRect();
return frameBox.width / frame.parent.innerWidth >= 0.8;
}
import { frameFillsAncestor } from '../frame';
describe('annotator/util/frame', () => {
let frames;
function createFrame() {
const frame = document.createElement('iframe');
frames.push(frame);
document.body.append(frame);
return frame;
}
beforeEach(() => {
frames = [];
});
afterEach(() => {
frames.forEach(f => f.remove());
});
describe('frameFillsAncestor', () => {
it('returns true if both frames are the same', () => {
assert.isTrue(frameFillsAncestor(window, window));
});
it('returns false if ancestor is not direct parent of frame', () => {
const child = createFrame();
assert.isFalse(frameFillsAncestor(child.contentWindow, window.parent));
});
it('returns true if frame fills parent', () => {
const child = createFrame();
child.style.width = `${window.innerWidth}px`;
assert.isTrue(frameFillsAncestor(child.contentWindow, window));
});
it('returns false if frame does not fill parent', () => {
const child = createFrame();
child.style.width = `${window.innerWidth * 0.5}px`;
assert.isFalse(frameFillsAncestor(child.contentWindow, window));
});
it('returns false if frames are not same-origin', () => {
const child = createFrame();
// Simulate cross-origin parent
Object.defineProperty(child.contentWindow, 'frameElement', {
value: null,
});
assert.isFalse(frameFillsAncestor(child.contentWindow, window));
});
});
});
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