Commit e1b2a3b0 authored by Randall Leeds's avatar Randall Leeds

Make the anchoring integration asynchronous

The core of anchoring in the guest code is now asynchronous.

- Anchoring annotations happens asynchronously in the setup function.

- Creating an annotation is asynchronous, and depends upon the resolution
  of the targets, which may depend on the resolution of the document metadata.
  The first events fire only after this happens.

- The Anchor API can now optionally return Promises when converting ranges
  and selectors, though all the present implementations are synchronous.
parent 9368bfc6
...@@ -45,17 +45,6 @@ module.exports = class Guest extends Annotator ...@@ -45,17 +45,6 @@ module.exports = class Guest extends Annotator
this.anchored = [] this.anchored = []
this.unanchored = [] this.unanchored = []
# Are going to be able to use the PDF plugin here?
if window.PDFTextMapper?.applicable()
# If we can, let's load the PDF plugin.
@options.PDF = {}
else
# If we can't use the PDF plugin,
# let's load the Document plugin instead.
@options.Document = {}
delete @options.app
cfOptions = cfOptions =
on: (event, handler) => on: (event, handler) =>
this.subscribe(event, handler) this.subscribe(event, handler)
...@@ -73,7 +62,6 @@ module.exports = class Guest extends Annotator ...@@ -73,7 +62,6 @@ module.exports = class Guest extends Annotator
this.publish(event, args) this.publish(event, args)
formatter: (annotation) => formatter: (annotation) =>
formatted = {} formatted = {}
formatted.uri = @getHref()
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 # Work around issue in jschannel where a repeated object is considered
...@@ -90,27 +78,21 @@ module.exports = class Guest extends Annotator ...@@ -90,27 +78,21 @@ module.exports = class Guest extends Annotator
if not @plugins[name] and Annotator.Plugin[name] if not @plugins[name] and Annotator.Plugin[name]
this.addPlugin(name, opts) this.addPlugin(name, opts)
# Utility function to remove the hash part from a URL # Get the document info
_removeHash: (url) -> getDocumentInfo: ->
url = new URL url if @plugins.PDF?
url.hash = "" metadataPromise = Promise.resolve(@plugins.PDF.getMetadata())
url.toString() uriPromise = Promise.resolve(@plugins.PDF.uri())
if @plugins.Document?
# Utility function to get the decoded form of the document URI uriPromise = Promise.resolve(@plugins.Document.uri())
getRawHref: -> metadataPromise = Promise.resolve(@plugins.Document.metadata)
if @plugins.PDF
@plugins.PDF.uri() return metadataPromise.then (metadata) =>
else return uriPromise.then (href) =>
@plugins.Document.uri() uri = new URL(href)
uri.hash = ''
# Utility function to get a de-hashed form of the document URI uri = uri.toString()
getHref: -> @_removeHash @getRawHref() return {uri, metadata}
# Utility function to filter metadata and de-hash the URIs
getMetadata: =>
metadata = @plugins.Document?.metadata
metadata.link?.forEach (link) => link.href = @_removeHash link.href
metadata
_connectAnnotationUISync: (crossframe) -> _connectAnnotationUISync: (crossframe) ->
crossframe.onConnect(=> this.publish('panelReady')) crossframe.onConnect(=> this.publish('panelReady'))
...@@ -126,18 +108,10 @@ module.exports = class Guest extends Annotator ...@@ -126,18 +108,10 @@ module.exports = class Guest extends Annotator
$(info.highlights).scrollintoview() $(info.highlights).scrollintoview()
return return
crossframe.on 'getDocumentInfo', (trans) => crossframe.on 'getDocumentInfo', (trans) =>
(@plugins.PDF?.getMetaData() ? Promise.reject())
.then (md) =>
trans.complete
uri: @getHref()
metadata: md
.catch (problem) =>
trans.complete
uri: @getHref()
metadata: @getMetadata()
.catch (e) ->
trans.delayReturn(true) trans.delayReturn(true)
this.getDocumentInfo()
.then((info) -> trans.complete(info))
.catch((reason) -> trans.error(reason))
crossframe.on 'setVisibleHighlights', (ctx, state) => crossframe.on 'setVisibleHighlights', (ctx, state) =>
this.publish 'setVisibleHighlights', state this.publish 'setVisibleHighlights', state
...@@ -171,18 +145,6 @@ module.exports = class Guest extends Annotator ...@@ -171,18 +145,6 @@ module.exports = class Guest extends Annotator
this.removeEvents() this.removeEvents()
setupAnnotation: (annotation) -> setupAnnotation: (annotation) ->
unless annotation.target?
if @selectedRanges?
annotation.target = (@_getTargetFromRange(r) for r in @selectedRanges)
@selectedRanges = null
else
annotation.target = [this.getHref()]
# Create a TextHighlight for a range.
highlightRange = (range) =>
normedRange = Annotator.Range.sniff(range).normalize(@element[0])
return highlight.highlightRange(normedRange)
# Factories to close over the loop variable, below. # Factories to close over the loop variable, below.
succeed = (target) -> succeed = (target) ->
(highlights) -> {annotation, target, highlights} (highlights) -> {annotation, target, highlights}
...@@ -204,29 +166,42 @@ module.exports = class Guest extends Annotator ...@@ -204,29 +166,42 @@ module.exports = class Guest extends Annotator
if results.length and not anchored if results.length and not anchored
annotation.$orphan = true annotation.$orphan = true
# Sync the results
this.plugins.CrossFrame.sync([annotation]) this.plugins.CrossFrame.sync([annotation])
# Anchor all the targets, highlighting the successes. # Create a TextHighlight for a range.
promises = for target in annotation.target ? [] when target.selector highlightRange = (range) =>
this.anchorTarget(target) normedRange = Annotator.Range.sniff(range).normalize(@element[0])
.then(highlightRange) return highlight.highlightRange(normedRange)
.then(succeed(target), fail(target))
# Collect the results. # Try to anchor all the targets
Promise.all(promises).then(finish) anchorTargets = (targets = []) =>
anchorPromises = for target in targets when target.selector
try
this.anchorTarget(target)
.then(highlightRange)
.then(succeed(target), fail(target))
catch error
Promise.reject(error).catch(fail(target))
return Promise.all(anchorPromises).then(finish)
annotation # Start anchoring in the background
anchorTargets(annotation.target)
createAnnotation: ->
annotation = super
this.plugins.CrossFrame.sync([annotation])
annotation annotation
createAnnotation: (annotation = {}) ->
ranges = @selectedRanges
@selectedRanges = null
return this.getDocumentInfo().then (info) =>
annotation.uri = info.uri
this.createTargets(ranges).then (targets) =>
annotation.target = targets
this.publish('beforeAnnotationCreated', [annotation])
return this.setupAnnotation(annotation)
createHighlight: -> createHighlight: ->
annotation = $highlight: true return this.createAnnotation({$highlight: true})
this.publish 'beforeAnnotationCreated', [annotation]
this.plugins.CrossFrame.sync([annotation])
annotation
deleteAnnotation: (annotation) -> deleteAnnotation: (annotation) ->
for info in @anchored when info.annotation is annotation for info in @anchored when info.annotation is annotation
...@@ -279,35 +254,39 @@ module.exports = class Guest extends Annotator ...@@ -279,35 +254,39 @@ module.exports = class Guest extends Annotator
if fragment? if fragment?
promise = promise.catch => promise = promise.catch =>
a = anchor.FragmentAnchor.fromSelector(fragment) a = anchor.FragmentAnchor.fromSelector(fragment)
r = a.toRange(root) Promise.resolve(a).then (a) ->
if quote?.exact? and r.toString() != quote.exact Promise.resolve(a.toRange(root)).then (r) ->
throw new Error('quote mismatch') if quote?.exact? and r.toString() != quote.exact
else throw new Error('quote mismatch')
return r else
return r
if range? if range?
promise = promise.catch => promise = promise.catch =>
a = anchor.RangeAnchor.fromSelector(range, root) a = anchor.RangeAnchor.fromSelector(range, root)
r = a.toRange(root) Promise.resolve(a).then (a) ->
if quote?.exact? and r.toString() != quote.exact Promise.resolve(a.toRange(root)).then (r) ->
throw new Error('quote mismatch') if quote?.exact? and r.toString() != quote.exact
else throw new Error('quote mismatch')
return r else
return r
if position? if position?
promise = promise.catch => promise = promise.catch =>
a = anchor.TextPositionAnchor.fromSelector(position) a = anchor.TextPositionAnchor.fromSelector(position)
r = a.toRange(root) Promise.resolve(a).then (a) ->
if quote?.exact? and r.toString() != quote.exact Promise.resolve(a.toRange(root)).then (r) ->
throw new Error('quote mismatch') if quote?.exact? and r.toString() != quote.exact
else throw new Error('quote mismatch')
return r else
return r
if quote? if quote?
promise = promise.catch => promise = promise.catch =>
# The quote is implicitly checked during range conversion. # The quote is implicitly checked during range conversion.
a = anchor.TextQuoteAnchor.fromSelector(quote, position) a = anchor.TextQuoteAnchor.fromSelector(quote, position)
r = a.toRange(root) Promise.resolve(a).then (a) ->
Promise.resolve(a.toRange(root))
return promise return promise
...@@ -333,18 +312,34 @@ module.exports = class Guest extends Annotator ...@@ -333,18 +312,34 @@ module.exports = class Guest extends Annotator
onAnchorMousedown: -> onAnchorMousedown: ->
# Create a target from a raw selection using all the anchor types. createTargets: (ranges) ->
_getTargetFromRange: (range) -> info = this.getDocumentInfo()
selector = for type in ANCHOR_TYPES if ranges?
targets = ranges.map (range) =>
this.createSelectors(range).then (selectors) =>
info.then (info) =>
return {
source: info.uri
selector: selectors
}
else
targets = [info.then(({uri}) -> {source: uri})]
return Promise.all(targets)
createSelectors: (range) ->
root = @element[0]
toSelector = (anchor) -> anchor.toSelector(root)
softFail = (reason) -> null
notNull = (selectors) -> (s for s in selectors when s?)
selectors = ANCHOR_TYPES.map (type) =>
try try
type.fromRange(range).toSelector(@element[0]) Promise.resolve(type.fromRange(range)).then (a) ->
Promise.resolve(a.toSelector(root))
, softFail
catch catch
continue Promise.resolve()
return Promise.all(selectors).then(notNull)
return {
source: this.getHref()
selector: selector
}
onSuccessfulSelection: (event, immediate) -> onSuccessfulSelection: (event, immediate) ->
unless event? unless event?
...@@ -442,8 +437,8 @@ module.exports = class Guest extends Annotator ...@@ -442,8 +437,8 @@ module.exports = class Guest extends Annotator
switch event.target.dataset.action switch event.target.dataset.action
when 'highlight' when 'highlight'
this.setVisibleHighlights true this.setVisibleHighlights true
this.setupAnnotation(this.createHighlight()) this.createHighlight()
when 'comment' when 'comment'
this.setupAnnotation(this.createAnnotation()) this.createAnnotation()
this.triggerShowFrame() this.triggerShowFrame()
Annotator.Util.getGlobal().getSelection().removeAllRanges() Annotator.Util.getGlobal().getSelection().removeAllRanges()
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