Commit 63c83eab authored by Randall Leeds's avatar Randall Leeds

break apart the annotation directive

Establish privacy and markdown directives. The reason is that
ngModelController uses strict equality when determining that the
parsed view value is different from the model value. Since Annotator
relies on the object identity of annotations to remain stable, it
was impractical to trigger a real view change on an annotation.

With this change, constituent values of the annotations are managed
in their own model controllers to better encapsulate the various
UI controls that make up an annotation.

In addition, some attention was put into cleaning up the templates
and clarifying the interactions with annotator events and the digest
cycle. The result is much better, I think.
parent 495109ea
......@@ -494,31 +494,27 @@ blockquote {
.annotation {
position: relative;
div.body {
@include force-wrap;
clear: both;
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
.upper-left {
font-weight: bold;
text-decoration: underline;
margin-bottom: .25em;
float: left;
.user {
font-weight: 800;
}
textarea.body {
min-height: 8em;
width: 100%;
}
.body {
div {
@include force-wrap;
clear: both;
margin: .25em 0 .5em;
p:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
}
.privacy {
float: right;
textarea {
min-height: 8em;
width: 100%;
}
}
.buttonbar {
......@@ -591,6 +587,10 @@ blockquote {
margin-bottom: 0;
}
.reply-icon {
display: none;
}
.user {
display: run-in;
margin-right: .25em;
......@@ -612,45 +612,26 @@ blockquote {
//MAGICONTROLS////////////////////////////////
.magicontrols {
@include pie-clearfix;
background-color: $bodyBackground;
float: right;
& > * {
@include transition(
opacity 0.1s ease-in-out .25s,
font-size .1s ease-in-out .25s,
margin .1s ease-in-out .25s,
opacity .1s ease-in-out .25s
);
@extend .small;
float: left;
& > .pull-left {
margin-right: 1em;
}
& > .pull-right {
margin-left: 1em;
}
.show {
font-size: 0;
@include transition(
opacity 0 ease-in-out .15s
);
opacity: 0;
}
.detail:hover & {
& > * {
font-size: 1em;
}
.show {
:hover > & {
opacity: 1;
}
.time {
opacity: 0;
text-size: 0;
}
}
}
.reply-count {
.detail .magicontrols & {
display: none;
}
}
......@@ -673,7 +654,9 @@ blockquote {
}
// Things not shown in the summary view
.annotator-controls, .magicontrols .show, .buttonbar {
display: none;
.magicontrols {
.reply-icon, .pull-right {
display: none;
}
}
}
......@@ -181,44 +181,27 @@ class App
class Annotation
this.$inject = [
'$element', '$location', '$scope', '$rootScope', '$timeout',
'annotator', 'drafts'
]
constructor: (
$element, $location, $scope, $rootScope, $timeout
annotator, drafts
) ->
publish_ = (args...) ->
# Publish after a timeout to escape this digest
# Annotator event callbacks don't expect a digest to be active
$timeout (-> annotator.publish args...), 0, false
$scope.privacyLevels = [
{name: 'Public', permissions: { 'read': ['group:__world__'] } },
{name: 'Private', permissions: { 'read': [] } }
]
this.$inject = ['$element', '$location', '$scope', 'annotator', 'drafts']
constructor: ($element, $location, $scope, annotator, drafts) ->
threading = annotator.threading
$scope.cancel = ->
$scope.editing = false
drafts.remove $scope.$modelValue
drafts.remove $scope.model.$modelValue
if $scope.unsaved
publish_ 'annotationDeleted', $scope.$modelValue
annotator.deleteAnnotation $scope.model.$modelValue
$scope.save = ->
$scope.editing = false
$scope.model.$setViewValue $scope.model.$viewValue
drafts.remove $scope.$modelValue
drafts.remove $scope.model.$modelValue
if $scope.unsaved
publish_ 'annotationCreated', $scope.$modelValue
annotator.publish 'annotationCreated', $scope.model.$modelValue
else
publish_ 'annotationUpdated', $scope.$modelValue
annotator.updateAnnotation $scope.model.$modelValue
$scope.reply = ->
unless annotator.plugins.Auth? and annotator.plugins.Auth.haveValidToken()
$rootScope.$broadcast 'showAuth', true
$scope.$emit 'showAuth', true
return
references =
......@@ -230,34 +213,6 @@ class Annotation
reply = angular.extend annotator.createAnnotation(),
thread: references.join '/'
$scope.getPrivacyLevel = (permissions) ->
for level in $scope.privacyLevels
roleSet = {}
# Construct a set (using a key->exist? mapping) of roles for each verb
for verb of permissions
roleSet[verb] = {}
for role in permissions[verb]
roleSet[verb][role] = true
# Check that no (verb, role) is missing from the role set
mismatch = false
for verb of level.permissions
for role in level.permissions[verb]
if roleSet[verb]?[role]?
delete roleSet[verb][role]
else
mismatch = true
break
# Check that no extra (verb, role) is missing from the privacy level
mismatch ||= Object.keys(roleSet[verb]).length
if mismatch then break else return level
# Unrecognized privacy level
name: 'Custom'
value: permissions
annotator.setupAnnotation reply
$scope.$on '$routeChangeStart', -> $scope.cancel() if $scope.editing
......@@ -266,36 +221,28 @@ class Annotation
$scope.$watch 'editing', (newValue) ->
if newValue then $timeout -> $element.find('textarea').focus()
# Check if this is a brand new annotation
if drafts.contains $scope.$modelValue
$scope.editing = true
$scope.unsaved = true
$scope.$watch 'model.$modelValue', (annotation) ->
if annotation?
$scope.thread = threading.getContainer annotation.id
$scope.directChildren = ->
if $scope.$modelValue? and threading.getContainer($scope.$modelValue.id).children?
return threading.getContainer($scope.$modelValue.id).children.length
0
# Check if this is a brand new annotation
if drafts.contains annotation
$scope.editing = true
$scope.allChildren = ->
if $scope.$modelValue? and threading.getContainer($scope.$modelValue.id).flattenChildren()?
return threading.getContainer($scope.$modelValue.id).flattenChildren().length
0
# Change when edit / delete is merged
$scope.unsaved = true
class Editor
this.$inject = ['$location', '$routeParams', '$scope', 'annotator']
constructor: ($location, $routeParams, $scope, annotator) ->
save = ->
$scope.$apply ->
$location.path('/viewer').replace()
annotator.provider.notify method: 'onEditorSubmit'
annotator.provider.notify method: 'onEditorHide'
$location.path('/viewer').search('id', $scope.annotation.id).replace()
annotator.provider.notify method: 'onEditorSubmit'
annotator.provider.notify method: 'onEditorHide'
cancel = ->
$scope.$apply ->
search = $location.search() or {}
delete search.id
$location.path('/viewer').search(search).replace()
annotator.provider.notify method: 'onEditorHide'
$location.path('/viewer').search('id', null).replace()
annotator.provider.notify method: 'onEditorHide'
annotator.subscribe 'annotationCreated', save
annotator.subscribe 'annotationDeleted', cancel
......
annotation = ['$filter', ($filter) ->
compile: (tElement, tAttrs, transclude) ->
# Adjust the ngModel directive to use the isolate scope binding.
# The expression will be bound in the isolate as '$modelValue'.
if tAttrs.ngModel
tAttrs.$set '$modelValue', tAttrs.ngModel, false
tAttrs.$set 'ngModel', '$modelValue', false
post: (scope, iElement, iAttrs, controller) ->
return unless controller
# Bind shift+enter to save
iElement.find('textarea').bind
keydown: (e) ->
if e.keyCode == 13 && e.shiftKey
e.preventDefault()
scope.save()
# Format the annotation for display
controller.$formatters.push (value) ->
return unless angular.isObject value
created: value.created
body: ($filter 'converter') (value.text or '')
text: value.text
user: value.user
privacy: scope.getPrivacyLevel value.permissions
controller.$parsers.push (value) ->
return unless angular.isObject value
if controller.$pristine
controller.$modelValue
else
angular.extend controller.$modelValue,
text: value.text
permissions: value.privacy.permissions
# Publish the controller
scope.model = controller
link: (scope, elem, attrs, controller) ->
return unless controller?
# Bind shift+enter to save
elem.bind
keydown: (e) ->
if e.keyCode == 13 && e.shiftKey
e.preventDefault()
scope.save()
# Publish the controller
scope.model = controller
controller: 'AnnotationController'
priority: 100 # Must run before ngModel
require: '?ngModel'
restrict: 'C'
scope:
$modelValue: '='
scope: {}
templateUrl: 'annotation.html'
]
markdown = ['$filter', ($filter) ->
link: (scope, elem, attrs, controller) ->
return unless controller?
# Format the annotation for display
controller.$formatters.push (value) ->
if scope.readonly
value
else if value
($filter 'converter') value
else
''
# Publish the controller
scope.model = controller
scope.$watch 'readonly', (newValue) ->
if newValue then elem.find('textarea').focus()
require: '?ngModel'
restrict: 'E'
scope:
readonly: '@'
required: '@'
templateUrl: 'markdown.html'
]
privacy = ->
levels = [
{name: 'Public', permissions: { 'read': ['group:__world__'] } },
{name: 'Private', permissions: { 'read': [] } }
]
getLevel = (permissions) ->
return unless permissions?
for level in levels
roleSet = {}
# Construct a set (using a key->exist? mapping) of roles for each verb
for verb of permissions
roleSet[verb] = {}
for role in permissions[verb]
roleSet[verb][role] = true
# Check that no (verb, role) is missing from the role set
mismatch = false
for verb of level.permissions
for role in level.permissions[verb]
if roleSet[verb]?[role]?
delete roleSet[verb][role]
else
mismatch = true
break
# Check that no extra (verb, role) is missing from the privacy level
mismatch ||= Object.keys(roleSet[verb]).length
if mismatch then break else return level
# Unrecognized privacy level
name: 'Custom'
value: permissions
link: (scope, elem, attrs, controller) ->
return unless controller?
controller.$formatters.push getLevel
controller.$parsers.push (privacy) -> privacy?.permissions
scope.model = controller
scope.levels = levels
require: '?ngModel'
restrict: 'E'
scope: true
templateUrl: 'privacy.html'
recursive = ['$compile', '$timeout', ($compile, $timeout) ->
compile: (tElement, tAttrs, transclude) ->
placeholder = angular.element '<!-- recursive -->'
......@@ -144,6 +192,8 @@ thread = ->
angular.module('h.directives', ['ngSanitize'])
.directive('annotation', annotation)
.directive('markdown', markdown)
.directive('privacy', privacy)
.directive('recursive', recursive)
.directive('resettable', resettable)
.directive('tabReveal', tabReveal)
......
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