Commit a1f2b97e authored by Aron Carroll's avatar Aron Carroll

Merge pull request #1436 from hypothesis/view-sort-overhaul

Overhaul view and sort behaviour.
parents d97a4389 e199ff71
...@@ -5,7 +5,6 @@ imports = [ ...@@ -5,7 +5,6 @@ imports = [
'h.controllers' 'h.controllers'
'h.controllers.AccountManagement' 'h.controllers.AccountManagement'
'h.directives' 'h.directives'
'h.directives.annotation'
'h.filters' 'h.filters'
'h.identity' 'h.identity'
'h.streamsearch' 'h.streamsearch'
...@@ -14,39 +13,17 @@ imports = [ ...@@ -14,39 +13,17 @@ imports = [
configure = [ configure = [
'$locationProvider', '$provide', '$routeProvider', '$sceDelegateProvider', '$locationProvider', '$routeProvider', '$sceDelegateProvider',
($locationProvider, $provide, $routeProvider, $sceDelegateProvider) -> ($locationProvider, $routeProvider, $sceDelegateProvider) ->
$locationProvider.html5Mode(true) $locationProvider.html5Mode(true)
# Disable annotating while drafting
$provide.decorator 'drafts', [
'annotator', '$delegate',
(annotator, $delegate) ->
{add, remove} = $delegate
$delegate.add = (draft) ->
add.call $delegate, draft
annotator.disableAnnotating $delegate.isEmpty()
$delegate.remove = (draft) ->
remove.call $delegate, draft
annotator.enableAnnotating $delegate.isEmpty()
$delegate
]
$routeProvider.when '/a/:id', $routeProvider.when '/a/:id',
controller: 'AnnotationViewerController' controller: 'AnnotationViewerController'
templateUrl: 'viewer.html' templateUrl: 'viewer.html'
$routeProvider.when '/editor',
controller: 'EditorController'
templateUrl: 'editor.html'
$routeProvider.when '/viewer', $routeProvider.when '/viewer',
controller: 'ViewerController' controller: 'ViewerController'
templateUrl: 'viewer.html' templateUrl: 'viewer.html'
$routeProvider.when '/page_search', reloadOnSearch: false
controller: 'SearchController'
templateUrl: 'page_search.html'
$routeProvider.when '/stream', $routeProvider.when '/stream',
controller: 'StreamSearchController' controller: 'StreamSearchController'
templateUrl: 'viewer.html' templateUrl: 'viewer.html'
......
This diff is collapsed.
imports = [
'ngSanitize'
'ngTagsInput'
'h.helpers.documentHelpers'
'h.services'
]
formInput = -> formInput = ->
link: (scope, elem, attr, [form, model, validator]) -> link: (scope, elem, attr, [form, model, validator]) ->
return unless form?.$name and model?.$name and validator return unless form?.$name and model?.$name and validator
...@@ -79,7 +87,6 @@ markdown = ['$filter', '$timeout', ($filter, $timeout) -> ...@@ -79,7 +87,6 @@ markdown = ['$filter', '$timeout', ($filter, $timeout) ->
unless readonly then $timeout -> input.focus() unless readonly then $timeout -> input.focus()
require: '?ngModel' require: '?ngModel'
restrict: 'E'
scope: scope:
readonly: '@' readonly: '@'
required: '@' required: '@'
...@@ -131,36 +138,6 @@ privacy = -> ...@@ -131,36 +138,6 @@ privacy = ->
templateUrl: 'privacy.html' templateUrl: 'privacy.html'
recursive = ['$compile', '$timeout', ($compile, $timeout) ->
compile: (tElement, tAttrs, transclude) ->
placeholder = angular.element '<!-- recursive -->'
attachQueue = []
tick = false
template = tElement.contents().clone()
tElement.html ''
transclude = $compile template, (scope, cloneAttachFn) ->
clone = placeholder.clone()
cloneAttachFn clone
$timeout ->
transclude scope, (el, scope) -> attachQueue.push [clone, el]
unless tick
tick = true
requestAnimationFrame ->
tick = false
for [clone, el] in attachQueue
clone.after el
clone.bind '$destroy', -> el.remove()
attachQueue = []
clone
post: (scope, iElement, iAttrs, controller) ->
transclude scope, (contents) -> iElement.append contents
restrict: 'A'
terminal: true
]
tabReveal = ['$parse', ($parse) -> tabReveal = ['$parse', ($parse) ->
compile: (tElement, tAttrs, transclude) -> compile: (tElement, tAttrs, transclude) ->
panes = [] panes = []
...@@ -205,57 +182,6 @@ tabReveal = ['$parse', ($parse) -> ...@@ -205,57 +182,6 @@ tabReveal = ['$parse', ($parse) ->
] ]
thread = ['$rootScope', '$window', ($rootScope, $window) ->
# Helper -- true if selection ends inside the target and is non-empty
ignoreClick = (event) ->
sel = $window.getSelection()
if sel.focusNode?.compareDocumentPosition(event.target) & 8
if sel.toString().length
return true
return false
link: (scope, elem, attr, ctrl) ->
childrenEditing = {}
# If this is supposed to be focused, then open it
if scope.annotation in ($rootScope.focused or [])
scope.collapsed = false
scope.$on "focusChange", ->
# XXX: This not needed to be done when the viewer and search will be unified
ann = scope.annotation ? scope.thread.message
if ann in $rootScope.focused
scope.collapsed = false
else
unless ann.references?.length
scope.collapsed = true
scope.toggleCollapsed = (event) ->
event.stopPropagation()
return if (ignoreClick event) or Object.keys(childrenEditing).length
scope.collapsed = !scope.collapsed
# XXX: This not needed to be done when the viewer and search will be unified
ann = scope.annotation ? scope.thread.message
if scope.collapsed
$rootScope.unFocus ann, true
else
scope.openDetails ann
$rootScope.focus ann, true
scope.$on 'toggleEditing', (event) ->
{$id, editing} = event.targetScope
if editing
scope.collapsed = false
unless childrenEditing[$id]
event.targetScope.$on '$destroy', ->
delete childrenEditing[$id]
childrenEditing[$id] = true
else
delete childrenEditing[$id]
restrict: 'C'
]
# TODO: Move this behaviour to a route. # TODO: Move this behaviour to a route.
showAccount = -> showAccount = ->
restrict: 'A' restrict: 'A'
...@@ -298,8 +224,9 @@ repeatAnim = -> ...@@ -298,8 +224,9 @@ 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')
scope.uclick = (event) -> scope.uclick = (event) ->
event.preventDefault() event.preventDefault()
...@@ -312,50 +239,6 @@ username = ['$filter', '$window', ($filter, $window) -> ...@@ -312,50 +239,6 @@ username = ['$filter', '$window', ($filter, $window) ->
template: '<span class="user" ng-click="uclick($event)">{{uname}}</span>' template: '<span class="user" ng-click="uclick($event)">{{uname}}</span>'
] ]
fuzzytime = ['$filter', '$window', ($filter, $window) ->
link: (scope, elem, attr, ctrl) ->
return unless ctrl?
elem
.find('a')
.bind 'click', (event) ->
event.stopPropagation()
ctrl.$render = ->
scope.ftime = ($filter 'fuzzyTime') ctrl.$viewValue
# Determining the timezone name
timezone = jstz.determine().name()
# The browser language
userLang = navigator.language || navigator.userLanguage
# Now to make a localized hint date, set the language
momentDate = moment ctrl.$viewValue
momentDate.lang(userLang)
# Try to localize to the browser's timezone
try
scope.hint = momentDate.tz(timezone).format('LLLL')
catch error
# For invalid timezone, use the default
scope.hint = momentDate.format('LLLL')
timefunct = ->
$window.setInterval =>
scope.ftime = ($filter 'fuzzyTime') ctrl.$viewValue
scope.$digest()
, 5000
scope.timer = timefunct()
scope.$on '$destroy', ->
$window.clearInterval scope.timer
require: '?ngModel'
restrict: 'E'
scope: true
template: '<a target="_blank" href="{{shared_link}}" title="{{hint}}">{{ftime | date:mediumDate}}</a>'
]
whenscrolled = -> whenscrolled = ->
link: (scope, elem, attr) -> link: (scope, elem, attr) ->
...@@ -378,15 +261,12 @@ match = -> ...@@ -378,15 +261,12 @@ match = ->
require: 'ngModel' require: 'ngModel'
angular.module('h.directives', ['ngSanitize', 'ngTagsInput']) angular.module('h.directives', imports)
.directive('formInput', formInput) .directive('formInput', formInput)
.directive('formValidate', formValidate) .directive('formValidate', formValidate)
.directive('fuzzytime', fuzzytime)
.directive('markdown', markdown) .directive('markdown', markdown)
.directive('privacy', privacy) .directive('privacy', privacy)
.directive('recursive', recursive)
.directive('tabReveal', tabReveal) .directive('tabReveal', tabReveal)
.directive('thread', thread)
.directive('username', username) .directive('username', username)
.directive('showAccount', showAccount) .directive('showAccount', showAccount)
.directive('repeatAnim', repeatAnim) .directive('repeatAnim', repeatAnim)
......
###*
# @ngdoc type
# @name deepCount.DeepCountController
#
# @description
# `DeepCountController` exports a single getter / setter that can be used
# to retrieve and manipulate a set of counters. Changes to these counters
# are bubbled and aggregated by any instances higher up in the DOM. Digests
# are performed from the top down, scheduled during animation frames, and
# debounced for performance.
###
DeepCountController = [
'$element', '$scope', 'render',
($element, $scope, render) ->
cancelFrame = null
counters = {}
parent = $element.parent().controller('deepCount')
###*
# @ngdoc method
# @name deepCount.DeepCountController#count
#
# @param {string} key An aggregate key.
# @param {number} delta If provided, the amount by which the aggregate
# for the given key should be incremented.
# @return {number} The value of the aggregate for the given key.
#
# @description
# Modify an aggregate when called with an argument and return its current
# value.
###
this.count = (key, delta) ->
if delta is undefined or delta is 0 then return counters[key] or 0
counters[key] ?= 0
counters[key] += delta
unless counters[key] then delete counters[key]
if parent
# Bubble updates.
parent.count key, delta
else
# Debounce digests from the top.
if cancelFrame then cancelFrame()
cancelFrame = render ->
$scope.$digest()
cancelFrame = null
counters[key] or 0
this
]
###*
# @ngdoc directive
# @name deepCount
# @restrict A
# @description
# Directive that instantiates
# {@link deepCount.DeepCountController DeepCountController} and exports it
# to the current scope under the name specified by the attribute parameter.
###
deepCount = [
'$parse',
($parse) ->
controller: 'DeepCountController'
link: (scope, elem, attrs, ctrl) ->
parsedCounterName = $parse attrs.deepCount
if parsedCounterName.assign
parsedCounterName.assign scope, angular.bind ctrl, ctrl.count
]
angular.module('h.directives')
.controller('DeepCountController', DeepCountController)
.directive('deepCount', deepCount)
simpleSearch = ['$parse', ($parse) -> simpleSearch = ['$parse', ($parse) ->
uuid = 0 uuid = 0
link: (scope, elem, attr, ctrl) -> link: (scope, elem, attr, ctrl) ->
_search = $parse(attr.onsearch)
_clear = $parse(attr.onclear)
scope.viewId = uuid++ scope.viewId = uuid++
scope.dosearch = ->
_search(scope, {"this": scope.searchtext})
scope.reset = (event) -> scope.reset = (event) ->
event.preventDefault() event.preventDefault()
scope.searchtext = '' scope.query = ''
_clear(scope) if attr.onclear
scope.search = (event) ->
event.preventDefault()
scope.query = scope.searchtext
scope.$watch attr.query, (query) -> scope.$watch 'query', (query) ->
if query? return if query is undefined
scope.searchtext = query scope.searchtext = query
_search(scope, {"this": scope.searchtext}) if query
scope.onSearch?(query: scope.searchtext)
else
scope.onClear?()
restrict: 'C' restrict: 'C'
scope:
query: '='
onSearch: '&'
onClear: '&'
template: ''' template: '''
<form class="simple-search-form" ng-class="!searchtext && 'simple-search-inactive'" name="searchBox" ng-submit="dosearch()"> <form class="simple-search-form" ng-class="!searchtext && 'simple-search-inactive'" name="searchBox" ng-submit="search($event)">
<input id="simple-search-{{viewId}}" class="simple-search-input" type="text" ng-model="searchtext" name="searchText" placeholder="Search…" /> <input id="simple-search-{{viewId}}" class="simple-search-input" type="text" ng-model="searchtext" name="searchText" placeholder="Search…" />
<label for="simple-search-{{viewId}}" class="simple-search-icon icon-search"></label> <label for="simple-search-{{viewId}}" class="simple-search-icon icon-search"></label>
<button class="simple-search-clear" type="reset" ng-hide="!searchtext" ng-click="reset($event)"> <button class="simple-search-clear" type="reset" ng-hide="!searchtext" ng-click="reset($event)">
......
### global -COLLAPSED_CLASS ###
COLLAPSED_CLASS = 'thread-collapsed'
###*
# @ngdoc type
# @name thread.ThreadController
#
# @property {Object} container The thread domain model. An instance of
# `mail.messageContainer`.
# @property {boolean} collapsed True if the thread is collapsed.
#
# @description
# `ThreadController` provides an API for the thread directive controlling
# the collapsing behavior.
###
ThreadController = [
->
@container = null
@collapsed = false
###*
# @ngdoc method
# @name thread.ThreadController#toggleCollapsed
# @description
# Toggle the collapsed property.
###
this.toggleCollapsed = ->
@collapsed = not @collapsed
this
]
###*
# @ngdoc directive
# @name thread
# @restrict A
# @description
# Directive that instantiates {@link thread.ThreadController ThreadController}.
#
# If the `thread-collapsed` attribute is specified, it is treated as an
# expression to watch in the context of the current scope that controls
# the collapsed state of the thread.
###
thread = [
'$parse', '$window', 'render',
($parse, $window, render) ->
linkFn = (scope, elem, attrs, [ctrl, counter]) ->
# Toggle collapse on click.
elem.on 'click', (event) ->
event.stopPropagation()
# Ignore if the target scope has been destroyed.
# Prevents collapsing when e.g. a child is deleted by a click event.
if angular.element(event.target).scope() is undefined
return
# Ignore if the user just created a non-empty selection.
sel = $window.getSelection()
if sel.containsNode(event.target, true) and sel.toString().length
return
# Ignore if the user clicked a link
if event.target.tagName in ['A', 'INPUT']
return unless angular.element(event.target).hasClass 'reply-count'
# Ignore a collapse if edit interactions are present in the view.
if counter?.count('edit') > 0 and not ctrl.collapsed
return
scope.$evalAsync ->
ctrl.toggleCollapsed()
# Queue a render frame to complete the binding and show the element.
render ->
ctrl.container = $parse(attrs.thread)(scope)
counter.count 'message', 1
scope.$digest()
scope.$on '$destroy', -> counter.count 'message', -1
# Add and remove the collapsed class when the collapsed property changes.
scope.$watch (-> ctrl.collapsed), (collapsed) ->
if collapsed
attrs.$addClass COLLAPSED_CLASS
else
attrs.$removeClass COLLAPSED_CLASS
# Watch the thread-collapsed attribute.
if attrs.threadCollapsed
scope.$watch $parse(attrs.threadCollapsed), (collapsed) ->
ctrl.toggleCollapsed() if !!collapsed != ctrl.collapsed
controller: 'ThreadController'
controllerAs: 'vm'
link: linkFn
require: ['thread', '?^deepCount']
scope: true
]
angular.module('h.directives')
.controller('ThreadController', ThreadController)
.directive('thread', thread)
###*
# @ngdoc type
# @name threadFilter.ThreadFilterController
#
# @property {boolean} match True if the last checked message was a match.
#
# @description
# `ThreadFilterController` provides an API for maintaining filtering over
# a message thread.
###
ThreadFilterController = [
'viewFilter'
(viewFilter) ->
@match = false
@_active = false
@_children = []
@_filters = null
@_frozen = false
###*
# @ngdoc method
# @name threadFilter.ThreadFilterController#active
#
# @param {boolean=} active New state
# @return {boolean} state
#
# @description
# This method is a getter / setter.
#
# Activate or deactivate filtering when called with an argument and
# return the activation status.
###
this.active = (active) ->
if active is undefined then return @_active
else if @active == active then return @_active
else
child.active active for child in @_children
if @_frozen then @_active else @_active = active
###*
# @ngdoc method
# @name threadFilter.ThreadFilterController#filters
#
# @param {Object=} filters New filters
# @return {Object} filters
#
# @description
# This method is a getter / setter.
#
# Set the filter configuration when called with an argument and return
# return the configuration.
###
this.filters = (filters) ->
if filters is undefined then return @_filters
else if @filters == filters then return @_filters
else
child.filters filters for child in @_children
if @_frozen then @_filters else @_filters = filters
###*
# @ngdoc method
# @name threadFilter.ThreadFilterController#freeze
#
# @param {boolean=} frozen New state
# @return {boolean} frozen state
#
# @description
# This method is a getter / setter.
#
# Freeze or unfreeze the filter when called with an argument and
# return the frozen state. A frozen filter will not change its activation
# state or filter configuration.
###
this.freeze = (frozen=true) ->
if frozen? then @_frozen = frozen else @_frozen
###*
# @ngdoc method
# @name threadFilter.ThreadFilterController#check
#
# @param {Object} container The `mail.messageContainer` to filter.
# @return {boolean} True if the message matches the filters, it has not
# yet been saved, or if filtering is not active.
#
# @description
# Check whether a message container carries a message matching the
# configured filters. If filtering is not active then the result is
# always `true`. Updates the `match` property to reflect the result.
###
this.check = (container) ->
unless container?.message then return false
if this.active()
@match = !!viewFilter.filter([container.message], @_filters).length
else
@match = true
###*
# @ngdoc method
# @name threadFilter.ThreadFilterController#registerChild
#
# @param {Object} target The child controller instance.
#
# @description
# Add another instance of the thread filter controller to the set of
# registered children. Changes in activation status and filter configuration
# are propagated to child controllers.
###
this.registerChild = (target) ->
@_children.push target
###*
# @ngdoc method
# @name threadFilter.ThreadFilterController#unregisterChild
#
# @param {Object} target The child controller instance.
#
# @description
# Remove a previously registered instance of the thread filter controller
# from the set of registered children.
###
this.unregisterChild = (target) ->
@_children = (child for child in @_children if child isnt target)
this
]
###*
# @ngdoc directive
# @name threadFilter
# @restrict A
# @description
# Directive that instantiates
# {@link threadFilter.ThreadFilterController ThreadController}.
#
# The threadFilter directive utilizes the {@link searchfilter searchfilter}
# service to parse the expression passed in the directive attribute as a
# faceted search query and configures its controller with the resulting
# filters. It watches the `match` property of the controller and updates
# its thread's message count under the 'filter' key.
###
threadFilter = [
'$parse', 'searchfilter'
($parse, searchfilter) ->
linkFn = (scope, elem, attrs, [ctrl, thread, counter]) ->
if counter?
scope.$watch (-> ctrl.match), (match, old) ->
if match and not old
counter.count 'match', 1
else if old
counter.count 'match', -1
scope.$on '$destroy', ->
if ctrl.match then counter.count 'match', -1
if parentCtrl = elem.parent().controller('threadFilter')
ctrl.filters parentCtrl.filters()
ctrl.active parentCtrl.active()
parentCtrl.registerChild ctrl
scope.$on '$destroy', -> parentCtrl.unregisterChild ctrl
else
scope.$watch $parse(attrs.threadFilter), (query) ->
unless query then return ctrl.active false
filters = searchfilter.generateFacetedFilter(query)
ctrl.filters filters
ctrl.active true
controller: 'ThreadFilterController'
controllerAs: 'threadFilter'
link: linkFn
require: ['threadFilter', 'thread', '?^deepCount']
]
angular.module('h.directives')
.controller('ThreadFilterController', ThreadFilterController)
.directive('threadFilter', threadFilter)
...@@ -40,6 +40,25 @@ fuzzyTime = (date) -> ...@@ -40,6 +40,25 @@ fuzzyTime = (date) ->
fuzzy = Math.round(delta / year) + ' years ago' fuzzy = Math.round(delta / year) + ' years ago'
fuzzy fuzzy
momentFilter = ->
(value, format) ->
# Determine the timezone name and browser language.
timezone = jstz.determine().name()
userLang = navigator.language || navigator.userLanguage
# Now make a localized date and set the language.
momentDate = moment value
momentDate.lang userLang
# Try to localize to the browser's timezone.
try
momentDate.tz(timezone).format('LLLL')
catch error
# For an invalid timezone, use the default.
momentDate.format('LLLL')
persona = (user, part='username') -> persona = (user, part='username') ->
part = ['term', 'username', 'provider'].indexOf(part) part = ['term', 'username', 'provider'].indexOf(part)
(user?.match /^acct:([^@]+)@(.+)/)?[part] (user?.match /^acct:([^@]+)@(.+)/)?[part]
...@@ -54,5 +73,6 @@ elide = (text, split_length) -> ...@@ -54,5 +73,6 @@ elide = (text, split_length) ->
angular.module('h.filters', []) angular.module('h.filters', [])
.filter('converter', -> (new Converter()).makeHtml) .filter('converter', -> (new Converter()).makeHtml)
.filter('fuzzyTime', -> fuzzyTime) .filter('fuzzyTime', -> fuzzyTime)
.filter('moment', momentFilter)
.filter('persona', -> persona) .filter('persona', -> persona)
.filter('elide', -> elide) .filter('elide', -> elide)
This diff is collapsed.
...@@ -43,29 +43,17 @@ class Annotator.Host extends Annotator.Guest ...@@ -43,29 +43,17 @@ class Annotator.Host extends Annotator.Guest
channel channel
.bind('showFrame', (ctx, routeName) => .bind('showFrame', (ctx) =>
unless @drag.enabled unless @drag.enabled
@frame.css 'margin-left': "#{-1 * @frame.width()}px" @frame.css 'margin-left': "#{-1 * @frame.width()}px"
@frame.removeClass 'annotator-no-transition' @frame.removeClass 'annotator-no-transition'
@frame.removeClass 'annotator-collapsed' @frame.removeClass 'annotator-collapsed'
switch routeName
when 'editor'
this.publish 'annotationEditorShown'
when 'viewer'
this.publish 'annotationViewerShown'
) )
.bind('hideFrame', (ctx, routeName) => .bind('hideFrame', (ctx) =>
@frame.css 'margin-left': '' @frame.css 'margin-left': ''
@frame.removeClass 'annotator-no-transition' @frame.removeClass 'annotator-no-transition'
@frame.addClass 'annotator-collapsed' @frame.addClass 'annotator-collapsed'
switch routeName
when 'editor'
this.publish 'annotationEditorHidden'
when 'viewer'
this.publish 'annotationViewerHidden'
) )
.bind('dragFrame', (ctx, screenX) => this._dragUpdate screenX) .bind('dragFrame', (ctx, screenX) => this._dragUpdate screenX)
......
...@@ -9,7 +9,6 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin ...@@ -9,7 +9,6 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
'annotationUpdated': 'annotationUpdated' 'annotationUpdated': 'annotationUpdated'
'annotationDeleted': 'annotationDeleted' 'annotationDeleted': 'annotationDeleted'
'annotationsLoaded': 'annotationsLoaded' 'annotationsLoaded': 'annotationsLoaded'
'enableAnnotating': 'enableAnnotating'
# Helper method for merging info from a remote target # Helper method for merging info from a remote target
@_mergeTarget: (local, remote, gateway) => @_mergeTarget: (local, remote, gateway) =>
...@@ -189,10 +188,6 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin ...@@ -189,10 +188,6 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
channel = Channel.build(options) channel = Channel.build(options)
## Remote method call bindings ## Remote method call bindings
.bind('setupAnnotation', (txn, annotation) =>
this._format (@annotator.setupAnnotation (this._parse annotation))
)
.bind('beforeCreateAnnotation', (txn, annotation) => .bind('beforeCreateAnnotation', (txn, annotation) =>
annotation = this._parse annotation annotation = this._parse annotation
delete @cache[annotation.$$tag] delete @cache[annotation.$$tag]
...@@ -226,29 +221,14 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin ...@@ -226,29 +221,14 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
res res
) )
## Notifications .bind('sync', (ctx, annotations) =>
.bind('loadAnnotations', (txn, annotations) => (this._format (this._parse a) for a in annotations)
# First, parse the existing ones, for any updates
oldOnes = (this._parse a for a in annotations when @cache[a.tag])
# Announce the changes in old annotations
if oldOnes.length
@selfPublish = true
@annotator.publish 'annotationsLoaded', [oldOnes]
delete @selfPublish
# Then collect the new ones
newOnes = (this._parse a for a in annotations when not @cache[a.tag])
if newOnes.length
@annotator.loadAnnotations newOnes
)
.bind('showEditor', (ctx, annotation) =>
@annotator.showEditor (this._parse annotation)
) )
.bind('enableAnnotating', (ctx, state) => ## Notifications
@annotator.enableAnnotating state, false .bind('loadAnnotations', (txn, annotations) =>
annotations = (this._parse a for a in annotations)
@annotator.loadAnnotations annotations
) )
# Send out a beacon to let other frames know to connect to us # Send out a beacon to let other frames know to connect to us
...@@ -288,10 +268,15 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin ...@@ -288,10 +268,15 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
$.when(deferreds...) $.when(deferreds...)
.then (results...) => .then (results...) =>
annotation = {} if Array.isArray(results[0])
for r in results when r isnt null acc = []
$.extend annotation, (this._parse r) foldFn = (_, cur) =>
options.callback? null, annotation (this._parse(a) for a in cur)
else
acc = {}
foldFn = (_, cur) =>
this._parse(cur)
options.callback? null, results.reduce(foldFn, acc)
.fail (failure) => .fail (failure) =>
options.callback? failure options.callback? failure
...@@ -366,14 +351,11 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin ...@@ -366,14 +351,11 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
this this
annotationsLoaded: (annotations) => annotationsLoaded: (annotations) =>
return if @selfPublish annotations = (this._format a for a in annotations when not a.$$tag)
unless annotations.length return unless annotations.length
console.log "Useless call to 'annotationsLoaded()' with an empty list"
console.trace()
return
this._notify this._notify
method: 'loadAnnotations' method: 'loadAnnotations'
params: (this._format a for a in annotations) params: annotations
this this
beforeCreateAnnotation: (annotation, cb) -> beforeCreateAnnotation: (annotation, cb) ->
...@@ -383,13 +365,6 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin ...@@ -383,13 +365,6 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
callback: cb callback: cb
annotation annotation
setupAnnotation: (annotation, cb) ->
this._call
method: 'setupAnnotation'
params: this._format annotation
callback: cb
annotation
createAnnotation: (annotation, cb) -> createAnnotation: (annotation, cb) ->
this._call this._call
method: 'createAnnotation' method: 'createAnnotation'
...@@ -411,13 +386,10 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin ...@@ -411,13 +386,10 @@ class Annotator.Plugin.Bridge extends Annotator.Plugin
callback: cb callback: cb
annotation annotation
showEditor: (annotation) -> sync: (annotations, cb) ->
this._notify annotations = (this._format a for a in annotations)
method: 'showEditor' this._call
params: this._format annotation method: 'sync'
params: annotations
callback: cb
this this
enableAnnotating: (state) ->
this._notify
method: 'enableAnnotating'
params: state
...@@ -58,16 +58,11 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin ...@@ -58,16 +58,11 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
'annotationsLoaded' 'annotationsLoaded'
] ]
for event in events for event in events
if event is 'annotationCreated' @annotator.subscribe event, this._scheduleUpdate
@annotator.subscribe event, =>
this._scheduleUpdate()
else
@annotator.subscribe event, this._scheduleUpdate
@element.on 'click', (event) => @element.on 'click', (event) =>
event.stopPropagation() event.stopPropagation()
@dynamicBucket = true @annotator.showFrame()
@annotator.showViewer "Screen", this._getDynamicBucket()
@element.on 'mouseup', (event) => @element.on 'mouseup', (event) =>
event.stopPropagation() event.stopPropagation()
...@@ -437,7 +432,6 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin ...@@ -437,7 +432,6 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
annotations = @buckets[bucket].slice() annotations = @buckets[bucket].slice()
annotator.selectAnnotations annotations, annotator.selectAnnotations annotations,
(d3.event.ctrlKey or d3.event.metaKey), (d3.event.ctrlKey or d3.event.metaKey),
(annotations.length is 1) # Only focus if there is only one
tabs.exit().remove() tabs.exit().remove()
...@@ -458,7 +452,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin ...@@ -458,7 +452,7 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
if (@buckets[d].length is 0) then 'none' else '' if (@buckets[d].length is 0) then 'none' else ''
if @dynamicBucket if @dynamicBucket
@annotator.updateViewer "Screen", this._getDynamicBucket() @annotator.updateViewer this._getDynamicBucket()
@tabs = tabs @tabs = tabs
...@@ -480,4 +474,4 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin ...@@ -480,4 +474,4 @@ class Annotator.Plugin.Heatmap extends Annotator.Plugin
# Simulate clicking on the comments tab # Simulate clicking on the comments tab
commentClick: => commentClick: =>
@dynamicBucket = false @dynamicBucket = false
annotator.showViewer "Comments", @buckets[@_getCommentBucket()] annotator.showViewer @buckets[@_getCommentBucket()]
class Annotator.Plugin.Threading extends Annotator.Plugin class Annotator.Plugin.Threading extends Annotator.Plugin
# These events maintain the awareness of annotations between the two
# communicating annotators.
events: events:
'beforeAnnotationCreated': 'beforeAnnotationCreated'
'annotationDeleted': 'annotationDeleted' 'annotationDeleted': 'annotationDeleted'
'annotationsLoaded': 'annotationsLoaded' 'annotationsLoaded': 'annotationsLoaded'
'beforeAnnotationCreated': 'beforeAnnotationCreated'
# Cache of annotations which have crossed the bridge for fast, encapsulated root: null
# association of annotations received in arguments to window-local copies.
cache: {}
pluginInit: -> pluginInit: ->
@annotator.threading = mail.messageThread() # Create a root container.
@root = mail.messageContainer()
thread: (annotation) -> # Mix in message thread properties, preserving local overrides.
# Get or create a thread to contain the annotation $.extend(this, mail.messageThread(), thread: this.thread)
thread = (@annotator.threading.getContainer annotation.id)
thread.message = annotation
# Attach the thread to its parent, if any. # TODO: Refactor the jwz API for progressive updates.
if annotation.references?.length # Right now the idTable is wiped when `messageThread.thread()` is called and
prev = annotation.references[annotation.references.length-1] # empty containers are pruned. We want to show empties so that replies attach
@annotator.threading.getContainer(prev).addChild thread # to missing parents and threads can be updates as new data arrives.
thread: (messages) ->
for message in messages
# Get or create a thread to contain the annotation
if message.id
thread = (this.getContainer message.id)
thread.message = message
else
# XXX: relies on outside code to update the idTable if the message
# later acquires an id.
thread = mail.messageContainer(message)
# Expose the thread to the annotation prev = @root
Object.defineProperty annotation, 'thread',
configurable: true
enumerable: false
writable: true
value: thread
# Update the id table references = message.references or []
@annotator.threading.idTable[annotation.id] = thread if typeof(message.references) == 'string'
references = [references]
thread # Build out an ancestry from the root
for reference in references
container = this.getContainer(reference)
unless container.parent? or container.hasDescendant(prev) # no cycles
prev.addChild(container)
prev = container
annotationDeleted: (annotation) => # Attach the thread at its leaf location
parent = annotation.thread.parent unless thread.hasDescendant(prev) # no cycles
annotation.thread.message = null # Break cyclic reference do ->
delete @annotator.threading.idTable[annotation.id] for child in prev.children when child.message is message
delete annotation.thread return # no dupes
if parent? then @annotator.threading.pruneEmpties parent prev.addChild(thread)
annotationsLoaded: (annotations) => this.pruneEmpties(@root)
this.thread a for a in annotations @root
beforeAnnotationCreated: (annotation) => beforeAnnotationCreated: (annotation) =>
# Assign temporary id. Threading relies on the id. this.thread([annotation])
Object.defineProperty annotation, 'id',
configurable: true annotationDeleted: ({id}) =>
enumerable: false container = this.getContainer id
writable: true container.message = null
value: window.btoa Math.random() this.pruneEmpties(@root)
this.thread annotation
annotationsLoaded: (annotations) =>
messages = (@root.flattenChildren() or []).concat(annotations)
this.thread(messages)
...@@ -73,7 +73,7 @@ class SearchFilter ...@@ -73,7 +73,7 @@ class SearchFilter
filter = term.slice 0, term.indexOf ":" filter = term.slice 0, term.indexOf ":"
unless filter? then filter = "" unless filter? then filter = ""
switch filter switch filter
when 'quote' then quote.push term[6..] when 'quote' then quote.push term[6..].toLowerCase()
when 'result' then result.push term[7..] when 'result' then result.push term[7..]
when 'since' when 'since'
# We'll turn this into seconds # We'll turn this into seconds
...@@ -109,44 +109,36 @@ class SearchFilter ...@@ -109,44 +109,36 @@ class SearchFilter
# Time given in year # Time given in year
t = /^(\d+)year$/.exec(time)[1] t = /^(\d+)year$/.exec(time)[1]
since.push t * 60 * 60 * 24 * 365 since.push t * 60 * 60 * 24 * 365
when 'tag' then tag.push term[4..] when 'tag' then tag.push term[4..].toLowerCase()
when 'text' then text.push term[5..] when 'text' then text.push term[5..].toLowerCase()
when 'uri' then uri.push term[4..] when 'uri' then uri.push term[4..].toLowerCase()
when 'user' then user.push term[5..] when 'user' then user.push term[5..].toLowerCase()
else any.push term else any.push term.toLowerCase()
any: any:
terms: any terms: any
operator: 'and' operator: 'and'
lowercase: true
quote: quote:
terms: quote terms: quote
operator: 'and' operator: 'and'
lowercase: true
result: result:
terms: result terms: result
operator: 'min' operator: 'min'
lowercase: false
since: since:
terms: since terms: since
operator: 'and' operator: 'and'
lowercase: false
tag: tag:
terms: tag terms: tag
operator: 'and' operator: 'and'
lowercase: true
text: text:
terms: text terms: text
operator: 'and' operator: 'and'
lowercase: true
uri: uri:
terms: uri terms: uri
operator: 'or' operator: 'or'
lowercase: true
user: user:
terms: user terms: user
operator: 'or' operator: 'or'
lowercase: true
# This class will process the results of search and generate the correct filter # This class will process the results of search and generate the correct filter
...@@ -235,19 +227,6 @@ class QueryParser ...@@ -235,19 +227,6 @@ class QueryParser
and_or: 'and' and_or: 'and'
fields: ['quote', 'tag', 'text', 'uri', 'user'] fields: ['quote', 'tag', 'text', 'uri', 'user']
parseModels: (models) ->
# Cluster facets together
categories = {}
for searchItem in models
category = searchItem.attributes.category
value = searchItem.attributes.value
if category of categories
categories[category].push value
else
categories[category] = [value]
categories
populateFilter: (filter, query) => populateFilter: (filter, query) =>
# Populate a filter with a query object # Populate a filter with a query object
for category, value of query for category, value of query
......
This diff is collapsed.
...@@ -9,13 +9,18 @@ imports = [ ...@@ -9,13 +9,18 @@ imports = [
class StreamSearch class StreamSearch
this.inject = [ this.inject = [
'$scope', '$rootScope', '$scope', '$rootScope', '$routeParams',
'queryparser', 'searchfilter', 'streamfilter' 'annotator', 'queryparser', 'searchfilter', 'streamfilter'
] ]
constructor: ( constructor: (
$scope, $rootScope, $scope, $rootScope, $routeParams
queryparser, searchfilter, streamfilter annotator, queryparser, searchfilter, streamfilter
) -> ) ->
# Clear out loaded annotations and threads
# XXX: Resolve threading, storage, and updater better for all routes.
annotator.plugins.Threading?.pluginInit()
annotator.plugins.Store?.annotations = []
# Initialize the base filter # Initialize the base filter
streamfilter streamfilter
.resetFilter() .resetFilter()
...@@ -23,18 +28,21 @@ class StreamSearch ...@@ -23,18 +28,21 @@ class StreamSearch
.setPastDataHits(50) .setPastDataHits(50)
# Apply query clauses # Apply query clauses
$scope.search.query = $routeParams.q
terms = searchfilter.generateFacetedFilter $scope.search.query terms = searchfilter.generateFacetedFilter $scope.search.query
queryparser.populateFilter streamfilter, terms queryparser.populateFilter streamfilter, terms
$scope.updater?.then (sock) -> $scope.isEmbedded = false
filter = streamfilter.getFilter() $scope.isStream = true
sock.send(JSON.stringify({filter}))
$scope.sort.name = 'Newest'
$rootScope.annotations = [] $scope.shouldShowThread = (container) -> true
$rootScope.applyView "Document" # Non-sensical, but best for the moment
$rootScope.applySort "Newest"
$scope.openDetails = (annotation) -> $scope.$watch 'updater', (updater) ->
updater?.then (sock) ->
filter = streamfilter.getFilter()
sock.send(JSON.stringify({filter}))
angular.module('h.streamsearch', imports, configure) angular.module('h.streamsearch', imports, configure)
......
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
(function() {
var pfx = ['ms', 'moz', 'webkit', 'o'];
for (var i = 0; i < pfx.length && !window.requestAnimationFrame ; i++) {
requestAnimationFrame = window[pfx[i]+'RequestAnimationFrame'];
cancelAnimationFrame = window[pfx[i]+'CancelRequestAnimationFrame'];
if (!cancelAnimationFrame) {
cancelAnimationFrame = window[pfx[i]+'CancelAnimationFrame'];
}
window.requestAnimationFrame = requestAnimationFrame;
window.cancelAnimationFrame = cancelAnimationFrame;
}
if (!window.requestAnimationFrame)
var lastTime = 0;
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}())
//ANNOTATION//////////////////////////////// //ANNOTATION////////////////////////////////
//This is for everything that is formatted as an annotation. //This is for everything that is formatted as an annotation.
.annotation { .annotation {
@include pie-clearfix;
font-family: $sans-font-family; font-family: $sans-font-family;
font-weight: 300; font-weight: 300;
position: relative; position: relative;
&:hover fuzzytime a, &:hover .reply-count { &:hover .annotation-timestamp, &:hover .reply-count {
color: $link-color; color: $link-color;
} }
.magicontrol.dropdown { .annotation-timestamp {
top: 4px;
}
fuzzytime {
line-height: 2; line-height: 2;
a {
color: $text-color;
&:hover { color: $link-color-hover; }
&:focus { outline: 0; }
}
}
.reply-count {
color: $text-color; color: $text-color;
&:hover { color: $link-color-hover; } &:hover { color: $link-color-hover; }
&:focus { outline: 0; } &:focus { outline: 0; }
} }
.annotation-header { margin-bottom: .8em }
.annotation-section { margin: .8em 0 }
.annotation-footer { margin-top: .8em }
.user { .user {
font-weight: bold; font-weight: bold;
font-size: 1.1em; font-size: 1.1em;
...@@ -39,80 +29,37 @@ ...@@ -39,80 +29,37 @@
text-decoration: underline; text-decoration: underline;
} }
} }
.buttonbar {
@include pie-clearfix;
margin: .25em 0;
.btn {
margin-right: .5em;
}
}
.tip {
@extend .small;
float: right;
}
.quote {
font-style: italic
}
}
// Temporary hack until sidebar and displayer are unified
.annotation-displayer {
.user {
&:hover {
color: $link-color-hover;
cursor: pointer;
text-decoration: underline;
}
}
}
//EXCERPT////////////////////////////////
.excerpt {
margin-bottom: 1em;
position: relative;
.more, .less {
font-size: .9em;
font-family: $sans-font-family;
font-weight: 300;
display: block;
text-align: right;
}
} }
.annotation-quote { .annotation-quote {
@include quote; @include quote;
} }
.annotation-citation-domain {
color: $gray-light;
font-size: .923em;
}
.icon-markdown { .icon-markdown {
color: $text-color; color: $text-color;
position: absolute; line-height: 1.4;
left: 9em; margin-left: .5em;
line-height: 1.5;
top: 0;
} }
//MAGICONTROL////////////////////////////////
.magicontrol { .magicontrol {
@include transition(opacity); margin-right: .8em;
@include transition-duration(.15s); color: $gray-lighter;
margin: 0 .4em;
opacity: 0;
&.open, :hover > & { &.dropdown {
@include transition-duration(.15s); top: 4px;
opacity: 1; }
.annotation:hover & {
color: $link-color;
} }
} }
.share-dialog { .share-dialog {
display: none;
a { a {
float: left; float: left;
line-height: 1.4; line-height: 1.4;
...@@ -129,13 +76,3 @@ ...@@ -129,13 +76,3 @@
width: 100%; width: 100%;
} }
} }
.stream-list {
& > * {
margin-bottom: .72em;
}
.card-emphasis {
@include box-shadow(6px 6px 8px -2px $gray-light);
}
}
...@@ -150,6 +150,7 @@ $input-border-radius: 2px; ...@@ -150,6 +150,7 @@ $input-border-radius: 2px;
@mixin quote { @mixin quote {
color: $gray; color: $gray;
font-family: $serif-font-family; font-family: $serif-font-family;
font-style: italic;
padding: 0 .615em; padding: 0 .615em;
border-left: 3px solid $gray-lighter; border-left: 3px solid $gray-lighter;
} }
......
...@@ -337,7 +337,6 @@ html { ...@@ -337,7 +337,6 @@ html {
} }
} }
//SEARCH HIGHLIGHTS//////////////////////////////// //SEARCH HIGHLIGHTS////////////////////////////////
.search-hl-active { .search-hl-active {
background: $highlight-color; background: $highlight-color;
...@@ -355,44 +354,3 @@ html { ...@@ -355,44 +354,3 @@ html {
box-shadow:3px 3px 4px #999999; box-shadow:3px 3px 4px #999999;
} }
} }
// View and Sort tabs ////////////////////
.viewsort {
@include single-transition(top, .25s);
@include transition-timing-function(cubic-bezier(0, 1, .55, 1));
width: 100%;
text-align: center;
position: fixed;
top: 29px;
right: 0;
z-index: 4;
&.ng-hide {
@include transition-timing-function(cubic-bezier(1, .55, 0, 1));
top: 0;
display:block!important;
overflow: hidden;
}
}
.viewsort > .dropdown {
@include smallshadow(0);
border-bottom-right-radius: 24px 72px;
border-bottom-left-radius: 24px 72px;
font-family: $sans-font-family;
font-weight: 300;
background: $white;
border: solid 1px $gray-lighter;
padding: 0 10px;
margin: 0 3px;
display: inline-block;
.dropdown-menu {
margin-top: 0;
&:before, &:after {
display: none;
}
}
}
@import 'base';
$thread-padding: 1em; $thread-padding: 1em;
$threadexp-width: .6em; $threadexp-width: .6em;
.stream-list {
& > * {
margin-bottom: .72em;
}
.card-emphasis {
box-shadow: 6px 6px 8px -2px $gray-light;
}
}
.thread-list {
margin-top: 0.5em;
}
.thread { .thread {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
& > * {
@include pie-clearfix;
}
& > ul { & > ul {
padding-left: $thread-padding + .15em; padding-left: $thread-padding + .15em;
margin-left: -$thread-padding; margin-left: -$thread-padding;
} }
.load-more { .reply-count {
@include pie-clearfix; color: $text-color;
font-family: $sans-font-family; &:focus { outline: 0; }
font-weight:bold; }
font-size: .8em;
&:hover .reply-count {
color: $link-color;
&:hover, &:focus {
color: $link-color-hover;
}
} }
.thread { .thread {
border-left: 1px dotted $gray-light; border-left: 1px dotted $gray-light;
height: 100%;
padding: 0; padding: 0;
padding-left: $thread-padding; padding-left: $thread-padding;
&.collapsed { &.thread-collapsed {
border-color: transparent; border-color: transparent;
& > .annotation { & > article markdown {
.body { display: none;
display: none;
}
.magicontrol {
display: none;
}
.reply-count {
font-style: italic;
}
} }
} }
} }
...@@ -73,43 +76,32 @@ $threadexp-width: .6em; ...@@ -73,43 +76,32 @@ $threadexp-width: .6em;
} }
} }
.annotation { &.thread-collapsed {
&.squished {
padding-left: 0;
}
}
&.collapsed {
&:hover { &:hover {
background-color: $gray-lightest; background-color: $gray-lightest;
} }
& > ul { & > ul {
max-height: 0; display: none;
overflow: hidden;
} }
& > .annotation { & > .thread-message {
markdown > div > * { .styled-text > * { display: none }
display: none; .styled-text *:first-child {
}
markdown > div > *:first-child {
display: block; display: block;
margin: 0;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
-o-text-overflow: ellipsis; -o-text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.indicators { .thread &, .thread & header, .thread & section { margin: 0 }
margin-right: .25em; .thread & footer { display: none }
}
} }
} }
} }
.annotation-citation-domain { .thread-message {
color: $gray-light; margin-bottom: .8em;
font-size: .923em;
} }
...@@ -40,7 +40,6 @@ module.exports = function(config) { ...@@ -40,7 +40,6 @@ module.exports = function(config) {
'h/static/scripts/vendor/moment-timezone.js', 'h/static/scripts/vendor/moment-timezone.js',
'h/static/scripts/vendor/moment-timezone-data.js', 'h/static/scripts/vendor/moment-timezone-data.js',
'h/static/scripts/vendor/Markdown.Converter.js', 'h/static/scripts/vendor/Markdown.Converter.js',
'h/static/scripts/vendor/polyfills/raf.js',
'h/static/scripts/vendor/sockjs-0.3.4.js', 'h/static/scripts/vendor/sockjs-0.3.4.js',
'h/static/scripts/vendor/uuid.js', 'h/static/scripts/vendor/uuid.js',
'h/static/scripts/hypothesis-auth.js', 'h/static/scripts/hypothesis-auth.js',
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
"karma-ng-html2js-preprocessor": "^0.1.0", "karma-ng-html2js-preprocessor": "^0.1.0",
"karma-phantomjs-launcher": "^0.1.4", "karma-phantomjs-launcher": "^0.1.4",
"mocha": "^1.20.1", "mocha": "^1.20.1",
"phantomjs": "1.9.7-10" "phantomjs": "^1.9.7"
}, },
"engines": { "engines": {
"node": "0.10.x" "node": "0.10.x"
......
assert = chai.assert assert = chai.assert
sandbox = sinon.sandbox.create()
describe 'h.directives.annotation', -> describe 'h.directives.annotation', ->
$scope = null $scope = null
annotator = null
annotation = null annotation = null
createController = null createController = null
flash = null
beforeEach module('h.directives.annotation') beforeEach module('h.directives')
beforeEach inject ($controller, $rootScope) -> beforeEach inject ($controller, $rootScope) ->
$scope = $rootScope.$new() $scope = $rootScope.$new()
$scope.model = $scope.annotationGet = (locals) -> annotation
annotator = {plugins: {}, publish: sandbox.spy()}
annotation =
id: 'deadbeef'
document: document:
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
$element: null annotator: annotator
$location: {} flash: flash
$rootScope: $rootScope
$sce: null afterEach ->
$timeout: sinon.spy() sandbox.restore()
$window: {}
annotator: {plugins: {}}
documentHelpers: {baseURI: '', absoluteURI: sinon.spy()}
drafts: null
it 'provides a document title', -> it 'provides a document title', ->
controller = createController() controller = createController()
assert.equal($scope.document.title, 'A special document') $scope.$digest()
assert.equal(controller.document.title, 'A special document')
it 'truncates long titles', -> it 'truncates long titles', ->
$scope.model.document.title = '''A very very very long title that really annotation.document.title = '''A very very very long title that really
shouldn't be found on a page on the internet.''' shouldn't be found on a page on the internet.'''
controller = createController() controller = createController()
assert.equal($scope.document.title, 'A very very very long title th…') $scope.$digest()
assert.equal(controller.document.title, 'A very very very long title th…')
it 'provides a document uri', -> it 'provides a document uri', ->
controller = createController() controller = createController()
assert.equal($scope.document.uri, 'http://example.com') $scope.$digest()
assert.equal(controller.document.uri, 'http://example.com')
it 'provides an extracted domain from the uri', -> it 'provides an extracted domain from the uri', ->
controller = createController() controller = createController()
assert.equal($scope.document.domain, 'example.com') $scope.$digest()
assert.equal(controller.document.domain, 'example.com')
it 'uses the domain for the title if the title is not present', -> it 'uses the domain for the title if the title is not present', ->
delete $scope.model.document.title delete annotation.document.title
controller = createController() controller = createController()
assert.equal($scope.document.title, 'example.com') $scope.$digest()
assert.equal(controller.document.title, 'example.com')
it 'skips the document object if no document is present on the annotation', -> it 'skips the document object if no document is present on the annotation', ->
delete $scope.model.document delete annotation.document
controller = createController() controller = createController()
assert.isUndefined($scope.document) $scope.$digest()
assert.isNull(controller.document)
it 'skips the document object if the annotation has no targets', -> it 'skips the document object if the annotation has no targets', ->
$scope.model.target = [] annotation.target = []
controller = createController() controller = createController()
assert.isUndefined($scope.document) $scope.$digest()
assert.isNull(controller.document)
describe '#reply', ->
controller = null
container = null
beforeEach ->
controller = createController()
it 'creates a new reply with the proper uri and references', ->
controller.reply()
match = sinon.match {references: [annotation.id], uri: annotation.uri}
assert.calledWith(annotator.publish, 'beforeAnnotationCreated', match)
describe '#toggleShared', ->
it 'sets the shared property', ->
controller = createController()
before = controller.shared
controller.toggleShared()
after = controller.shared
assert.equal(before, !after)
...@@ -4,6 +4,7 @@ describe 'h.directives', -> ...@@ -4,6 +4,7 @@ describe 'h.directives', ->
$scope = null $scope = null
$compile = null $compile = null
fakeWindow = null fakeWindow = null
isolate = null
beforeEach module('h.directives') beforeEach module('h.directives')
...@@ -20,23 +21,24 @@ describe 'h.directives', -> ...@@ -20,23 +21,24 @@ describe 'h.directives', ->
template= ''' template= '''
<div class="simpleSearch" <div class="simpleSearch"
query="query" query="query"
onsearch="update(this)" on-search="update(query)"
onclear="clear()"> on-clear="clear()">
</div> </div>
''' '''
$element = $compile(angular.element(template))($scope) $element = $compile(angular.element(template))($scope)
$scope.$digest() $scope.$digest()
isolate = $element.isolateScope()
it 'updates the search-bar', -> it 'updates the search-bar', ->
$scope.query = "Test query" $scope.query = "Test query"
$scope.$digest() $scope.$digest()
assert.equal($scope.searchtext, $scope.query) assert.equal(isolate.searchtext, $scope.query)
it 'calls the given search function', -> it 'calls the given search function', ->
$scope.query = "Test query" isolate.searchtext = "Test query"
$scope.$digest() isolate.$digest()
$element.triggerHandler('submit') $element.find('form').triggerHandler('submit')
sinon.assert.calledWith($scope.update, "Test query") sinon.assert.calledWith($scope.update, "Test query")
it 'calls the given clear function', -> it 'calls the given clear function', ->
...@@ -44,10 +46,18 @@ describe 'h.directives', -> ...@@ -44,10 +46,18 @@ describe 'h.directives', ->
assert($scope.clear.called) assert($scope.clear.called)
it 'clears the search-bar', -> it 'clears the search-bar', ->
isolate.searchtext = "Test query"
isolate.$digest()
$element.find('.simple-search-clear').click()
assert.equal(isolate.searchtext, '')
it 'invokes callbacks when the input model changes', ->
$scope.query = "Test query" $scope.query = "Test query"
$scope.$digest() $scope.$digest()
$element.find('.simple-search-clear').click() sinon.assert.calledOnce($scope.update)
assert.equal($scope.searchtext, '') $scope.query = ""
$scope.$digest()
sinon.assert.calledOnce($scope.clear)
it 'adds a class to the form when there is no input value', -> it 'adds a class to the form when there is no input value', ->
$form = $element.find('.simple-search-form') $form = $element.find('.simple-search-form')
......
assert = chai.assert
describe 'h.directives.thread', ->
$attrs = null
$scope = null
$element = null
container = null
createController = null
flash = null
beforeEach module('h.directives')
beforeEach inject ($controller, $rootScope) ->
$scope = $rootScope.$new()
flash = sinon.spy()
createController = ->
controller = $controller 'ThreadController'
controller
describe '#toggleCollapsed', ->
it 'sets the collapsed property', ->
controller = createController()
before = controller.collapsed
controller.toggleCollapsed()
after = controller.collapsed
assert.equal(before, !after)
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