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

Refactor drafting and authentication interactions

Make changes to allow multiple top-level drafts, drafting before
authenticating, keeping drafts while performing page searches,
and replying to search results.

- Factor out the userAuthorize option passed to the Permissions
  plugin into a local function in controllers.coffee.

- Only load the Permissions plugin when logged in so that the
  plugin options do not leak information about the last logged in
  user and unattributed drafts have no user or permissions field.

- When switching to highlight mode, show the sidebar and flash an
  info message informing the user that they will need to log in
  before annotations are saved, but don't prohibit highlighting.

- Simplify the logic in the guest for highlight mode by offloading
  work to the annotation controller. The "inject" property is no
  longer needed. The annotation controller saves drafts of highlights
  if they have no user and persists them to the server as soon as
  the user is set.

- When logging in, the app controller simply loops through all the
  drafts are publishes 'beforeAnnotationUpdated' to give the
  Permissions plugin a chance to fire.

- When initializing the Store plugin, don't perform any search at
  all when not logged in. This change makes "single player" mode
  work even before logging in.

- Allow hiding the sidebar when editing. Simplify the guest code
  that no longer needs to track the sidebar state by listening for
  'annotationEditorHide' and 'annotationEditorSubmit' and the
  corresponding sidebar code that fired these. They're nonsensical
  now because there isn't exactly one "editor".

- Remove the concept of an 'ongoing edit' entirely, simplifying the
  sidebar code even further. Any number of edits are allowed and
  top level ones are not treated specially.
parent 60504257
...@@ -9,6 +9,32 @@ imports = [ ...@@ -9,6 +9,32 @@ imports = [
] ]
# User authorization function for the Permissions plugin.
authorizeAction = (action, annotation, user) ->
if annotation.permissions
tokens = annotation.permissions[action] || []
if tokens.length == 0
# Empty or missing tokens array: only admin can perform action.
return false
for token in tokens
if user == token
return true
if token == 'group:__world__'
return true
# No tokens matched: action should not be performed.
return false
# Coarse-grained authorization
else if annotation.user
return user and user == annotation.user
# No authorization info on annotation: free-for-all!
true
class App class App
this.$inject = [ this.$inject = [
'$location', '$q', '$route', '$scope', '$timeout', '$location', '$q', '$route', '$scope', '$timeout',
...@@ -75,18 +101,20 @@ class App ...@@ -75,18 +101,20 @@ class App
"""Initialize the storage component.""" """Initialize the storage component."""
Store = plugins.Store Store = plugins.Store
delete plugins.Store delete plugins.Store
annotator.addPlugin 'Store', annotator.options.Store
$scope.store = plugins.Store if $scope.persona or annotator.socialView.name is 'none'
annotator.addPlugin 'Store', annotator.options.Store
$scope.store = plugins.Store
_id = $route.current.params.id _id = $route.current.params.id
_promise = null _promise = null
# Load any initial annotations that should be displayed # Load any initial annotations that should be displayed
if _id if _id
# XXX: Two requests here is less than ideal # XXX: Two requests here is less than ideal
plugins.Store.loadAnnotationsFromSearch({_id}).then -> plugins.Store.loadAnnotationsFromSearch({_id}).then ->
plugins.Store.loadAnnotationsFromSearch({references: _id}) plugins.Store.loadAnnotationsFromSearch({references: _id})
return unless Store return unless Store
Store.destroy() Store.destroy()
...@@ -112,7 +140,7 @@ class App ...@@ -112,7 +140,7 @@ class App
cull = (acc, annotation) -> cull = (acc, annotation) ->
if annotator.tool is 'highlight' and annotation.user != user if annotator.tool is 'highlight' and annotation.user != user
acc.drop.push annotation acc.drop.push annotation
else if plugins.Permissions.authorize('read', annotation, user) else if authorizeAction 'read', annotation, user
acc.keep.push annotation acc.keep.push annotation
else else
acc.drop.push annotation acc.drop.push annotation
...@@ -120,7 +148,11 @@ class App ...@@ -120,7 +148,11 @@ class App
{keep, drop} = Store.annotations.reduce cull, {keep: [], drop: []} {keep, drop} = Store.annotations.reduce cull, {keep: [], drop: []}
Store.annotations = [] Store.annotations = []
plugins.Store.annotations = keep
if plugins.Store?
plugins.Store.annotations = keep
else
drop = drop.concat keep
# Clean up the ones that should be removed. # Clean up the ones that should be removed.
do cleanup = (drop) -> do cleanup = (drop) ->
...@@ -177,10 +209,6 @@ class App ...@@ -177,10 +209,6 @@ class App
_dfdSock.promise _dfdSock.promise
onlogin = (assertion) -> onlogin = (assertion) ->
# Delete any old Auth plugin.
plugins.Auth?.destroy()
delete plugins.Auth
# Configure the Auth plugin with the issued assertion as refresh token. # Configure the Auth plugin with the issued assertion as refresh token.
annotator.addPlugin 'Auth', annotator.addPlugin 'Auth',
tokenUrl: documentHelpers.absoluteURI( tokenUrl: documentHelpers.absoluteURI(
...@@ -188,22 +216,27 @@ class App ...@@ -188,22 +216,27 @@ class App
# Set the user from the token. # Set the user from the token.
plugins.Auth.withToken (token) -> plugins.Auth.withToken (token) ->
plugins.Permissions._setAuthFromToken(token) annotator.addPlugin 'Permissions',
loggedInUser = plugins.Permissions.user.replace /^acct:/, '' user: token.userId
userAuthorize: authorizeAction
permissions:
read: [token.userId]
update: [token.userId]
delete: [token.userId]
admin: [token.userId]
loggedInUser = token.userId.replace /^acct:/, ''
reset() reset()
onlogout = -> onlogout = ->
return unless drafts.discard()
plugins.Auth?.element.removeData('annotator:headers') plugins.Auth?.element.removeData('annotator:headers')
plugins.Auth?.destroy()
delete plugins.Auth delete plugins.Auth
plugins.Permissions.setUser(null) plugins.Permissions?.setUser(null)
plugins.Permissions?.destroy()
# XXX: Temporary workaround until Annotator v2.0 or v1.2.10 delete plugins.Permissions
plugins.Permissions.options.permissions =
read: []
update: []
delete: []
admin: []
loggedInUser = null loggedInUser = null
reset() reset()
...@@ -212,11 +245,9 @@ class App ...@@ -212,11 +245,9 @@ class App
# Do not rely on the identity service to invoke callbacks within an # Do not rely on the identity service to invoke callbacks within an
# angular digest cycle. # angular digest cycle.
$scope.$evalAsync -> $scope.$evalAsync ->
if $scope.ongoingHighlightSwitch # Update any edits in progress.
$scope.ongoingHighlightSwitch = false for draft in drafts.all()
annotator.setTool 'highlight' annotator.publish 'beforeAnnotationCreated', draft
else
annotator.setTool 'comment'
# Convert the verified user id to the format used by the API. # Convert the verified user id to the format used by the API.
persona = loggedInUser persona = loggedInUser
...@@ -231,19 +262,6 @@ class App ...@@ -231,19 +262,6 @@ class App
initStore() initStore()
initUpdater() initUpdater()
annotator.subscribe 'annotationCreated', (annotation) ->
if annotation is $scope.ongoingEdit?.message
delete $scope.ongoingEdit
for p in providers
p.channel.notify method: 'onEditorSubmit'
p.channel.notify method: 'onEditorHide'
annotator.subscribe 'annotationDeleted', (annotation) ->
if annotation is $scope.ongoingEdit?.message
delete $scope.ongoingEdit
for p in providers
p.channel.notify method: 'onEditorHide'
annotator.subscribe 'serviceDiscovery', (options) -> annotator.subscribe 'serviceDiscovery', (options) ->
annotator.options.Store ?= {} annotator.options.Store ?= {}
angular.extend annotator.options.Store, options angular.extend annotator.options.Store, options
...@@ -253,21 +271,11 @@ class App ...@@ -253,21 +271,11 @@ class App
$scope.$watch 'socialView.name', (newValue, oldValue) -> $scope.$watch 'socialView.name', (newValue, oldValue) ->
return if newValue is oldValue return if newValue is oldValue
initStore()
if $scope.persona if newValue is 'single-player' and not $scope.persona
initStore()
else if newValue is 'single-player'
identity.request()
$scope.$watch 'frame.visible', (newValue, oldValue) ->
if newValue
annotator.show() annotator.show()
annotator.host.notify method: 'showFrame' flash 'info',
else if oldValue 'You will need to sign in for your highlights to be saved.'
annotator.hide()
annotator.host.notify method: 'hideFrame'
for p in annotator.providers
p.channel.notify method: 'setActiveHighlights'
$scope.$watch 'sort.name', (name) -> $scope.$watch 'sort.name', (name) ->
return unless name return unless name
...@@ -299,8 +307,6 @@ class App ...@@ -299,8 +307,6 @@ class App
sock.send(JSON.stringify(sockmsg)) sock.send(JSON.stringify(sockmsg))
$scope.authTimeout = -> $scope.authTimeout = ->
delete $scope.ongoingEdit
$scope.ongoingHighlightSwitch = false
flash 'info', flash 'info',
'For your security, the forms have been reset due to inactivity.' 'For your security, the forms have been reset due to inactivity.'
...@@ -310,7 +316,6 @@ class App ...@@ -310,7 +316,6 @@ class App
$scope.selectedAnnotationsCount = 0 $scope.selectedAnnotationsCount = 0
$scope.baseURI = documentHelpers.baseURI $scope.baseURI = documentHelpers.baseURI
$scope.frame = visible: false
$scope.id = identity $scope.id = identity
$scope.model = persona: undefined $scope.model = persona: undefined
...@@ -324,8 +329,7 @@ class App ...@@ -324,8 +329,7 @@ class App
update: (query) -> update: (query) ->
unless angular.equals $location.search()['q'], query unless angular.equals $location.search()['q'], query
if drafts.discard() $location.search('q', query or null)
$location.search('q', query or null)
$scope.socialView = annotator.socialView $scope.socialView = annotator.socialView
$scope.sort = name: 'Location' $scope.sort = name: 'Location'
...@@ -372,8 +376,6 @@ class Viewer ...@@ -372,8 +376,6 @@ class Viewer
$scope.activate = (annotation) -> $scope.activate = (annotation) ->
if angular.isObject annotation if angular.isObject annotation
highlights = [annotation.$$tag] highlights = [annotation.$$tag]
else if $scope.ongoingEdit
highlights = [$scope.ongoingEdit.message.$$tag]
else else
highlights = [] highlights = []
for p in annotator.providers for p in annotator.providers
...@@ -383,7 +385,7 @@ class Viewer ...@@ -383,7 +385,7 @@ class Viewer
$scope.shouldShowThread = (container) -> $scope.shouldShowThread = (container) ->
if $routeParams.q if $routeParams.q
container.shown container.shown or not container.message.id
else if $scope.selectedAnnotations? else if $scope.selectedAnnotations?
if container.parent is $scope.threading.root if container.parent is $scope.threading.root
$scope.selectedAnnotations[container.message?.id] $scope.selectedAnnotations[container.message?.id]
...@@ -409,10 +411,11 @@ class Viewer ...@@ -409,10 +411,11 @@ class Viewer
render = (acc, container) -> render = (acc, container) ->
{lastShown, renderList} = acc {lastShown, renderList} = acc
id = container.message?.id
container.more = 0 container.more = 0
container.order = renderList.length container.order = renderList.length
container.renderList = renderList container.renderList = renderList
container.shown = matchSet is null or matchSet[container.message?.id]? container.shown = matchSet is null or not id or matchSet[id]
renderList.push container renderList.push container
if container.shown if container.shown
......
...@@ -225,8 +225,11 @@ repeatAnim = -> ...@@ -225,8 +225,11 @@ repeatAnim = ->
username = ['$filter', '$window', ($filter, $window) -> username = ['$filter', '$window', ($filter, $window) ->
link: (scope, elem, attr) -> link: (scope, elem, attr) ->
scope.$watch 'user', -> scope.$watch 'user', (user) ->
scope.uname = $filter('persona')(scope.user, 'username') if user
scope.uname = $filter('persona')(scope.user, 'username')
else
scope.uname = '<nobody>'
scope.uclick = (event) -> scope.uclick = (event) ->
event.preventDefault() event.preventDefault()
......
...@@ -14,7 +14,6 @@ extractURIComponent = (uri, component) -> ...@@ -14,7 +14,6 @@ extractURIComponent = (uri, component) ->
# A non-public annotation requires only a target (e.g. a highlight). # A non-public annotation requires only a target (e.g. a highlight).
validate = (value) -> validate = (value) ->
return unless angular.isObject value return unless angular.isObject value
return unless value.user or value.deleted
worldReadable = 'group:__world__' in (value.permissions?.read or []) worldReadable = 'group:__world__' in (value.permissions?.read or [])
(value.tags?.length or value.text?.length) or (value.tags?.length or value.text?.length) or
(value.target?.length and not worldReadable) (value.target?.length and not worldReadable)
...@@ -38,8 +37,8 @@ validate = (value) -> ...@@ -38,8 +37,8 @@ validate = (value) ->
# {@link annotator annotator service} for persistence. # {@link annotator annotator service} for persistence.
### ###
AnnotationController = [ AnnotationController = [
'$scope', 'annotator', 'drafts', '$scope', 'annotator', 'drafts', 'flash'
($scope, annotator, drafts) -> ($scope, annotator, drafts, flash) ->
@annotation = {} @annotation = {}
@action = 'view' @action = 'view'
@document = null @document = null
...@@ -47,7 +46,8 @@ AnnotationController = [ ...@@ -47,7 +46,8 @@ AnnotationController = [
@editing = false @editing = false
@embedded = false @embedded = false
model = null highlight = annotator.tool is 'highlight'
model = $scope.annotationGet()
original = null original = null
vm = this vm = this
...@@ -80,7 +80,7 @@ AnnotationController = [ ...@@ -80,7 +80,7 @@ AnnotationController = [
# @description Switches the view to an editor. # @description Switches the view to an editor.
### ###
this.edit = -> this.edit = ->
drafts.add model, => this.revert() drafts.add model, -> vm.revert()
@action = if model.id? then 'edit' else 'create' @action = if model.id? then 'edit' else 'create'
@editing = true @editing = true
@preview = 'no' @preview = 'no'
...@@ -105,7 +105,10 @@ AnnotationController = [ ...@@ -105,7 +105,10 @@ AnnotationController = [
# @description Saves any edits and returns to the viewer. # @description Saves any edits and returns to the viewer.
### ###
this.save = -> this.save = ->
return unless validate(@annotation) unless model.user or model.deleted
return flash 'info', 'Please sign in to save annotations.'
unless validate(@annotation)
return flash 'info', 'Please add text or a tag before publishing.'
@editing = false @editing = false
angular.extend model, @annotation, angular.extend model, @annotation,
...@@ -124,11 +127,6 @@ AnnotationController = [ ...@@ -124,11 +127,6 @@ AnnotationController = [
# changes. Initializes brand new annotations and # changes. Initializes brand new annotations and
### ###
this.render = -> this.render = ->
# Initialize brand new annotations.
unless model.id? or drafts.contains model
annotator.publish 'beforeAnnotationCreated', model
this.edit()
# Extend the view model with a copy of the domain model. # Extend the view model with a copy of the domain model.
# Note that copy is used so that deep properties aren't shared. # Note that copy is used so that deep properties aren't shared.
angular.extend @annotation, angular.copy model angular.extend @annotation, angular.copy model
...@@ -158,12 +156,26 @@ AnnotationController = [ ...@@ -158,12 +156,26 @@ AnnotationController = [
$scope.$on 'threadCollapse', (event) -> $scope.$on 'threadCollapse', (event) ->
event.preventDefault() if vm.editing event.preventDefault() if vm.editing
# Render on updates # Render on updates.
$scope.$watchCollection (-> $scope.annotationGet()), (value) -> $scope.$watch (-> model.updated), (updated) ->
model = value if updated then drafts.remove model
if value? vm.render()
if value.updated then drafts.remove model
vm.render() # Save highlights once logged in.
$scope.$watch (-> model.user), (user) ->
return unless highlight and not model.references
if user
annotator.publish 'annotationCreated', model
else
drafts.add model, -> vm.revert()
# Initialize brand now annotations
unless model.id?
if model.references
annotator.publish 'beforeAnnotationCreated', model
vm.edit()
else if not highlight
vm.edit()
this this
] ]
......
...@@ -17,8 +17,8 @@ COLLAPSED_CLASS = 'thread-collapsed' ...@@ -17,8 +17,8 @@ COLLAPSED_CLASS = 'thread-collapsed'
# replying and sharing. # replying and sharing.
### ###
ThreadController = [ ThreadController = [
'$attrs', '$element', '$parse', '$scope', 'render', '$attrs', '$element', '$parse', '$scope', 'flash', 'render',
($attrs, $element, $parse, $scope, render) -> ($attrs, $element, $parse, $scope, flash, render) ->
@container = null @container = null
@collapsed = false @collapsed = false
@hover = false @hover = false
...@@ -33,6 +33,8 @@ ThreadController = [ ...@@ -33,6 +33,8 @@ ThreadController = [
# Creates a new message in reply to this thread. # Creates a new message in reply to this thread.
### ###
this.reply = -> this.reply = ->
unless @container.message.id
return flash 'error', 'You must publish this before replying to it.'
if @collapsed then this.toggleCollapsed() if @collapsed then this.toggleCollapsed()
# Extract the references value from this container. # Extract the references value from this container.
...@@ -71,6 +73,8 @@ ThreadController = [ ...@@ -71,6 +73,8 @@ ThreadController = [
# Toggle the shared property. # Toggle the shared property.
### ###
this.toggleShared = -> this.toggleShared = ->
unless @container.message.id
return flash 'error', 'You must publish this before sharing it.'
@shared = not @shared @shared = not @shared
if @shared if @shared
# Focus and select the share link # Focus and select the share link
......
...@@ -191,8 +191,12 @@ class Annotator.Guest extends Annotator ...@@ -191,8 +191,12 @@ class Annotator.Guest extends Annotator
_setupWrapper: -> _setupWrapper: ->
@wrapper = @element @wrapper = @element
.on 'click', => .on 'click', (event) =>
unless @selectedTargets?.length if @selectedTargets?.length
if @tool is 'highlight'
# Create the annotation
annotation = this.setupAnnotation(this.createAnnotation())
else
@hideFrame() @hideFrame()
this this
...@@ -269,23 +273,14 @@ class Annotator.Guest extends Annotator ...@@ -269,23 +273,14 @@ class Annotator.Guest extends Annotator
return confirm "You have selected a very short piece of text: only " + length + " chars. Are you sure you want to highlight this?" return confirm "You have selected a very short piece of text: only " + length + " chars. Are you sure you want to highlight this?"
onSuccessfulSelection: (event, immediate) -> onSuccessfulSelection: (event, immediate) ->
return unless @canAnnotate
if @tool is 'highlight' if @tool is 'highlight'
# Describe the selection with targets
@selectedTargets = (@_getTargetFromSelection(s) for s in event.segments)
# Do we really want to make this selection? # Do we really want to make this selection?
return false unless this.confirmSelection() return false unless this.confirmSelection()
# Describe the selection with targets
# Create the annotation @selectedTargets = (@_getTargetFromSelection(s) for s in event.segments)
annotation = {inject: true} return
this.publish 'beforeAnnotationCreated', annotation super
# Set up the annotation
annotation = this.setupAnnotation annotation
this.publish 'annotationCreated', annotation
else
super
onAnchorMouseover: (event) -> onAnchorMouseover: (event) ->
this.addEmphasis event.data.getAnnotations event this.addEmphasis event.data.getAnnotations event
...@@ -333,18 +328,7 @@ class Annotator.Guest extends Annotator ...@@ -333,18 +328,7 @@ class Annotator.Guest extends Annotator
@element.removeClass markerClass @element.removeClass markerClass
addComment: -> addComment: ->
sel = @selectedTargets # Save the selection this.showEditor(this.createAnnotation())
# Nuke the selection, since we won't be using that.
# We will attach this to the end of the document.
# Our override for setupAnnotation will add that highlight.
@selectedTargets = []
this.onAdderClick() # Open editor (with 0 targets)
@selectedTargets = sel # restore the selection
# Is this annotation a comment?
isComment: (annotation) ->
# No targets and no references means that this is a comment.
not (annotation.inject or annotation.references?.length or annotation.target?.length)
# Open the sidebar # Open the sidebar
showFrame: -> showFrame: ->
...@@ -359,10 +343,12 @@ class Annotator.Guest extends Annotator ...@@ -359,10 +343,12 @@ class Annotator.Guest extends Annotator
method: 'addToken' method: 'addToken'
params: token params: token
onAdderMousedown: angular.noop
onAdderClick: (event) => onAdderClick: (event) =>
event?.preventDefault() event.preventDefault()
event.stopPropagation()
@adder.hide() @adder.hide()
@inAdderClick = false
annotation = this.setupAnnotation(this.createAnnotation()) annotation = this.setupAnnotation(this.createAnnotation())
this.showEditor(annotation) this.showEditor(annotation)
......
...@@ -21,6 +21,7 @@ renderFactory = ['$$rAF', ($$rAF) -> ...@@ -21,6 +21,7 @@ renderFactory = ['$$rAF', ($$rAF) ->
class Hypothesis extends Annotator class Hypothesis extends Annotator
events: events:
'beforeAnnotationCreated': 'digest'
'annotationCreated': 'digest' 'annotationCreated': 'digest'
'annotationDeleted': 'digest' 'annotationDeleted': 'digest'
'annotationUpdated': 'digest' 'annotationUpdated': 'digest'
...@@ -30,34 +31,6 @@ class Hypothesis extends Annotator ...@@ -30,34 +31,6 @@ class Hypothesis extends Annotator
options: options:
noDocAccess: true noDocAccess: true
Discovery: {} Discovery: {}
Permissions:
userAuthorize: (action, annotation, user) ->
if annotation.permissions
tokens = annotation.permissions[action] || []
if tokens.length == 0
# Empty or missing tokens array: only admin can perform action.
return false
for token in tokens
if this.userId(user) == token
return true
if token == 'group:__world__'
return true
if token == 'group:__authenticated__' and this.user?
return true
# No tokens matched: action should not be performed.
return false
# Coarse-grained authorization
else if annotation.user
return user and this.userId(user) == this.userId(annotation.user)
# No authorization info on annotation: free-for-all!
true
showEditPermissionsCheckbox: false,
showViewPermissionsCheckbox: false,
Threading: {} Threading: {}
# Internal state # Internal state
...@@ -69,8 +42,10 @@ class Hypothesis extends Annotator ...@@ -69,8 +42,10 @@ class Hypothesis extends Annotator
# Here as a noop just to make the Permissions plugin happy # Here as a noop just to make the Permissions plugin happy
# XXX: Change me when Annotator stops assuming things about viewers # XXX: Change me when Annotator stops assuming things about viewers
editor:
addField: angular.noop
viewer: viewer:
addField: (-> ) addField: angular.noop
this.$inject = ['$document', '$window'] this.$inject = ['$document', '$window']
constructor: ( $document, $window ) -> constructor: ( $document, $window ) ->
...@@ -146,27 +121,6 @@ class Hypothesis extends Annotator ...@@ -146,27 +121,6 @@ class Hypothesis extends Annotator
unless annotation.highlights? unless annotation.highlights?
annotation.highlights = [] annotation.highlights = []
# Register it with the draft service, except when it's an injection
# This is an injection. Delete the marker.
if annotation.inject
# Set permissions for private
permissions = @plugins.Permissions
userId = permissions.options.userId permissions.user
annotation.permissions =
read: [userId]
admin: [userId]
update: [userId]
delete: [userId]
# Set default owner permissions on all annotations
for event in ['beforeAnnotationCreated', 'beforeAnnotationUpdated']
this.subscribe event, (annotation) =>
permissions = @plugins.Permissions
if permissions.user?
userId = permissions.options.userId(permissions.user)
for action, roles of annotation.permissions
unless userId in roles then roles.push userId
_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
...@@ -187,9 +141,7 @@ class Hypothesis extends Annotator ...@@ -187,9 +141,7 @@ class Hypothesis extends Annotator
.bind('back', => .bind('back', =>
# Navigate "back" out of the interface. # Navigate "back" out of the interface.
@element.scope().$apply => @element.scope().$apply => this.hide()
return if @element.scope().ongoingEdit
this.hide()
) )
.bind('open', => .bind('open', =>
...@@ -294,17 +246,15 @@ class Hypothesis extends Annotator ...@@ -294,17 +246,15 @@ class Hypothesis extends Annotator
this this
showEditor: (annotation) -> showEditor: (annotation) ->
scope = @element.scope() delete @element.scope().selectedAnnotations
scope.ongoingEdit = mail.messageContainer(annotation)
delete scope.selectedAnnotations
this.show() this.show()
this this
show: -> show: ->
@element.scope().frame.visible = true @host.notify method: 'showFrame'
hide: -> hide: ->
@element.scope().frame.visible = false @host.notify method: 'hideFrame'
digest: -> digest: ->
@element.scope().$evalAsync angular.noop @element.scope().$evalAsync angular.noop
...@@ -375,18 +325,8 @@ class Hypothesis extends Annotator ...@@ -375,18 +325,8 @@ class Hypothesis extends Annotator
setTool: (name) -> setTool: (name) ->
return if name is @tool return if name is @tool
return unless @element.injector().get('drafts').discard()
if name is 'highlight' if name is 'highlight'
# Check login state first
unless @plugins.Permissions?.user
scope = @element.scope()
# If we are not logged in, start the auth process
scope.ongoingHighlightSwitch = true
@element.injector().get('identity').request()
this.show()
return
this.socialView.name = 'single-player' this.socialView.name = 'single-player'
else else
this.socialView.name = 'none' this.socialView.name = 'none'
...@@ -409,45 +349,47 @@ class Hypothesis extends Annotator ...@@ -409,45 +349,47 @@ class Hypothesis extends Annotator
class DraftProvider class DraftProvider
drafts: null _drafts: null
constructor: -> constructor: ->
this.drafts = [] this._drafts = []
$get: -> this $get: -> this
add: (draft, cb) -> @drafts.push {draft, cb} all: -> draft for {draft} in @_drafts
add: (draft, cb) -> @_drafts.push {draft, cb}
remove: (draft) -> remove: (draft) ->
remove = [] remove = []
for d, i in @drafts for d, i in @_drafts
remove.push i if d.draft is draft remove.push i if d.draft is draft
while remove.length while remove.length
@drafts.splice(remove.pop(), 1) @_drafts.splice(remove.pop(), 1)
contains: (draft) -> contains: (draft) ->
for d in @drafts for d in @_drafts
if d.draft is draft then return true if d.draft is draft then return true
return false return false
isEmpty: -> @drafts.length is 0 isEmpty: -> @_drafts.length is 0
discard: -> discard: ->
text = text =
switch @drafts.length switch @_drafts.length
when 0 then null when 0 then null
when 1 when 1
"""You have an unsaved reply. """You have an unsaved reply.
Do you really want to discard this draft?""" Do you really want to discard this draft?"""
else else
"""You have #{@drafts.length} unsaved replies. """You have #{@_drafts.length} unsaved replies.
Do you really want to discard these drafts?""" Do you really want to discard these drafts?"""
if @drafts.length is 0 or confirm text if @_drafts.length is 0 or confirm text
discarded = @drafts.slice() discarded = @_drafts.slice()
@drafts = [] @_drafts = []
d.cb?() for d in discarded d.cb?() for d in discarded
true true
else else
......
...@@ -9,10 +9,6 @@ ...@@ -9,10 +9,6 @@
color: $link-color; color: $link-color;
} }
.magicontrol.dropdown {
top: 4px;
}
fuzzytime { fuzzytime {
line-height: 2; line-height: 2;
...@@ -35,7 +31,7 @@ ...@@ -35,7 +31,7 @@
} }
.annotation-actions { .annotation-actions {
margin-top: 4px; margin-top: .4em;
} }
.annotation-quote { .annotation-quote {
......
...@@ -4,6 +4,7 @@ describe 'h.directives.annotation', -> ...@@ -4,6 +4,7 @@ describe 'h.directives.annotation', ->
$scope = null $scope = null
annotation = null annotation = null
createController = null createController = null
flash = null
beforeEach module('h.directives') beforeEach module('h.directives')
...@@ -15,11 +16,13 @@ describe 'h.directives.annotation', -> ...@@ -15,11 +16,13 @@ describe 'h.directives.annotation', ->
title: 'A special document' title: 'A special document'
target: [{}] target: [{}]
uri: 'http://example.com' uri: 'http://example.com'
flash = sinon.spy()
createController = -> createController = ->
$controller 'AnnotationController', $controller 'AnnotationController',
$scope: $scope $scope: $scope
annotator: {plugins: {}, publish: sinon.spy()} annotator: {plugins: {}, publish: sinon.spy()}
flash: flash
it 'provides a document title', -> it 'provides a document title', ->
controller = createController() controller = createController()
......
...@@ -7,6 +7,7 @@ describe 'h.directives.thread', -> ...@@ -7,6 +7,7 @@ describe 'h.directives.thread', ->
$element = null $element = null
container = null container = null
createController = null createController = null
flash = null
beforeEach module('h.directives') beforeEach module('h.directives')
...@@ -16,13 +17,14 @@ describe 'h.directives.thread', -> ...@@ -16,13 +17,14 @@ describe 'h.directives.thread', ->
$removeClass: sinon.spy() $removeClass: sinon.spy()
thread: 'thread' thread: 'thread'
$scope = $rootScope.$new() $scope = $rootScope.$new()
flash = sinon.spy()
render = (value, cb) -> cb(value) render = (value, cb) -> cb(value)
createController = -> createController = ->
$scope.thread = mail.messageContainer() $scope.thread = mail.messageContainer()
$scope.thread.message = id: 'foo', uri: 'http://example.com/' $scope.thread.message = id: 'foo', uri: 'http://example.com/'
$element = angular.element('<div thread="thread"><input /></div>') $element = angular.element('<div thread="thread"><input /></div>')
$controller 'ThreadController', {$attrs, $element, $scope, render} $controller 'ThreadController', {$attrs, $element, $scope, flash, render}
afterEach -> afterEach ->
sandbox.restore() sandbox.restore()
......
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