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'; ...@@ -17,6 +17,7 @@ import { createIntegration } from './integrations';
import * as rangeUtil from './range-util'; import * as rangeUtil from './range-util';
import { SelectionObserver, selectedRange } from './selection-observer'; import { SelectionObserver, selectedRange } from './selection-observer';
import { findClosestOffscreenAnchor } from './util/buckets'; import { findClosestOffscreenAnchor } from './util/buckets';
import { frameFillsAncestor } from './util/frame';
import { normalizeURI } from './util/url'; import { normalizeURI } from './util/url';
/** /**
...@@ -187,7 +188,7 @@ export class Guest { ...@@ -187,7 +188,7 @@ export class Guest {
* @type {PortRPC<HostToGuestEvent, GuestToHostEvent>} * @type {PortRPC<HostToGuestEvent, GuestToHostEvent>}
*/ */
this._hostRPC = new PortRPC(); this._hostRPC = new PortRPC();
this._connectHost(); this._connectHost(hostFrame);
/** /**
* Channel for guest-sidebar communication. * Channel for guest-sidebar communication.
...@@ -301,7 +302,8 @@ export class Guest { ...@@ -301,7 +302,8 @@ export class Guest {
} }
} }
async _connectHost() { /** @param {Window} hostFrame */
async _connectHost(hostFrame) {
this._hostRPC.on('clearSelection', () => { this._hostRPC.on('clearSelection', () => {
if (selectedRange(document)) { if (selectedRange(document)) {
this._informHostOnNextSelectionClear = false; this._informHostOnNextSelectionClear = false;
...@@ -339,7 +341,7 @@ export class Guest { ...@@ -339,7 +341,7 @@ export class Guest {
'sidebarLayoutChanged', 'sidebarLayoutChanged',
/** @param {SidebarLayout} sidebarLayout */ /** @param {SidebarLayout} sidebarLayout */
sidebarLayout => { sidebarLayout => {
if (this._frameIdentifier === null) { if (frameFillsAncestor(window, hostFrame)) {
this.fitSideBySide(sidebarLayout); this.fitSideBySide(sidebarLayout);
} }
} }
......
...@@ -39,6 +39,7 @@ describe('Guest', () => { ...@@ -39,6 +39,7 @@ describe('Guest', () => {
let fakeBucketBarClient; let fakeBucketBarClient;
let fakeCreateIntegration; let fakeCreateIntegration;
let fakeFindClosestOffscreenAnchor; let fakeFindClosestOffscreenAnchor;
let fakeFrameFillsAncestor;
let fakeIntegration; let fakeIntegration;
let fakePortFinder; let fakePortFinder;
let FakePortRPC; let FakePortRPC;
...@@ -123,6 +124,8 @@ describe('Guest', () => { ...@@ -123,6 +124,8 @@ describe('Guest', () => {
fakeFindClosestOffscreenAnchor = sinon.stub(); fakeFindClosestOffscreenAnchor = sinon.stub();
fakeFrameFillsAncestor = sinon.stub().returns(true);
fakeIntegration = { fakeIntegration = {
anchor: sinon.stub(), anchor: sinon.stub(),
canAnnotate: sinon.stub().returns(true), canAnnotate: sinon.stub().returns(true),
...@@ -178,6 +181,9 @@ describe('Guest', () => { ...@@ -178,6 +181,9 @@ describe('Guest', () => {
'./util/buckets': { './util/buckets': {
findClosestOffscreenAnchor: fakeFindClosestOffscreenAnchor, findClosestOffscreenAnchor: fakeFindClosestOffscreenAnchor,
}, },
'./util/frame': {
frameFillsAncestor: fakeFrameFillsAncestor,
},
}); });
}); });
...@@ -189,18 +195,21 @@ describe('Guest', () => { ...@@ -189,18 +195,21 @@ describe('Guest', () => {
describe('events from host frame', () => { describe('events from host frame', () => {
describe('on "sidebarLayoutChanged" event', () => { describe('on "sidebarLayoutChanged" event', () => {
it('calls fitSideBySide if `Guest` is the main annotatable frame', () => { it('calls fitSideBySide if guest frame fills host frame', () => {
createGuest(); createGuest();
const dummyLayout = {}; const dummyLayout = {};
fakeFrameFillsAncestor.withArgs(window, hostFrame).returns(true);
emitHostEvent('sidebarLayoutChanged', dummyLayout); emitHostEvent('sidebarLayoutChanged', dummyLayout);
assert.calledWith(fakeFrameFillsAncestor, window, hostFrame);
assert.calledWith(fakeIntegration.fitSideBySide, dummyLayout); 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' }); createGuest({ subFrameIdentifier: 'dummy' });
const dummyLayout = {}; const dummyLayout = {};
fakeFrameFillsAncestor.withArgs(window, hostFrame).returns(false);
emitHostEvent('sidebarLayoutChanged', dummyLayout); 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