Commit 6e41b134 authored by Randall Leeds's avatar Randall Leeds

Refactor anchoring interface and sorting

Make setupAnnotation into a tiny compatibility wrapper around a new
method #anchor() method that returns a promise of the anchors. This
is cleaner than trying to store the anchors on the annotation itself,
which creates circular references, and it means there's no need to
strip these properties when serializes annotations across the frame
boundary.

Sort annotations by TextPositionSelector in the widget display. This
provides greater visual stability -- cards don't change positions
after anchoring -- at the cost of potentially out of order cards when
anchors have moved.
parent f2ae1a5b
...@@ -161,7 +161,7 @@ module.exports = class Guest extends Annotator ...@@ -161,7 +161,7 @@ module.exports = class Guest extends Annotator
this.removeEvents() this.removeEvents()
setupAnnotation: (annotation) -> anchor: (annotation) ->
self = this self = this
root = @element[0] root = @element[0]
...@@ -200,22 +200,13 @@ module.exports = class Guest extends Annotator ...@@ -200,22 +200,13 @@ module.exports = class Guest extends Annotator
return animationPromise -> return animationPromise ->
range = Annotator.Range.sniff(anchor.range) range = Annotator.Range.sniff(anchor.range)
normedRange = range.normalize(root) normedRange = range.normalize(root)
highlights = highlighter.highlightRange(normedRange) highlights = highlighter.highlightRange(normedRange)
rect = highlighter.getBoundingClientRect(highlights)
$(highlights).data('annotation', anchor.annotation) $(highlights).data('annotation', anchor.annotation)
anchor.highlights = highlights anchor.highlights = highlights
anchor.pos =
left: rect.left + window.scrollX
top: rect.top + window.scrollY
return anchor return anchor
sync = (anchors) -> sync = (anchors) ->
# Store the results of anchoring. # Store the results of anchoring.
annotation.$anchors = ({pos} for {pos} in anchors)
annotation.$orphan = anchors.length > 0 annotation.$orphan = anchors.length > 0
for anchor in anchors for anchor in anchors
if anchor.range? if anchor.range?
...@@ -228,6 +219,8 @@ module.exports = class Guest extends Annotator ...@@ -228,6 +219,8 @@ module.exports = class Guest extends Annotator
self.plugins.BucketBar?.update() self.plugins.BucketBar?.update()
self.plugins.CrossFrame?.sync([annotation]) self.plugins.CrossFrame?.sync([annotation])
return anchors
# Remove all the anchors for this annotation from the instance storage. # Remove all the anchors for this annotation from the instance storage.
for anchor in self.anchors.splice(0, self.anchors.length) for anchor in self.anchors.splice(0, self.anchors.length)
if anchor.annotation is annotation if anchor.annotation is annotation
...@@ -253,9 +246,12 @@ module.exports = class Guest extends Annotator ...@@ -253,9 +246,12 @@ module.exports = class Guest extends Annotator
anchor = locate(target).then(highlight) anchor = locate(target).then(highlight)
anchors.push(anchor) anchors.push(anchor)
# Wait for all the anchoring tasks to complete then call sync. return Promise.all(anchors).then(sync)
Promise.all(anchors).then(sync)
# Provided for backward compatibility.
# Currently only used by #loadAnnotations().
setupAnnotation: (annotation) ->
this.anchor(annotation)
return annotation return annotation
createAnnotation: (annotation = {}) -> createAnnotation: (annotation = {}) ->
...@@ -286,8 +282,8 @@ module.exports = class Guest extends Annotator ...@@ -286,8 +282,8 @@ module.exports = class Guest extends Annotator
metadata = info.then(setDocumentInfo) metadata = info.then(setDocumentInfo)
targets = Promise.all([info, selectors]).then(setTargets) targets = Promise.all([info, selectors]).then(setTargets)
targets.then(-> self.setupAnnotation(annotation))
targets.then(-> self.publish('beforeAnnotationCreated', [annotation])) targets.then(-> self.publish('beforeAnnotationCreated', [annotation]))
targets.then(-> self.anchor(annotation))
annotation annotation
......
...@@ -104,7 +104,7 @@ class PDF extends Annotator.Plugin ...@@ -104,7 +104,7 @@ class PDF extends Annotator.Plugin
break break
for annotation in refreshAnnotations for annotation in refreshAnnotations
annotator.setupAnnotation(annotation) annotator.anchor(annotation)
Annotator.Plugin.PDF = PDF Annotator.Plugin.PDF = PDF
......
...@@ -17,13 +17,6 @@ Guest = proxyquire('../guest', { ...@@ -17,13 +17,6 @@ Guest = proxyquire('../guest', {
$ = require('jquery') $ = require('jquery')
waitForSync = (annotation) ->
if annotation.$anchors?
return Promise.resolve()
else
return new Promise(setTimeout).then(-> waitForSync(annotation))
describe 'Guest', -> describe 'Guest', ->
sandbox = null sandbox = null
fakeCrossFrame = null fakeCrossFrame = null
...@@ -294,7 +287,7 @@ describe 'Guest', -> ...@@ -294,7 +287,7 @@ describe 'Guest', ->
annotation = guest.createAnnotation(annotation) annotation = guest.createAnnotation(annotation)
assert.equal(annotation.foo, 'bar') assert.equal(annotation.foo, 'bar')
describe 'setupAnnotation()', -> describe 'anchor()', ->
el = null el = null
range = null range = null
...@@ -312,8 +305,7 @@ describe 'Guest', -> ...@@ -312,8 +305,7 @@ describe 'Guest', ->
it "doesn't declare annotation without targets as orphans", (done) -> it "doesn't declare annotation without targets as orphans", (done) ->
guest = createGuest() guest = createGuest()
annotation = target: [] annotation = target: []
guest.setupAnnotation(annotation) guest.anchor(annotation).then ->
waitForSync(annotation).then ->
assert.isFalse(annotation.$orphan) assert.isFalse(annotation.$orphan)
done() done()
...@@ -321,8 +313,7 @@ describe 'Guest', -> ...@@ -321,8 +313,7 @@ describe 'Guest', ->
guest = createGuest() guest = createGuest()
annotation = target: [{selector: "test"}] annotation = target: [{selector: "test"}]
sandbox.stub(anchoring, 'anchor').returns(Promise.resolve(range)) sandbox.stub(anchoring, 'anchor').returns(Promise.resolve(range))
guest.setupAnnotation(annotation) guest.anchor(annotation).then ->
waitForSync(annotation).then ->
assert.isFalse(annotation.$orphan) assert.isFalse(annotation.$orphan)
done() done()
...@@ -330,8 +321,7 @@ describe 'Guest', -> ...@@ -330,8 +321,7 @@ describe 'Guest', ->
guest = createGuest() guest = createGuest()
annotation = target: [{selector: 'broken selector'}] annotation = target: [{selector: 'broken selector'}]
sandbox.stub(anchoring, 'anchor').returns(Promise.reject()) sandbox.stub(anchoring, 'anchor').returns(Promise.reject())
guest.setupAnnotation(annotation) guest.anchor(annotation).then ->
waitForSync(annotation).then ->
assert.isTrue(annotation.$orphan) assert.isTrue(annotation.$orphan)
done() done()
...@@ -342,25 +332,21 @@ describe 'Guest', -> ...@@ -342,25 +332,21 @@ describe 'Guest', ->
guest.plugins.BucketBar = guest.plugins.BucketBar =
update: sinon.stub() update: sinon.stub()
annotation = {} annotation = {}
guest.setupAnnotation(annotation) guest.anchor(annotation).then ->
waitForSync(annotation).then ->
assert.called(guest.plugins.BucketBar.update) assert.called(guest.plugins.BucketBar.update)
assert.called(guest.plugins.CrossFrame.sync) assert.called(guest.plugins.CrossFrame.sync)
done() done()
it 'saves the anchor positions on the annotation', (done) -> it 'returns a promise of the anchors for the annotation', (done) ->
guest = createGuest() guest = createGuest()
highlights = [document.createElement('span')]
sandbox.stub(anchoring, 'anchor').returns(Promise.resolve(range)) sandbox.stub(anchoring, 'anchor').returns(Promise.resolve(range))
clientRect = {top: 100, left: 200} sandbox.stub(highlighter, 'highlightRange').returns(highlights)
window.scrollX = 50 target = [{selector: []}]
window.scrollY = 25 promise = guest.anchor({target: [target]})
sandbox.stub(highlighter, 'getBoundingClientRect').returns(clientRect) assert.instanceOf(promise, Promise)
annotation = guest.setupAnnotation({target: [{selector: []}]}) promise.then (anchors) ->
waitForSync(annotation).then -> assert.equal(anchors.length, 1)
assert.equal(annotation.$anchors.length, 1)
pos = annotation.$anchors[0].pos
assert.equal(pos.top, 125)
assert.equal(pos.left, 250)
done() done()
it 'adds the anchor to the "anchors" instance property"', (done) -> it 'adds the anchor to the "anchors" instance property"', (done) ->
...@@ -369,8 +355,8 @@ describe 'Guest', -> ...@@ -369,8 +355,8 @@ describe 'Guest', ->
sandbox.stub(anchoring, 'anchor').returns(Promise.resolve(range)) sandbox.stub(anchoring, 'anchor').returns(Promise.resolve(range))
sandbox.stub(highlighter, 'highlightRange').returns(highlights) sandbox.stub(highlighter, 'highlightRange').returns(highlights)
target = [{selector: []}] target = [{selector: []}]
annotation = guest.setupAnnotation({target: [target]}) annotation = {target: [target]}
waitForSync(annotation).then -> guest.anchor(annotation).then ->
assert.equal(guest.anchors.length, 1) assert.equal(guest.anchors.length, 1)
assert.strictEqual(guest.anchors[0].annotation, annotation) assert.strictEqual(guest.anchors[0].annotation, annotation)
assert.strictEqual(guest.anchors[0].target, target) assert.strictEqual(guest.anchors[0].target, target)
...@@ -385,8 +371,7 @@ describe 'Guest', -> ...@@ -385,8 +371,7 @@ describe 'Guest', ->
guest = createGuest() guest = createGuest()
guest.anchors = [{annotation, target, highlights}] guest.anchors = [{annotation, target, highlights}]
removeHighlights = sandbox.stub(highlighter, 'removeHighlights') removeHighlights = sandbox.stub(highlighter, 'removeHighlights')
guest.setupAnnotation(annotation) guest.anchor(annotation).then ->
waitForSync(annotation).then ->
assert.equal(guest.anchors.length, 0) assert.equal(guest.anchors.length, 0)
assert.calledWith(removeHighlights, highlights) assert.calledWith(removeHighlights, highlights)
done() done()
...@@ -395,11 +380,8 @@ describe 'Guest', -> ...@@ -395,11 +380,8 @@ describe 'Guest', ->
guest = createGuest() guest = createGuest()
annotation = target: [{selector: "test"}] annotation = target: [{selector: "test"}]
stub = sandbox.stub(anchoring, 'anchor').returns(Promise.resolve(range)) stub = sandbox.stub(anchoring, 'anchor').returns(Promise.resolve(range))
guest.setupAnnotation(annotation) guest.anchor(annotation).then ->
waitForSync(annotation).then -> guest.anchor(annotation).then ->
delete annotation.$anchors
guest.setupAnnotation(annotation)
waitForSync(annotation).then ->
assert.equal(guest.anchors.length, 1) assert.equal(guest.anchors.length, 1)
assert.calledOnce(stub) assert.calledOnce(stub)
done() done()
......
...@@ -82,11 +82,13 @@ module.exports = class AppController ...@@ -82,11 +82,13 @@ module.exports = class AppController
predicate = switch name predicate = switch name
when 'Newest' then ['-!!message', '-message.updated'] when 'Newest' then ['-!!message', '-message.updated']
when 'Oldest' then ['-!!message', 'message.updated'] when 'Oldest' then ['-!!message', 'message.updated']
when 'Location' then [ when 'Location' then (thread) ->
'-!!message' if thread.message?
'message.$anchors[0].pos.top' for target in thread.message.target ? []
'message.$anchors[0].pos.left' for selector in target.selector ? []
] if selector.type is 'TextPositionSelector'
return selector.start
return Number.POSITIVE_INFINITY
$scope.sort = {name, predicate} $scope.sort = {name, predicate}
$scope.$watch 'auth.user', (newVal, oldVal) -> $scope.$watch 'auth.user', (newVal, oldVal) ->
......
...@@ -18,10 +18,7 @@ module.exports = class CrossFrame ...@@ -18,10 +18,7 @@ module.exports = class CrossFrame
new Discovery($window, options) new Discovery($window, options)
createAnnotationSync = -> createAnnotationSync = ->
whitelist = [ whitelist = ['$highlight', '$orphan','target', 'document', 'uri']
'$anchors', '$highlight', '$orphan',
'target', 'document', 'uri'
]
options = options =
formatter: (annotation) -> formatter: (annotation) ->
formatted = {} formatted = {}
......
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