Commit 11430695 authored by Randall Leeds's avatar Randall Leeds

Start replacing Store plugin with ngResource

parent 5964631d
......@@ -12,22 +12,12 @@ imports = [
resolve =
storeConfig: ['$q', 'annotator', ($q, annotator) ->
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
]
store: ['store', (store) -> store.$promise]
configure = [
'$locationProvider', '$routeProvider', '$sceDelegateProvider', 'streamerProvider',
($locationProvider, $routeProvider, $sceDelegateProvider, streamerProvider) ->
'$locationProvider', '$routeProvider', '$sceDelegateProvider',
($locationProvider, $routeProvider, $sceDelegateProvider) ->
$locationProvider.html5Mode(true)
$routeProvider.when '/a/:id',
......
class AppController
this.$inject = [
'$location', '$route', '$scope', '$timeout', '$window',
'annotator', 'auth', 'documentHelpers', 'drafts', 'flash', 'identity',
'annotator', 'auth', 'documentHelpers', 'drafts', 'identity',
'permissions', 'streamer', 'streamfilter'
]
constructor: (
$location, $route, $scope, $timeout, $window,
annotator, auth, documentHelpers, drafts, flash, identity,
annotator, auth, documentHelpers, drafts, identity,
permissions, streamer, streamfilter,
) ->
......@@ -21,106 +21,33 @@ class AppController
return unless data?.length
switch action
when 'create', 'update', 'past'
plugins.Store?._onLoadAnnotations data
annotator.loadAnnotations data
when 'delete'
for annotation in data
annotation = plugins.Threading.idTable[annotation.id]?.message
continue unless annotation?
plugins.Store?.unregisterAnnotation(annotation)
annotator.deleteAnnotation(annotation)
annotator.publish 'annotationDeleted', (annotation)
streamer.onmessage = (data) ->
if !data or data.type != 'annotation-notification'
return
return if !data or data.type != 'annotation-notification'
action = data.options.action
payload = data.payload
if $scope.socialView.name is 'single-player'
payload = payload.filter (ann) -> ann.user is auth.user
applyUpdates(action, payload)
$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 = ->
$scope.dialog.visible = false
reset = ->
$scope.dialog.visible = false
# Update any edits in progress.
for draft in drafts.all()
annotator.publish 'beforeAnnotationCreated', draft
# Reload services
initStore()
streamer.close()
streamer.open($window.WebSocket, streamerUrl)
$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.$on '$routeChangeStart', (event, newRoute, oldRoute) ->
return if newRoute.redirectTo
# Clean up any annotations that need to be unloaded.
for id, container of $scope.threading.idTable when container.message
# Remove annotations not belonging to this user when highlighting.
if annotator.tool is 'highlight' and annotation.user != auth.user
annotator.publish 'annotationDeleted', container.message
drafts.remove annotation
# Remove annotations the user is not authorized to view.
else if not permissions.permits 'read', container.message, auth.user
annotator.publish 'annotationDeleted', container.message
drafts.remove container.message
$scope.$watch 'sort.name', (name) ->
return unless name
......@@ -130,20 +57,27 @@ class AppController
when 'Location' then ['-!!message', 'message.target[0].pos.top']
$scope.sort = {name, predicate}
$scope.$watch 'store.entities', (entities, oldEntities) ->
return if entities is oldEntities
$scope.$watch (-> auth.user), (newVal, oldVal) ->
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
streamfilter
.resetFilter()
.addClause('/uri', 'one_of', entities)
# Update any edits in progress.
for draft in drafts.all()
annotator.publish 'beforeAnnotationCreated', draft
streamer.send({filter: streamfilter.getFilter()})
# Reopen the streamer.
streamer.close()
streamer.open($window.WebSocket, streamerUrl)
$scope.$watch 'auth.user', (newVal, oldVal) ->
return if newVal is undefined
reset()
$scope.login() if isFirstRun and not (newVal or oldVal)
# Reload the view.
$route.reload()
$scope.login = ->
$scope.dialog.visible = true
......@@ -177,7 +111,6 @@ class AppController
delete $scope.selectedAnnotations
delete $scope.selectedAnnotationsCount
$scope.socialView = annotator.socialView
$scope.sort = name: 'Location'
$scope.threading = plugins.Threading
......@@ -185,21 +118,16 @@ class AppController
class AnnotationViewerController
this.$inject = [
'$location', '$routeParams', '$scope',
'annotator', 'streamer', 'streamfilter'
'annotator', 'streamer', 'store', 'streamfilter'
]
constructor: (
$location, $routeParams, $scope,
annotator, streamer, streamfilter
annotator, streamer, store, streamfilter
) ->
# Tells the view that these annotations are standalone
$scope.isEmbedded = 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
# to annotations loaded into the stream.
$scope.focus = angular.noop
......@@ -210,11 +138,10 @@ class AnnotationViewerController
$location.path('/stream').search('q', query)
id = $routeParams.id
$scope.$watch 'store', ->
if $scope.store
$scope.store.loadAnnotationsFromSearch({_id: id}).then ->
$scope.store.loadAnnotationsFromSearch({references: id})
store.search.get _id: $routeParams.id, ({rows}) ->
annotator.loadAnnotations(rows)
store.search.get references: $routeParams.id, ({rows}) ->
annotator.loadAnnotations(rows)
streamfilter
.setPastDataNone()
......@@ -225,12 +152,44 @@ class AnnotationViewerController
streamer.send({filter: streamfilter.getFilter()})
class ViewerController
this.$inject = ['$scope', 'annotator']
constructor: ( $scope, annotator ) ->
this.$inject = [
'$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
$scope.isEmbedded = 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) ->
if angular.isObject annotation
highlights = [annotation.$$tag]
......
......@@ -159,9 +159,11 @@ AnnotationController = [
switch @action
when 'create'
annotator.publish 'annotationCreated', model
model.$create().then ->
annotator.publish 'annotationCreated', model
when 'delete', 'edit'
annotator.publish 'annotationUpdated', model
model.$update(id: model.id).then ->
annotator.publish 'annotationUpdated', model
@editing = false
@action = 'view'
......@@ -181,8 +183,7 @@ AnnotationController = [
# Construct the reply.
references = [references..., id]
reply = {references, uri}
annotator.publish 'beforeAnnotationCreated', reply
reply = annotator.createAnnotation {references, uri}
if auth.user?
if permissions.isPublic model.permissions
......@@ -275,9 +276,10 @@ AnnotationController = [
# Save highlights once logged in.
if highlight and this.isHighlight()
if auth.user
model.permissions = permissions.private()
annotator.publish 'annotationCreated', model
if model.user and not model.id
highlight = false # skip this on future updates
model.$create().then ->
annotator.publish 'annotationCreated', model
highlight = false # skip this on future updates
else
drafts.add model, => this.revert()
......
......@@ -6,6 +6,7 @@ class Annotator.Plugin.Threading extends Annotator.Plugin
events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
'annotationCreated': 'annotationCreated'
'annotationDeleted': 'annotationDeleted'
'annotationsLoaded': 'annotationsLoaded'
......@@ -59,14 +60,25 @@ class Annotator.Plugin.Threading extends Annotator.Plugin
if !container.message && container.children.length == 0
parent.removeChild(container)
delete this.idTable[container.message?.id]
beforeAnnotationCreated: (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}) =>
container = this.getContainer id
container.message = null
this.pruneEmpties(@root)
container = this.idTable[id]
if container?
container.message = null
this.pruneEmpties(@root)
annotationsLoaded: (annotations) =>
messages = (@root.flattenChildren() or []).concat(annotations)
......
......@@ -29,15 +29,12 @@ renderFactory = ['$$rAF', ($$rAF) ->
class Hypothesis extends Annotator
events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
'annotationCreated': 'digest'
'annotationDeleted': 'annotationDeleted'
'annotationUpdated': 'digest'
'annotationsLoaded': 'digest'
# Plugin configuration
options:
noDocAccess: true
Discovery: {}
Threading: {}
# Internal state
......@@ -47,17 +44,12 @@ class Hypothesis extends Annotator
tool: 'comment'
visibleHighlights: false
this.$inject = ['$document', '$window']
constructor: ( $document, $window ) ->
this.$inject = ['$document', '$window', 'store']
constructor: ( $document, $window, store ) ->
super ($document.find 'body')
window.annotator = this
@providers = []
@socialView =
name: "none" # "single-player"
this.patch_store()
@store = store
# Load plugins
for own name, opts of @options
......@@ -69,13 +61,13 @@ class Hypothesis extends Annotator
whitelist = ['target', 'document', 'uri']
this.addPlugin 'Bridge',
gateway: true
formatter: (annotation) =>
formatter: (annotation) ->
formatted = {}
for k, v of annotation when k in whitelist
formatted[k] = v
formatted
parser: (annotation) =>
parsed = {}
parser: (annotation) ->
parsed = new store.annotation()
for k, v of annotation when k in whitelist
parsed[k] = v
parsed
......@@ -84,21 +76,16 @@ class Hypothesis extends Annotator
window: source
origin: origin
scope: "#{scope}:provider"
onReady: =>
if source is $window.parent then @host = channel
entities = []
onReady: => if source is $window.parent then @host = channel
channel = this._setupXDM options
provider = channel: channel, entities: []
channel.call
method: 'getDocumentInfo'
success: (info) =>
entityUris = {}
entityUris[info.uri] = true
for link in info.metadata.link
entityUris[link.href] = true if link.href
for href of entityUris
entities.push href
this.plugins.Store?.loadAnnotations()
provider.entities = (link.href for link in info.metadata.link)
@providers.push provider
@element.scope().$digest()
this.digest()
# Allow the host to define it's own state
......@@ -111,10 +98,6 @@ class Hypothesis extends Annotator
method: 'setVisibleHighlights'
params: this.visibleHighlights
@providers.push
channel: channel
entities: entities
_setupXDM: (options) ->
# jschannel chokes FF and Chrome extension origins.
if (options.origin.match /^chrome-extension:\/\//) or
......@@ -195,6 +178,27 @@ class Hypothesis extends Annotator
_setupDocumentAccessStrategies: -> 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.
setupAnnotation: (annotation) -> annotation
......@@ -263,77 +267,9 @@ class Hypothesis extends Annotator
if scope.selectedAnnotations?[annotation.id]
delete scope.selectedAnnotations[annotation.id]
@_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) ->
return if name is @tool
if name is 'highlight'
this.socialView.name = 'single-player'
else
this.socialView.name = 'none'
@tool = name
this.publish 'setTool', name
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
# Give the application a chance to initialize the connection
self.onopen(name: 'open')
# Process queued messages
self._sendQueue()
......
class StreamSearchController
this.inject = [
'$scope', '$rootScope', '$routeParams',
'annotator', 'queryparser', 'searchfilter', 'streamer', 'streamfilter'
'annotator', 'auth', 'queryparser', 'searchfilter', 'store',
'streamer', 'streamfilter'
]
constructor: (
$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
streamfilter
.resetFilter()
.setMatchPolicyIncludeAll()
.setPastDataHits(50)
# Apply query clauses
$scope.search.query = $routeParams.q
......@@ -30,10 +26,14 @@ class StreamSearchController
$scope.shouldShowThread = (container) -> true
streamer.send({filter: streamfilter.getFilter()})
$scope.$on '$destroy', ->
$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')
.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