Commit 397f269a authored by Robert Knight's avatar Robert Knight

Convert `CrossFrame` class to JS

Convert this class to JS and make some basic documentation improvements.
parent 2e0eee75
......@@ -29,7 +29,6 @@ import DocumentPlugin from './plugin/document';
import Guest from './guest';
// @ts-expect-error
import BucketBarPlugin from './plugin/bucket-bar';
// @ts-expect-error
import CrossFramePlugin from './plugin/cross-frame';
// @ts-expect-error
import PDFPlugin from './plugin/pdf';
......
Plugin = require('../plugin')
{ default: AnnotationSync } = require('../annotation-sync')
{ default: Bridge } = require('../../shared/bridge')
{ default: Discovery } = require('../../shared/discovery')
FrameUtil = require('../util/frame-util')
{ default: FrameObserver } = require('../frame-observer')
# Extracts individual keys from an object and returns a new one.
extract = extract = (obj, keys...) ->
ret = {}
ret[key] = obj[key] for key in keys when obj.hasOwnProperty(key)
ret
# Class for establishing a messaging connection to the parent sidebar as well
# as keeping the annotation state in sync with the sidebar application, this
# frame acts as the bridge client, the sidebar is the server. This plugin
# can also be used to send messages through to the sidebar using the
# call method. This plugin also enables the discovery and management of
# not yet known frames in a multiple frame scenario.
module.exports = class CrossFrame extends Plugin
constructor: (elem, options) ->
super
config = options.config
opts = extract(options, 'server')
discovery = new Discovery(window, opts)
bridge = new Bridge()
opts = extract(options, 'on', 'emit')
annotationSync = new AnnotationSync(bridge, opts)
frameObserver = new FrameObserver(elem)
frameIdentifiers = new Map()
this.pluginInit = ->
onDiscoveryCallback = (source, origin, token) ->
bridge.createChannel(source, origin, token)
discovery.startDiscovery(onDiscoveryCallback)
frameObserver.observe(_injectToFrame, _iframeUnloaded);
this.destroy = ->
# super doesnt work here :(
Plugin::destroy.apply(this, arguments)
bridge.destroy()
discovery.stopDiscovery()
frameObserver.disconnect()
this.sync = (annotations, cb) ->
annotationSync.sync(annotations, cb)
this.on = (event, fn) ->
bridge.on(event, fn)
this.call = (message, args...) ->
bridge.call(message, args...)
this.onConnect = (fn) ->
bridge.onConnect(fn)
_injectToFrame = (frame) ->
if !FrameUtil.hasHypothesis(frame)
# Take the embed script location from the config
# until an alternative solution comes around.
clientUrl = config.clientUrl
FrameUtil.isLoaded frame, () ->
subFrameIdentifier = discovery.generateToken()
frameIdentifiers.set(frame, subFrameIdentifier)
injectedConfig = Object.assign({}, config, {subFrameIdentifier})
FrameUtil.injectHypothesis(frame, clientUrl, injectedConfig)
_iframeUnloaded = (frame) ->
bridge.call('destroyFrame', frameIdentifiers.get(frame))
frameIdentifiers.delete(frame)
// @ts-expect-error - `Plugin` base class is still written in CoffeeScript
import Plugin from '../plugin';
import AnnotationSync from '../annotation-sync';
import Bridge from '../../shared/bridge';
import Discovery from '../../shared/discovery';
import * as frameUtil from '../util/frame-util';
import FrameObserver from '../frame-observer';
/**
* @typedef {import('../../types/annotator').AnnotationData} AnnotationData
*/
/**
* `CrossFrame` provides a connection from the annotator to the sidebar.
*
* It can be used to publish events to and subscribe to events from the sidebar.
*
* This class also has logic for injecting Hypothesis into iframes that
* are added to the page if they have the `enable-annotation` attribute set
* and are same-origin with the current document.
*/
export default class CrossFrame extends Plugin {
constructor(element, options) {
super(element, options);
const { config, server, on, emit } = options;
const discovery = new Discovery(window, { server });
const bridge = new Bridge();
const annotationSync = new AnnotationSync(bridge, { on, emit });
const frameObserver = new FrameObserver(element);
const frameIdentifiers = new Map();
/**
* Inject Hypothesis into a newly-discovered iframe.
*/
const injectToFrame = frame => {
if (!frameUtil.hasHypothesis(frame)) {
const { clientUrl } = config;
frameUtil.isLoaded(frame, () => {
const subFrameIdentifier = discovery.generateToken();
frameIdentifiers.set(frame, subFrameIdentifier);
const injectedConfig = Object.assign({}, config, {
subFrameIdentifier,
});
frameUtil.injectHypothesis(frame, clientUrl, injectedConfig);
});
}
};
const iframeUnloaded = frame => {
bridge.call('destroyFrame', frameIdentifiers.get(frame));
frameIdentifiers.delete(frame);
};
/**
* Initiate the connection to the sidebar.
*/
this.pluginInit = () => {
const onDiscoveryCallback = (source, origin, token) =>
bridge.createChannel(source, origin, token);
discovery.startDiscovery(onDiscoveryCallback);
frameObserver.observe(injectToFrame, iframeUnloaded);
};
/**
* Remove the connection between the sidebar and annotator.
*/
this.destroy = () => {
bridge.destroy();
discovery.stopDiscovery();
frameObserver.disconnect();
super.destroy();
};
/**
* Notify the sidebar about new annotations created in the page.
*
* @param {AnnotationData[]} annotations
*/
this.sync = annotations => annotationSync.sync(annotations);
/**
* Subscribe to an event from the sidebar.
*
* @param {string} event
* @param {Function} callback
*/
this.on = (event, callback) => bridge.on(event, callback);
/**
* Call an RPC method exposed by the sidebar to the annotator.
*
* @param {string} method
* @param {any[]} args
*/
this.call = (method, ...args) => bridge.call(method, ...args);
/**
* Register a callback to be invoked once the connection to the sidebar
* is set up.
*
* @param {Function} callback
*/
this.onConnect = callback => bridge.onConnect(callback);
}
}
$ = require('jquery')
Plugin = require('../../plugin')
CrossFrame = require('../cross-frame')
{ $imports } = require('../cross-frame')
describe 'CrossFrame', ->
fakeDiscovery = null
fakeBridge = null
fakeAnnotationSync = null
proxyDiscovery = null
proxyBridge = null
proxyAnnotationSync = null
sandbox = sinon.createSandbox()
createCrossFrame = (options) ->
defaults =
config: {}
on: sandbox.stub()
emit: sandbox.stub()
element = document.createElement('div')
return new CrossFrame(element, $.extend({}, defaults, options))
beforeEach ->
fakeDiscovery =
startDiscovery: sandbox.stub()
stopDiscovery: sandbox.stub()
fakeBridge =
destroy: sandbox.stub()
createChannel: sandbox.stub()
onConnect: sandbox.stub()
call: sandbox.stub()
on: sandbox.stub()
fakeAnnotationSync =
sync: sandbox.stub()
proxyAnnotationSync = sandbox.stub().returns(fakeAnnotationSync)
proxyDiscovery = sandbox.stub().returns(fakeDiscovery)
proxyBridge = sandbox.stub().returns(fakeBridge)
$imports.$mock({
'../plugin': Plugin,
'../annotation-sync': proxyAnnotationSync,
'../../shared/bridge': proxyBridge,
'../../shared/discovery': proxyDiscovery
})
afterEach ->
sandbox.restore()
$imports.$restore()
describe 'CrossFrame constructor', ->
it 'instantiates the Discovery component', ->
createCrossFrame()
assert.calledWith(proxyDiscovery, window)
it 'passes the options along to the bridge', ->
createCrossFrame(server: true)
assert.calledWith(proxyDiscovery, window, server: true)
it 'instantiates the CrossFrame component', ->
createCrossFrame()
assert.calledWith(proxyDiscovery)
it 'instantiates the AnnotationSync component', ->
createCrossFrame()
assert.called(proxyAnnotationSync)
it 'passes along options to AnnotationSync', ->
createCrossFrame()
assert.calledWith(proxyAnnotationSync, fakeBridge, {
on: sinon.match.func
emit: sinon.match.func
})
describe '.pluginInit', ->
it 'starts the discovery of new channels', ->
bridge = createCrossFrame()
bridge.pluginInit()
assert.called(fakeDiscovery.startDiscovery)
it 'creates a channel when a new frame is discovered', ->
bridge = createCrossFrame()
bridge.pluginInit()
fakeDiscovery.startDiscovery.yield('SOURCE', 'ORIGIN', 'TOKEN')
assert.called(fakeBridge.createChannel)
assert.calledWith(fakeBridge.createChannel, 'SOURCE', 'ORIGIN', 'TOKEN')
describe '.destroy', ->
it 'stops the discovery of new frames', ->
cf = createCrossFrame()
cf.destroy()
assert.called(fakeDiscovery.stopDiscovery)
it 'destroys the bridge object', ->
cf = createCrossFrame()
cf.destroy()
assert.called(fakeBridge.destroy)
describe '.sync', ->
it 'syncs the annotations with the other frame', ->
bridge = createCrossFrame()
bridge.sync()
assert.called(fakeAnnotationSync.sync)
describe '.on', ->
it 'proxies the call to the bridge', ->
bridge = createCrossFrame()
bridge.on('event', 'arg')
assert.calledWith(fakeBridge.on, 'event', 'arg')
describe '.call', ->
it 'proxies the call to the bridge', ->
bridge = createCrossFrame()
bridge.call('method', 'arg1', 'arg2')
assert.calledWith(fakeBridge.call, 'method', 'arg1', 'arg2')
describe '.onConnect', ->
it 'proxies the call to the bridge', ->
bridge = createCrossFrame()
fn = ->
bridge.onConnect(fn)
assert.calledWith(fakeBridge.onConnect, fn)
import Plugin from '../../plugin';
import CrossFrame from '../cross-frame';
import { $imports } from '../cross-frame';
describe('CrossFrame', () => {
let fakeDiscovery;
let fakeBridge;
let fakeAnnotationSync;
let proxyDiscovery;
let proxyBridge;
let proxyAnnotationSync;
const createCrossFrame = options => {
const defaults = {
config: {},
on: sinon.stub(),
emit: sinon.stub(),
};
const element = document.createElement('div');
return new CrossFrame(element, { ...defaults, ...options });
};
beforeEach(() => {
fakeDiscovery = {
startDiscovery: sinon.stub(),
stopDiscovery: sinon.stub(),
};
fakeBridge = {
destroy: sinon.stub(),
createChannel: sinon.stub(),
onConnect: sinon.stub(),
call: sinon.stub(),
on: sinon.stub(),
};
fakeAnnotationSync = { sync: sinon.stub() };
proxyAnnotationSync = sinon.stub().returns(fakeAnnotationSync);
proxyDiscovery = sinon.stub().returns(fakeDiscovery);
proxyBridge = sinon.stub().returns(fakeBridge);
$imports.$mock({
'../plugin': Plugin,
'../annotation-sync': proxyAnnotationSync,
'../../shared/bridge': proxyBridge,
'../../shared/discovery': proxyDiscovery,
});
});
afterEach(() => {
$imports.$restore();
});
describe('CrossFrame constructor', () => {
it('instantiates the Discovery component', () => {
createCrossFrame();
assert.calledWith(proxyDiscovery, window);
});
it('passes the options along to the bridge', () => {
createCrossFrame({ server: true });
assert.calledWith(proxyDiscovery, window, { server: true });
});
it('instantiates the CrossFrame component', () => {
createCrossFrame();
assert.calledWith(proxyDiscovery);
});
it('instantiates the AnnotationSync component', () => {
createCrossFrame();
assert.called(proxyAnnotationSync);
});
it('passes along options to AnnotationSync', () => {
createCrossFrame();
assert.calledWith(proxyAnnotationSync, fakeBridge, {
on: sinon.match.func,
emit: sinon.match.func,
});
});
});
describe('#pluginInit', () => {
it('starts the discovery of new channels', () => {
const bridge = createCrossFrame();
bridge.pluginInit();
assert.called(fakeDiscovery.startDiscovery);
});
it('creates a channel when a new frame is discovered', () => {
const bridge = createCrossFrame();
bridge.pluginInit();
fakeDiscovery.startDiscovery.yield('SOURCE', 'ORIGIN', 'TOKEN');
assert.called(fakeBridge.createChannel);
assert.calledWith(fakeBridge.createChannel, 'SOURCE', 'ORIGIN', 'TOKEN');
});
});
describe('#destroy', () => {
it('stops the discovery of new frames', () => {
const cf = createCrossFrame();
cf.destroy();
assert.called(fakeDiscovery.stopDiscovery);
});
it('destroys the bridge object', () => {
const cf = createCrossFrame();
cf.destroy();
assert.called(fakeBridge.destroy);
});
});
describe('#sync', () => {
it('syncs the annotations with the other frame', () => {
const bridge = createCrossFrame();
bridge.sync();
assert.called(fakeAnnotationSync.sync);
});
});
describe('#on', () => {
it('proxies the call to the bridge', () => {
const bridge = createCrossFrame();
bridge.on('event', 'arg');
assert.calledWith(fakeBridge.on, 'event', 'arg');
});
});
describe('#call', () => {
it('proxies the call to the bridge', () => {
const bridge = createCrossFrame();
bridge.call('method', 'arg1', 'arg2');
assert.calledWith(fakeBridge.call, 'method', 'arg1', 'arg2');
});
});
describe('#onConnect', () => {
it('proxies the call to the bridge', () => {
const bridge = createCrossFrame();
const fn = () => {};
bridge.onConnect(fn);
assert.calledWith(fakeBridge.onConnect, fn);
});
});
});
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