Commit 11430695 authored by Randall Leeds's avatar Randall Leeds

Start replacing Store plugin with ngResource

parent 5964631d
...@@ -12,22 +12,12 @@ imports = [ ...@@ -12,22 +12,12 @@ imports = [
resolve = resolve =
storeConfig: ['$q', 'annotator', ($q, annotator) -> store: ['store', (store) -> store.$promise]
if annotator.plugins.Store then return
storeReady = $q.defer()
resolve = (options) ->
annotator.options.Store ?= {}
angular.extend annotator.options.Store, options
storeReady.resolve()
annotator.subscribe 'serviceDiscovery', resolve
storeReady.promise.finally ->
annotator.unsubscribe 'serviceDiscovery', resolve
]
configure = [ configure = [
'$locationProvider', '$routeProvider', '$sceDelegateProvider', 'streamerProvider', '$locationProvider', '$routeProvider', '$sceDelegateProvider',
($locationProvider, $routeProvider, $sceDelegateProvider, streamerProvider) -> ($locationProvider, $routeProvider, $sceDelegateProvider) ->
$locationProvider.html5Mode(true) $locationProvider.html5Mode(true)
$routeProvider.when '/a/:id', $routeProvider.when '/a/:id',
......
class AppController class AppController
this.$inject = [ this.$inject = [
'$location', '$route', '$scope', '$timeout', '$window', '$location', '$route', '$scope', '$timeout', '$window',
'annotator', 'auth', 'documentHelpers', 'drafts', 'flash', 'identity', 'annotator', 'auth', 'documentHelpers', 'drafts', 'identity',
'permissions', 'streamer', 'streamfilter' 'permissions', 'streamer', 'streamfilter'
] ]
constructor: ( constructor: (
$location, $route, $scope, $timeout, $window, $location, $route, $scope, $timeout, $window,
annotator, auth, documentHelpers, drafts, flash, identity, annotator, auth, documentHelpers, drafts, identity,
permissions, streamer, streamfilter, permissions, streamer, streamfilter,
) -> ) ->
...@@ -21,106 +21,33 @@ class AppController ...@@ -21,106 +21,33 @@ class AppController
return unless data?.length return unless data?.length
switch action switch action
when 'create', 'update', 'past' when 'create', 'update', 'past'
plugins.Store?._onLoadAnnotations data annotator.loadAnnotations data
when 'delete' when 'delete'
for annotation in data for annotation in data
annotation = plugins.Threading.idTable[annotation.id]?.message annotator.publish 'annotationDeleted', (annotation)
continue unless annotation?
plugins.Store?.unregisterAnnotation(annotation)
annotator.deleteAnnotation(annotation)
streamer.onmessage = (data) -> streamer.onmessage = (data) ->
if !data or data.type != 'annotation-notification' return if !data or data.type != 'annotation-notification'
return
action = data.options.action action = data.options.action
payload = data.payload payload = data.payload
if $scope.socialView.name is 'single-player'
payload = payload.filter (ann) -> ann.user is auth.user
applyUpdates(action, payload) applyUpdates(action, payload)
$scope.$digest() $scope.$digest()
initStore = ->
# Initialize the storage component.
Store = plugins.Store
delete plugins.Store
if auth.user or annotator.socialView.name is 'none'
annotator.addPlugin 'Store', annotator.options.Store
$scope.store = plugins.Store
return unless Store
Store.destroy()
# XXX: Hacky hacky stuff to ensure that any search requests in-flight
# at this time have no effect when they resolve and that future events
# have no effect on this Store. Unfortunately, it's not possible to
# unregister all the events or properly unload the Store because the
# registration loses the closure. The approach here is perhaps
# cleaner than fishing them out of the jQuery private data.
# * Overwrite the Store's handle to the annotator, giving it one
# with a noop `loadAnnotations` method.
Store.annotator = loadAnnotations: angular.noop
# * Make all api requests into a noop.
Store._apiRequest = angular.noop
# * Ignore pending searches
Store._onLoadAnnotations = angular.noop
# * Make the update function into a noop.
Store.updateAnnotation = angular.noop
# Sort out which annotations should remain in place.
user = auth.user
view = annotator.socialView.name
cull = (acc, annotation) ->
if view is 'single-player' and annotation.user != user
acc.drop.push annotation
else if permissions.permits('read', annotation, user)
acc.keep.push annotation
else
acc.drop.push annotation
acc
{keep, drop} = Store.annotations.reduce cull, {keep: [], drop: []}
Store.annotations = []
if plugins.Store?
plugins.Store.annotations = keep
else
drop = drop.concat keep
# Clean up the ones that should be removed.
do cleanup = (drop) ->
return if drop.length == 0
[first, rest...] = drop
annotator.deleteAnnotation first
$timeout -> cleanup rest
oncancel = -> oncancel = ->
$scope.dialog.visible = false $scope.dialog.visible = false
reset = -> $scope.$on '$routeChangeStart', (event, newRoute, oldRoute) ->
$scope.dialog.visible = false return if newRoute.redirectTo
# Clean up any annotations that need to be unloaded.
# Update any edits in progress. for id, container of $scope.threading.idTable when container.message
for draft in drafts.all() # Remove annotations not belonging to this user when highlighting.
annotator.publish 'beforeAnnotationCreated', draft if annotator.tool is 'highlight' and annotation.user != auth.user
annotator.publish 'annotationDeleted', container.message
# Reload services drafts.remove annotation
initStore() # Remove annotations the user is not authorized to view.
else if not permissions.permits 'read', container.message, auth.user
streamer.close() annotator.publish 'annotationDeleted', container.message
streamer.open($window.WebSocket, streamerUrl) drafts.remove container.message
$scope.$watch 'socialView.name', (newValue, oldValue) ->
return if newValue is oldValue
initStore()
if newValue is 'single-player' and not auth.user
annotator.show()
flash 'info',
'You will need to sign in for your highlights to be saved.'
$scope.$watch 'sort.name', (name) -> $scope.$watch 'sort.name', (name) ->
return unless name return unless name
...@@ -130,20 +57,27 @@ class AppController ...@@ -130,20 +57,27 @@ class AppController
when 'Location' then ['-!!message', 'message.target[0].pos.top'] when 'Location' then ['-!!message', 'message.target[0].pos.top']
$scope.sort = {name, predicate} $scope.sort = {name, predicate}
$scope.$watch 'store.entities', (entities, oldEntities) -> $scope.$watch (-> auth.user), (newVal, oldVal) ->
return if entities is oldEntities return if newVal is oldVal
if isFirstRun and not (newVal or oldVal)
$scope.login()
else
$scope.dialog.visible = false
# Skip the remaining if this is the first evaluation.
return if oldVal is undefined
if entities.length # Update any edits in progress.
streamfilter for draft in drafts.all()
.resetFilter() annotator.publish 'beforeAnnotationCreated', draft
.addClause('/uri', 'one_of', entities)
streamer.send({filter: streamfilter.getFilter()}) # Reopen the streamer.
streamer.close()
streamer.open($window.WebSocket, streamerUrl)
$scope.$watch 'auth.user', (newVal, oldVal) -> # Reload the view.
return if newVal is undefined $route.reload()
reset()
$scope.login() if isFirstRun and not (newVal or oldVal)
$scope.login = -> $scope.login = ->
$scope.dialog.visible = true $scope.dialog.visible = true
...@@ -177,7 +111,6 @@ class AppController ...@@ -177,7 +111,6 @@ class AppController
delete $scope.selectedAnnotations delete $scope.selectedAnnotations
delete $scope.selectedAnnotationsCount delete $scope.selectedAnnotationsCount
$scope.socialView = annotator.socialView
$scope.sort = name: 'Location' $scope.sort = name: 'Location'
$scope.threading = plugins.Threading $scope.threading = plugins.Threading
...@@ -185,21 +118,16 @@ class AppController ...@@ -185,21 +118,16 @@ class AppController
class AnnotationViewerController class AnnotationViewerController
this.$inject = [ this.$inject = [
'$location', '$routeParams', '$scope', '$location', '$routeParams', '$scope',
'annotator', 'streamer', 'streamfilter' 'annotator', 'streamer', 'store', 'streamfilter'
] ]
constructor: ( constructor: (
$location, $routeParams, $scope, $location, $routeParams, $scope,
annotator, streamer, streamfilter annotator, streamer, store, streamfilter
) -> ) ->
# Tells the view that these annotations are standalone # Tells the view that these annotations are standalone
$scope.isEmbedded = false $scope.isEmbedded = false
$scope.isStream = false $scope.isStream = false
# Clear out loaded annotations and threads
# XXX: Resolve threading, storage, and streamer better for all routes.
annotator.plugins.Threading?.pluginInit()
annotator.plugins.Store?.annotations = []
# Provide no-ops until these methods are moved elsewere. They only apply # Provide no-ops until these methods are moved elsewere. They only apply
# to annotations loaded into the stream. # to annotations loaded into the stream.
$scope.focus = angular.noop $scope.focus = angular.noop
...@@ -210,11 +138,10 @@ class AnnotationViewerController ...@@ -210,11 +138,10 @@ class AnnotationViewerController
$location.path('/stream').search('q', query) $location.path('/stream').search('q', query)
id = $routeParams.id id = $routeParams.id
store.search.get _id: $routeParams.id, ({rows}) ->
$scope.$watch 'store', -> annotator.loadAnnotations(rows)
if $scope.store store.search.get references: $routeParams.id, ({rows}) ->
$scope.store.loadAnnotationsFromSearch({_id: id}).then -> annotator.loadAnnotations(rows)
$scope.store.loadAnnotationsFromSearch({references: id})
streamfilter streamfilter
.setPastDataNone() .setPastDataNone()
...@@ -225,12 +152,44 @@ class AnnotationViewerController ...@@ -225,12 +152,44 @@ class AnnotationViewerController
streamer.send({filter: streamfilter.getFilter()}) streamer.send({filter: streamfilter.getFilter()})
class ViewerController class ViewerController
this.$inject = ['$scope', 'annotator'] this.$inject = [
constructor: ( $scope, annotator ) -> '$scope', '$route',
'annotator', 'auth', 'flash', 'streamer', 'streamfilter', 'store'
]
constructor: (
$scope, $route,
annotator, auth, flash, streamer, streamfilter, store
) ->
# Tells the view that these annotations are embedded into the owner doc # Tells the view that these annotations are embedded into the owner doc
$scope.isEmbedded = true $scope.isEmbedded = true
$scope.isStream = true $scope.isStream = true
loaded = []
loadAnnotations = ->
if annotator.tool is 'highlight'
return unless auth.user
query = user: auth.user
for p in annotator.providers
for e in p.entities when e not in loaded
loaded.push e
store.search.get angular.extend(uri: e, query), (results) ->
annotator.loadAnnotations(results.rows)
streamfilter.resetFilter().addClause('/uri', 'one_of', loaded)
if auth.user and annotator.tool is 'highlight'
streamfilter.addClause('/user', auth.user)
streamer.send({filter: streamfilter.getFilter()})
$scope.$watch (-> annotator.tool), (newVal, oldVal) ->
return if newVal is oldVal
$route.reload()
$scope.$watchCollection (-> annotator.providers), loadAnnotations
$scope.focus = (annotation) -> $scope.focus = (annotation) ->
if angular.isObject annotation if angular.isObject annotation
highlights = [annotation.$$tag] highlights = [annotation.$$tag]
......
...@@ -159,9 +159,11 @@ AnnotationController = [ ...@@ -159,9 +159,11 @@ AnnotationController = [
switch @action switch @action
when 'create' when 'create'
annotator.publish 'annotationCreated', model model.$create().then ->
annotator.publish 'annotationCreated', model
when 'delete', 'edit' when 'delete', 'edit'
annotator.publish 'annotationUpdated', model model.$update(id: model.id).then ->
annotator.publish 'annotationUpdated', model
@editing = false @editing = false
@action = 'view' @action = 'view'
...@@ -181,8 +183,7 @@ AnnotationController = [ ...@@ -181,8 +183,7 @@ AnnotationController = [
# Construct the reply. # Construct the reply.
references = [references..., id] references = [references..., id]
reply = {references, uri} reply = annotator.createAnnotation {references, uri}
annotator.publish 'beforeAnnotationCreated', reply
if auth.user? if auth.user?
if permissions.isPublic model.permissions if permissions.isPublic model.permissions
...@@ -275,9 +276,10 @@ AnnotationController = [ ...@@ -275,9 +276,10 @@ AnnotationController = [
# Save highlights once logged in. # Save highlights once logged in.
if highlight and this.isHighlight() if highlight and this.isHighlight()
if auth.user if model.user and not model.id
model.permissions = permissions.private() highlight = false # skip this on future updates
annotator.publish 'annotationCreated', model model.$create().then ->
annotator.publish 'annotationCreated', model
highlight = false # skip this on future updates highlight = false # skip this on future updates
else else
drafts.add model, => this.revert() drafts.add model, => this.revert()
......
...@@ -6,6 +6,7 @@ class Annotator.Plugin.Threading extends Annotator.Plugin ...@@ -6,6 +6,7 @@ class Annotator.Plugin.Threading extends Annotator.Plugin
events: events:
'beforeAnnotationCreated': 'beforeAnnotationCreated' 'beforeAnnotationCreated': 'beforeAnnotationCreated'
'annotationCreated': 'annotationCreated'
'annotationDeleted': 'annotationDeleted' 'annotationDeleted': 'annotationDeleted'
'annotationsLoaded': 'annotationsLoaded' 'annotationsLoaded': 'annotationsLoaded'
...@@ -59,14 +60,25 @@ class Annotator.Plugin.Threading extends Annotator.Plugin ...@@ -59,14 +60,25 @@ class Annotator.Plugin.Threading extends Annotator.Plugin
if !container.message && container.children.length == 0 if !container.message && container.children.length == 0
parent.removeChild(container) parent.removeChild(container)
delete this.idTable[container.message?.id]
beforeAnnotationCreated: (annotation) => beforeAnnotationCreated: (annotation) =>
this.thread([annotation]) this.thread([annotation])
annotationCreated: (annotation) =>
references = annotation.references or []
if typeof(annotation.references) == 'string' then references = []
ref = references[references.length-1]
parent = if ref then @idTable[ref] else @root
for child in (parent.children or []) when child.message is annotation
@idTable[annotation.id] = child
break
annotationDeleted: ({id}) => annotationDeleted: ({id}) =>
container = this.getContainer id container = this.idTable[id]
container.message = null if container?
this.pruneEmpties(@root) container.message = null
this.pruneEmpties(@root)
annotationsLoaded: (annotations) => annotationsLoaded: (annotations) =>
messages = (@root.flattenChildren() or []).concat(annotations) messages = (@root.flattenChildren() or []).concat(annotations)
......
...@@ -29,15 +29,12 @@ renderFactory = ['$$rAF', ($$rAF) -> ...@@ -29,15 +29,12 @@ renderFactory = ['$$rAF', ($$rAF) ->
class Hypothesis extends Annotator class Hypothesis extends Annotator
events: events:
'beforeAnnotationCreated': 'beforeAnnotationCreated' 'beforeAnnotationCreated': 'beforeAnnotationCreated'
'annotationCreated': 'digest'
'annotationDeleted': 'annotationDeleted' 'annotationDeleted': 'annotationDeleted'
'annotationUpdated': 'digest'
'annotationsLoaded': 'digest' 'annotationsLoaded': 'digest'
# Plugin configuration # Plugin configuration
options: options:
noDocAccess: true noDocAccess: true
Discovery: {}
Threading: {} Threading: {}
# Internal state # Internal state
...@@ -47,17 +44,12 @@ class Hypothesis extends Annotator ...@@ -47,17 +44,12 @@ class Hypothesis extends Annotator
tool: 'comment' tool: 'comment'
visibleHighlights: false visibleHighlights: false
this.$inject = ['$document', '$window'] this.$inject = ['$document', '$window', 'store']
constructor: ( $document, $window ) -> constructor: ( $document, $window, store ) ->
super ($document.find 'body') super ($document.find 'body')
window.annotator = this
@providers = [] @providers = []
@socialView = @store = store
name: "none" # "single-player"
this.patch_store()
# Load plugins # Load plugins
for own name, opts of @options for own name, opts of @options
...@@ -69,13 +61,13 @@ class Hypothesis extends Annotator ...@@ -69,13 +61,13 @@ class Hypothesis extends Annotator
whitelist = ['target', 'document', 'uri'] whitelist = ['target', 'document', 'uri']
this.addPlugin 'Bridge', this.addPlugin 'Bridge',
gateway: true gateway: true
formatter: (annotation) => formatter: (annotation) ->
formatted = {} formatted = {}
for k, v of annotation when k in whitelist for k, v of annotation when k in whitelist
formatted[k] = v formatted[k] = v
formatted formatted
parser: (annotation) => parser: (annotation) ->
parsed = {} parsed = new store.annotation()
for k, v of annotation when k in whitelist for k, v of annotation when k in whitelist
parsed[k] = v parsed[k] = v
parsed parsed
...@@ -84,21 +76,16 @@ class Hypothesis extends Annotator ...@@ -84,21 +76,16 @@ class Hypothesis extends Annotator
window: source window: source
origin: origin origin: origin
scope: "#{scope}:provider" scope: "#{scope}:provider"
onReady: => onReady: => if source is $window.parent then @host = channel
if source is $window.parent then @host = channel
entities = []
channel = this._setupXDM options channel = this._setupXDM options
provider = channel: channel, entities: []
channel.call channel.call
method: 'getDocumentInfo' method: 'getDocumentInfo'
success: (info) => success: (info) =>
entityUris = {} provider.entities = (link.href for link in info.metadata.link)
entityUris[info.uri] = true @providers.push provider
for link in info.metadata.link @element.scope().$digest()
entityUris[link.href] = true if link.href
for href of entityUris
entities.push href
this.plugins.Store?.loadAnnotations()
this.digest() this.digest()
# Allow the host to define it's own state # Allow the host to define it's own state
...@@ -111,10 +98,6 @@ class Hypothesis extends Annotator ...@@ -111,10 +98,6 @@ class Hypothesis extends Annotator
method: 'setVisibleHighlights' method: 'setVisibleHighlights'
params: this.visibleHighlights params: this.visibleHighlights
@providers.push
channel: channel
entities: entities
_setupXDM: (options) -> _setupXDM: (options) ->
# jschannel chokes FF and Chrome extension origins. # jschannel chokes FF and Chrome extension origins.
if (options.origin.match /^chrome-extension:\/\//) or if (options.origin.match /^chrome-extension:\/\//) or
...@@ -195,6 +178,27 @@ class Hypothesis extends Annotator ...@@ -195,6 +178,27 @@ class Hypothesis extends Annotator
_setupDocumentAccessStrategies: -> this _setupDocumentAccessStrategies: -> this
_scan: -> this _scan: -> this
createAnnotation: (annotation) ->
annotation = new @store.annotation(annotation)
this.publish 'beforeAnnotationCreated', annotation
annotation
deleteAnnotation: (annotation) ->
annotation.$delete(id: annotation.id).then =>
this.publish 'annotationDeleted', annotation
annotation
loadAnnotations: (annotations) ->
annotations = for annotation in annotations
container = @plugins.Threading.idTable[annotation.id]
if container?.message
angular.copy annotation, container.message
this.publish 'annotationUpdated', container.message
continue
else
annotation
super (new @store.annotation(a) for a in annotations)
# Do nothing in the app frame, let the host handle it. # Do nothing in the app frame, let the host handle it.
setupAnnotation: (annotation) -> annotation setupAnnotation: (annotation) -> annotation
...@@ -263,77 +267,9 @@ class Hypothesis extends Annotator ...@@ -263,77 +267,9 @@ class Hypothesis extends Annotator
if scope.selectedAnnotations?[annotation.id] if scope.selectedAnnotations?[annotation.id]
delete scope.selectedAnnotations[annotation.id] delete scope.selectedAnnotations[annotation.id]
@_setSelectedAnnotations scope.selectedAnnotations @_setSelectedAnnotations scope.selectedAnnotations
@digest()
patch_store: ->
scope = @element.scope()
Store = Annotator.Plugin.Store
# When the Store plugin is first instantiated, don't load annotations.
# They will be loaded manually as entities are registered by participating
# frames.
Store.prototype.loadAnnotations = ->
query = limit: 1000
@annotator.considerSocialView.call @annotator, query
entities = {}
for p in @annotator.providers
for uri in p.entities
unless entities[uri]?
entities[uri] = true
this.loadAnnotationsFromSearch (angular.extend {}, query, uri: uri)
this.entities = Object.keys(entities)
# When the store plugin finishes a request, update the annotation
# using a monkey-patched update function which updates the threading
# if the annotation has a newly-assigned id and ensures that the id
# is enumerable.
Store.prototype.updateAnnotation = (annotation, data) =>
# Update the annotation with the new data
annotation = angular.extend annotation, data
# Update the thread table
update = (parent) ->
for child in parent.children when child.message is annotation
scope.threading.idTable[data.id] = child
return true
return false
# Check its references
references = annotation.references or []
if typeof(annotation.references) == 'string' then references = []
for ref in references.slice().reverse()
container = scope.threading.idTable[ref]
continue unless container?
break if update container
# Check the root
update scope.threading.root
# Update the view
this.digest()
considerSocialView: (query) ->
switch @socialView.name
when "none"
# Sweet, nothing to do, just clean up previous filters
delete query.user
when "single-player"
if @user?
query.user = @element.injector().get('auth').user
else
delete query.user
setTool: (name) -> setTool: (name) ->
return if name is @tool return if name is @tool
if name is 'highlight'
this.socialView.name = 'single-player'
else
this.socialView.name = 'none'
@tool = name @tool = name
this.publish 'setTool', name this.publish 'setTool', name
for p in @providers for p in @providers
......
angular.module('h')
.service('store', [
'$document', '$http', '$resource',
($document, $http, $resource) ->
svc = $document.find('link')
.filter -> @rel is 'service' and @type is 'application/annotatorsvc+json'
.filter -> @href
.prop('href')
store =
$resolved: false
$promise: $http.get(svc)
.finally -> store.$resolved = true
.then (response) ->
for name, actions of response.data.links
store[name] = $resource(actions.url or svc, {}, actions)
store
])
...@@ -59,7 +59,6 @@ class Streamer ...@@ -59,7 +59,6 @@ class Streamer
# Give the application a chance to initialize the connection # Give the application a chance to initialize the connection
self.onopen(name: 'open') self.onopen(name: 'open')
# Process queued messages # Process queued messages
self._sendQueue() self._sendQueue()
......
class StreamSearchController class StreamSearchController
this.inject = [ this.inject = [
'$scope', '$rootScope', '$routeParams', '$scope', '$rootScope', '$routeParams',
'annotator', 'queryparser', 'searchfilter', 'streamer', 'streamfilter' 'annotator', 'auth', 'queryparser', 'searchfilter', 'store',
'streamer', 'streamfilter'
] ]
constructor: ( constructor: (
$scope, $rootScope, $routeParams $scope, $rootScope, $routeParams
annotator, queryparser, searchfilter, streamer, streamfilter annotator, auth, queryparser, searchfilter, store,
streamer, streamfilter
) -> ) ->
# Clear out loaded annotations and threads
# XXX: Resolve threading, storage, and streamer better for all routes.
annotator.plugins.Threading?.pluginInit()
annotator.plugins.Store?.annotations = []
# Initialize the base filter # Initialize the base filter
streamfilter streamfilter
.resetFilter() .resetFilter()
.setMatchPolicyIncludeAll() .setMatchPolicyIncludeAll()
.setPastDataHits(50)
# Apply query clauses # Apply query clauses
$scope.search.query = $routeParams.q $scope.search.query = $routeParams.q
...@@ -30,10 +26,14 @@ class StreamSearchController ...@@ -30,10 +26,14 @@ class StreamSearchController
$scope.shouldShowThread = (container) -> true $scope.shouldShowThread = (container) -> true
streamer.send({filter: streamfilter.getFilter()})
$scope.$on '$destroy', -> $scope.$on '$destroy', ->
$scope.search.query = '' $scope.search.query = ''
$scope.$watch (-> auth.user), ->
query = angular.extend limit: 10, $scope.search.query
store.search.get query, ({rows}) ->
annotator.loadAnnotations(rows)
angular.module('h') angular.module('h')
.controller('StreamSearchController', StreamSearchController) .controller('StreamSearchController', StreamSearchController)
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