Commit f1b019f0 authored by Randall Leeds's avatar Randall Leeds

Switch out jschannel for frame-rpc

The frame-rpc module is a proper CommonJS module and it's footprint
is really tiny. Its protocol is simpler. It doesn't handle connection
and buffering, but with our onConnect callback we don't need buffering.
The connection handshake that jschannel did was so trivial it's been
re-implemented here (each side tries to connect to the other once).

We have to handle timeouts ourselves, but that's all hidden in the
bridge code. In exchange, we get a simpler API, we get rid of the
call/notify distinction in favor of just passing a callback or not
and we avoid the excess overhead of the recursion guard in the
serialization code that was giving us false positive issues with
the document title.

This is one step toward removing all the browserify-shim requiring
libraries from the injected bundle, which will eventually fix #2397.
parent 7372f1b7
...@@ -58,49 +58,47 @@ module.exports = class AnnotationSync ...@@ -58,49 +58,47 @@ module.exports = class AnnotationSync
getAnnotationForTag: (tag) -> getAnnotationForTag: (tag) ->
@cache[tag] or null @cache[tag] or null
sync: (annotations, cb) -> sync: (annotations) ->
annotations = (this._format a for a in annotations) annotations = (this._format a for a in annotations)
@bridge.call @bridge.call 'sync', annotations, (err, annotations = []) =>
method: 'sync' for a in annotations
params: annotations this._parse(a)
callback: cb
this this
# Handlers for messages arriving through a channel # Handlers for messages arriving through a channel
_channelListeners: _channelListeners:
'beforeCreateAnnotation': (txn, body) -> 'beforeCreateAnnotation': (body, cb) ->
annotation = this._parse(body) annotation = this._parse(body)
delete @cache[annotation.$$tag] delete @cache[annotation.$$tag]
@_emit 'beforeAnnotationCreated', annotation @_emit 'beforeAnnotationCreated', annotation
@cache[annotation.$$tag] = annotation @cache[annotation.$$tag] = annotation
this._format annotation cb(null, this._format(annotation))
'createAnnotation': (txn, body) -> 'createAnnotation': (body, cb) ->
annotation = this._parse(body) annotation = this._parse(body)
delete @cache[annotation.$$tag] delete @cache[annotation.$$tag]
@_emit 'annotationCreated', annotation @_emit 'annotationCreated', annotation
@cache[annotation.$$tag] = annotation @cache[annotation.$$tag] = annotation
this._format annotation cb(null, this._format(annotation))
'updateAnnotation': (txn, body) -> 'updateAnnotation': (body, cb) ->
annotation = this._parse(body) annotation = this._parse(body)
delete @cache[annotation.$$tag] delete @cache[annotation.$$tag]
@_emit('beforeAnnotationUpdated', annotation) @_emit('beforeAnnotationUpdated', annotation)
@_emit('annotationUpdated', annotation) @_emit('annotationUpdated', annotation)
@cache[annotation.$$tag] = annotation @cache[annotation.$$tag] = annotation
this._format annotation cb(null, this._format(annotation))
'deleteAnnotation': (txn, body) -> 'deleteAnnotation': (body, cb) ->
annotation = this._parse(body) annotation = this._parse(body)
delete @cache[annotation.$$tag] delete @cache[annotation.$$tag]
@_emit('annotationDeleted', annotation) @_emit('annotationDeleted', annotation)
res = this._format(annotation) cb(null, this._format(annotation))
res
'sync': (ctx, bodies) -> 'sync': (bodies, cb) ->
(this._format(this._parse(b)) for b in bodies) cb(null, (this._format(this._parse(b)) for b in bodies))
'loadAnnotations': (txn, bodies) -> 'loadAnnotations': (bodies) ->
annotations = (this._parse(a) for a in bodies) annotations = (this._parse(a) for a in bodies)
@_emit('loadAnnotations', annotations) @_emit('loadAnnotations', annotations)
...@@ -127,17 +125,13 @@ module.exports = class AnnotationSync ...@@ -127,17 +125,13 @@ module.exports = class AnnotationSync
'annotationsLoaded': (annotations) -> 'annotationsLoaded': (annotations) ->
bodies = (this._format a for a in annotations when not a.$$tag) bodies = (this._format a for a in annotations when not a.$$tag)
return unless bodies.length return unless bodies.length
@bridge.notify @bridge.call('loadAnnotations', bodies)
method: 'loadAnnotations'
params: bodies
_syncCache: (channel) -> _syncCache: (channel) ->
# Synchronise (here to there) the items in our cache # Synchronise (here to there) the items in our cache
annotations = (this._format a for t, a of @cache) annotations = (this._format a for t, a of @cache)
if annotations.length if annotations.length
channel.notify channel.call('loadAnnotations', annotations)
method: 'loadAnnotations'
params: annotations
_mkCallRemotelyAndParseResults: (method, callBack) -> _mkCallRemotelyAndParseResults: (method, callBack) ->
(annotation) => (annotation) =>
...@@ -148,11 +142,7 @@ module.exports = class AnnotationSync ...@@ -148,11 +142,7 @@ module.exports = class AnnotationSync
callBack? failure, results callBack? failure, results
# Call the remote method # Call the remote method
options = @bridge.call(method, this._format(annotation), wrappedCallback)
method: method
callback: wrappedCallback
params: this._format(annotation)
@bridge.call(options)
# Parse returned message bodies to update cache with any changes made remotely # Parse returned message bodies to update cache with any changes made remotely
_parseResults: (results) -> _parseResults: (results) ->
......
...@@ -18,18 +18,18 @@ module.exports = class AnnotationUISync ...@@ -18,18 +18,18 @@ module.exports = class AnnotationUISync
tags.map(annotationSync.getAnnotationForTag, annotationSync) tags.map(annotationSync.getAnnotationForTag, annotationSync)
channelListeners = channelListeners =
showAnnotations: (ctx, tags=[]) -> showAnnotations: (tags=[]) ->
annotations = getAnnotationsByTags(tags) annotations = getAnnotationsByTags(tags)
annotationUI.selectAnnotations(annotations) annotationUI.selectAnnotations(annotations)
focusAnnotations: (ctx, tags=[]) -> focusAnnotations: (tags=[]) ->
annotations = getAnnotationsByTags(tags) annotations = getAnnotationsByTags(tags)
annotationUI.focusAnnotations(annotations) annotationUI.focusAnnotations(annotations)
toggleAnnotationSelection: (ctx, tags=[]) -> toggleAnnotationSelection: (tags=[]) ->
annotations = getAnnotationsByTags(tags) annotations = getAnnotationsByTags(tags)
annotationUI.xorSelectedAnnotations(annotations) annotationUI.xorSelectedAnnotations(annotations)
setVisibleHighlights: (ctx, state) -> setVisibleHighlights: (state) ->
annotationUI.visibleHighlights = Boolean(state) annotationUI.visibleHighlights = Boolean(state)
bridge.notify(method: 'setVisibleHighlights', params: state) bridge.call('setVisibleHighlights', state)
# Because the channel events are all outside of the angular framework we # Because the channel events are all outside of the angular framework we
# need to inform Angular that it needs to re-check it's state and re-draw # need to inform Angular that it needs to re-check it's state and re-draw
...@@ -45,8 +45,6 @@ module.exports = class AnnotationUISync ...@@ -45,8 +45,6 @@ module.exports = class AnnotationUISync
onConnect = (channel, source) -> onConnect = (channel, source) ->
# Allow the host to define its own state # Allow the host to define its own state
unless source is $window.parent unless source is $window.parent
channel.notify channel.call('setVisibleHighlights', annotationUI.visibleHighlights)
method: 'setVisibleHighlights'
params: annotationUI.visibleHighlights
bridge.onConnect(onConnect) bridge.onConnect(onConnect)
...@@ -72,10 +72,6 @@ module.exports = class Guest extends Annotator ...@@ -72,10 +72,6 @@ module.exports = class Guest extends Annotator
formatted = {} formatted = {}
for k, v of annotation when k isnt 'anchors' for k, v of annotation when k isnt 'anchors'
formatted[k] = v formatted[k] = v
# Work around issue in jschannel where a repeated object is considered
# recursive, even if it is not its own ancestor.
if formatted.document?.title
formatted.document.title = formatted.document.title.slice()
formatted formatted
this.addPlugin('CrossFrame', cfOptions) this.addPlugin('CrossFrame', cfOptions)
...@@ -115,21 +111,20 @@ module.exports = class Guest extends Annotator ...@@ -115,21 +111,20 @@ module.exports = class Guest extends Annotator
crossframe.onConnect(=> this.publish('panelReady')) crossframe.onConnect(=> this.publish('panelReady'))
crossframe.on('onEditorHide', this.onEditorHide) crossframe.on('onEditorHide', this.onEditorHide)
crossframe.on('onEditorSubmit', this.onEditorSubmit) crossframe.on('onEditorSubmit', this.onEditorSubmit)
crossframe.on 'focusAnnotations', (ctx, tags=[]) => crossframe.on 'focusAnnotations', (tags=[]) =>
for anchor in @anchors when anchor.highlights? for anchor in @anchors when anchor.highlights?
toggle = anchor.annotation.$$tag in tags toggle = anchor.annotation.$$tag in tags
$(anchor.highlights).toggleClass('annotator-hl-focused', toggle) $(anchor.highlights).toggleClass('annotator-hl-focused', toggle)
crossframe.on 'scrollToAnnotation', (ctx, tag) => crossframe.on 'scrollToAnnotation', (tag) =>
for anchor in @anchors when anchor.highlights? for anchor in @anchors when anchor.highlights?
if anchor.annotation.$$tag is tag if anchor.annotation.$$tag is tag
$(anchor.highlights).scrollintoview() $(anchor.highlights).scrollintoview()
return return
crossframe.on 'getDocumentInfo', (trans) => crossframe.on 'getDocumentInfo', (cb) =>
trans.delayReturn(true)
this.getDocumentInfo() this.getDocumentInfo()
.then((info) -> trans.complete(info)) .then((info) -> cb(null, info))
.catch((reason) -> trans.error(reason)) .catch((reason) -> cb(reason))
crossframe.on 'setVisibleHighlights', (ctx, state) => crossframe.on 'setVisibleHighlights', (state) =>
this.publish 'setVisibleHighlights', state this.publish 'setVisibleHighlights', state
_setupWrapper: -> _setupWrapper: ->
...@@ -315,24 +310,20 @@ module.exports = class Guest extends Annotator ...@@ -315,24 +310,20 @@ module.exports = class Guest extends Annotator
return annotation return annotation
showAnnotations: (annotations) => showAnnotations: (annotations) =>
@crossframe?.notify tags = (a.$$tag for a in annotations)
method: "showAnnotations" @crossframe?.call('showAnnotations', tags)
params: (a.$$tag for a in annotations)
toggleAnnotationSelection: (annotations) => toggleAnnotationSelection: (annotations) =>
@crossframe?.notify tags = (a.$$tag for a in annotations)
method: "toggleAnnotationSelection" @crossframe?.call('toggleAnnotationSelection', tags)
params: (a.$$tag for a in annotations)
updateAnnotations: (annotations) => updateAnnotations: (annotations) =>
@crossframe?.notify tags = (a.$$tag for a in annotations)
method: "updateAnnotations" @crossframe?.call('updateAnnotations', tags)
params: (a.$$tag for a in annotations)
focusAnnotations: (annotations) => focusAnnotations: (annotations) =>
@crossframe?.notify tags = (a.$$tag for a in annotations)
method: "focusAnnotations" @crossframe?.call('focusAnnotations', tags)
params: (a.$$tag for a in annotations)
onSuccessfulSelection: (event, immediate) -> onSuccessfulSelection: (event, immediate) ->
unless event? unless event?
...@@ -396,11 +387,7 @@ module.exports = class Guest extends Annotator ...@@ -396,11 +387,7 @@ module.exports = class Guest extends Annotator
# Pass true to show the highlights in the frame or false to disable. # Pass true to show the highlights in the frame or false to disable.
setVisibleHighlights: (shouldShowHighlights) -> setVisibleHighlights: (shouldShowHighlights) ->
return if @visibleHighlights == shouldShowHighlights return if @visibleHighlights == shouldShowHighlights
@crossframe?.call('setVisibleHighlights', shouldShowHighlights)
@crossframe?.notify
method: 'setVisibleHighlights'
params: shouldShowHighlights
this.toggleHighlightClass(shouldShowHighlights) this.toggleHighlightClass(shouldShowHighlights)
toggleHighlightClass: (shouldShowHighlights) -> toggleHighlightClass: (shouldShowHighlights) ->
...@@ -413,11 +400,11 @@ module.exports = class Guest extends Annotator ...@@ -413,11 +400,11 @@ module.exports = class Guest extends Annotator
# Open the sidebar # Open the sidebar
showFrame: -> showFrame: ->
@crossframe?.notify method: 'open' @crossframe?.call('open')
# Close the sidebar # Close the sidebar
hideFrame: -> hideFrame: ->
@crossframe?.notify method: 'back' @crossframe?.call('back')
onAdderMouseup: (event) -> onAdderMouseup: (event) ->
event.preventDefault() event.preventDefault()
......
...@@ -12,7 +12,7 @@ extract = extract = (obj, keys...) -> ...@@ -12,7 +12,7 @@ extract = extract = (obj, keys...) ->
# as keeping the annotation state in sync with the sidebar application, this # 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 # 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 # can also be used to send messages through to the sidebar using the
# notify method. # call method.
module.exports = class CrossFrame extends Annotator.Plugin module.exports = class CrossFrame extends Annotator.Plugin
constructor: (elem, options) -> constructor: (elem, options) ->
super super
...@@ -41,8 +41,8 @@ module.exports = class CrossFrame extends Annotator.Plugin ...@@ -41,8 +41,8 @@ module.exports = class CrossFrame extends Annotator.Plugin
this.on = (event, fn) -> this.on = (event, fn) ->
bridge.on(event, fn) bridge.on(event, fn)
this.notify = (message) -> this.call = (message, args...) ->
bridge.notify(message) bridge.call(message, args...)
this.onConnect = (fn) -> this.onConnect = (fn) ->
bridge.onConnect(fn) bridge.onConnect(fn)
...@@ -21,7 +21,7 @@ describe 'Annotator.Plugin.CrossFrame', -> ...@@ -21,7 +21,7 @@ describe 'Annotator.Plugin.CrossFrame', ->
fakeBridge = fakeBridge =
createChannel: sandbox.stub() createChannel: sandbox.stub()
onConnect: sandbox.stub() onConnect: sandbox.stub()
notify: sandbox.stub() call: sandbox.stub()
on: sandbox.stub() on: sandbox.stub()
fakeAnnotationSync = fakeAnnotationSync =
...@@ -91,11 +91,11 @@ describe 'Annotator.Plugin.CrossFrame', -> ...@@ -91,11 +91,11 @@ describe 'Annotator.Plugin.CrossFrame', ->
bridge.on('event', 'arg') bridge.on('event', 'arg')
assert.calledWith(fakeBridge.on, 'event', 'arg') assert.calledWith(fakeBridge.on, 'event', 'arg')
describe '.notify', -> describe '.call', ->
it 'proxies the call to the bridge', -> it 'proxies the call to the bridge', ->
bridge = createCrossFrame() bridge = createCrossFrame()
bridge.notify(method: 'method') bridge.call('method', 'arg1', 'arg2')
assert.calledWith(fakeBridge.notify, method: 'method') assert.calledWith(fakeBridge.call, 'method', 'arg1', 'arg2')
describe '.onConnect', -> describe '.onConnect', ->
it 'proxies the call to the bridge', -> it 'proxies the call to the bridge', ->
......
...@@ -144,18 +144,11 @@ describe 'Guest', -> ...@@ -144,18 +144,11 @@ describe 'Guest', ->
formatted = options.formatter(ann) formatted = options.formatter(ann)
assert.equal(formatted.$$tag, 'tag1') assert.equal(formatted.$$tag, 'tag1')
it 'strips the "anchors" property', -> it 'strips properties that are not whitelisted', ->
ann = {$$tag: 'tag1', anchors: []} ann = {$$tag: 'tag1', anchors: []}
formatted = options.formatter(ann) formatted = options.formatter(ann)
assert.notProperty(formatted, 'anchors') assert.notProperty(formatted, 'anchors')
it 'clones the document.title array if present', ->
title = ['Page Title']
ann = {$$tag: 'tag1', document: {title: title}}
formatted = options.formatter(ann)
assert.notStrictEqual(title, formatted.document.title)
assert.deepEqual(title, formatted.document.title)
describe 'annotation UI events', -> describe 'annotation UI events', ->
emitGuestEvent = (event, args...) -> emitGuestEvent = (event, args...) ->
fn(args...) for [evt, fn] in fakeCrossFrame.on.args when event == evt fn(args...) for [evt, fn] in fakeCrossFrame.on.args when event == evt
...@@ -183,7 +176,7 @@ describe 'Guest', -> ...@@ -183,7 +176,7 @@ describe 'Guest', ->
{annotation: {$$tag: 'tag1'}, highlights: highlight0.toArray()} {annotation: {$$tag: 'tag1'}, highlights: highlight0.toArray()}
{annotation: {$$tag: 'tag2'}, highlights: highlight1.toArray()} {annotation: {$$tag: 'tag2'}, highlights: highlight1.toArray()}
] ]
emitGuestEvent('focusAnnotations', 'ctx', ['tag1']) emitGuestEvent('focusAnnotations', ['tag1'])
assert.isTrue(highlight0.hasClass('annotator-hl-focused')) assert.isTrue(highlight0.hasClass('annotator-hl-focused'))
it 'unfocuses any annotations without a matching tag', -> it 'unfocuses any annotations without a matching tag', ->
...@@ -210,7 +203,7 @@ describe 'Guest', -> ...@@ -210,7 +203,7 @@ describe 'Guest', ->
guest.anchors = [ guest.anchors = [
{annotation: {$$tag: 'tag1'}, highlights: highlight.toArray()} {annotation: {$$tag: 'tag1'}, highlights: highlight.toArray()}
] ]
emitGuestEvent('scrollToAnnotation', 'ctx', 'tag1') emitGuestEvent('scrollToAnnotation', 'tag1')
assert.calledOn($.fn.scrollintoview, sinon.match(highlight)) assert.calledOn($.fn.scrollintoview, sinon.match(highlight))
describe 'on "getDocumentInfo" event', -> describe 'on "getDocumentInfo" event', ->
...@@ -227,7 +220,7 @@ describe 'Guest', -> ...@@ -227,7 +220,7 @@ describe 'Guest', ->
sandbox.restore() sandbox.restore()
it 'calls the callback with the href and pdf metadata', (done) -> it 'calls the callback with the href and pdf metadata', (done) ->
assertComplete = (payload) -> assertComplete = (err, payload) ->
try try
assert.equal(payload.uri, document.location.href) assert.equal(payload.uri, document.location.href)
assert.equal(payload.metadata, metadata) assert.equal(payload.metadata, metadata)
...@@ -235,15 +228,14 @@ describe 'Guest', -> ...@@ -235,15 +228,14 @@ describe 'Guest', ->
catch e catch e
done(e) done(e)
ctx = {complete: assertComplete, delayReturn: sandbox.stub()}
metadata = {title: 'hi'} metadata = {title: 'hi'}
promise = Promise.resolve(metadata) promise = Promise.resolve(metadata)
guest.plugins.PDF.getMetadata.returns(promise) guest.plugins.PDF.getMetadata.returns(promise)
emitGuestEvent('getDocumentInfo', ctx) emitGuestEvent('getDocumentInfo', assertComplete)
it 'calls the callback with the href and basic metadata if pdf fails', (done) -> it 'calls the callback with the href and basic metadata if pdf fails', (done) ->
assertComplete = (payload) -> assertComplete = (err, payload) ->
try try
assert.equal(payload.uri, window.location.href) assert.equal(payload.uri, window.location.href)
assert.deepEqual(payload.metadata, metadata) assert.deepEqual(payload.metadata, metadata)
...@@ -251,20 +243,11 @@ describe 'Guest', -> ...@@ -251,20 +243,11 @@ describe 'Guest', ->
catch e catch e
done(e) done(e)
ctx = {complete: assertComplete, delayReturn: sandbox.stub()}
metadata = {title: 'hi', link: [{href: window.location.href}]} metadata = {title: 'hi', link: [{href: window.location.href}]}
promise = Promise.reject(new Error('Not a PDF document')) promise = Promise.reject(new Error('Not a PDF document'))
guest.plugins.PDF.getMetadata.returns(promise) guest.plugins.PDF.getMetadata.returns(promise)
emitGuestEvent('getDocumentInfo', ctx) emitGuestEvent('getDocumentInfo', assertComplete)
it 'notifies the channel that the return value is async', ->
delete guest.plugins.PDF
ctx = {complete: sandbox.stub(), delayReturn: sandbox.stub()}
emitGuestEvent('getDocumentInfo', ctx)
assert.calledWith(ctx.delayReturn, true)
describe 'onAdderMouseUp', -> describe 'onAdderMouseUp', ->
it 'it prevents the default browser action when triggered', () -> it 'it prevents the default browser action when triggered', () ->
......
...@@ -16,7 +16,7 @@ describe 'Host', -> ...@@ -16,7 +16,7 @@ describe 'Host', ->
fakeCrossFrame = {} fakeCrossFrame = {}
fakeCrossFrame.onConnect = sandbox.stub().returns(fakeCrossFrame) fakeCrossFrame.onConnect = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.on = sandbox.stub().returns(fakeCrossFrame) fakeCrossFrame.on = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.notify = sandbox.stub().returns(fakeCrossFrame) fakeCrossFrame.call = sandbox.spy()
Annotator.Plugin.CrossFrame = -> fakeCrossFrame Annotator.Plugin.CrossFrame = -> fakeCrossFrame
......
$ = require('jquery') extend = require('extend')
Channel = require('jschannel') RPC = require('frame-rpc')
# The Bridge service sets up a channel between frames # The Bridge service sets up a channel between frames
# and provides an events API on top of it. # and provides an events API on top of it.
...@@ -14,20 +14,28 @@ module.exports = class Bridge ...@@ -14,20 +14,28 @@ module.exports = class Bridge
@channelListeners = {} @channelListeners = {}
@onConnectListeners = [] @onConnectListeners = []
createChannel: (source, origin, scope) -> createChannel: (source, origin, token) ->
channel = null
connected = false
ready = =>
return if connected
connected = true
for cb in @onConnectListeners
cb.call(null, channel, source)
connect = (_token, cb) =>
if _token is token
cb()
ready()
listeners = extend({connect}, @channelListeners)
# Set up a channel # Set up a channel
channelOptions = channel = new RPC(window, source, origin, listeners)
window: source
origin: origin
scope: scope
onReady: (channel) =>
for callback in @onConnectListeners
callback.call(this, channel, source)
channel = this._buildChannel channelOptions
# Attach channel message listeners # Fire off a connection attempt
for own method, callback of @channelListeners channel.call('connect', token, ready)
channel.bind method, callback
# Store the newly created channel in our collection # Store the newly created channel in our collection
@links.push @links.push
...@@ -38,55 +46,50 @@ module.exports = class Bridge ...@@ -38,55 +46,50 @@ module.exports = class Bridge
# Make a method call on all links, collect the results and pass them to a # Make a method call on all links, collect the results and pass them to a
# callback when all results are collected. Parameters: # callback when all results are collected. Parameters:
# - options.method (required): name of remote method to call # - method (required): name of remote method to call
# - options.params: parameters to pass to remote method # - args...: parameters to pass to remote method
# - options.callback: called with array of results # - callback: (optional) called with error, if any, and an Array of results
call: (options) -> call: (method, args...) ->
cb = null
if typeof(args[args.length - 1]) is 'function'
cb = args[args.length - 1]
args = args.slice(0, -1)
_makeDestroyFn = (c) => _makeDestroyFn = (c) =>
(error, reason) => (error) =>
c.destroy() c.destroy()
@links = (l for l in @links when l.channel isnt c) @links = (l for l in @links when l.channel isnt c)
throw error
deferreds = @links.map (l) -> promises = @links.map (l) ->
d = $.Deferred().fail(_makeDestroyFn l.channel) p = new Promise (resolve, reject) ->
callOptions = { timeout = setTimeout((-> resolve(null)), 1000)
method: options.method try
params: options.params l.channel.call method, args..., (err, result) ->
success: (result) -> d.resolve result clearTimeout(timeout)
error: (error, reason) -> if err then reject(err) else resolve(result)
if error isnt 'timeout_error' catch err
d.reject error, reason reject(err)
else
d.resolve null # Don't assign here. The disconnect is handled asynchronously.
timeout: 1000 return p.catch(_makeDestroyFn(l.channel))
}
l.channel.call callOptions resultPromise = Promise.all(promises)
d.promise()
if cb?
$.when(deferreds...) resultPromise = resultPromise
.then (results...) => .then((results) -> cb(null, results))
options.callback? null, results .catch((error) -> cb(error))
.fail (failure) =>
options.callback? failure return resultPromise
# Publish a notification to all links
notify: (options) ->
for l in @links
l.channel.notify options
return
on: (method, callback) -> on: (method, callback) ->
if @channelListeners[method] if @channelListeners[method]
throw new Error("Listener '#{method}' already bound in Bridge") throw new Error("Listener '#{method}' already bound in Bridge")
@channelListeners[method] = callback @channelListeners[method] = callback
for l in @links
l.channel.bind method, callback
return this return this
off: (method) -> off: (method) ->
for l in @links
l.channel.unbind method
delete @channelListeners[method] delete @channelListeners[method]
return this return this
...@@ -94,11 +97,3 @@ module.exports = class Bridge ...@@ -94,11 +97,3 @@ module.exports = class Bridge
onConnect: (callback) -> onConnect: (callback) ->
@onConnectListeners.push(callback) @onConnectListeners.push(callback)
this this
# Construct a channel to another frame
_buildChannel: (options) ->
# jschannel chokes on FF and Chrome extension origins.
if (options.origin.match /^chrome-extension:\/\//) or
(options.origin.match /^resource:\/\//)
options = $.extend {}, options, {origin: '*'}
channel = Channel.build(options)
...@@ -45,9 +45,10 @@ module.exports = class CrossFrame ...@@ -45,9 +45,10 @@ module.exports = class CrossFrame
new AnnotationUISync($rootScope, $window, bridge, annotationSync, annotationUI) new AnnotationUISync($rootScope, $window, bridge, annotationSync, annotationUI)
addFrame = (channel) => addFrame = (channel) =>
channel.call channel.call 'getDocumentInfo', (err, info) =>
method: 'getDocumentInfo' if err
success: (info) => channel.destroy()
else
$rootScope.$apply => $rootScope.$apply =>
@frames.push({channel: channel, uri: info.uri}) @frames.push({channel: channel, uri: info.uri})
...@@ -62,6 +63,6 @@ module.exports = class CrossFrame ...@@ -62,6 +63,6 @@ module.exports = class CrossFrame
bridge.createChannel(source, origin, token) bridge.createChannel(source, origin, token)
discovery.startDiscovery(onDiscoveryCallback) discovery.startDiscovery(onDiscoveryCallback)
this.notify = bridge.notify.bind(bridge) this.call = bridge.call.bind(bridge)
this.notify = -> throw new Error('connect() must be called before notify()') this.call = -> throw new Error('connect() must be called before call()')
...@@ -11,13 +11,13 @@ module.exports = [ ...@@ -11,13 +11,13 @@ module.exports = [
'$window', 'bridge' '$window', 'bridge'
($window, bridge) -> ($window, bridge) ->
host = host =
showSidebar: -> notifyHost method: 'showFrame' showSidebar: -> callHost('showFrame')
hideSidebar: -> notifyHost method: 'hideFrame' hideSidebar: -> callHost('hideFrame')
# Sends a message to the host frame # Sends a message to the host frame
notifyHost = (message) -> callHost = (method) ->
for {channel, window} in bridge.links when window is $window.parent for {channel, window} in bridge.links when window is $window.parent
channel.notify(message) channel.call(method)
break break
channelListeners = channelListeners =
......
...@@ -5,7 +5,7 @@ describe 'AnnotationSync', -> ...@@ -5,7 +5,7 @@ describe 'AnnotationSync', ->
publish = null publish = null
fakeBridge = null fakeBridge = null
createAnnotationSync = null createAnnotationSync = null
createChannel = -> {notify: sandbox.stub()} createChannel = -> {call: sandbox.stub()}
options = null options = null
PARENT_WINDOW = 'PARENT_WINDOW' PARENT_WINDOW = 'PARENT_WINDOW'
...@@ -16,13 +16,12 @@ describe 'AnnotationSync', -> ...@@ -16,13 +16,12 @@ describe 'AnnotationSync', ->
beforeEach module('h') beforeEach module('h')
beforeEach inject (AnnotationSync, $rootScope) -> beforeEach inject (AnnotationSync, $rootScope) ->
listeners = {} listeners = {}
publish = ({method, params}) -> listeners[method]('ctx', params) publish = (method, args...) -> listeners[method](args...)
fakeWindow = parent: PARENT_WINDOW fakeWindow = parent: PARENT_WINDOW
fakeBridge = fakeBridge =
on: sandbox.spy((method, fn) -> listeners[method] = fn) on: sandbox.spy((method, fn) -> listeners[method] = fn)
call: sandbox.stub() call: sandbox.stub()
notify: sandbox.stub()
onConnect: sandbox.stub() onConnect: sandbox.stub()
links: [] links: []
...@@ -47,11 +46,9 @@ describe 'AnnotationSync', -> ...@@ -47,11 +46,9 @@ describe 'AnnotationSync', ->
channel = createChannel() channel = createChannel()
fakeBridge.onConnect.yield(channel) fakeBridge.onConnect.yield(channel)
assert.called(channel.notify) assert.called(channel.call)
assert.calledWith(channel.notify, { assert.calledWith(channel.call, 'loadAnnotations',
method: 'loadAnnotations' [tag: 'tag1', msg: ann])
params: [tag: 'tag1', msg: ann]
})
it 'does nothing if the cache is empty', -> it 'does nothing if the cache is empty', ->
annSync = createAnnotationSync() annSync = createAnnotationSync()
...@@ -59,7 +56,7 @@ describe 'AnnotationSync', -> ...@@ -59,7 +56,7 @@ describe 'AnnotationSync', ->
channel = createChannel() channel = createChannel()
fakeBridge.onConnect.yield(channel) fakeBridge.onConnect.yield(channel)
assert.notCalled(channel.notify) assert.notCalled(channel.call)
describe '.getAnnotationForTag', -> describe '.getAnnotationForTag', ->
it 'returns the annotation if present in the cache', -> it 'returns the annotation if present in the cache', ->
...@@ -80,18 +77,20 @@ describe 'AnnotationSync', -> ...@@ -80,18 +77,20 @@ describe 'AnnotationSync', ->
it 'broadcasts the "' + publishEvent + '" event over the local event bus', -> it 'broadcasts the "' + publishEvent + '" event over the local event bus', ->
ann = {id: 1, $$tag: 'tag1'} ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync() annSync = createAnnotationSync()
publish(method: channelEvent, params: {msg: ann}) publish(channelEvent, {msg: ann}, ->)
assert.called(options.emit) assert.called(options.emit)
assert.calledWith(options.emit, publishEvent, ann) assert.calledWith(options.emit, publishEvent, ann)
assertReturnValue = (channelEvent) -> assertReturnValue = (channelEvent) ->
it 'returns a formatted annotation to be sent to the calling frame', -> it 'calls back with a formatted annotation', (done) ->
ann = {id: 1, $$tag: 'tag1'} ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync() annSync = createAnnotationSync()
ret = publish(method: channelEvent, params: {msg: ann}) callback = (err, ret) ->
assert.isNull(err)
assert.deepEqual(ret, {tag: 'tag1', msg: ann}) assert.deepEqual(ret, {tag: 'tag1', msg: ann})
done()
publish(channelEvent, {msg: ann}, callback)
assertCacheState = (channelEvent) -> assertCacheState = (channelEvent) ->
it 'removes an existing entry from the cache before the event is triggered', -> it 'removes an existing entry from the cache before the event is triggered', ->
...@@ -101,13 +100,13 @@ describe 'AnnotationSync', -> ...@@ -101,13 +100,13 @@ describe 'AnnotationSync', ->
annSync = createAnnotationSync() annSync = createAnnotationSync()
annSync.cache['tag1'] = ann annSync.cache['tag1'] = ann
publish(method: channelEvent, params: {msg: ann}) publish(channelEvent, {msg: ann}, ->)
it 'ensures the annotation is inserted in the cache', -> it 'ensures the annotation is inserted in the cache', ->
ann = {id: 1, $$tag: 'tag1'} ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync() annSync = createAnnotationSync()
publish(method: channelEvent, params: {msg: ann}) publish(channelEvent, {msg: ann}, ->)
assert.equal(annSync.cache['tag1'], ann) assert.equal(annSync.cache['tag1'], ann)
...@@ -138,29 +137,33 @@ describe 'AnnotationSync', -> ...@@ -138,29 +137,33 @@ describe 'AnnotationSync', ->
annSync = createAnnotationSync() annSync = createAnnotationSync()
annSync.cache['tag1'] = ann annSync.cache['tag1'] = ann
publish(method: 'deleteAnnotation', params: {msg: ann}) publish('deleteAnnotation', {msg: ann}, ->)
it 'removes the annotation from the cache', -> it 'removes the annotation from the cache', ->
ann = {id: 1, $$tag: 'tag1'} ann = {id: 1, $$tag: 'tag1'}
annSync = createAnnotationSync() annSync = createAnnotationSync()
publish(method: 'deleteAnnotation', params: {msg: ann}) publish('deleteAnnotation', {msg: ann}, ->)
assert(!annSync.cache['tag1']) assert(!annSync.cache['tag1'])
describe 'the "sync" event', -> describe 'the "sync" event', ->
it 'returns an array of parsed and formatted annotations', -> it 'calls back with parsed and formatted annotations', (done) ->
options.parser = sinon.spy((x) -> x) options.parser = sinon.spy((x) -> x)
options.formatter = sinon.spy((x) -> x) options.formatter = sinon.spy((x) -> x)
annSync = createAnnotationSync() annSync = createAnnotationSync()
annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3, $$tag: 'tag3'}] annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3, $$tag: 'tag3'}]
bodies = ({msg: ann, tag: ann.$$tag} for ann in annotations) bodies = ({msg: ann, tag: ann.$$tag} for ann in annotations)
ret = publish(method: 'sync', params: bodies)
assert.deepEqual(ret, ret) callback = (err, ret) ->
assert.isNull(err)
assert.deepEqual(ret, bodies)
assert.called(options.parser) assert.called(options.parser)
assert.called(options.formatter) assert.called(options.formatter)
done()
publish('sync', bodies, callback)
describe 'the "loadAnnotations" event', -> describe 'the "loadAnnotations" event', ->
it 'publishes the "loadAnnotations" event with parsed annotations', -> it 'publishes the "loadAnnotations" event with parsed annotations', ->
...@@ -169,7 +172,7 @@ describe 'AnnotationSync', -> ...@@ -169,7 +172,7 @@ describe 'AnnotationSync', ->
annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3, $$tag: 'tag3'}] annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3, $$tag: 'tag3'}]
bodies = ({msg: ann, tag: ann.$$tag} for ann in annotations) bodies = ({msg: ann, tag: ann.$$tag} for ann in annotations)
ret = publish(method: 'loadAnnotations', params: bodies) publish('loadAnnotations', bodies, ->)
assert.called(options.parser) assert.called(options.parser)
assert.calledWith(options.emit, 'loadAnnotations', annotations) assert.calledWith(options.emit, 'loadAnnotations', annotations)
...@@ -182,11 +185,8 @@ describe 'AnnotationSync', -> ...@@ -182,11 +185,8 @@ describe 'AnnotationSync', ->
options.emit('beforeAnnotationCreated', ann) options.emit('beforeAnnotationCreated', ann)
assert.called(fakeBridge.call) assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, { assert.calledWith(fakeBridge.call, 'beforeCreateAnnotation',
method: 'beforeCreateAnnotation', {msg: ann, tag: ann.$$tag}, sinon.match.func)
params: {msg: ann, tag: ann.$$tag},
callback: sinon.match.func
})
it 'returns early if the annotation has a tag', -> it 'returns early if the annotation has a tag', ->
ann = {id: 1, $$tag: 'tag1'} ann = {id: 1, $$tag: 'tag1'}
...@@ -203,11 +203,8 @@ describe 'AnnotationSync', -> ...@@ -203,11 +203,8 @@ describe 'AnnotationSync', ->
options.emit('annotationCreated', ann) options.emit('annotationCreated', ann)
assert.called(fakeBridge.call) assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, { assert.calledWith(fakeBridge.call, 'createAnnotation',
method: 'createAnnotation', {msg: ann, tag: ann.$$tag}, sinon.match.func)
params: {msg: ann, tag: ann.$$tag},
callback: sinon.match.func
})
it 'returns early if the annotation has a tag but is not cached', -> it 'returns early if the annotation has a tag but is not cached', ->
ann = {id: 1, $$tag: 'tag1'} ann = {id: 1, $$tag: 'tag1'}
...@@ -231,11 +228,8 @@ describe 'AnnotationSync', -> ...@@ -231,11 +228,8 @@ describe 'AnnotationSync', ->
options.emit('annotationUpdated', ann) options.emit('annotationUpdated', ann)
assert.called(fakeBridge.call) assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, { assert.calledWith(fakeBridge.call, 'updateAnnotation',
method: 'updateAnnotation', {msg: ann, tag: ann.$$tag}, sinon.match.func)
params: {msg: ann, tag: ann.$$tag},
callback: sinon.match.func
})
it 'returns early if the annotation has a tag but is not cached', -> it 'returns early if the annotation has a tag but is not cached', ->
ann = {id: 1, $$tag: 'tag1'} ann = {id: 1, $$tag: 'tag1'}
...@@ -259,11 +253,8 @@ describe 'AnnotationSync', -> ...@@ -259,11 +253,8 @@ describe 'AnnotationSync', ->
options.emit('annotationDeleted', ann) options.emit('annotationDeleted', ann)
assert.called(fakeBridge.call) assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.call, { assert.calledWith(fakeBridge.call, 'deleteAnnotation',
method: 'deleteAnnotation', {msg: ann, tag: ann.$$tag}, sinon.match.func)
params: {msg: ann, tag: ann.$$tag},
callback: sinon.match.func
})
it 'parses the result returned by the call', -> it 'parses the result returned by the call', ->
ann = {id: 1, $$tag: 'tag1'} ann = {id: 1, $$tag: 'tag1'}
...@@ -273,7 +264,7 @@ describe 'AnnotationSync', -> ...@@ -273,7 +264,7 @@ describe 'AnnotationSync', ->
options.emit('annotationDeleted', ann) options.emit('annotationDeleted', ann)
body = {msg: {}, tag: 'tag1'} body = {msg: {}, tag: 'tag1'}
fakeBridge.call.yieldTo('callback', null, [body]) fakeBridge.call.yield(null, [body])
assert.called(options.parser) assert.called(options.parser)
assert.calledWith(options.parser, {}) assert.calledWith(options.parser, {})
...@@ -283,7 +274,7 @@ describe 'AnnotationSync', -> ...@@ -283,7 +274,7 @@ describe 'AnnotationSync', ->
annSync.cache.tag1 = ann annSync.cache.tag1 = ann
options.emit('annotationDeleted', ann) options.emit('annotationDeleted', ann)
fakeBridge.call.yieldTo('callback', null, []) fakeBridge.call.yield(null, [])
assert.isUndefined(annSync.cache.tag1) assert.isUndefined(annSync.cache.tag1)
it 'does not remove the annotation from the cache if an error occurs', -> it 'does not remove the annotation from the cache if an error occurs', ->
...@@ -292,7 +283,7 @@ describe 'AnnotationSync', -> ...@@ -292,7 +283,7 @@ describe 'AnnotationSync', ->
annSync.cache.tag1 = ann annSync.cache.tag1 = ann
options.emit('annotationDeleted', ann) options.emit('annotationDeleted', ann)
fakeBridge.call.yieldTo('callback', new Error('Error'), []) fakeBridge.call.yield(new Error('Error'), [])
assert.equal(annSync.cache.tag1, ann) assert.equal(annSync.cache.tag1, ann)
it 'returns early if the annotation has a tag but is not cached', -> it 'returns early if the annotation has a tag but is not cached', ->
...@@ -326,11 +317,9 @@ describe 'AnnotationSync', -> ...@@ -326,11 +317,9 @@ describe 'AnnotationSync', ->
annSync = createAnnotationSync() annSync = createAnnotationSync()
options.emit('annotationsLoaded', annotations) options.emit('annotationsLoaded', annotations)
assert.called(fakeBridge.notify) assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.notify, { assert.calledWith(fakeBridge.call, 'loadAnnotations',
method: 'loadAnnotations', ({msg: a, tag: a.$$tag} for a in annotations))
params: {msg: a, tag: a.$$tag} for a in annotations
})
it 'does not send annotations that have already been tagged', -> it 'does not send annotations that have already been tagged', ->
annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3}] annotations = [{id: 1, $$tag: 'tag1'}, {id: 2, $$tag: 'tag2'}, {id: 3}]
...@@ -338,14 +327,12 @@ describe 'AnnotationSync', -> ...@@ -338,14 +327,12 @@ describe 'AnnotationSync', ->
annSync = createAnnotationSync() annSync = createAnnotationSync()
options.emit('annotationsLoaded', annotations) options.emit('annotationsLoaded', annotations)
assert.called(fakeBridge.notify) assert.called(fakeBridge.call)
assert.calledWith(fakeBridge.notify, { assert.calledWith(fakeBridge.call, 'loadAnnotations',
method: 'loadAnnotations', [{msg: annotations[2], tag: annotations[2].$$tag}])
params: [{msg: annotations[2], tag: annotations[2].$$tag}]
})
it 'returns early if no annotations are loaded', -> it 'returns early if no annotations are loaded', ->
annSync = createAnnotationSync() annSync = createAnnotationSync()
options.emit('annotationsLoaded', []) options.emit('annotationsLoaded', [])
assert.notCalled(fakeBridge.notify) assert.notCalled(fakeBridge.call)
...@@ -9,7 +9,7 @@ describe 'AnnotationUISync', -> ...@@ -9,7 +9,7 @@ describe 'AnnotationUISync', ->
fakeAnnotationUI = null fakeAnnotationUI = null
fakeAnnotationSync = null fakeAnnotationSync = null
createAnnotationUISync = null createAnnotationUISync = null
createChannel = -> {notify: sandbox.stub()} createChannel = -> {call: sandbox.stub()}
PARENT_WINDOW = 'PARENT_WINDOW' PARENT_WINDOW = 'PARENT_WINDOW'
before -> before ->
...@@ -20,12 +20,12 @@ describe 'AnnotationUISync', -> ...@@ -20,12 +20,12 @@ describe 'AnnotationUISync', ->
beforeEach inject (AnnotationUISync, $rootScope) -> beforeEach inject (AnnotationUISync, $rootScope) ->
$digest = sandbox.stub($rootScope, '$digest') $digest = sandbox.stub($rootScope, '$digest')
listeners = {} listeners = {}
publish = ({method, params}) -> listeners[method]('ctx', params) publish = (method, args...) -> listeners[method](args...)
fakeWindow = parent: PARENT_WINDOW fakeWindow = parent: PARENT_WINDOW
fakeBridge = fakeBridge =
on: sandbox.spy((method, fn) -> listeners[method] = fn) on: sandbox.spy((method, fn) -> listeners[method] = fn)
notify: sandbox.stub() call: sandbox.stub()
onConnect: sandbox.stub() onConnect: sandbox.stub()
links: [ links: [
{window: PARENT_WINDOW, channel: createChannel()} {window: PARENT_WINDOW, channel: createChannel()}
...@@ -54,26 +54,20 @@ describe 'AnnotationUISync', -> ...@@ -54,26 +54,20 @@ describe 'AnnotationUISync', ->
createAnnotationUISync() createAnnotationUISync()
assert.calledWith(channel.notify, { assert.calledWith(channel.call, 'setVisibleHighlights', false)
method: 'setVisibleHighlights'
params: false
})
describe 'when the source is the parent window', -> describe 'when the source is the parent window', ->
it 'does nothing', -> it 'does nothing', ->
channel = notify: sandbox.stub() channel = call: sandbox.stub()
fakeBridge.onConnect.callsArgWith(0, channel, PARENT_WINDOW) fakeBridge.onConnect.callsArgWith(0, channel, PARENT_WINDOW)
createAnnotationUISync() createAnnotationUISync()
assert.notCalled(channel.notify) assert.notCalled(channel.call)
describe 'on "showAnnotations" event', -> describe 'on "showAnnotations" event', ->
it 'updates the annotationUI to include the shown annotations', -> it 'updates the annotationUI to include the shown annotations', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('showAnnotations', ['tag1', 'tag2', 'tag3'])
method: 'showAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
assert.called(fakeAnnotationUI.selectAnnotations) assert.called(fakeAnnotationUI.selectAnnotations)
assert.calledWith(fakeAnnotationUI.selectAnnotations, [ assert.calledWith(fakeAnnotationUI.selectAnnotations, [
{id: 1}, {id: 2}, {id: 3} {id: 1}, {id: 2}, {id: 3}
...@@ -81,19 +75,13 @@ describe 'AnnotationUISync', -> ...@@ -81,19 +75,13 @@ describe 'AnnotationUISync', ->
it 'triggers a digest', -> it 'triggers a digest', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('showAnnotations', ['tag1', 'tag2', 'tag3'])
method: 'showAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
assert.called($digest) assert.called($digest)
describe 'on "focusAnnotations" event', -> describe 'on "focusAnnotations" event', ->
it 'updates the annotationUI to show the provided annotations', -> it 'updates the annotationUI to show the provided annotations', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('focusAnnotations', ['tag1', 'tag2', 'tag3'])
method: 'focusAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
assert.called(fakeAnnotationUI.focusAnnotations) assert.called(fakeAnnotationUI.focusAnnotations)
assert.calledWith(fakeAnnotationUI.focusAnnotations, [ assert.calledWith(fakeAnnotationUI.focusAnnotations, [
{id: 1}, {id: 2}, {id: 3} {id: 1}, {id: 2}, {id: 3}
...@@ -101,19 +89,13 @@ describe 'AnnotationUISync', -> ...@@ -101,19 +89,13 @@ describe 'AnnotationUISync', ->
it 'triggers a digest', -> it 'triggers a digest', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('focusAnnotations', ['tag1', 'tag2', 'tag3'])
method: 'focusAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
assert.called($digest) assert.called($digest)
describe 'on "toggleAnnotationSelection" event', -> describe 'on "toggleAnnotationSelection" event', ->
it 'updates the annotationUI to show the provided annotations', -> it 'updates the annotationUI to show the provided annotations', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('toggleAnnotationSelection', ['tag1', 'tag2', 'tag3'])
method: 'toggleAnnotationSelection',
params: ['tag1', 'tag2', 'tag3']
})
assert.called(fakeAnnotationUI.xorSelectedAnnotations) assert.called(fakeAnnotationUI.xorSelectedAnnotations)
assert.calledWith(fakeAnnotationUI.xorSelectedAnnotations, [ assert.calledWith(fakeAnnotationUI.xorSelectedAnnotations, [
{id: 1}, {id: 2}, {id: 3} {id: 1}, {id: 2}, {id: 3}
...@@ -121,36 +103,21 @@ describe 'AnnotationUISync', -> ...@@ -121,36 +103,21 @@ describe 'AnnotationUISync', ->
it 'triggers a digest', -> it 'triggers a digest', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('toggleAnnotationSelection', ['tag1', 'tag2', 'tag3'])
method: 'toggleAnnotationSelection',
params: ['tag1', 'tag2', 'tag3']
})
assert.called($digest) assert.called($digest)
describe 'on "setVisibleHighlights" event', -> describe 'on "setVisibleHighlights" event', ->
it 'updates the annotationUI with the new value', -> it 'updates the annotationUI with the new value', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('setVisibleHighlights', true)
method: 'setVisibleHighlights',
params: true
})
assert.equal(fakeAnnotationUI.visibleHighlights, true) assert.equal(fakeAnnotationUI.visibleHighlights, true)
it 'notifies the other frames of the change', -> it 'notifies the other frames of the change', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('setVisibleHighlights', true)
method: 'setVisibleHighlights', assert.calledWith(fakeBridge.call, 'setVisibleHighlights', true)
params: true
})
assert.calledWith(fakeBridge.notify, {
method: 'setVisibleHighlights'
params: true
})
it 'triggers a digest of the application state', -> it 'triggers a digest of the application state', ->
createAnnotationUISync() createAnnotationUISync()
publish({ publish('setVisibleHighlights', true)
method: 'setVisibleHighlights',
params: true
})
assert.called($digest) assert.called($digest)
This diff is collapsed.
...@@ -26,7 +26,7 @@ describe 'CrossFrame', -> ...@@ -26,7 +26,7 @@ describe 'CrossFrame', ->
fakeDiscovery = fakeDiscovery =
startDiscovery: sandbox.stub() startDiscovery: sandbox.stub()
fakeBridge = fakeBridge =
notify: sandbox.stub() call: sandbox.stub()
createChannel: sandbox.stub() createChannel: sandbox.stub()
onConnect: sandbox.stub() onConnect: sandbox.stub()
fakeAnnotationSync = {} fakeAnnotationSync = {}
...@@ -61,28 +61,16 @@ describe 'CrossFrame', -> ...@@ -61,28 +61,16 @@ describe 'CrossFrame', ->
it 'queries discovered frames for metadata', -> it 'queries discovered frames for metadata', ->
uri = 'http://example.com' uri = 'http://example.com'
channel = {call: sandbox.stub().yieldsTo('success', {uri: uri})} channel = {call: sandbox.stub().yields(null, {uri: uri})}
fakeBridge.onConnect.yields(channel) fakeBridge.onConnect.yields(channel)
crossframe.connect() crossframe.connect()
assert.calledWith(channel.call, { assert.calledWith(channel.call, 'getDocumentInfo', sinon.match.func)
method: 'getDocumentInfo'
success: sinon.match.func
})
it 'updates the frames array', -> it 'updates the frames array', ->
uri = 'http://example.com' uri = 'http://example.com'
channel = {call: sandbox.stub().yieldsTo('success', {uri: uri})} channel = {call: sandbox.stub().yields(null, {uri: uri})}
fakeBridge.onConnect.yields(channel) fakeBridge.onConnect.yields(channel)
crossframe.connect() crossframe.connect()
assert.deepEqual(crossframe.frames, [ assert.deepEqual(crossframe.frames, [
{channel: channel, uri: uri} {channel: channel, uri: uri}
]) ])
describe '.notify()', ->
it 'proxies the call to the bridge', ->
message = {method: 'foo', params: 'bar'}
crossframe.connect() # create the bridge.
crossframe.notify(message)
assert.calledOn(fakeBridge.notify, fakeBridge)
assert.calledWith(fakeBridge.notify, message)
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
describe 'host', -> describe 'host', ->
sandbox = null sandbox = null
host = null host = null
createChannel = -> notify: sandbox.stub() createChannel = -> call: sandbox.stub()
fakeBridge = null fakeBridge = null
$digest = null $digest = null
publish = null publish = null
...@@ -22,13 +22,13 @@ describe 'host', -> ...@@ -22,13 +22,13 @@ describe 'host', ->
listeners = {} listeners = {}
publish = ({method, params}) -> publish = (method, args...) ->
listeners[method]('ctx', params) listeners[method](args...)
fakeBridge = fakeBridge =
ls: listeners ls: listeners
on: sandbox.spy (method, fn) -> listeners[method] = fn on: sandbox.spy (method, fn) -> listeners[method] = fn
notify: sandbox.stub() call: sandbox.stub()
onConnect: sandbox.stub() onConnect: sandbox.stub()
links: [ links: [
{window: PARENT_WINDOW, channel: createChannel()} {window: PARENT_WINDOW, channel: createChannel()}
...@@ -53,16 +53,16 @@ describe 'host', -> ...@@ -53,16 +53,16 @@ describe 'host', ->
describe 'showSidebar()', -> describe 'showSidebar()', ->
it 'sends the "showFrame" message to the host only', -> it 'sends the "showFrame" message to the host only', ->
host.showSidebar() host.showSidebar()
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'showFrame') assert.calledWith(fakeBridge.links[0].channel.call, 'showFrame')
assert.notCalled(fakeBridge.links[1].channel.notify) assert.notCalled(fakeBridge.links[1].channel.call)
assert.notCalled(fakeBridge.links[2].channel.notify) assert.notCalled(fakeBridge.links[2].channel.call)
describe 'hideSidebar()', -> describe 'hideSidebar()', ->
it 'sends the "hideFrame" message to the host only', -> it 'sends the "hideFrame" message to the host only', ->
host.hideSidebar() host.hideSidebar()
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'hideFrame') assert.calledWith(fakeBridge.links[0].channel.call, 'hideFrame')
assert.notCalled(fakeBridge.links[1].channel.notify) assert.notCalled(fakeBridge.links[1].channel.call)
assert.notCalled(fakeBridge.links[2].channel.notify) assert.notCalled(fakeBridge.links[2].channel.call)
describe 'reacting to the bridge', -> describe 'reacting to the bridge', ->
...@@ -70,12 +70,12 @@ describe 'host', -> ...@@ -70,12 +70,12 @@ describe 'host', ->
it 'triggers the hideSidebar() API', -> it 'triggers the hideSidebar() API', ->
sandbox.spy host, "hideSidebar" sandbox.spy host, "hideSidebar"
publish method: 'back' publish 'back'
assert.called host.hideSidebar assert.called host.hideSidebar
describe 'on "open" event', -> describe 'on "open" event', ->
it 'triggers the showSidebar() API', -> it 'triggers the showSidebar() API', ->
sandbox.spy host, "showSidebar" sandbox.spy host, "showSidebar"
publish method: 'open' publish 'open'
assert.called host.showSidebar assert.called host.showSidebar
This diff is collapsed.
...@@ -51,15 +51,11 @@ module.exports = class WidgetController ...@@ -51,15 +51,11 @@ module.exports = class WidgetController
highlights = [annotation.$$tag] highlights = [annotation.$$tag]
else else
highlights = [] highlights = []
crossframe.notify crossframe.call('focusAnnotations', highlights)
method: 'focusAnnotations'
params: highlights
$scope.scrollTo = (annotation) -> $scope.scrollTo = (annotation) ->
if angular.isObject annotation if angular.isObject annotation
crossframe.notify crossframe.call('scrollToAnnotation', annotation.$$tag)
method: 'scrollToAnnotation'
params: annotation.$$tag
$scope.shouldShowThread = (container) -> $scope.shouldShowThread = (container) ->
if annotationUI.hasSelectedAnnotations() and not container.parent.parent if annotationUI.hasSelectedAnnotations() and not container.parent.parent
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"dom-seek": "^1.0.1", "dom-seek": "^1.0.1",
"es6-promise": "^2.1.0", "es6-promise": "^2.1.0",
"extend": "^2.0.0", "extend": "^2.0.0",
"frame-rpc": "^1.3.1",
"hammerjs": "^2.0.4", "hammerjs": "^2.0.4",
"jquery": "1.11.1", "jquery": "1.11.1",
"jstimezonedetect": "1.0.5", "jstimezonedetect": "1.0.5",
...@@ -82,8 +83,7 @@ ...@@ -82,8 +83,7 @@
"es6-promise": "./node_modules/es6-promise/dist/es6-promise.js", "es6-promise": "./node_modules/es6-promise/dist/es6-promise.js",
"hammerjs": "./node_modules/hammerjs/hammer.js", "hammerjs": "./node_modules/hammerjs/hammer.js",
"jquery": "./node_modules/jquery/dist/jquery.js", "jquery": "./node_modules/jquery/dist/jquery.js",
"jquery-scrollintoview": "./h/static/scripts/vendor/jquery.scrollintoview.js", "jquery-scrollintoview": "./h/static/scripts/vendor/jquery.scrollintoview.js"
"jschannel": "./h/static/scripts/vendor/jschannel.js"
}, },
"browserify-shim": { "browserify-shim": {
"annotator": { "annotator": {
...@@ -111,7 +111,6 @@ ...@@ -111,7 +111,6 @@
"depends": [ "depends": [
"jquery" "jquery"
] ]
}, }
"jschannel": "Channel"
} }
} }
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