Commit c5f11c91 authored by Gerben's avatar Gerben Committed by Aron Carroll

Make Bridge plugin use extracted cross-frame code

parent 3e4b617e
$ = Annotator.$
class Annotator.Plugin.Bridge extends Annotator.Plugin
# These events maintain the awareness of annotations between the two
# communicating annotators.
events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
'annotationCreated': 'annotationCreated'
'annotationUpdated': 'annotationUpdated'
'annotationDeleted': 'annotationDeleted'
'annotationsLoaded': 'annotationsLoaded'
# Plugin configuration
options:
# Origins allowed to communicate on the channel
origin: '*'
# Scope identifier to distinguish this channel from any others
scope: 'annotator:bridge'
# When this is true, this bridge will act as a gateway and, similar to DHCP,
# offer to connect to bridges in other frames it discovers.
gateway: false
# A callback to invoke when a connection is established. The function is
# passed two arguments, the source window and origin of the other frame.
onConnect: -> true
# Formats an annotation for sending across the bridge
formatter: (annotation) -> annotation
# Parses an annotation received from the bridge
parser: (annotation) -> annotation
# Merge function. If specified, it will be called with the local copy of
# an annotation and a parsed copy received as an argument to an RPC call
# to reconcile any differences. The default behavior is to merge all
# keys of the remote object into the local copy
merge: (local, remote) ->
for k, v of remote
local[k] = v
local
# Cache of annotations which have crossed the bridge for fast, encapsulated
# association of annotations received in arguments to window-local copies.
cache: null
# Connected bridge links
links: null
constructor: (elem, options) ->
super
@cache = {}
@links = []
@discovery = new CrossFrameDiscovery(options.discoveryOptions)
@bridge = new CrossFrameBridge(options.bridgeOptions)
@annotationSync = new AnnotationSync(options.annotationSyncOptions, @bridge)
pluginInit: ->
$(window).on 'message', this._onMessage
this._beacon()
onDiscoveryCallback = (source, origin, token) =>
@bridge.createChannel(source, origin, token)
@discovery.startDiscovery(onDiscoveryCallback)
destroy: ->
super
$(window).off 'message', this._onMessage
# Assign a non-enumerable tag to objects which cross the bridge.
# This tag is used to identify the objects between message.
_tag: (msg, tag) ->
return msg if msg.$$tag
tag = tag or (window.btoa Math.random())
Object.defineProperty msg, '$$tag', value: tag
@cache[tag] = msg
msg
# Parse an annotation from a RPC with the configured parser
_parse: ({tag, msg}) ->
local = @cache[tag]
remote = @options.parser msg
if local?
merged = @options.merge local, remote
else
merged = remote
this._tag merged, tag
# Format an annotation for RPC with the configured formatter
_format: (annotation) ->
this._tag annotation
msg = @options.formatter annotation
tag: annotation.$$tag
msg: msg
# Construct a channel to another frame
_build: (options) ->
# jschannel chokes on FF and Chrome extension origins.
if (options.origin.match /^chrome-extension:\/\//) or
(options.origin.match /^resource:\/\//)
options.origin = '*'
channel = Channel.build(options)
## Remote method call bindings
.bind('beforeCreateAnnotation', (txn, annotation) =>
annotation = this._parse annotation
delete @cache[annotation.$$tag]
@annotator.publish 'beforeAnnotationCreated', annotation
@cache[annotation.$$tag] = annotation
this._format annotation
)
.bind('createAnnotation', (txn, annotation) =>
annotation = this._parse annotation
delete @cache[annotation.$$tag]
@annotator.publish 'annotationCreated', annotation
@cache[annotation.$$tag] = annotation
this._format annotation
)
.bind('updateAnnotation', (txn, annotation) =>
annotation = this._parse annotation
delete @cache[annotation.$$tag]
annotation = @annotator.updateAnnotation annotation
@cache[annotation.$$tag] = annotation
this._format annotation
)
.bind('deleteAnnotation', (txn, annotation) =>
annotation = this._parse annotation
delete @cache[annotation.$$tag]
annotation = @annotator.deleteAnnotation annotation
res = this._format annotation
delete @cache[annotation.$$tag]
res
)
.bind('sync', (ctx, annotations) =>
(this._format (this._parse a) for a in annotations)
)
## Notifications
.bind('loadAnnotations', (txn, annotations) =>
annotations = (this._parse a for a in annotations)
@annotator.loadAnnotations annotations
)
# Send out a beacon to let other frames know to connect to us
_beacon: ->
queue = [window.top]
while queue.length
parent = queue.shift()
if parent isnt window
parent.postMessage '__annotator_dhcp_discovery', @options.origin
for child in parent.frames
queue.push child
# Make a method call on all links
_call: (options) ->
_makeDestroyFn = (c) =>
(error, reason) =>
c.destroy()
@links = (l for l in @links when l.channel isnt c)
deferreds = @links.map (l) ->
d = $.Deferred().fail (_makeDestroyFn l.channel)
options = $.extend {}, options,
success: (result) -> d.resolve result
error: (error, reason) ->
if error isnt 'timeout_error'
d.reject error, reason
else
d.resolve null
timeout: 1000
l.channel.call options
d.promise()
$.when(deferreds...)
.then (results...) =>
if Array.isArray(results[0])
acc = []
foldFn = (_, cur) =>
(this._parse(a) for a in cur)
else
acc = {}
foldFn = (_, cur) =>
this._parse(cur)
options.callback? null, results.reduce(foldFn, acc)
.fail (failure) =>
options.callback? failure
# Publish a notification to all links
_notify: (options) ->
for l in @links
l.channel.notify options
_onMessage: (e) =>
{source, origin, data} = e.originalEvent
match = data.match? /^__annotator_dhcp_(discovery|ack|offer)(:\d+)?$/
return unless match
if match[1] is 'discovery'
if @options.gateway
scope = ':' + ('' + Math.random()).replace(/\D/g, '')
source.postMessage '__annotator_dhcp_offer' + scope, origin
else
source.postMessage '__annotator_dhcp_ack', origin
return
else if match[1] is 'ack'
if @options.gateway
scope = ':' + ('' + Math.random()).replace(/\D/g, '')
source.postMessage '__annotator_dhcp_offer' + scope, origin
else
return
else if match[1] is 'offer'
if @options.gateway
return
else
scope = match[2]
scope = @options.scope + scope
options = $.extend {}, @options,
window: source
origin: origin
scope: scope
onReady: =>
options.onConnect.call @annotator, source, origin, scope
annotations = (this._format a for t, a of @cache)
if annotations.length
channel.notify
method: 'loadAnnotations'
params: annotations
channel = this._build options
@links.push
channel: channel
window: source
beforeAnnotationCreated: (annotation) =>
return if annotation.$$tag?
this.beforeCreateAnnotation annotation
this
annotationCreated: (annotation) =>
return unless annotation.$$tag? and @cache[annotation.$$tag]
this.createAnnotation annotation
this
annotationUpdated: (annotation) =>
return unless annotation.$$tag? and @cache[annotation.$$tag]
this.updateAnnotation annotation
this
annotationDeleted: (annotation) =>
return unless annotation.$$tag? and @cache[annotation.$$tag]
this.deleteAnnotation annotation, (err) =>
if err then @annotator.setupAnnotation annotation
else delete @cache[annotation.$$tag]
this
annotationsLoaded: (annotations) =>
annotations = (this._format a for a in annotations when not a.$$tag)
return unless annotations.length
this._notify
method: 'loadAnnotations'
params: annotations
this
beforeCreateAnnotation: (annotation, cb) ->
this._call
method: 'beforeCreateAnnotation'
params: this._format annotation
callback: cb
annotation
createAnnotation: (annotation, cb) ->
this._call
method: 'createAnnotation'
params: this._format annotation
callback: cb
annotation
updateAnnotation: (annotation, cb) ->
this._call
method: 'updateAnnotation'
params: this._format annotation
callback: cb
annotation
deleteAnnotation: (annotation, cb) ->
this._call
method: 'deleteAnnotation'
params: this._format annotation
callback: cb
annotation
@discovery.stopDiscovery()
return
sync: (annotations, cb) ->
annotations = (this._format a for a in annotations)
this._call
method: 'sync'
params: annotations
callback: cb
this
@annotationSync.sync(annotations, cb)
......@@ -52,24 +52,29 @@ class Annotator.Guest extends Annotator
delete @options.app
this.addPlugin 'Bridge',
formatter: (annotation) =>
formatted = {}
formatted['uri'] = @getHref()
for k, v of annotation when k isnt 'anchors'
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
onConnect: (source, origin, scope) =>
@panel = this._setupXDM
window: source
origin: origin
scope: "#{scope}:provider"
onReady: =>
this.publish('panelReady')
bridgePluginOptions =
discoveryOptions: {}
bridgeOptions:
onConnect: (source, origin, scope) => # TODO
@panel = this._setupXDM
window: source
origin: origin
scope: "#{scope}:provider"
onReady: =>
this.publish('panelReady')
annotationSyncOptions:
formatter: (annotation) =>
formatted = {}
formatted['uri'] = @getHref()
for k, v of annotation when k isnt 'anchors'
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
this.addPlugin 'Bridge', bridgePluginOptions
# Load plugins
for own name, opts of @options
......
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