Commit 853d6e8a authored by Sean Hammond's avatar Sean Hammond

Merge branch '1755-handle-internal-server-error-in-registration-form' of...

Merge branch '1755-handle-internal-server-error-in-registration-form' of github.com:hypothesis/h into 1755-handle-internal-server-error-in-registration-form

Conflicts:
	h/static/scripts/account/auth-controller.coffee
parents 819260eb 83413b9e
class AccountController
@inject = [ '$scope', '$filter',
'auth', 'flash', 'formHelpers', 'identity', 'session']
'auth', 'flash', 'formRespond', 'identity', 'session']
constructor: ($scope, $filter,
auth, flash, formHelpers, identity, session) ->
auth, flash, formRespond, identity, session) ->
persona_filter = $filter('persona')
$scope.subscriptionDescription =
reply: 'Someone replies to one of my annotations'
......@@ -24,7 +24,7 @@ class AccountController
onError = (form, response) ->
if response.status >= 400 and response.status < 500
formHelpers.applyValidationErrors(form, response.data.errors)
formRespond(form, response.data.errors)
else
if response.data.flash
for own type, msgs of response.data.flash
......@@ -62,7 +62,7 @@ class AccountController
promise.$promise.then(successHandler, errorHandler)
$scope.submit = (form) ->
formHelpers.applyValidationErrors(form)
formRespond(form)
return unless form.$valid
username = persona_filter auth.user
......
class AuthController
this.$inject = ['$scope', '$timeout', 'flash', 'session', 'formHelpers']
constructor: ( $scope, $timeout, flash, session, formHelpers ) ->
this.$inject = ['$scope', '$timeout', 'flash', 'session', 'formRespond']
constructor: ( $scope, $timeout, flash, session, formRespond ) ->
timeout = null
success = (data) ->
......@@ -23,10 +23,10 @@ class AuthController
catch
reason = "Oops, something went wrong on the server. Please try again
later!"
formHelpers.applyValidationErrors(form, errors, reason)
formRespond(form, errors, reason)
this.submit = (form) ->
formHelpers.applyValidationErrors(form)
formRespond(form)
return unless form.$valid
$scope.$broadcast 'formState', form.$name, 'loading'
......
......@@ -9,7 +9,7 @@ describe 'h:AccountController', ->
fakeFlash = null
fakeSession = null
fakeIdentity = null
fakeFormHelpers = null
fakeFormRespond = null
fakeAuth = null
editProfilePromise = null
disableUserPromise = null
......@@ -33,8 +33,7 @@ describe 'h:AccountController', ->
error: sandbox.spy()
fakeIdentity =
logout: sandbox.spy()
fakeFormHelpers =
applyValidationErrors: sandbox.spy()
fakeFormRespond = sandbox.spy()
fakeAuth =
user: 'egon@columbia.edu'
......@@ -44,7 +43,7 @@ describe 'h:AccountController', ->
$provide.value 'session', fakeSession
$provide.value 'flash', fakeFlash
$provide.value 'identity', fakeIdentity
$provide.value 'formHelpers', fakeFormHelpers
$provide.value 'formRespond', fakeFormRespond
$provide.value 'auth', fakeAuth
return
......@@ -110,7 +109,7 @@ describe 'h:AccountController', ->
errors:
pwd: 'this is wrong'
assert.calledWith fakeFormHelpers.applyValidationErrors, fakeForm,
assert.calledWith fakeFormRespond, fakeForm,
pwd: 'this is wrong'
it 'displays a flash message on success', ->
......@@ -205,7 +204,7 @@ describe 'h:AccountController', ->
errors:
pwd: 'this is wrong'
assert.calledWith fakeFormHelpers.applyValidationErrors, fakeForm,
assert.calledWith fakeFormRespond, fakeForm,
pwd: 'this is wrong'
it 'displays a flash message if a server error occurs', ->
......
......@@ -19,7 +19,7 @@ class MockSession
finally: sandbox.stub()
mockFlash = info: sandbox.spy()
mockFormHelpers = applyValidationErrors: sandbox.spy()
mockFormRespond = sandbox.spy()
describe 'h:AuthController', ->
$scope = null
......@@ -39,7 +39,7 @@ describe 'h:AuthController', ->
$provide.value '$timeout', sandbox.spy()
$provide.value 'flash', mockFlash
$provide.value 'session', new MockSession()
$provide.value 'formHelpers', mockFormHelpers
$provide.value 'formRespond', mockFormRespond
return
beforeEach inject (_$controller_, $rootScope, _$timeout_, _session_) ->
......@@ -83,7 +83,7 @@ describe 'h:AuthController', ->
auth.submit(form)
assert.calledWith mockFormHelpers.applyValidationErrors, form,
assert.calledWith mockFormRespond, form,
{username: 'taken'},
'registration error'
......@@ -104,8 +104,7 @@ describe 'h:AuthController', ->
authCtrl.submit(form)
assert.calledWith(
mockFormHelpers.applyValidationErrors, form, undefined, reason)
assert.calledWith(mockFormRespond, form, undefined, reason)
it 'should emit an auth event once authenticated', ->
form =
......
# Wraps the annotation store to trigger events for the CRUD actions
class AnnotationMapperService
this.$inject = ['$rootScope', 'threading', 'store']
constructor: ($rootScope, threading, store) ->
this.setupAnnotation = (ann) -> ann
module.exports = [
'$rootScope', 'threading', 'store',
($rootScope, threading, store) ->
setupAnnotation: (ann) -> ann
this.loadAnnotations = (annotations) ->
loadAnnotations: (annotations) ->
annotations = for annotation in annotations
container = threading.idTable[annotation.id]
if container?.message
......@@ -17,14 +17,13 @@ class AnnotationMapperService
annotations = (new store.AnnotationResource(a) for a in annotations)
$rootScope.$emit('annotationsLoaded', annotations)
this.createAnnotation = (annotation) ->
createAnnotation: (annotation) ->
annotation = new store.AnnotationResource(annotation)
$rootScope.$emit('beforeAnnotationCreated', annotation)
annotation
this.deleteAnnotation = (annotation) ->
deleteAnnotation: (annotation) ->
annotation.$delete(id: annotation.id).then ->
$rootScope.$emit('annotationDeleted', annotation)
annotation
angular.module('h').service('annotationMapper', AnnotationMapperService)
]
class AnnotationSync
module.exports = class AnnotationSync
# Default configuration
options:
# Formats an annotation into a message body for sending across the bridge.
......@@ -186,8 +186,3 @@ class AnnotationSync
tag: ann.$$tag
msg: @options.formatter(ann)
}
if angular?
angular.module('h').value('AnnotationSync', AnnotationSync)
else
Annotator.Plugin.CrossFrame.AnnotationSync = AnnotationSync
# Watch the UI state and update scope properties.
module.exports = class AnnotationUIController
this.$inject = ['$rootScope', '$scope', 'annotationUI']
constructor: ( $rootScope, $scope, annotationUI ) ->
$rootScope.$watch (-> annotationUI.selectedAnnotationMap), (map={}) ->
count = Object.keys(map).length
$scope.selectedAnnotationsCount = count
if count
$scope.selectedAnnotations = map
else
$scope.selectedAnnotations = null
$rootScope.$watch (-> annotationUI.focusedAnnotationMap), (map={}) ->
$scope.focusedAnnotations = map
$rootScope.$on 'annotationDeleted', (event, annotation) ->
annotationUI.removeSelectedAnnotation(annotation)
# Holds the current state between the current state of the annotator in the
# attached iframes for display in the sidebar. This covers both tool and
# rendered state such as selected highlights.
createAnnotationUI = ->
value = (selection) ->
if Object.keys(selection).length then selection else null
{
visibleHighlights: false
# Contains a map of annotation tag:true pairs.
focusedAnnotationMap: null
# Contains a map of annotation id:true pairs.
selectedAnnotationMap: null
###*
# @ngdoc method
# @name annotationUI.focusedAnnotations
# @returns nothing
# @description Takes an array of annotations and uses them to set
# the focusedAnnotationMap.
###
focusAnnotations: (annotations) ->
selection = {}
selection[$$tag] = true for {$$tag} in annotations
@focusedAnnotationMap = value(selection)
###*
# @ngdoc method
# @name annotationUI.hasSelectedAnnotations
# @returns true if there are any selected annotations.
###
hasSelectedAnnotations: ->
!!@selectedAnnotationMap
###*
# @ngdoc method
# @name annotationUI.isAnnotationSelected
# @returns true if the provided annotation is selected.
###
isAnnotationSelected: (id) ->
!!@selectedAnnotationMap?[id]
###*
# @ngdoc method
# @name annotationUI.selectAnnotations
# @returns nothing
# @description Takes an array of annotation objects and uses them to
# set the selectedAnnotationMap property.
###
selectAnnotations: (annotations) ->
selection = {}
selection[id] = true for {id} in annotations
@selectedAnnotationMap = value(selection)
###*
# @ngdoc method
# @name annotationUI.xorSelectedAnnotations()
# @returns nothing
# @description takes an array of annotations and adds them to the
# selectedAnnotationMap if not present otherwise removes them.
###
xorSelectedAnnotations: (annotations) ->
selection = angular.extend({}, @selectedAnnotationMap)
for {id} in annotations
if selection[id]
delete selection[id]
else
selection[id] = true
@selectedAnnotationMap = value(selection)
###*
# @ngdoc method
# @name annotationUI.removeSelectedAnnotation()
# @returns nothing
# @description removes an annotation from the current selection.
###
removeSelectedAnnotation: (annotation) ->
selection = angular.extend({}, @selectedAnnotationMap)
if selection
delete selection[annotation.id]
@selectedAnnotationMap = value(selection)
###*
# @ngdoc method
# @name annotationUI.clearSelectedAnnotations()
# @returns nothing
# @description removes all annotations from the current selection.
###
clearSelectedAnnotations: ->
@selectedAnnotationMap = null
}
angular.module('h').factory('annotationUI', createAnnotationUI)
# Uses a channel between the sidebar and the attached providers to ensure
# the interface remains in sync.
class AnnotationUISync
module.exports = class AnnotationUISync
###*
# @name AnnotationUISync
# @param {$window} $window An Angular window service.
......@@ -50,5 +50,3 @@ class AnnotationUISync
params: annotationUI.visibleHighlights
bridge.onConnect(onConnect)
angular.module('h').value('AnnotationUISync', AnnotationUISync)
value = (selection) ->
if Object.keys(selection).length then selection else null
# Holds the current state between the current state of the annotator in the
# attached iframes for display in the sidebar. This covers both tool and
# rendered state such as selected highlights.
module.exports = ->
visibleHighlights: false
# Contains a map of annotation tag:true pairs.
focusedAnnotationMap: null
# Contains a map of annotation id:true pairs.
selectedAnnotationMap: null
###*
# @ngdoc method
# @name annotationUI.focusedAnnotations
# @returns nothing
# @description Takes an array of annotations and uses them to set
# the focusedAnnotationMap.
###
focusAnnotations: (annotations) ->
selection = {}
selection[$$tag] = true for {$$tag} in annotations
@focusedAnnotationMap = value(selection)
###*
# @ngdoc method
# @name annotationUI.hasSelectedAnnotations
# @returns true if there are any selected annotations.
###
hasSelectedAnnotations: ->
!!@selectedAnnotationMap
###*
# @ngdoc method
# @name annotationUI.isAnnotationSelected
# @returns true if the provided annotation is selected.
###
isAnnotationSelected: (id) ->
!!@selectedAnnotationMap?[id]
###*
# @ngdoc method
# @name annotationUI.selectAnnotations
# @returns nothing
# @description Takes an array of annotation objects and uses them to
# set the selectedAnnotationMap property.
###
selectAnnotations: (annotations) ->
selection = {}
selection[id] = true for {id} in annotations
@selectedAnnotationMap = value(selection)
###*
# @ngdoc method
# @name annotationUI.xorSelectedAnnotations()
# @returns nothing
# @description takes an array of annotations and adds them to the
# selectedAnnotationMap if not present otherwise removes them.
###
xorSelectedAnnotations: (annotations) ->
selection = angular.extend({}, @selectedAnnotationMap)
for {id} in annotations
if selection[id]
delete selection[id]
else
selection[id] = true
@selectedAnnotationMap = value(selection)
###*
# @ngdoc method
# @name annotationUI.removeSelectedAnnotation()
# @returns nothing
# @description removes an annotation from the current selection.
###
removeSelectedAnnotation: (annotation) ->
selection = angular.extend({}, @selectedAnnotationMap)
if selection
delete selection[annotation.id]
@selectedAnnotationMap = value(selection)
###*
# @ngdoc method
# @name annotationUI.clearSelectedAnnotations()
# @returns nothing
# @description removes all annotations from the current selection.
###
clearSelectedAnnotations: ->
@selectedAnnotationMap = null
angular = require('angular')
module.exports = class AnnotationViewerController
this.$inject = [
'$location', '$routeParams', '$scope',
'streamer', 'store', 'streamFilter', 'annotationMapper'
]
constructor: (
$location, $routeParams, $scope,
streamer, store, streamFilter, annotationMapper
) ->
# Tells the view that these annotations are standalone
$scope.isEmbedded = false
$scope.isStream = false
# Provide no-ops until these methods are moved elsewere. They only apply
# to annotations loaded into the stream.
$scope.focus = angular.noop
$scope.shouldShowThread = -> true
$scope.search.update = (query) ->
$location.path('/stream').search('q', query)
id = $routeParams.id
store.SearchResource.get _id: id, ({rows}) ->
annotationMapper.loadAnnotations(rows)
$scope.threadRoot = children: [$scope.threading.getContainer(id)]
store.SearchResource.get references: id, ({rows}) ->
annotationMapper.loadAnnotations(rows)
streamFilter
.setMatchPolicyIncludeAny()
.addClause('/references', 'first_of', id, true)
.addClause('/id', 'equals', id, true)
streamer.send({filter: streamFilter.getFilter()})
$ = require('jquery')
Annotator = require('annotator')
Channel = require('jschannel')
$ = Annotator.$
require('diff-match-patch')
require('dom-text-mapper')
require('dom-text-matcher')
require('page-text-mapper-core')
require('text-match-engines')
module.exports = class Annotator.Guest extends Annotator
module.exports = class Guest extends Annotator
SHOW_HIGHLIGHTS_CLASS = 'annotator-highlights-always-on'
# Events to be bound on Annotator#element.
......
Annotator = require('annotator')
$ = Annotator.$
Guest = require('./guest')
module.exports = class Host extends Guest
# Drag state variables
drag:
delta: 0
enabled: false
last: null
tick: false
constructor: (element, options) ->
# Create the iframe
if document.baseURI and window.PDFView?
# XXX: Hack around PDF.js resource: origin. Bug in jschannel?
hostOrigin = '*'
else
hostOrigin = window.location.origin
# XXX: Hack for missing window.location.origin in FF
hostOrigin ?= window.location.protocol + "//" + window.location.host
src = options.app
if options.firstRun
# Allow options.app to contain query string params.
src = src + (if '?' in src then '&' else '?') + 'firstrun'
app = $('<iframe></iframe>')
.attr('name', 'hyp_sidebar_frame')
.attr('seamless', '')
.attr('src', src)
super element, options, dontScan: true
this._addCrossFrameListeners()
app.appendTo(@frame)
if options.firstRun
this.on 'panelReady', => this.showFrame(transition: false)
# Host frame dictates the toolbar options.
this.on 'panelReady', =>
this.anchoring._scan() # Scan the document
# Guest is designed to respond to events rather than direct method
# calls. If we call set directly the other plugins will never recieve
# these events and the UI will be out of sync.
this.publish('setVisibleHighlights', !!options.showHighlights)
if @plugins.BucketBar?
this._setupDragEvents()
@plugins.BucketBar.element.on 'click', (event) =>
if @frame.hasClass 'annotator-collapsed'
this.showFrame()
showFrame: (options={transition: true}) ->
unless @drag.enabled
@frame.css 'margin-left': "#{-1 * @frame.width()}px"
if options.transition
@frame.removeClass 'annotator-no-transition'
else
@frame.addClass 'annotator-no-transition'
@frame.removeClass 'annotator-collapsed'
if @toolbar?
@toolbar.find('.annotator-toolbar-toggle')
.removeClass('h-icon-chevron-left')
.addClass('h-icon-chevron-right')
hideFrame: ->
@frame.css 'margin-left': ''
@frame.removeClass 'annotator-no-transition'
@frame.addClass 'annotator-collapsed'
if @toolbar?
@toolbar.find('.annotator-toolbar-toggle')
.removeClass('h-icon-chevron-right')
.addClass('h-icon-chevron-left')
_addCrossFrameListeners: ->
@crossframe.on('showFrame', this.showFrame.bind(this, null))
@crossframe.on('hideFrame', this.hideFrame.bind(this, null))
_setupDragEvents: ->
el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
el.width = el.height = 1
@element.append el
dragStart = (event) =>
event.dataTransfer.dropEffect = 'none'
event.dataTransfer.effectAllowed = 'none'
event.dataTransfer.setData 'text/plain', ''
event.dataTransfer.setDragImage el, 0, 0
@drag.enabled = true
@drag.last = event.screenX
m = parseInt (getComputedStyle @frame[0]).marginLeft
@frame.css
'margin-left': "#{m}px"
this.showFrame()
dragEnd = (event) =>
@drag.enabled = false
@drag.last = null
for handle in [@plugins.BucketBar.element[0], @plugins.Toolbar.buttons[0]]
handle.draggable = true
handle.addEventListener 'dragstart', dragStart
handle.addEventListener 'dragend', dragEnd
document.addEventListener 'dragover', (event) =>
this._dragUpdate event.screenX
_dragUpdate: (screenX) =>
unless @drag.enabled then return
if @drag.last?
@drag.delta += screenX - @drag.last
@drag.last = screenX
unless @drag.tick
@drag.tick = true
window.requestAnimationFrame this._dragRefresh
_dragRefresh: =>
d = @drag.delta
@drag.delta = 0
@drag.tick = false
m = parseInt (getComputedStyle @frame[0]).marginLeft
w = -1 * m
m += d
w -= d
@frame.addClass 'annotator-no-transition'
@frame.css
'margin-left': "#{m}px"
width: "#{w}px"
......@@ -13,7 +13,7 @@ extract = extract = (obj, keys...) ->
# frame acts as the bridge client, the sidebar is the server. This plugin
# can also be used to send messages through to the sidebar using the
# notify method.
CrossFrame = class Annotator.Plugin.CrossFrame extends Annotator.Plugin
module.exports = class CrossFrame extends Annotator.Plugin
constructor: (elem, options) ->
super
......@@ -46,5 +46,3 @@ CrossFrame = class Annotator.Plugin.CrossFrame extends Annotator.Plugin
this.onConnect = (fn) ->
bridge.onConnect(fn)
exports.CrossFrame = CrossFrame
cf = require('../cross-frame')
CrossFrame = require('../cross-frame')
assert = chai.assert
sinon.assert.expose(assert, prefix: '')
......@@ -15,7 +15,7 @@ describe 'Annotator.Plugin.CrossFrame', ->
on: sandbox.stub()
emit: sandbox.stub()
element = document.createElement('div')
return new cf.CrossFrame(element, $.extend({}, defaults, options))
return new CrossFrame(element, $.extend({}, defaults, options))
beforeEach ->
fakeDiscovery =
......@@ -31,9 +31,9 @@ describe 'Annotator.Plugin.CrossFrame', ->
fakeAnnotationSync =
sync: sandbox.stub()
cf.CrossFrame.AnnotationSync = sandbox.stub().returns(fakeAnnotationSync)
cf.CrossFrame.Discovery = sandbox.stub().returns(fakeDiscovery)
cf.CrossFrame.Bridge = sandbox.stub().returns(fakeBridge)
CrossFrame.AnnotationSync = sandbox.stub().returns(fakeAnnotationSync)
CrossFrame.Discovery = sandbox.stub().returns(fakeDiscovery)
CrossFrame.Bridge = sandbox.stub().returns(fakeBridge)
afterEach ->
sandbox.restore()
......@@ -41,24 +41,24 @@ describe 'Annotator.Plugin.CrossFrame', ->
describe 'constructor', ->
it 'instantiates the Discovery component', ->
createCrossFrame()
assert.calledWith(cf.CrossFrame.Discovery, window)
assert.calledWith(CrossFrame.Discovery, window)
it 'passes the options along to the bridge', ->
createCrossFrame(server: true)
assert.calledWith(cf.CrossFrame.Discovery, window, server: true)
assert.calledWith(CrossFrame.Discovery, window, server: true)
it 'instantiates the CrossFrame component', ->
createCrossFrame()
assert.calledWith(cf.CrossFrame.Discovery)
assert.calledWith(CrossFrame.Discovery)
it 'instantiates the AnnotationSync component', ->
createCrossFrame()
assert.called(cf.CrossFrame.AnnotationSync)
assert.called(CrossFrame.AnnotationSync)
it 'passes along options to AnnotationSync', ->
formatter = (x) -> x
createCrossFrame(formatter: formatter)
assert.calledWith(cf.CrossFrame.AnnotationSync, fakeBridge, {
assert.calledWith(CrossFrame.AnnotationSync, fakeBridge, {
on: sinon.match.func
emit: sinon.match.func
formatter: formatter
......
......@@ -5,7 +5,7 @@ assert = chai.assert
sinon.assert.expose(assert, prefix: '')
describe 'Annotator.Guest', ->
describe 'Guest', ->
sandbox = null
fakeCrossFrame = null
......@@ -174,14 +174,14 @@ describe 'Annotator.Guest', ->
describe 'on "onEditorHide" event', ->
it 'hides the editor', ->
target = sandbox.stub(Annotator.Guest.prototype, 'onEditorHide')
target = sandbox.stub(Guest.prototype, 'onEditorHide')
guest = createGuest()
emitGuestEvent('onEditorHide')
assert.called(target)
describe 'on "onEditorSubmit" event', ->
it 'sumbits the editor', ->
target = sandbox.stub(Annotator.Guest.prototype, 'onEditorSubmit')
target = sandbox.stub(Guest.prototype, 'onEditorSubmit')
guest = createGuest()
emitGuestEvent('onEditorSubmit')
assert.called(target)
......
Annotator = require('annotator')
Host = require('../host')
assert = chai.assert
sinon.assert.expose(assert, prefix: '')
describe 'Host', ->
sandbox = sinon.sandbox.create()
fakeCrossFrame = null
createHost = (options={}) ->
element = document.createElement('div')
return new Host(element, options)
beforeEach ->
# Disable Annotator's ridiculous logging.
sandbox.stub(console, 'log')
fakeCrossFrame = {}
fakeCrossFrame.onConnect = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.on = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.notify = sandbox.stub().returns(fakeCrossFrame)
Annotator.Plugin.CrossFrame = -> fakeCrossFrame
afterEach -> sandbox.restore()
describe 'options', ->
it 'enables highlighting when showHighlights option is provided', (done) ->
host = createHost(showHighlights: true)
host.on 'panelReady', ->
assert.isTrue(host.visibleHighlights)
done()
host.publish('panelReady')
it 'does not enable highlighting when no showHighlights option is provided', (done) ->
host = createHost({})
host.on 'panelReady', ->
assert.isFalse(host.visibleHighlights)
done()
host.publish('panelReady')
describe 'crossframe listeners', ->
emitHostEvent = (event, args...) ->
fn(args...) for [evt, fn] in fakeCrossFrame.on.args when event == evt
describe 'on "showFrame" event', ->
it 'shows the frame', ->
target = sandbox.stub(Host.prototype, 'showFrame')
host = createHost()
emitHostEvent('showFrame')
assert.called(target)
describe 'on "hideFrame" event', ->
it 'hides the frame', ->
target = sandbox.stub(Host.prototype, 'hideFrame')
host = createHost()
emitHostEvent('hideFrame')
assert.called(target)
# Watch the UI state and update scope properties.
class AnnotationUIController
this.$inject = ['$rootScope', '$scope', 'annotationUI']
constructor: ( $rootScope, $scope, annotationUI ) ->
$rootScope.$watch (-> annotationUI.selectedAnnotationMap), (map={}) ->
count = Object.keys(map).length
$scope.selectedAnnotationsCount = count
if count
$scope.selectedAnnotations = map
else
$scope.selectedAnnotations = null
$rootScope.$watch (-> annotationUI.focusedAnnotationMap), (map={}) ->
$scope.focusedAnnotations = map
$rootScope.$on 'annotationDeleted', (event, annotation) ->
annotationUI.removeSelectedAnnotation(annotation)
angular = require('angular')
class AppController
module.exports = class AppController
this.$inject = [
'$controller', '$document', '$location', '$rootScope', '$route', '$scope',
'$window',
......@@ -33,7 +16,7 @@ class AppController
permissions, streamer, annotationUI,
annotationMapper, threading
) ->
$controller(AnnotationUIController, {$scope})
$controller('AnnotationUIController', {$scope})
$scope.auth = auth
isFirstRun = $location.search().hasOwnProperty('firstrun')
......@@ -136,116 +119,3 @@ class AppController
$scope.sort = name: 'Location'
$scope.threading = threading
$scope.threadRoot = $scope.threading?.root
class AnnotationViewerController
this.$inject = [
'$location', '$routeParams', '$scope',
'streamer', 'store', 'streamfilter', 'annotationMapper'
]
constructor: (
$location, $routeParams, $scope,
streamer, store, streamfilter, annotationMapper
) ->
# Tells the view that these annotations are standalone
$scope.isEmbedded = false
$scope.isStream = false
# Provide no-ops until these methods are moved elsewere. They only apply
# to annotations loaded into the stream.
$scope.focus = angular.noop
$scope.shouldShowThread = -> true
$scope.search.update = (query) ->
$location.path('/stream').search('q', query)
id = $routeParams.id
store.SearchResource.get _id: id, ({rows}) ->
annotationMapper.loadAnnotations(rows)
$scope.threadRoot = children: [$scope.threading.getContainer(id)]
store.SearchResource.get references: id, ({rows}) ->
annotationMapper.loadAnnotations(rows)
streamfilter
.setMatchPolicyIncludeAny()
.addClause('/references', 'first_of', id, true)
.addClause('/id', 'equals', id, true)
streamer.send({filter: streamfilter.getFilter()})
class ViewerController
this.$inject = [
'$scope', '$route', 'annotationUI', 'crossframe', 'annotationMapper',
'auth', 'streamer', 'streamfilter', 'store'
]
constructor: (
$scope, $route, annotationUI, crossframe, annotationMapper,
auth, streamer, streamfilter, store
) ->
# Tells the view that these annotations are embedded into the owner doc
$scope.isEmbedded = true
$scope.isStream = true
loaded = []
_loadAnnotationsFrom = (query, offset) ->
queryCore =
limit: 20
offset: offset
sort: 'created'
order: 'asc'
q = angular.extend(queryCore, query)
store.SearchResource.get q, (results) ->
total = results.total
offset += results.rows.length
if offset < total
_loadAnnotationsFrom query, offset
annotationMapper.loadAnnotations(results.rows)
loadAnnotations = ->
query = {}
for p in crossframe.providers
for e in p.entities when e not in loaded
loaded.push e
q = angular.extend(uri: e, query)
_loadAnnotationsFrom q, 0
streamfilter.resetFilter().addClause('/uri', 'one_of', loaded)
streamer.send({filter: streamfilter.getFilter()})
$scope.$watchCollection (-> crossframe.providers), loadAnnotations
$scope.focus = (annotation) ->
if angular.isObject annotation
highlights = [annotation.$$tag]
else
highlights = []
crossframe.notify
method: 'focusAnnotations'
params: highlights
$scope.scrollTo = (annotation) ->
if angular.isObject annotation
crossframe.notify
method: 'scrollToAnnotation'
params: annotation.$$tag
$scope.shouldShowThread = (container) ->
if annotationUI.hasSelectedAnnotations() and not container.parent.parent
annotationUI.isAnnotationSelected(container.message?.id)
else
true
$scope.hasFocus = (annotation) ->
!!($scope.focusedAnnotations ? {})[annotation?.$$tag]
angular.module('h')
.controller('AppController', AppController)
.controller('ViewerController', ViewerController)
.controller('AnnotationViewerController', AnnotationViewerController)
.controller('AnnotationUIController', AnnotationUIController)
Annotator = require('annotator')
angular = require('angular')
uuid = require('node-uuid')
# These services are provided in their own angular modules and thus must be
# loaded first.
require('./identity-service')
require('./streamer-service')
imports = [
'ngAnimate'
'ngRoute'
'ngSanitize'
'ngTagsInput'
'h.flash'
'h.helpers'
'h.identity'
'h.session'
'h.streamer'
]
resolve =
auth: ['$q', '$rootScope', 'auth', ($q, $rootScope, auth) ->
dfd = $q.defer()
......@@ -52,12 +35,12 @@ configureRoutes = ['$routeProvider', ($routeProvider) ->
templateUrl: 'viewer.html'
resolve: resolve
$routeProvider.when '/viewer',
controller: 'ViewerController'
controller: 'WidgetController'
templateUrl: 'viewer.html'
reloadOnSearch: false
resolve: resolve
$routeProvider.when '/stream',
controller: 'StreamSearchController'
controller: 'StreamController'
templateUrl: 'viewer.html'
resolve: resolve
$routeProvider.otherwise
......@@ -93,50 +76,81 @@ setupStreamer = [
$http.defaults.headers.common['X-Client-Id'] = clientId
]
module = angular.module('h', imports)
module.exports = angular.module('h', [
'bootstrap'
'ngAnimate'
'ngResource'
'ngRoute'
'ngSanitize'
'ngTagsInput'
'toastr'
])
.config(configureDocument)
.config(configureLocation)
.config(configureRoutes)
.config(configureTemplates)
unless mocha? # Crude method of detecting test environment.
module.run(setupCrossFrame)
module.run(setupStreamer)
module.run(setupHost)
require('./vendor/annotator.auth.js')
require('./annotator/monkey')
require('./controllers')
require('./directives')
require('./directives/annotation')
require('./directives/deep-count')
require('./directives/markdown')
require('./directives/privacy')
require('./directives/simple-search')
require('./directives/status-button')
require('./directives/thread-filter')
require('./directives/thread')
require('./filters')
require('./searchfilters')
require('./services')
require('./annotation-mapper-service')
require('./annotation-ui-service')
require('./auth-service')
require('./cross-frame-service')
require('./host-service')
require('./flash-service')
require('./permissions-service')
require('./local-storage-service')
require('./store-service')
require('./threading-service')
require('./streamsearch')
require('./annotation-sync')
require('./annotation-ui-sync')
require('./bridge')
require('./discovery')
.controller('AppController', require('./app-controller'))
.controller('AnnotationUIController', require('./annotation-ui-controller'))
.controller('AnnotationViewerController', require('./annotation-viewer-controller'))
.controller('StreamController', require('./stream-controller'))
.controller('WidgetController', require('./widget-controller'))
.directive('annotation', require('./directive/annotation'))
.directive('deepCount', require('./directive/deep-count'))
.directive('formInput', require('./directive/form-input'))
.directive('formValidate', require('./directive/form-validate'))
.directive('markdown', require('./directive/markdown'))
.directive('privacy', require('./directive/privacy'))
.directive('repeatAnim', require('./directive/repeat-anim'))
.directive('simpleSearch', require('./directive/simple-search'))
.directive('statusButton', require('./directive/status-button'))
.directive('thread', require('./directive/thread'))
.directive('threadFilter', require('./directive/thread-filter'))
.directive('whenscrolled', require('./directive/whenscrolled'))
.directive('match', require('./directive/match'))
.directive('tabbable', require('./directive/tabbable'))
.directive('tabReveal', require('./directive/tab-reveal'))
.filter('converter', require('./filter/converter'))
.filter('moment', require('./filter/moment'))
.filter('persona', require('./filter/persona'))
.filter('urlencode', require('./filter/urlencode'))
.provider('identity', require('./identity'))
.provider('session', require('./session'))
.service('annotator', -> new Annotator(angular.element('<div>')))
.service('annotationMapper', require('./annotation-mapper'))
.service('annotationUI', require('./annotation-ui'))
.service('auth', require('./auth'))
.service('bridge', require('./bridge'))
.service('crossframe', require('./cross-frame'))
.service('drafts', require('./drafts'))
.service('flash', require('./flash'))
.service('formRespond', require('./form-respond'))
.service('host', require('./host'))
.service('localStorage', require('./local-storage'))
.service('permissions', require('./permissions'))
.service('pulse', require('./pulse'))
.service('queryParser', require('./query-parser'))
.service('render', require('./render'))
.service('searchFilter', require('./search-filter'))
.service('store', require('./store'))
.service('streamFilter', require('./stream-filter'))
.service('streamer', require('./streamer'))
.service('tags', require('./tags'))
.service('time', require('./time'))
.service('threading', require('./threading'))
.service('unicode', require('./unicode'))
.service('viewFilter', require('./view-filter'))
.value('xsrf', token: null)
.value('AnnotationSync', require('./annotation-sync'))
.value('AnnotationUISync', require('./annotation-ui-sync'))
.value('Discovery', require('./discovery'))
.run(setupCrossFrame)
.run(setupStreamer)
.run(setupHost)
require('./vendor/annotator.auth.js')
###*
# @ngdoc service
# @name Auth
# @name auth
#
# @description
# The 'Auth' service exposes the currently logged in user for other components,
......@@ -8,19 +11,16 @@
# and provides a method for permitting a certain operation for a user with a
# given annotation
###
class Auth
this.$inject = ['$document', '$http', '$location', '$rootScope',
'annotator', 'identity']
constructor: ( $document, $http, $location, $rootScope,
annotator, identity) ->
module.exports = [
'$document', '$http', '$location', '$rootScope', 'annotator', 'identity'
($document, $http, $location, $rootScope, annotator, identity) ->
{plugins} = annotator
_checkingToken = false
@user = undefined
auth = user: undefined
# TODO: Remove this once Auth has been migrated.
$rootScope.$on 'beforeAnnotationCreated', (event, annotation) =>
annotation.user = @user
annotation.user = auth.user
annotation.permissions = {}
annotator.publish('beforeAnnotationCreated', annotation)
......@@ -49,7 +49,7 @@ class Auth
# Set the user from the token.
plugins.Auth.withToken (payload) =>
_checkingToken = false
@user = payload.userId
auth.user = payload.userId
token = plugins.Auth.token
$http.defaults.headers.common['X-Annotator-Auth-Token'] = token
$rootScope.$apply()
......@@ -62,7 +62,7 @@ class Auth
delete plugins.Auth
delete $http.defaults.headers.common['X-Annotator-Auth-Token']
@user = null
auth.user = null
_checkingToken = false
# Fired after the identity-service requested authentication (both after
......@@ -70,11 +70,9 @@ class Auth
# has failed and if yes, it sets the user value to null. (Otherwise the
# onlogin method would set it to userId)
onready = =>
if @user is undefined and not _checkingToken
@user = null
if auth.user is undefined and not _checkingToken
auth.user = null
identity.watch {onlogin, onlogout, onready}
angular.module('h')
.service('auth', Auth)
return auth
]
......@@ -3,7 +3,7 @@ Channel = require('jschannel')
# The Bridge service sets up a channel between frames
# and provides an events API on top of it.
class Bridge
module.exports = class Bridge
# Connected links to other frames
links: null
channelListeners: null
......@@ -102,8 +102,3 @@ class Bridge
(options.origin.match /^resource:\/\//)
options = $.extend {}, options, {origin: '*'}
channel = Channel.build(options)
if angular?
angular.module('h').service 'bridge', Bridge
else
Annotator.Plugin.CrossFrame.Bridge = Bridge
# Instantiates all objects used for cross frame discovery and communication.
class CrossFrameService
module.exports = class CrossFrame
providers: null
this.inject = [
......@@ -68,5 +68,3 @@ class CrossFrameService
this.notify = bridge.notify.bind(bridge)
this.notify = -> throw new Error('connect() must be called before notify()')
angular.module('h').service('crossframe', CrossFrameService)
......@@ -30,11 +30,11 @@ validate = (value) ->
###
AnnotationController = [
'$scope', '$timeout', '$q', '$rootScope', '$document',
'auth', 'drafts', 'flash', 'permissions', 'tagHelpers',
'timeHelpers', 'annotationUI', 'annotationMapper'
'auth', 'drafts', 'flash', 'permissions', 'tags', 'time',
'annotationUI', 'annotationMapper'
($scope, $timeout, $q, $rootScope, $document,
auth, drafts, flash, permissions, tagHelpers,
timeHelpers, annotationUI, annotationMapper) ->
auth, drafts, flash, permissions, tags, time,
annotationUI, annotationMapper) ->
@annotation = {}
@action = 'view'
......@@ -58,7 +58,7 @@ AnnotationController = [
# the tags to show in autocomplete.
###
this.tagsAutoComplete = (query) ->
$q.when(tagHelpers.filterTags(query))
$q.when(tags.filter(query))
###*
# @ngdoc method
......@@ -156,9 +156,9 @@ AnnotationController = [
return flash.info('Please add text or a tag before publishing.')
# Update stored tags with the new tags of this annotation
tags = @annotation.tags.filter (tag) ->
newTags = @annotation.tags.filter (tag) ->
tag.text not in (model.tags or [])
tagHelpers.storeTags(tags)
tags.store(newTags)
angular.extend model, @annotation,
tags: (tag.text for tag in @annotation.tags)
......@@ -250,8 +250,8 @@ AnnotationController = [
@showDiff = undefined
updateTimestamp = (repeat=false) =>
@timestamp = timeHelpers.toFuzzyString model.updated
fuzzyUpdate = timeHelpers.nextFuzzyUpdate model.updated
@timestamp = time.toFuzzyString model.updated
fuzzyUpdate = time.nextFuzzyUpdate model.updated
nextUpdate = (1000 * fuzzyUpdate) + 500
return unless repeat
$timeout =>
......@@ -312,7 +312,7 @@ AnnotationController = [
# value is used to signal whether the annotation is being displayed inside
# an embedded widget.
###
annotationDirective = [
module.exports = [
'$document',
($document) ->
linkFn = (scope, elem, attrs, [ctrl, thread, threadFilter, counter]) ->
......@@ -359,7 +359,7 @@ annotationDirective = [
scope.$on '$destroy', ->
if ctrl.editing then counter?.count 'edit', -1
controller: 'AnnotationController'
controller: AnnotationController
controllerAs: 'vm'
link: linkFn
require: ['annotation', '?^thread', '?^threadFilter', '?^deepCount']
......@@ -370,8 +370,3 @@ annotationDirective = [
showReplyCount: '@annotationShowReplyCount'
templateUrl: 'annotation.html'
]
angular.module('h')
.controller('AnnotationController', AnnotationController)
.directive('annotation', annotationDirective)
......@@ -62,17 +62,12 @@ DeepCountController = [
# {@link deepCount.DeepCountController DeepCountController} and exports it
# to the current scope under the name specified by the attribute parameter.
###
deepCount = [
module.exports = [
'$parse',
($parse) ->
controller: 'DeepCountController'
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')
.controller('DeepCountController', DeepCountController)
.directive('deepCount', deepCount)
# Shared helper methods for working with form controllers.
createFormHelpers = ->
# Takes a FormControllers instance and an object of errors returned by the
# API and updates the validity of the form. The field.$errors.response
# property will be true if there are errors and the responseErrorMessage
# will contain the API error message.
applyValidationErrors: (form, errors, reason) ->
for own field, error of errors
form[field].$setValidity('response', false)
form[field].responseErrorMessage = error
form.$setValidity('response', !reason)
form.responseErrorMessage = reason
formInput = ->
module.exports = ->
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
fieldClassName = 'form-field'
errorClassName = 'form-field-error'
......@@ -45,32 +30,3 @@ formInput = ->
require: ['^?form', '?ngModel', '^?formValidate']
restrict: 'C'
formValidate = ->
controller: ->
controls = {}
addControl: (control) ->
if control.$name
controls[control.$name] = control
removeControl: (control) ->
if control.$name
delete controls[control.$name]
submit: ->
# make all the controls dirty and re-render them
for _, control of controls
control.$setViewValue(control.$viewValue)
control.$render()
link: (scope, elem, attr, ctrl) ->
elem.on 'submit', ->
ctrl.submit()
angular.module('h.helpers')
.directive('formInput', formInput)
.directive('formValidate', formValidate)
.factory('formHelpers', createFormHelpers)
module.exports = ->
controller: ->
controls = {}
addControl: (control) ->
if control.$name
controls[control.$name] = control
removeControl: (control) ->
if control.$name
delete controls[control.$name]
submit: ->
# make all the controls dirty and re-render them
for _, control of controls
control.$setViewValue(control.$viewValue)
control.$render()
link: (scope, elem, attr, ctrl) ->
elem.on 'submit', ->
ctrl.submit()
......@@ -21,7 +21,7 @@ loadMathJax = ->
# the markdown editor.
###
markdown = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitize, $sce, $timeout) ->
module.exports = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitize, $sce, $timeout) ->
link: (scope, elem, attr, ctrl) ->
return unless ctrl?
......@@ -353,6 +353,3 @@ markdown = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitize, $sc
required: '@'
templateUrl: 'markdown.html'
]
angular.module('h')
.directive('markdown', markdown)
module.exports = ->
link: (scope, elem, attr, input) ->
validate = ->
scope.$evalAsync ->
input.$setValidity('match', scope.match == input.$modelValue)
elem.on('keyup', validate)
scope.$watch('match', validate)
scope:
match: '='
restrict: 'A'
require: 'ngModel'
privacy = ['localstorage', 'permissions', (localstorage, permissions) ->
module.exports = ['localStorage', 'permissions', (localStorage, permissions) ->
VISIBILITY_KEY ='hypothesis.visibility'
VISIBILITY_PUBLIC = 'public'
VISIBILITY_PRIVATE = 'private'
......@@ -44,7 +44,7 @@ privacy = ['localstorage', 'permissions', (localstorage, permissions) ->
controller.$render = ->
unless controller.$modelValue.read?.length
name = localstorage.getItem VISIBILITY_KEY
name = localStorage.getItem VISIBILITY_KEY
name ?= VISIBILITY_PUBLIC
level = getLevel(name)
controller.$setViewValue level
......@@ -53,7 +53,7 @@ privacy = ['localstorage', 'permissions', (localstorage, permissions) ->
scope.levels = levels
scope.setLevel = (level) ->
localstorage.setItem VISIBILITY_KEY, level.name
localStorage.setItem VISIBILITY_KEY, level.name
controller.$setViewValue level
controller.$render()
scope.isPublic = isPublic
......@@ -63,6 +63,3 @@ privacy = ['localstorage', 'permissions', (localstorage, permissions) ->
scope: {}
templateUrl: 'privacy.html'
]
angular.module('h')
.directive('privacy', privacy)
repeatAnim = ->
module.exports = ->
restrict: 'A'
scope:
array: '='
......@@ -27,30 +27,3 @@ repeatAnim = ->
.css({ 'margin-left': itemElm.width() })
.animate({ 'margin-left': '0px' }, 1500)
return
whenscrolled = ->
link: (scope, elem, attr) ->
elem.bind 'scroll', ->
{clientHeight, scrollHeight, scrollTop} = elem[0]
if scrollHeight - scrollTop <= clientHeight + 40
scope.$apply attr.whenscrolled
match = ->
link: (scope, elem, attr, input) ->
validate = ->
scope.$evalAsync ->
input.$setValidity('match', scope.match == input.$modelValue)
elem.on('keyup', validate)
scope.$watch('match', validate)
scope:
match: '='
restrict: 'A'
require: 'ngModel'
angular.module('h')
.directive('repeatAnim', repeatAnim)
.directive('whenscrolled', whenscrolled)
.directive('match', match)
simpleSearch = ['$parse', ($parse) ->
module.exports = ['$parse', ($parse) ->
uuid = 0
link: (scope, elem, attr, ctrl) ->
scope.viewId = uuid++
......@@ -32,7 +32,3 @@ simpleSearch = ['$parse', ($parse) ->
</form>
'''
]
angular.module('h')
.directive('simpleSearch', simpleSearch)
......@@ -7,7 +7,7 @@
# Example
#
# <button status-button="test-form">Submit</button>
statusButton = ->
module.exports = ->
STATE_ATTRIBUTE = 'status-button-state'
STATE_LOADING = 'loading'
STATE_SUCCESS = 'success'
......@@ -39,7 +39,3 @@ statusButton = ->
formState = ''
elem.attr(STATE_ATTRIBUTE, formState)
transclude: 'element'
angular.module('h')
.directive('statusButton', statusButton)
# Extend the tabbable directive from angular-bootstrap with autofocus
tabbable = ['$timeout', ($timeout) ->
link: (scope, elem, attrs, ctrl) ->
return unless ctrl
render = ctrl.$render
ctrl.$render = ->
render.call(ctrl)
$timeout ->
elem
.find(':input')
.filter(':visible:first')
.focus()
, false
require: '?ngModel'
restrict: 'C'
]
tabReveal = ['$parse', ($parse) ->
module.exports = ['$parse', ($parse) ->
compile: (tElement, tAttrs, transclude) ->
panes = []
hiddenPanesGet = $parse tAttrs.tabReveal
......@@ -57,8 +39,3 @@ tabReveal = ['$parse', ($parse) ->
angular.element(tabs[i]).css 'display', 'none'
require: ['ngModel', 'tabbable']
]
angular.module('h.helpers')
.directive('tabbable', tabbable)
.directive('tabReveal', tabReveal)
# Extend the tabbable directive from angular-bootstrap with autofocus
module.exports = tabbable = ['$timeout', ($timeout) ->
link: (scope, elem, attrs, ctrl) ->
return unless ctrl
render = ctrl.$render
ctrl.$render = ->
render.call(ctrl)
$timeout ->
elem
.find(':input')
.filter(':visible:first')
.focus()
, false
require: '?ngModel'
restrict: 'C'
]
......@@ -3,13 +3,15 @@
assert = chai.assert
describe 'h.directives.annotation', ->
describe 'annotation', ->
$compile = null
$document = null
$element = null
$scope = null
$timeout = null
annotation = null
createController = null
controller = null
isolateScope = null
fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeAuth = null
......@@ -19,14 +21,21 @@ describe 'h.directives.annotation', ->
fakePermissions = null
fakePersonaFilter = null
fakeStore = null
fakeTagHelpers = null
fakeTimeHelpers = null
fakeTags = null
fakeTime = null
fakeUrlEncodeFilter = null
sandbox = null
createDirective = ->
$element = angular.element('<div annotation="annotation">')
$compile($element)($scope)
$scope.$digest()
controller = $element.controller('annotation')
isolateScope = $element.isolateScope()
before ->
angular.module('h', [])
require('../annotation')
.directive('annotation', require('../annotation'))
beforeEach module('h')
beforeEach module('h.templates')
......@@ -59,11 +68,10 @@ describe 'h.directives.annotation', ->
private: sandbox.stub().returns({read: ['justme']})
}
fakePersonaFilter = sandbox.stub().returnsArg(0)
fakeTagsHelpers = {
filterTags: sandbox.stub().returns('a while ago')
refreshTags: sandbox.stub().returns(30)
fakeTags = {
filter: sandbox.stub().returns('a while ago')
}
fakeTimeHelpers = {
fakeTime = {
toFuzzyString: sandbox.stub().returns('a while ago')
nextFuzzyUpdate: sandbox.stub().returns(30)
}
......@@ -78,18 +86,17 @@ describe 'h.directives.annotation', ->
$provide.value 'permissions', fakePermissions
$provide.value 'personaFilter', fakePersonaFilter
$provide.value 'store', fakeStore
$provide.value 'tagHelpers', fakeTagHelpers
$provide.value 'timeHelpers', fakeTimeHelpers
$provide.value 'tags', fakeTags
$provide.value 'time', fakeTime
$provide.value 'urlencodeFilter', fakeUrlEncodeFilter
return
beforeEach inject (_$compile_, $controller, _$document_, $rootScope, _$timeout_) ->
beforeEach inject (_$compile_, _$document_, $rootScope, _$timeout_) ->
$compile = _$compile_
$document = _$document_
$timeout = _$timeout_
$scope = $rootScope.$new()
$scope.annotationGet = (locals) -> annotation
annotation =
$scope.annotation = annotation =
id: 'deadbeef'
document:
title: 'A special document'
......@@ -97,10 +104,6 @@ describe 'h.directives.annotation', ->
uri: 'http://example.com'
user: 'acct:bill@localhost'
createController = ->
$controller 'AnnotationController',
$scope: $scope
afterEach ->
sandbox.restore()
......@@ -115,7 +118,7 @@ describe 'h.directives.annotation', ->
it 'persists upon login', ->
delete annotation.id
delete annotation.user
controller = createController()
createDirective()
$scope.$digest()
assert.notCalled annotation.$create
annotation.user = 'acct:ted@wyldstallyns.com'
......@@ -124,16 +127,15 @@ describe 'h.directives.annotation', ->
it 'is private', ->
delete annotation.id
controller = createController()
createDirective()
$scope.$digest()
assert.deepEqual annotation.permissions, {read: ['justme']}
describe '#reply', ->
controller = null
container = null
beforeEach ->
controller = createController()
createDirective()
annotation.permissions =
read: ['acct:joe@localhost']
......@@ -161,24 +163,19 @@ describe 'h.directives.annotation', ->
assert.deepEqual(reply.permissions, {read: ['justme']})
describe '#render', ->
controller = null
beforeEach ->
controller = createController()
createDirective()
sandbox.spy(controller, 'render')
afterEach ->
sandbox.restore()
it 'is called exactly once during the first digest', ->
$scope.$digest()
assert.calledOnce(controller.render)
it 'is called exactly once on model changes', ->
assert.notCalled(controller.render)
annotation.delete = true
$scope.$digest()
assert.calledOnce(controller.render)
$scope.$digest()
assert.calledOnce(controller.render) # still
annotation.booz = 'baz'
$scope.$digest()
......@@ -337,10 +334,10 @@ describe 'h.directives.annotation', ->
assert.equal(controller.timestamp, 'a while ago')
it 'is updated after a timeout', ->
fakeTimeHelpers.nextFuzzyUpdate.returns(10)
fakeTime.nextFuzzyUpdate.returns(10)
$scope.$digest()
clock.tick(11000)
fakeTimeHelpers.toFuzzyString.returns('ages ago')
fakeTime.toFuzzyString.returns('ages ago')
$timeout.flush()
assert.equal(controller.timestamp, 'ages ago')
......@@ -351,41 +348,32 @@ describe 'h.directives.annotation', ->
$timeout.verifyNoPendingTasks()
describe 'share', ->
$element = null
$isolateScope = null
dialog = null
beforeEach ->
template = '<div annotation="annotation">'
$scope.annotation = annotation
$element = $compile(template)($scope)
$scope.$digest()
$isolateScope = $element.isolateScope()
dialog = $element.find('.share-dialog-wrapper')
it 'sets and unsets the open class on the share wrapper', ->
$element.find('a').filter(-> this.title == 'Share').click()
$isolateScope.$digest()
isolateScope.$digest()
assert.ok(dialog.hasClass('open'))
$document.click()
assert.notOk(dialog.hasClass('open'))
describe 'annotationUpdate event', ->
controller = null
beforeEach ->
controller = createController()
sandbox.spy($scope, '$emit')
createDirective()
sandbox.spy(isolateScope, '$emit')
annotation.updated = '123'
$scope.$digest()
it "does not fire when this user's annotations are updated", ->
annotation.updated = '456'
$scope.$digest()
assert.notCalled($scope.$emit)
assert.notCalled(isolateScope.$emit)
it "fires when another user's annotation is updated", ->
fakeAuth.user = 'acct:jane@localhost'
annotation.updated = '456'
$scope.$digest()
assert.calledWith($scope.$emit, 'annotationUpdate')
assert.calledWith(isolateScope.$emit, 'annotationUpdate')
{module, inject} = require('angular-mock')
assert = chai.assert
angular = require('angular')
describe 'form-input', ->
$compile = null
$field = null
$scope = null
before ->
angular.module('h', ['ng'])
.directive('formInput', require('../form-input'))
.directive('formValidate', require('../form-validate'))
beforeEach module('h')
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
beforeEach ->
$scope.model = {username: undefined}
template = '''
<form form-validate name="login" onsubmit="return false">
<div class="form-field">
<input type="text" class="form-input" name="username"
ng-model="model.username" name="username"
required ng-minlength="3" />
</div>
</form>
'''
$field = $compile(angular.element(template))($scope).find('div')
$scope.$digest()
it 'should remove an error class to an valid field on change', ->
$field.addClass('form-field-error')
$input = $field.find('[name=username]').addClass('form-field-error')
$input.controller('ngModel').$setViewValue('abc')
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
assert.notInclude($input.prop('className'), 'form-field-error')
it 'should apply an error class to an invalid field on render', ->
$input = $field.find('[name=username]')
$input.triggerHandler('input') # set dirty
$input.controller('ngModel').$render()
assert.include($field.prop('className'), 'form-field-error')
it 'should remove an error class from a valid field on render', ->
$field.addClass('form-field-error')
$input = $field.find('[name=username]')
$input.val('abc').triggerHandler('input')
$input.controller('ngModel').$render()
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should remove an error class on valid input', ->
$field.addClass('form-field-error')
$input = $field.find('[name=username]')
$input.val('abc').triggerHandler('input')
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should not add an error class on invalid input', ->
$input = $field.find('[name=username]')
$input.val('ab').triggerHandler('input')
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should reset the "response" error when the view changes', ->
$input = $field.find('[name=username]')
controller = $input.controller('ngModel')
controller.$setViewValue('abc')
controller.$setValidity('response', false)
controller.responseErrorMessage = 'fail'
$scope.$digest()
assert.include($field.prop('className'), 'form-field-error', 'Fail fast check')
controller.$setViewValue('abc')
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should hide errors if the model is marked as pristine', ->
$field.addClass('form-field-error')
$input = $field.find('[name=username]')
controller = $input.controller('ngModel')
$input.triggerHandler('input') # set dirty
controller.$setValidity('response', false)
controller.responseErrorMessage = 'fail'
$scope.$digest()
assert.include($field.prop('className'), 'form-field-error', 'Fail fast check')
# Then clear it out and mark it as pristine
controller.$setPristine()
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
{module, inject} = require('angular-mock')
assert = chai.assert
angular = require('angular')
describe 'form-validate', ->
$compile = null
$element = null
$scope = null
controller = null
before ->
angular.module('h', [])
.directive('formValidate', require('../form-validate'))
beforeEach module('h')
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
template = '<form form-validate onsubmit="return false"></form>'
$element = $compile(angular.element(template))($scope)
controller = $element.controller('formValidate')
it 'performs validation and rendering on registered controls on submit', ->
mockControl =
'$name': 'babbleflux'
'$setViewValue': sinon.spy()
'$render': sinon.spy()
controller.addControl(mockControl)
$element.triggerHandler('submit')
assert.calledOnce(mockControl.$setViewValue)
assert.calledOnce(mockControl.$render)
mockControl2 =
'$name': 'dubbledabble'
'$setViewValue': sinon.spy()
'$render': sinon.spy()
controller.removeControl(mockControl)
controller.addControl(mockControl2)
$element.triggerHandler('submit')
assert.calledOnce(mockControl.$setViewValue)
assert.calledOnce(mockControl.$render)
assert.calledOnce(mockControl2.$setViewValue)
assert.calledOnce(mockControl2.$render)
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'match', ->
$compile = null
$element = null
$isolateScope = null
$scope = null
before ->
angular.module('h', [])
.directive('match', require('../match'))
beforeEach module('h')
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
beforeEach ->
$scope.model = {a: 1, b: 1}
$element = $compile('<input name="confirmation" ng-model="model.b" match="model.a" />')($scope)
$isolateScope = $element.isolateScope()
$scope.$digest()
it 'is valid if both properties have the same value', ->
controller = $element.controller('ngModel')
assert.isFalse(controller.$error.match)
it 'is invalid if the local property differs', ->
$isolateScope.match = 2
$isolateScope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
it 'is invalid if the matched property differs', ->
$scope.model.a = 2
$scope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
it 'is invalid if the input itself is changed', ->
$element.val('2').trigger('input').keyup()
$scope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
......@@ -6,7 +6,7 @@ VISIBILITY_KEY ='hypothesis.visibility'
VISIBILITY_PUBLIC = 'public'
VISIBILITY_PRIVATE = 'private'
describe 'h.directives.privacy', ->
describe 'privacy', ->
$compile = null
$scope = null
$window = null
......@@ -17,7 +17,7 @@ describe 'h.directives.privacy', ->
before ->
angular.module('h', [])
require('../privacy')
.directive('privacy', require('../privacy'))
beforeEach module('h')
beforeEach module('h.templates')
......@@ -45,7 +45,7 @@ describe 'h.directives.privacy', ->
}
$provide.value 'auth', fakeAuth
$provide.value 'localstorage', fakeLocalStorage
$provide.value 'localStorage', fakeLocalStorage
$provide.value 'permissions', fakePermissions
return
......
......@@ -12,7 +12,7 @@ describe 'h:directives.simple-search', ->
before ->
angular.module('h', [])
require('../simple-search')
.directive('simpleSearch', require('../simple-search'))
beforeEach module('h')
......@@ -24,7 +24,7 @@ describe 'h:directives.simple-search', ->
$scope.clear = sinon.spy()
template= '''
<div class="simpleSearch"
<div class="simple-search"
query="query"
on-search="update(query)"
on-clear="clear()">
......
......@@ -10,7 +10,7 @@ describe 'h:directives.status-button', ->
before ->
angular.module('h', [])
require('../status-button')
.directive('statusButton', require('../status-button'))
beforeEach module('h')
......
......@@ -4,31 +4,49 @@ assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'h:directives.thread', ->
describe 'thread', ->
$compile = null
$element = null
$scope = null
controller = null
fakePulse = null
fakeRender = null
sandbox = null
createDirective = ->
$element = angular.element('<div thread>')
$compile($element)($scope)
$scope.$digest()
controller = $element.controller('thread')
before ->
angular.module('h', [])
require('../thread')
.directive('thread', require('../thread'))
describe '.ThreadController', ->
$scope = null
createController = null
beforeEach module('h')
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakePulse = sandbox.spy()
fakeRender = sandbox.spy()
$provide.value 'pulse', fakePulse
$provide.value 'render', fakeRender
return
beforeEach inject ($controller, $rootScope) ->
$scope = $rootScope.$new()
beforeEach inject (_$compile_, $rootScope) ->
$compile = _$compile_
$scope = $rootScope.$new()
createController = ->
controller = $controller 'ThreadController'
controller
afterEach ->
sandbox.restore()
describe 'controller', ->
describe '#toggleCollapsed', ->
controller = null
count = null
beforeEach ->
controller = createController()
createDirective()
count = sinon.stub().returns(0)
count.withArgs('message').returns(2)
controller.counter = {count: count}
......@@ -59,11 +77,10 @@ describe 'h:directives.thread', ->
assert.isTrue(controller.collapsed)
describe '#shouldShowAsReply', ->
controller = null
count = null
beforeEach ->
controller = createController()
createDirective()
count = sinon.stub().returns(0)
controller.counter = {count: count}
......@@ -102,11 +119,10 @@ describe 'h:directives.thread', ->
describe '#shouldShowNumReplies', ->
count = null
controller = null
filterActive = false
beforeEach ->
controller = createController()
createDirective()
count = sinon.stub()
controller.counter = {count: count}
controller.filter = {active: -> filterActive}
......@@ -135,10 +151,9 @@ describe 'h:directives.thread', ->
assert.isFalse(controller.shouldShowNumReplies())
describe '#numReplies', ->
controller = null
beforeEach ->
controller = createController()
createDirective()
it 'returns zero when there is no counter', ->
assert.equal(controller.numReplies(), 0)
......@@ -151,10 +166,9 @@ describe 'h:directives.thread', ->
assert.equal(controller.numReplies(), 4)
describe '#shouldShowLoadMore', ->
controller = null
beforeEach ->
controller = createController()
createDirective()
describe 'when the thread filter is not active', ->
it 'is false with an empty container', ->
......@@ -176,10 +190,9 @@ describe 'h:directives.thread', ->
assert.isTrue(controller.shouldShowLoadMore())
describe '#loadMore', ->
controller = null
beforeEach ->
controller = createController()
createDirective()
it 'uncollapses the thread', ->
sinon.spy(controller, 'toggleCollapsed')
......@@ -201,45 +214,23 @@ describe 'h:directives.thread', ->
assert.calledWith(controller.filter.active, false)
describe '#matchesFilter', ->
controller = null
beforeEach ->
controller = createController()
createDirective()
it 'is true by default', ->
assert.isTrue(controller.matchesFilter())
it 'checks with the thread filter to see if the root annotation matches', ->
it 'checks with the thread filter to see if the annotation matches', ->
check = sinon.stub().returns(false)
controller.filter = {check: check}
controller.container = {}
assert.isFalse(controller.matchesFilter())
assert.calledWith(check, controller.container)
describe '.thread', ->
createElement = null
$element = null
fakePulse = null
fakeRender = null
sandbox = null
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakePulse = sandbox.spy()
fakeRender = sandbox.spy()
$provide.value 'pulse', fakePulse
$provide.value 'render', fakeRender
return
beforeEach inject ($compile, $rootScope) ->
$element = $compile('<div thread></div>')($rootScope.$new())
$rootScope.$digest()
afterEach ->
sandbox.restore()
describe 'directive', ->
beforeEach ->
createDirective()
it 'pulses the current thread on an annotationUpdated event', ->
$element.scope().$emit('annotationUpdate')
......
......@@ -134,15 +134,15 @@ ThreadFilterController = [
# Directive that instantiates
# {@link threadFilter.ThreadFilterController ThreadController}.
#
# The threadFilter directive utilizes the {@link searchfilter searchfilter}
# 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) ->
module.exports = [
'$parse', 'searchFilter'
($parse, searchFilter) ->
linkFn = (scope, elem, attrs, [ctrl, counter]) ->
if counter?
scope.$watch (-> ctrl.match), (match, old) ->
......@@ -162,17 +162,12 @@ threadFilter = [
else
scope.$watch $parse(attrs.threadFilter), (query) ->
unless query then return ctrl.active false
filters = searchfilter.generateFacetedFilter(query)
filters = searchFilter.generateFacetedFilter(query)
ctrl.filters filters
ctrl.active true
controller: 'ThreadFilterController'
controller: ThreadFilterController
controllerAs: 'threadFilter'
link: linkFn
require: ['threadFilter', '?^deepCount']
]
angular.module('h')
.controller('ThreadFilterController', ThreadFilterController)
.directive('threadFilter', threadFilter)
......@@ -152,23 +152,6 @@ ThreadController = [
]
pulseFactory = [
'$animate',
($animate) ->
###*
# @ngdoc service
# @name pulse
# @param {Element} elem Element to pulse.
# @description
# Pulses an element to indicate activity in that element.
###
(elem) ->
$animate.addClass elem, 'pulse', ->
$animate.removeClass(elem, 'pulse')
]
###*
# @ngdoc function
# @name isHiddenThread
......@@ -190,7 +173,7 @@ isHiddenThread = (elem) ->
# @description
# Directive that instantiates {@link thread.ThreadController ThreadController}.
###
thread = [
module.exports = [
'$parse', '$window', 'pulse', 'render',
($parse, $window, pulse, render) ->
linkFn = (scope, elem, attrs, [ctrl, counter, filter]) ->
......@@ -228,15 +211,9 @@ thread = [
ctrl.container = thread
scope.$digest()
controller: 'ThreadController'
controller: ThreadController
controllerAs: 'vm'
link: linkFn
require: ['thread', '?^deepCount', '?^threadFilter']
scope: true
]
angular.module('h')
.controller('ThreadController', ThreadController)
.directive('thread', thread)
.factory('pulse', pulseFactory)
module.exports = ->
link: (scope, elem, attr) ->
elem.bind 'scroll', ->
{clientHeight, scrollHeight, scrollTop} = elem[0]
if scrollHeight - scrollTop <= clientHeight + 40
scope.$apply attr.whenscrolled
......@@ -20,7 +20,7 @@
# // Establish a message bus to the new server window.
# server.stopDiscovery();
# }
class Discovery
module.exports = class Discovery
# Origins allowed to communicate on the channel
server: false
......@@ -141,8 +141,3 @@ class Discovery
_generateToken: ->
('' + Math.random()).replace(/\D/g, '')
if angular?
angular.module('h').value('Discovery', Discovery)
else
Annotator.Plugin.CrossFrame.Discovery = Discovery
module.exports = ->
_drafts = []
all: -> draft for {draft} in _drafts
add: (draft, cb) -> _drafts.push {draft, cb}
remove: (draft) ->
remove = []
for d, i in _drafts
remove.push i if d.draft is draft
while remove.length
_drafts.splice(remove.pop(), 1)
contains: (draft) ->
for d in _drafts
if d.draft is draft then return true
return false
isEmpty: -> _drafts.length is 0
discard: ->
text =
switch _drafts.length
when 0 then null
when 1
"""You have an unsaved reply.
Do you really want to discard this draft?"""
else
"""You have #{_drafts.length} unsaved replies.
Do you really want to discard these drafts?"""
if _drafts.length is 0 or confirm text
discarded = _drafts.slice()
_drafts = []
d.cb?() for d in discarded
true
else
false
var Markdown = require('../vendor/Markdown.Converter');
function Converter() {
Markdown.Converter.call(this);
this.hooks.chain('preConversion', function (text) {
return text || '';
});
this.hooks.chain('postConversion', function (text) {
return text.replace(/<a href=/g, "<a target=\"_blank\" href=");
});
}
module.exports = function () {
return (new Converter()).makeHtml;
};
module.exports = ['$window', function ($window) {
return function (value, format) {
// Determine the timezone name and browser language.
var timezone = jstz.determine().name();
var userLang = $window.navigator.language || $window.navigator.userLanguage;
// Now make a localized date and set the language.
var momentDate = moment(value);
momentDate.lang(userLang);
// Try to localize to the browser's timezone.
try {
return momentDate.tz(timezone).format('LLLL');
} catch (error) {
// For an invalid timezone, use the default.
return momentDate.format('LLLL');
}
};
}];
module.exports = function () {
return function (user, part) {
if (typeof(part) === 'undefined') {
part = 'username';
}
var index = ['term', 'username', 'provider'].indexOf(part);
var groups = null;
if (typeof(user) !== 'undefined' && user !== null) {
groups = user.match(/^acct:([^@]+)@(.+)/);
}
if (groups) {
return groups[index];
} else if (part !== 'provider') {
return user;
}
};
};
var angularMock = require('angular-mock');
var module = angularMock.module;
var inject = angularMock.inject;
var assert = chai.assert;
sinon.assert.expose(assert, {prefix: null});
describe('persona', function () {
var filter = null;
var term = 'acct:hacker@example.com';
before(function () {
angular.module('h', []).filter('persona', require('../persona'));
});
beforeEach(module('h'));
beforeEach(inject(function ($filter) {
filter = $filter('persona');
}));
it('should return the whole term by request', function () {
var result = filter('acct:hacker@example.com', 'term');
assert.equal(result, 'acct:hacker@example.com');
});
it('should return the requested part', function () {
assert.equal(filter(term), 'hacker');
assert.equal(filter(term, 'term'), term);
assert.equal(filter(term, 'username'), 'hacker');
assert.equal(filter(term, 'provider'), 'example.com');
});
it('should pass unrecognized terms as username or term', function () {
assert.equal(filter('bogus'), 'bogus');
assert.equal(filter('bogus', 'username'), 'bogus');
});
it('should handle error cases', function () {
assert.notOk(filter());
assert.notOk(filter('bogus', 'provider'));
});
});
var angularMock = require('angular-mock');
var module = angularMock.module;
var inject = angularMock.inject;
var assert = chai.assert;
sinon.assert.expose(assert, {prefix: null});
describe('urlencode', function () {
var filter = null;
before(function () {
angular.module('h', []).filter('urlencode', require('../urlencode'));
});
beforeEach(module('h'));
beforeEach(inject(function ($filter) {
filter = $filter('urlencode');
}));
it('encodes reserved characters in the term', function () {
assert.equal(filter('#hello world'), '%23hello%20world');
});
});
module.exports = function () {
return function (value) {
return encodeURIComponent(value);
};
};
angular = require('angular')
Markdown = require('./vendor/Markdown.Converter')
class Converter extends Markdown.Converter
constructor: ->
super
this.hooks.chain "preConversion", (text) ->
if text then text else ""
this.hooks.chain "postConversion", (text) ->
text.replace /<a href=/g, "<a target=\"_blank\" href="
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')
personaFilter = ->
(user, part='username') ->
index = ['term', 'username', 'provider'].indexOf(part)
groups = user?.match /^acct:([^@]+)@(.+)/
if groups
groups[index]
else if part != 'provider'
user
urlEncodeFilter = ->
(value) -> encodeURIComponent(value)
angular.module('h')
.filter('converter', -> (new Converter()).makeHtml)
.filter('moment', momentFilter)
.filter('persona', personaFilter)
.filter('urlencode', urlEncodeFilter)
angular.module('h.flash', ['toastr']).factory('flash', [
'toastr', (toastr) ->
info: angular.bind(toastr, toastr.info)
success: angular.bind(toastr, toastr.success)
warning: angular.bind(toastr, toastr.warning)
error: angular.bind(toastr, toastr.error)
])
module.exports = ['toastr', (toastr) ->
info: angular.bind(toastr, toastr.info)
success: angular.bind(toastr, toastr.success)
warning: angular.bind(toastr, toastr.warning)
error: angular.bind(toastr, toastr.error)
]
# Takes a FormController instance and an object of errors returned by the
# API and updates the validity of the form. The field.$errors.response
# property will be true if there are errors and the responseErrorMessage
# will contain the API error message.
module.exports = ->
(form, errors, reason) ->
for own field, error of errors
form[field].$setValidity('response', false)
form[field].responseErrorMessage = error
form.$setValidity('response', !reason)
form.responseErrorMessage = reason
angular = require('angular')
angular.module('h.helpers', ['bootstrap'])
require('./form-helpers')
require('./string-helpers')
require('./tag-helpers')
require('./time-helpers')
require('./ui-helpers')
require('./xsrf-service')
angular = require('angular')
unorm = require('../vendor/unorm')
# Shared helper methods for working with strings/unicode strings
# For unicode normalization we use the unorm library
createStringHelpers = ->
# Current unicode combining characters
# from http://xregexp.com/addons/unicode/unicode-categories.js line:30
allMarks = /[\u0300-\u036F\u0483-\u0489\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E4-\u08FE\u0900-\u0903\u093A-\u093C\u093E-\u094F\u0951-\u0957\u0962\u0963\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E2\u09E3\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A70\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C82\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0D02\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0F18\u0F19\u0F35\u0F37\u0F39\u0F3E\u0F3F\u0F71-\u0F84\u0F86\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102B-\u103E\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F\u109A-\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17B4-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u192B\u1930-\u193B\u19B0-\u19C0\u19C8\u19C9\u1A17-\u1A1B\u1A55-\u1A5E\u1A60-\u1A7C\u1A7F\u1B00-\u1B04\u1B34-\u1B44\u1B6B-\u1B73\u1B80-\u1B82\u1BA1-\u1BAD\u1BE6-\u1BF3\u1C24-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE8\u1CED\u1CF2-\u1CF4\u1DC0-\u1DE6\u1DFC-\u1DFF\u20D0-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302F\u3099\u309A\uA66F-\uA672\uA674-\uA67D\uA69F\uA6F0\uA6F1\uA802\uA806\uA80B\uA823-\uA827\uA880\uA881\uA8B4-\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA953\uA980-\uA983\uA9B3-\uA9C0\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA7B\uAAB0\uAAB2-\uAAB4\uAAB7\uAAB8\uAABE\uAABF\uAAC1\uAAEB-\uAAEF\uAAF5\uAAF6\uABE3-\uABEA\uABEC\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE26]/g;
uniFold: (str) ->
# normalize
str = unorm.nfkd(str)
# remove all marks
str.replace allMarks, ''
angular.module('h.helpers')
.service('stringHelpers', createStringHelpers)
{module, inject} = require('angular-mock')
assert = chai.assert
angular = require('angular')
describe 'h.helpers:form-helpers', ->
$compile = null
$scope = null
formHelpers = null
before ->
angular.module('h.helpers', [])
require('../form-helpers')
beforeEach module('h.helpers')
beforeEach inject (_$compile_, _$rootScope_, _formHelpers_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
formHelpers = _formHelpers_
describe '.formValidate', ->
$element = null
beforeEach ->
$scope.model = {username: undefined}
template = '''
<form form-validate name="login" onsubmit="return false">
<div class="form-field">
<input type="text" class="form-input" name="username"
ng-model="model.username" name="username"
required ng-minlength="3" />
</div>
</form>
'''
$element = $compile(angular.element(template))($scope)
$scope.$digest()
it 'should remove an error class to an valid field on change', ->
$field = $element.find('.form-field').addClass('form-field-error')
$input = $element.find('[name=username]').addClass('form-field-error')
$input.controller('ngModel').$setViewValue('abc')
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
assert.notInclude($input.prop('className'), 'form-field-error')
it 'should apply an error class to an invalid field on submit', ->
$field = $element.find('.form-field')
$element.triggerHandler('submit')
assert.include($field.prop('className'), 'form-field-error')
it 'should remove an error class from a valid field on submit', ->
$field = $element.find('.form-field').addClass('form-field-error')
$input = $element.find('[name=username]')
$input.val('abc').triggerHandler('input')
$element.triggerHandler('submit')
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should apply an error class if the form recieves errors after a submit action', ->
$element.trigger('submit')
$element.controller('form').username.$setValidity('response', false)
$field = $element.find('.form-field')
assert.include $field.prop('className'), 'form-field-error'
it 'should remove an error class on valid input when the view model changes', ->
$field = $element.find('.form-field').addClass('form-field-error')
$input = $element.find('[name=username]')
$input.val('abc').triggerHandler('input')
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should not add an error class on invalid input on when the view changes', ->
$field = $element.find('.form-field')
$input = $element.find('[name=username]')
$input.val('ab').triggerHandler('input')
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should reset the "response" error when the view changes', ->
$field = $element.find('.form-field')
$input = $element.find('[name=username]')
controller = $input.controller('ngModel')
controller.$setViewValue('abc')
# Submit Event
$element.triggerHandler('submit')
controller.$setValidity('response', false)
controller.responseErrorMessage = 'fail'
$scope.$digest()
assert.include($field.prop('className'), 'form-field-error', 'Fail fast check')
controller.$setViewValue('abc')
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
it 'should hide errors if the model is marked as pristine', ->
$field = $element.find('.form-field').addClass('form-field-error')
$input = $element.find('[name=username]')
controller = $input.controller('ngModel')
# Submit Event
$element.triggerHandler('submit')
controller.$setValidity('response', false)
controller.responseErrorMessage = 'fail'
$scope.$digest()
assert.include($field.prop('className'), 'form-field-error', 'Fail fast check')
# Then clear it out and mark it as pristine
controller.$setPristine()
$scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error')
describe '.applyValidationErrors', ->
form = null
beforeEach ->
form =
$setValidity: sinon.spy()
username: {$setValidity: sinon.spy()}
password: {$setValidity: sinon.spy()}
it 'sets the "response" error key for each field with errors', ->
formHelpers.applyValidationErrors form,
username: 'must be at least 3 characters'
password: 'must be present'
assert.calledWith(form.username.$setValidity, 'response', false)
assert.calledWith(form.password.$setValidity, 'response', false)
it 'adds an error message to each input controller', ->
formHelpers.applyValidationErrors form,
username: 'must be at least 3 characters'
password: 'must be present'
assert.equal(form.username.responseErrorMessage, 'must be at least 3 characters')
assert.equal(form.password.responseErrorMessage, 'must be present')
it 'sets the "response" error key if the form has a failure reason', ->
formHelpers.applyValidationErrors form, null, 'fail'
assert.calledWith(form.$setValidity, 'response', false)
it 'adds an reason message as the response error', ->
formHelpers.applyValidationErrors form, null, 'fail'
assert.equal(form.responseErrorMessage, 'fail')
angular.module('h.helpers').value('xsrf', token: null)
###*
# @ngdoc service
# @name host
#
# @description
# The `host` service relays the instructions the sidebar needs to send
# to the host document. (As opposed to all guests)
# It uses the bridge service to talk to the host.
###
class HostService
this.inject = ['$window', 'bridge']
constructor: ( $window, bridge ) ->
# Sends a message to the host frame
@_notifyHost = (message) ->
for {channel, window} in bridge.links when window is $window.parent
channel.notify(message)
break
channelListeners =
back: => @hideSidebar()
open: => @showSidebar()
for own channel, listener of channelListeners
bridge.on(channel, listener)
# Tell the host to show the sidebar
showSidebar: => @_notifyHost method: 'showFrame'
# Tell the host to hide the sidebar
hideSidebar: => @_notifyHost method: 'hideFrame'
angular.module('h').service('host', HostService)
$ = require('jquery')
Annotator = require('annotator')
Guest = require('./guest')
module.exports = class Annotator.Host extends Annotator.Guest
# Drag state variables
drag:
delta: 0
enabled: false
last: null
tick: false
constructor: (element, options) ->
# Create the iframe
if document.baseURI and window.PDFView?
# XXX: Hack around PDF.js resource: origin. Bug in jschannel?
hostOrigin = '*'
else
hostOrigin = window.location.origin
# XXX: Hack for missing window.location.origin in FF
hostOrigin ?= window.location.protocol + "//" + window.location.host
src = options.app
if options.firstRun
# Allow options.app to contain query string params.
src = src + (if '?' in src then '&' else '?') + 'firstrun'
app = $('<iframe></iframe>')
.attr('name', 'hyp_sidebar_frame')
.attr('seamless', '')
.attr('src', src)
super element, options, dontScan: true
this._addCrossFrameListeners()
app.appendTo(@frame)
if options.firstRun
this.on 'panelReady', => this.showFrame(transition: false)
# Host frame dictates the toolbar options.
this.on 'panelReady', =>
this.anchoring._scan() # Scan the document
# Guest is designed to respond to events rather than direct method
# calls. If we call set directly the other plugins will never recieve
# these events and the UI will be out of sync.
this.publish('setVisibleHighlights', !!options.showHighlights)
if @plugins.BucketBar?
this._setupDragEvents()
@plugins.BucketBar.element.on 'click', (event) =>
if @frame.hasClass 'annotator-collapsed'
this.showFrame()
showFrame: (options={transition: true}) ->
unless @drag.enabled
@frame.css 'margin-left': "#{-1 * @frame.width()}px"
if options.transition
@frame.removeClass 'annotator-no-transition'
else
@frame.addClass 'annotator-no-transition'
@frame.removeClass 'annotator-collapsed'
if @toolbar?
@toolbar.find('.annotator-toolbar-toggle')
.removeClass('h-icon-chevron-left')
.addClass('h-icon-chevron-right')
hideFrame: ->
@frame.css 'margin-left': ''
@frame.removeClass 'annotator-no-transition'
@frame.addClass 'annotator-collapsed'
if @toolbar?
@toolbar.find('.annotator-toolbar-toggle')
.removeClass('h-icon-chevron-right')
.addClass('h-icon-chevron-left')
_addCrossFrameListeners: ->
@crossframe.on('showFrame', this.showFrame.bind(this, null))
@crossframe.on('hideFrame', this.hideFrame.bind(this, null))
_setupDragEvents: ->
el = document.createElementNS 'http://www.w3.org/1999/xhtml', 'canvas'
el.width = el.height = 1
@element.append el
dragStart = (event) =>
event.dataTransfer.dropEffect = 'none'
event.dataTransfer.effectAllowed = 'none'
event.dataTransfer.setData 'text/plain', ''
event.dataTransfer.setDragImage el, 0, 0
@drag.enabled = true
@drag.last = event.screenX
m = parseInt (getComputedStyle @frame[0]).marginLeft
@frame.css
'margin-left': "#{m}px"
this.showFrame()
dragEnd = (event) =>
@drag.enabled = false
@drag.last = null
for handle in [@plugins.BucketBar.element[0], @plugins.Toolbar.buttons[0]]
handle.draggable = true
handle.addEventListener 'dragstart', dragStart
handle.addEventListener 'dragend', dragEnd
document.addEventListener 'dragover', (event) =>
this._dragUpdate event.screenX
_dragUpdate: (screenX) =>
unless @drag.enabled then return
if @drag.last?
@drag.delta += screenX - @drag.last
@drag.last = screenX
unless @drag.tick
@drag.tick = true
window.requestAnimationFrame this._dragRefresh
_dragRefresh: =>
d = @drag.delta
@drag.delta = 0
@drag.tick = false
m = parseInt (getComputedStyle @frame[0]).marginLeft
w = -1 * m
m += d
w -= d
@frame.addClass 'annotator-no-transition'
@frame.css
'margin-left': "#{m}px"
width: "#{w}px"
###*
# @ngdoc service
# @name host
#
# @description
# The `host` service relays the instructions the sidebar needs to send
# to the host document. (As opposed to all guests)
# It uses the bridge service to talk to the host.
###
module.exports = [
'$window', 'bridge'
($window, bridge) ->
host =
showSidebar: -> notifyHost method: 'showFrame'
hideSidebar: -> notifyHost method: 'hideFrame'
# Sends a message to the host frame
notifyHost = (message) ->
for {channel, window} in bridge.links when window is $window.parent
channel.notify(message)
break
channelListeners =
back: -> host.hideSidebar()
open: -> host.showSidebar()
for own channel, listener of channelListeners
bridge.on(channel, listener)
return host
]
var Annotator = require('annotator');
// Monkeypatch annotator!
require('./scripts/annotator/monkey');
require('./annotator/monkey');
// Applications
Annotator.Guest = require('./annotator/guest')
Annotator.Host = require('./annotator/host')
// Cross-frame communication
require('./scripts/annotator/plugin/cross-frame');
require('./scripts/annotation-sync');
require('./scripts/bridge');
require('./scripts/discovery');
Annotator.Plugin.CrossFrame = require('./annotator/plugin/cross-frame')
Annotator.Plugin.CrossFrame.Bridge = require('./bridge')
Annotator.Plugin.CrossFrame.AnnotationSync = require('./annotation-sync')
Annotator.Plugin.CrossFrame.Discovery = require('./discovery')
// Document plugin
require('./scripts/vendor/annotator.document');
require('./vendor/annotator.document');
// Bucket bar
require('./scripts/annotator/plugin/bucket-bar');
require('./annotator/plugin/bucket-bar');
// Toolbar
require('./scripts/annotator/plugin/toolbar');
require('./annotator/plugin/toolbar');
// Drawing highlights
require('./scripts/annotator/plugin/texthighlights');
require('./annotator/plugin/texthighlights');
// Creating selections
require('./scripts/annotator/plugin/textselection');
require('./annotator/plugin/textselection');
// URL fragments
require('./scripts/annotator/plugin/fragmentselector');
// Anchoring
require('./scripts/vendor/dom_text_mapper');
require('./scripts/annotator/plugin/enhancedanchoring');
require('./scripts/annotator/plugin/domtextmapper');
require('./scripts/annotator/plugin/textposition');
require('./scripts/annotator/plugin/textquote');
require('./scripts/annotator/plugin/textrange');
// PDF
require('./scripts/vendor/page_text_mapper_core');
require('./scripts/annotator/plugin/pdf');
// Fuzzy
require('./scripts/vendor/dom_text_matcher');
require('./scripts/annotator/plugin/fuzzytextanchors');
var Klass = require('./scripts/host');
require('./annotator/plugin/fragmentselector');
// Anchoring dependencies
require('diff-match-patch')
require('dom-text-mapper')
require('dom-text-matcher')
require('page-text-mapper-core')
require('text-match-engines')
// Anchoring plugins
require('./annotator/plugin/enhancedanchoring');
require('./annotator/plugin/domtextmapper');
require('./annotator/plugin/fuzzytextanchors');
require('./annotator/plugin/pdf');
require('./annotator/plugin/textquote');
require('./annotator/plugin/textposition');
require('./annotator/plugin/textrange');
var Klass = Annotator.Host;
var docs = 'https://github.com/hypothesis/h/blob/master/README.rst#customized-embedding';
var options = {
app: jQuery('link[type="application/annotator+html"]').attr('href'),
......
......@@ -28,7 +28,7 @@
# An application wishing to export an identity provider should override all
# of the public methods of this provider.
###
identityProvider = ->
module.exports = ->
checkAuthentication: ['$q', ($q) ->
$q.reject 'Not implemented idenityProvider#checkAuthentication.'
]
......@@ -94,7 +94,3 @@ identityProvider = ->
result = $injector.invoke(provider.checkAuthentication, provider)
$q.when(result).then(onlogin).finally(-> onready?())
]
angular.module('h.identity', [])
.provider('identity', identityProvider)
......@@ -46,7 +46,8 @@ module.exports = function(config) {
'test/bootstrap.coffee',
// Tests
'**/*-test.coffee'
'**/*-test.coffee',
'**/*-test.js'
],
......@@ -63,7 +64,8 @@ module.exports = function(config) {
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'**/*.coffee': ['browserify'],
'**/*-test.js': ['browserify'],
'**/*-test.coffee': ['browserify'],
'../../templates/client/*.html': ['ng-html2js'],
},
......
localstorage = ['$window', ($window) ->
module.exports = ['$window', ($window) ->
# Detection is needed because we run often as a third party widget and
# third party storage blocking often blocks cookies and local storage
# https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js
......@@ -33,6 +33,3 @@ localstorage = ['$window', ($window) ->
storage.removeItem key
}
]
angular.module('h')
.service('localstorage', localstorage)
......@@ -6,7 +6,7 @@
# This service can set default permissions to annotations properly and
# offers some utility functions regarding those.
###
class Permissions
module.exports = ['auth', (auth) ->
ALL_PERMISSIONS = {}
GROUP_WORLD = 'group:__world__'
ADMIN_PARTY = [{
......@@ -15,37 +15,45 @@ class Permissions
action: ALL_PERMISSIONS
}]
this.$inject = ['auth']
constructor: (auth) ->
###*
# @ngdoc method
# @name permissions#private
#
# Sets permissions for a private annotation
# Typical use: annotation.permissions = permissions.private()
###
@private = ->
return {
read: [auth.user]
update: [auth.user]
delete: [auth.user]
admin: [auth.user]
}
# Creates access control list from context.permissions
_acl = (context) ->
parts =
for action, roles of context.permissions or []
for role in roles
allow: true
principal: role
action: action
if parts.length
Array::concat parts...
else
ADMIN_PARTY
###*
# @ngdoc method
# @name permissions#private
#
# Sets permissions for a public annotation
# Typical use: annotation.permissions = permissions.public()
###
@public = ->
return {
read: [GROUP_WORLD]
update: [auth.user]
delete: [auth.user]
admin: [auth.user]
}
###*
# @ngdoc method
# @name permissions#private
#
# Sets permissions for a private annotation
# Typical use: annotation.permissions = permissions.private()
###
private: ->
read: [auth.user]
update: [auth.user]
delete: [auth.user]
admin: [auth.user]
###*
# @ngdoc method
# @name permissions#private
#
# Sets permissions for a public annotation
# Typical use: annotation.permissions = permissions.public()
###
public: ->
read: [GROUP_WORLD]
update: [auth.user]
delete: [auth.user]
admin: [auth.user]
###*
# @ngdoc method
......@@ -70,20 +78,6 @@ class Permissions
isPrivate: (permissions, user) ->
user and angular.equals(permissions?.read or [], [user])
# Creates access-level-control object list
_acl = (context) ->
parts =
for action, roles of context.permissions or []
for role in roles
allow: true
principal: role
action: action
if parts.length
Array::concat parts...
else
ADMIN_PARTY
###*
# @ngdoc method
# @name permissions#permits
......@@ -105,7 +99,4 @@ class Permissions
return ace.allow
false
angular.module('h')
.service('permissions', Permissions)
]
###*
# @ngdoc service
# @name pulse
# @param {Element} elem Element to pulse.
# @description
# Pulses an element to indicate activity in that element.
###
module.exports = ['$animate', ($animate) ->
(elem) ->
$animate.addClass elem, 'pulse', ->
$animate.removeClass(elem, 'pulse')
]
# This class will process the results of search and generate the correct filter
# It expects the following dict format as rules
# { facet_name : {
# formatter: to format the value (optional)
# path: json path mapping to the annotation field
# case_sensitive: true|false (default: false)
# and_or: and|or for multiple values should it threat them as 'or' or 'and' (def: or)
# operator: if given it'll use this operator regardless of other circumstances
#
# options: backend specific options
# options.es: elasticsearch specific options
# options.es.query_type : can be: simple (term), query_string, match, multi_match
# defaults to: simple, determines which es query type to use
# options.es.cutoff_frequency: if set, the query will be given a cutoff_frequency for this facet
# options.es.and_or: match and multi_match queries can use this, defaults to and
# options.es.match_type: multi_match query type
# options.es.fields: fields to search for in multi-match query
# }
# The models is the direct output from visualsearch
module.exports = class QueryParser
rules:
user:
path: '/user'
and_or: 'or'
text:
path: '/text'
and_or: 'and'
tag:
path: '/tags'
and_or: 'and'
quote:
path: '/quote'
and_or: 'and'
uri:
formatter: (uri) ->
uri.toLowerCase()
path: '/uri'
and_or: 'or'
options:
es:
query_type: 'match'
cutoff_frequency: 0.001
and_or: 'and'
since:
formatter: (past) ->
seconds =
switch past
when '5 min' then 5*60
when '30 min' then 30*60
when '1 hour' then 60*60
when '12 hours' then 12*60*60
when '1 day' then 24*60*60
when '1 week' then 7*24*60*60
when '1 month' then 30*24*60*60
when '1 year' then 365*24*60*60
new Date(new Date().valueOf() - seconds*1000)
path: '/created'
and_or: 'and'
operator: 'ge'
any:
and_or: 'and'
path: ['/quote', '/tags', '/text', '/uri', '/user']
options:
es:
query_type: 'multi_match'
match_type: 'cross_fields'
and_or: 'and'
fields: ['quote', 'tags', 'text', 'uri', 'user']
populateFilter: (filter, query) =>
# Populate a filter with a query object
for category, value of query
unless @rules[category]? then continue
terms = value.terms
unless terms.length then continue
rule = @rules[category]
# Now generate the clause with the help of the rule
case_sensitive = if rule.case_sensitive? then rule.case_sensitive else false
and_or = if rule.and_or? then rule.and_or else 'or'
mapped_field = if rule.path? then rule.path else '/'+category
if and_or is 'or'
oper_part = if rule.operator? then rule.operator else 'match_of'
value_part = []
for term in terms
t = if rule.formatter then rule.formatter term else term
value_part.push t
filter.addClause mapped_field, oper_part, value_part, case_sensitive, rule.options
else
oper_part = if rule.operator? then rule.operator else 'matches'
for val in terms
value_part = if rule.formatter then rule.formatter val else val
filter.addClause mapped_field, oper_part, value_part, case_sensitive, rule.options
###*
# @ngdoc service
# @name render
# @param {function()} fn A function to execute in a future animation frame.
# @returns {function()} A function to cancel the execution.
# @description
# The render service is a wrapper around `window#requestAnimationFrame()` for
# scheduling sequential updates in successive animation frames. It has the
# same signature as the original function, but will queue successive calls
# for future frames so that at most one callback is handled per animation frame.
# Use this service to schedule DOM-intensive digests.
###
module.exports = ['$$rAF', ($$rAF) ->
cancel = null
queue = []
render = ->
return cancel = null if queue.length is 0
do queue.shift()
$$rAF(render)
(fn) ->
queue.push fn
unless cancel then cancel = $$rAF(render)
-> queue = (f for f in queue when f isnt fn)
]
# This class will parse the search filter and produce a faceted search filter object
# It expects a search query string where the search term are separated by space character
# and collects them into the given term arrays
class SearchFilter
module.exports = class SearchFilter
# This function will slice the search-text input
# Slice character: space,
......@@ -165,185 +165,3 @@ class SearchFilter
user:
terms: user
operator: 'or'
# This class will process the results of search and generate the correct filter
# It expects the following dict format as rules
# { facet_name : {
# formatter: to format the value (optional)
# path: json path mapping to the annotation field
# case_sensitive: true|false (default: false)
# and_or: and|or for multiple values should it threat them as 'or' or 'and' (def: or)
# operator: if given it'll use this operator regardless of other circumstances
#
# options: backend specific options
# options.es: elasticsearch specific options
# options.es.query_type : can be: simple (term), query_string, match, multi_match
# defaults to: simple, determines which es query type to use
# options.es.cutoff_frequency: if set, the query will be given a cutoff_frequency for this facet
# options.es.and_or: match and multi_match queries can use this, defaults to and
# options.es.match_type: multi_match query type
# options.es.fields: fields to search for in multi-match query
# }
# The models is the direct output from visualsearch
class QueryParser
rules:
user:
path: '/user'
and_or: 'or'
text:
path: '/text'
and_or: 'and'
tag:
path: '/tags'
and_or: 'and'
quote:
path: '/quote'
and_or: 'and'
uri:
formatter: (uri) ->
uri.toLowerCase()
path: '/uri'
and_or: 'or'
options:
es:
query_type: 'match'
cutoff_frequency: 0.001
and_or: 'and'
since:
formatter: (past) ->
seconds =
switch past
when '5 min' then 5*60
when '30 min' then 30*60
when '1 hour' then 60*60
when '12 hours' then 12*60*60
when '1 day' then 24*60*60
when '1 week' then 7*24*60*60
when '1 month' then 30*24*60*60
when '1 year' then 365*24*60*60
new Date(new Date().valueOf() - seconds*1000)
path: '/created'
and_or: 'and'
operator: 'ge'
any:
and_or: 'and'
path: ['/quote', '/tags', '/text', '/uri', '/user']
options:
es:
query_type: 'multi_match'
match_type: 'cross_fields'
and_or: 'and'
fields: ['quote', 'tags', 'text', 'uri', 'user']
populateFilter: (filter, query) =>
# Populate a filter with a query object
for category, value of query
unless @rules[category]? then continue
terms = value.terms
unless terms.length then continue
rule = @rules[category]
# Now generate the clause with the help of the rule
case_sensitive = if rule.case_sensitive? then rule.case_sensitive else false
and_or = if rule.and_or? then rule.and_or else 'or'
mapped_field = if rule.path? then rule.path else '/'+category
if and_or is 'or'
oper_part = if rule.operator? then rule.operator else 'match_of'
value_part = []
for term in terms
t = if rule.formatter then rule.formatter term else term
value_part.push t
filter.addClause mapped_field, oper_part, value_part, case_sensitive, rule.options
else
oper_part = if rule.operator? then rule.operator else 'matches'
for val in terms
value_part = if rule.formatter then rule.formatter val else val
filter.addClause mapped_field, oper_part, value_part, case_sensitive, rule.options
class StreamFilter
strategies: ['include_any', 'include_all', 'exclude_any', 'exclude_all']
filter:
match_policy : 'include_any'
clauses : []
actions :
create: true
update: true
delete: true
constructor: ->
getFilter: -> return @filter
getMatchPolicy: -> return @filter.match_policy
getClauses: -> return @filter.clauses
getActions: -> return @filter.actions
getActionCreate: -> return @filter.actions.create
getActionUpdate: -> return @filter.actions.update
getActionDelete: -> return @filter.actions.delete
setMatchPolicy: (policy) ->
@filter.match_policy = policy
this
setMatchPolicyIncludeAny: ->
@filter.match_policy = 'include_any'
this
setMatchPolicyIncludeAll: ->
@filter.match_policy = 'include_all'
this
setMatchPolicyExcludeAny: ->
@filter.match_policy = 'exclude_any'
this
setMatchPolicyExcludeAll: ->
@filter.match_policy = 'exclude_all'
this
setActions: (actions) ->
@filter.actions = actions
this
setActionCreate: (action) ->
@filter.actions.create = action
this
setActionUpdate: (action) ->
@filter.actions.update = action
this
setActionDelete: (action) ->
@filter.actions.delete = action
this
noClauses: ->
@filter.clauses = []
this
addClause: (field, operator, value, case_sensitive = false, options = {}) ->
@filter.clauses.push
field: field
operator: operator
value: value
case_sensitive: case_sensitive
options: options
this
resetFilter: ->
@setMatchPolicyIncludeAny()
@setActionCreate(true)
@setActionUpdate(true)
@setActionDelete(true)
@noClauses()
this
angular.module('h')
.service('searchfilter', SearchFilter)
.service('queryparser', QueryParser)
.service('streamfilter', StreamFilter)
angular = require('angular')
###*
# @ngdoc provider
# @name sessionProvider
......@@ -9,7 +11,7 @@
# that return the state of the users session after modifying it through
# registration, authentication, or account management.
###
class SessionProvider
module.exports = class SessionProvider
actions: null
options: null
......@@ -77,7 +79,3 @@ class SessionProvider
endpoint = new URL('/app', base).href
$resource(endpoint, {}, actions)
]
angular.module('h.session')
.provider('session', SessionProvider)
angular = require('angular')
angular.module('h.session', ['ngResource', 'h.helpers'])
require('./session-service')
......@@ -9,8 +9,7 @@
# constructor for each endpoint eg. store.AnnotationResource() and
# store.SearchResource().
###
angular.module('h')
.service('store', [
module.exports = [
'$document', '$http', '$resource',
($document, $http, $resource) ->
......@@ -37,4 +36,4 @@ angular.module('h')
prop = "#{camelize(name)}Resource"
store[prop] = $resource(actions.url or svc, {}, actions)
store
])
]
class StreamSearchController
angular = require('angular')
module.exports = class StreamController
this.inject = [
'$scope', '$rootScope', '$routeParams',
'auth', 'queryparser', 'searchfilter', 'store',
'streamer', 'streamfilter', 'annotationMapper'
'auth', 'queryParser', 'searchFilter', 'store',
'streamer', 'streamFilter', 'annotationMapper'
]
constructor: (
$scope, $rootScope, $routeParams
auth, queryparser, searchfilter, store,
streamer, streamfilter, annotationMapper
auth, queryParser, searchFilter, store,
streamer, streamFilter, annotationMapper
) ->
# Initialize the base filter
streamfilter
streamFilter
.resetFilter()
.setMatchPolicyIncludeAll()
# Apply query clauses
$scope.search.query = $routeParams.q
terms = searchfilter.generateFacetedFilter $scope.search.query
queryparser.populateFilter streamfilter, terms
streamer.send({filter: streamfilter.getFilter()})
terms = searchFilter.generateFacetedFilter $scope.search.query
queryParser.populateFilter streamFilter, terms
streamer.send({filter: streamFilter.getFilter()})
# Perform the search
searchParams = searchfilter.toObject $scope.search.query
searchParams = searchFilter.toObject $scope.search.query
query = angular.extend limit: 10, searchParams
store.SearchResource.get query, ({rows}) ->
annotationMapper.loadAnnotations(rows)
......@@ -35,6 +38,3 @@ class StreamSearchController
$scope.$on '$destroy', ->
$scope.search.query = ''
angular.module('h')
.controller('StreamSearchController', StreamSearchController)
module.exports = class StreamFilter
strategies: ['include_any', 'include_all', 'exclude_any', 'exclude_all']
filter:
match_policy : 'include_any'
clauses : []
actions :
create: true
update: true
delete: true
constructor: ->
getFilter: -> return @filter
getMatchPolicy: -> return @filter.match_policy
getClauses: -> return @filter.clauses
getActions: -> return @filter.actions
getActionCreate: -> return @filter.actions.create
getActionUpdate: -> return @filter.actions.update
getActionDelete: -> return @filter.actions.delete
setMatchPolicy: (policy) ->
@filter.match_policy = policy
this
setMatchPolicyIncludeAny: ->
@filter.match_policy = 'include_any'
this
setMatchPolicyIncludeAll: ->
@filter.match_policy = 'include_all'
this
setMatchPolicyExcludeAny: ->
@filter.match_policy = 'exclude_any'
this
setMatchPolicyExcludeAll: ->
@filter.match_policy = 'exclude_all'
this
setActions: (actions) ->
@filter.actions = actions
this
setActionCreate: (action) ->
@filter.actions.create = action
this
setActionUpdate: (action) ->
@filter.actions.update = action
this
setActionDelete: (action) ->
@filter.actions.delete = action
this
noClauses: ->
@filter.clauses = []
this
addClause: (field, operator, value, case_sensitive = false, options = {}) ->
@filter.clauses.push
field: field
operator: operator
value: value
case_sensitive: case_sensitive
options: options
this
resetFilter: ->
@setMatchPolicyIncludeAny()
@setActionCreate(true)
@setActionUpdate(true)
@setActionDelete(true)
@noClauses()
this
......@@ -12,7 +12,7 @@ ST_CLOSED = 3
# @description
# Provides access to the streamer websocket.
###
class Streamer
module.exports = class Streamer
constructor: ->
this.clientId = null
......@@ -119,7 +119,3 @@ class Streamer
backoff = (index, max) ->
index = Math.min(index, max)
return 500 * Math.random() * (Math.pow(2, index) - 1)
angular.module('h.streamer', [])
.service('streamer', Streamer)
createTagHelpers = ['localstorage', (localstorage) ->
module.exports = ['localStorage', (localStorage) ->
TAGS_LIST_KEY = 'hypothesis.user.tags.list'
TAGS_MAP_KEY = 'hypothesis.user.tags.map'
filterTags: (query) ->
savedTags = localstorage.getObject TAGS_LIST_KEY
filter: (query) ->
savedTags = localStorage.getObject TAGS_LIST_KEY
savedTags ?= []
# Only show tags having query as a substring
......@@ -14,8 +14,8 @@ createTagHelpers = ['localstorage', (localstorage) ->
# Add newly added tags from an annotation to the stored ones and refresh
# timestamp for every tags used.
storeTags: (tags) ->
savedTags = localstorage.getObject TAGS_MAP_KEY
store: (tags) ->
savedTags = localStorage.getObject TAGS_MAP_KEY
savedTags ?= {}
for tag in tags
......@@ -31,7 +31,7 @@ createTagHelpers = ['localstorage', (localstorage) ->
updated: Date.now()
}
localstorage.setObject TAGS_MAP_KEY, savedTags
localStorage.setObject TAGS_MAP_KEY, savedTags
tagsList = []
for tag of savedTags
......@@ -47,8 +47,5 @@ createTagHelpers = ['localstorage', (localstorage) ->
return 0
tagsList = tagsList.sort(compareFn)
localstorage.setObject TAGS_LIST_KEY, tagsList
localStorage.setObject TAGS_LIST_KEY, tagsList
]
angular.module('h.helpers')
.service('tagHelpers', createTagHelpers)
......@@ -4,7 +4,7 @@ assert = chai.assert
sinon.assert.expose(assert, prefix: '')
describe 'AnnotationMapperService', ->
describe 'annotationMapper', ->
sandbox = sinon.sandbox.create()
$rootScope = null
......@@ -14,7 +14,7 @@ describe 'AnnotationMapperService', ->
before ->
angular.module('h', [])
require('../annotation-mapper-service')
.service('annotationMapper', require('../annotation-mapper'))
beforeEach module('h')
beforeEach module ($provide) ->
......@@ -125,4 +125,3 @@ describe 'AnnotationMapperService', ->
p = Promise.resolve()
ann = {$delete: sandbox.stub().returns(p)}
assert.equal(annotationMapper.deleteAnnotation(ann), ann)
......@@ -15,7 +15,7 @@ describe 'AnnotationSync', ->
before ->
angular.module('h', [])
require('../annotation-sync')
.value('AnnotationSync', require('../annotation-sync'))
beforeEach module('h')
beforeEach inject (AnnotationSync, $rootScope) ->
......
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'AnnotationUIController', ->
$scope = null
$rootScope = null
annotationUI = null
sandbox = null
before ->
angular.module('h', [])
.controller('AnnotationUIController', require('../annotation-ui-controller'))
beforeEach module('h')
beforeEach inject ($controller, _$rootScope_) ->
sandbox = sinon.sandbox.create()
$rootScope = _$rootScope_
$scope = $rootScope.$new()
$scope.search = {}
annotationUI =
tool: 'comment'
selectedAnnotationMap: null
focusedAnnotationsMap: null
removeSelectedAnnotation: sandbox.stub()
$controller 'AnnotationUIController', {$scope, annotationUI}
afterEach ->
sandbox.restore()
it 'updates the view when the selection changes', ->
annotationUI.selectedAnnotationMap = {1: true, 2: true}
$rootScope.$digest()
assert.deepEqual($scope.selectedAnnotations, {1: true, 2: true})
it 'updates the selection counter when the selection changes', ->
annotationUI.selectedAnnotationMap = {1: true, 2: true}
$rootScope.$digest()
assert.deepEqual($scope.selectedAnnotationsCount, 2)
it 'clears the selection when no annotations are selected', ->
annotationUI.selectedAnnotationMap = {}
$rootScope.$digest()
assert.deepEqual($scope.selectedAnnotations, null)
assert.deepEqual($scope.selectedAnnotationsCount, 0)
it 'updates the focused annotations when the focus map changes', ->
annotationUI.focusedAnnotationMap = {1: true, 2: true}
$rootScope.$digest()
assert.deepEqual($scope.focusedAnnotations, {1: true, 2: true})
describe 'on annotationDeleted', ->
it 'removes the deleted annotation from the selection', ->
$rootScope.$emit('annotationDeleted', {id: 1})
assert.calledWith(annotationUI.removeSelectedAnnotation, {id: 1})
......@@ -18,7 +18,7 @@ describe 'AnnotationUISync', ->
before ->
angular.module('h', [])
require('../annotation-ui-sync')
.value('AnnotationUISync', require('../annotation-ui-sync'))
beforeEach module('h')
beforeEach inject (AnnotationUISync, $rootScope) ->
......
......@@ -4,12 +4,12 @@ assert = chai.assert
sinon.assert.expose(assert, prefix: '')
describe 'AnnotationUI', ->
describe 'annotationUI', ->
annotationUI = null
before ->
angular.module('h', [])
require('../annotation-ui-service')
.service('annotationUI', require('../annotation-ui'))
beforeEach module('h')
beforeEach inject (_annotationUI_) ->
......
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'AnnotationViewerController', ->
annotationViewerController = null
before ->
angular.module('h', ['ngRoute'])
.controller('AnnotationViewerController', require('../annotation-viewer-controller'))
beforeEach inject ($controller, $rootScope) ->
$scope = $rootScope.$new()
$scope.search = {}
annotationViewerController = $controller 'AnnotationViewerController',
$scope: $scope
it 'sets the isEmbedded property to false', ->
assert.isFalse($scope.isEmbedded)
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'AppController', ->
$controller = null
$scope = null
fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeAuth = null
fakeDrafts = null
fakeIdentity = null
fakeLocation = null
fakeParams = null
fakePermissions = null
fakeStore = null
fakeStreamer = null
fakeStreamFilter = null
fakeThreading = null
sandbox = null
createController = ->
$controller('AppController', {$scope: $scope})
before ->
angular.module('h', ['ngRoute'])
.controller('AppController', require('../app-controller'))
.controller('AnnotationUIController', angular.noop)
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeAnnotationMapper = {
loadAnnotations: sandbox.spy()
}
fakeAnnotationUI = {
tool: 'comment'
clearSelectedAnnotations: sandbox.spy()
}
fakeAuth = {
user: undefined
}
fakeDrafts = {
remove: sandbox.spy()
all: sandbox.stub().returns([])
discard: sandbox.spy()
}
fakeIdentity = {
watch: sandbox.spy()
request: sandbox.spy()
}
fakeLocation = {
search: sandbox.stub().returns({})
}
fakeParams = {id: 'test'}
fakePermissions = {
permits: sandbox.stub().returns(true)
}
fakeStore = {
SearchResource: {
get: sinon.spy()
}
}
fakeStreamer = {
open: sandbox.spy()
close: sandbox.spy()
send: sandbox.spy()
}
fakeStreamFilter = {
setMatchPolicyIncludeAny: sandbox.stub().returnsThis()
addClause: sandbox.stub().returnsThis()
getFilter: sandbox.stub().returns({})
}
fakeThreading = {
idTable: {}
register: (annotation) ->
@idTable[annotation.id] = message: annotation
}
$provide.value 'annotationMapper', fakeAnnotationMapper
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value 'auth', fakeAuth
$provide.value 'drafts', fakeDrafts
$provide.value 'identity', fakeIdentity
$provide.value '$location', fakeLocation
$provide.value '$routeParams', fakeParams
$provide.value 'permissions', fakePermissions
$provide.value 'store', fakeStore
$provide.value 'streamer', fakeStreamer
$provide.value 'streamfilter', fakeStreamFilter
$provide.value 'threading', fakeThreading
return
beforeEach inject (_$controller_, $rootScope) ->
$controller = _$controller_
$scope = $rootScope.$new()
$scope.$digest = sinon.spy()
afterEach ->
sandbox.restore()
it 'does not show login form for logged in users', ->
createController()
assert.isFalse($scope.dialog.visible)
describe 'applyUpdate', ->
it 'calls annotationMapper.loadAnnotations() upon "create" action', ->
createController()
anns = ["my", "annotations"]
fakeStreamer.onmessage
type: "annotation-notification"
options: action: "create"
payload: anns
assert.calledWith fakeAnnotationMapper.loadAnnotations, anns
it 'calls annotationMapper.loadAnnotations() upon "update" action', ->
createController()
anns = ["my", "annotations"]
fakeStreamer.onmessage
type: "annotation-notification"
options: action: "update"
payload: anns
assert.calledWith fakeAnnotationMapper.loadAnnotations, anns
it 'calls annotationMapper.loadAnnotations() upon "past" action', ->
createController()
anns = ["my", "annotations"]
fakeStreamer.onmessage
type: "annotation-notification"
options: action: "past"
payload: anns
assert.calledWith fakeAnnotationMapper.loadAnnotations, anns
it 'looks up annotations at threading upon "delete" action', ->
createController()
$scope.$emit = sinon.spy()
# Prepare the annotation that we have locally
localAnnotation =
id: "fake ID"
data: "local data"
# Introduce our annotation into threading
fakeThreading.register localAnnotation
# Prepare the annotation that will come "from the wire"
remoteAnnotation =
id: localAnnotation.id # same id as locally
data: "remote data" # different data
# Simulate a delete action
fakeStreamer.onmessage
type: "annotation-notification"
options: action: "delete"
payload: [ remoteAnnotation ]
assert.calledWith $scope.$emit, "annotationDeleted", localAnnotation
......@@ -11,7 +11,7 @@ describe 'h', ->
before ->
angular.module('h', [])
require('../auth-service')
.factory('auth', require('../auth'))
beforeEach module('h')
......
......@@ -12,7 +12,7 @@ describe 'Bridge', ->
before ->
angular.module('h', [])
require('../bridge')
.service('bridge', require('../bridge'))
beforeEach module('h')
beforeEach inject (_bridge_) ->
......
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'h:controllers', ->
before ->
angular.module('h', ['ngRoute'])
require('../controllers')
describe 'AppController', ->
$scope = null
fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeAuth = null
fakeDrafts = null
fakeIdentity = null
fakeLocation = null
fakeParams = null
fakePermissions = null
fakeStore = null
fakeStreamer = null
fakeStreamFilter = null
fakeThreading = null
sandbox = null
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeAnnotationMapper = {
loadAnnotations: sandbox.spy()
}
fakeAnnotationUI = {
tool: 'comment'
clearSelectedAnnotations: sandbox.spy()
}
fakeAuth = {
user: undefined
}
fakeDrafts = {
remove: sandbox.spy()
all: sandbox.stub().returns([])
discard: sandbox.spy()
}
fakeIdentity = {
watch: sandbox.spy()
request: sandbox.spy()
}
fakeLocation = {
search: sandbox.stub().returns({})
}
fakeParams = {id: 'test'}
fakePermissions = {
permits: sandbox.stub().returns(true)
}
fakeStore = {
SearchResource: {
get: sinon.spy()
}
}
fakeStreamer = {
open: sandbox.spy()
close: sandbox.spy()
send: sandbox.spy()
}
fakeStreamFilter = {
setMatchPolicyIncludeAny: sandbox.stub().returnsThis()
addClause: sandbox.stub().returnsThis()
getFilter: sandbox.stub().returns({})
}
fakeThreading = {
idTable: {}
register: (annotation) ->
@idTable[annotation.id] = message: annotation
}
$provide.value 'annotationMapper', fakeAnnotationMapper
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value 'auth', fakeAuth
$provide.value 'drafts', fakeDrafts
$provide.value 'identity', fakeIdentity
$provide.value '$location', fakeLocation
$provide.value '$routeParams', fakeParams
$provide.value 'permissions', fakePermissions
$provide.value 'store', fakeStore
$provide.value 'streamer', fakeStreamer
$provide.value 'streamfilter', fakeStreamFilter
$provide.value 'threading', fakeThreading
return
afterEach ->
sandbox.restore()
describe 'AppController', ->
createController = null
beforeEach inject ($controller, $rootScope) ->
$scope = $rootScope.$new()
$scope.$digest = sinon.spy()
createController = ->
$controller('AppController', {$scope: $scope})
it 'does not show login form for logged in users', ->
createController()
assert.isFalse($scope.dialog.visible)
describe 'applyUpdate', ->
it 'calls annotationMapper.loadAnnotations() upon "create" action', ->
createController()
anns = ["my", "annotations"]
fakeStreamer.onmessage
type: "annotation-notification"
options: action: "create"
payload: anns
assert.calledWith fakeAnnotationMapper.loadAnnotations, anns
it 'calls annotationMapper.loadAnnotations() upon "update" action', ->
createController()
anns = ["my", "annotations"]
fakeStreamer.onmessage
type: "annotation-notification"
options: action: "update"
payload: anns
assert.calledWith fakeAnnotationMapper.loadAnnotations, anns
it 'calls annotationMapper.loadAnnotations() upon "past" action', ->
createController()
anns = ["my", "annotations"]
fakeStreamer.onmessage
type: "annotation-notification"
options: action: "past"
payload: anns
assert.calledWith fakeAnnotationMapper.loadAnnotations, anns
it 'looks up annotations at threading upon "delete" action', ->
createController()
$scope.$emit = sinon.spy()
# Prepare the annotation that we have locally
localAnnotation =
id: "fake ID"
data: "local data"
# Introduce our annotation into threading
fakeThreading.register localAnnotation
# Prepare the annotation that will come "from the wire"
remoteAnnotation =
id: localAnnotation.id # same id as locally
data: "remote data" # different data
# Simulate a delete action
fakeStreamer.onmessage
type: "annotation-notification"
options: action: "delete"
payload: [ remoteAnnotation ]
assert.calledWith $scope.$emit, "annotationDeleted", localAnnotation
describe 'AnnotationViewerController', ->
annotationViewer = null
beforeEach inject ($controller, $rootScope) ->
$scope = $rootScope.$new()
$scope.search = {}
annotationViewer = $controller 'AnnotationViewerController',
$scope: $scope
it 'sets the isEmbedded property to false', ->
assert.isFalse($scope.isEmbedded)
describe 'ViewerController', ->
$scope = null
fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeAuth = null
fakeCrossFrame = null
fakeStore = null
fakeStreamer = null
fakeStreamFilter = null
sandbox = null
viewer = null
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeAnnotationMapper = {loadAnnotations: sandbox.spy()}
fakeAnnotationUI = {
tool: 'comment'
clearSelectedAnnotations: sandbox.spy()
}
fakeAuth = {user: null}
fakeCrossFrame = {providers: []}
fakeStore = {
SearchResource:
get: (query, callback) ->
offset = query.offset or 0
limit = query.limit or 20
result =
total: 100
rows: [offset..offset+limit-1]
callback result
}
fakeStreamer = {
open: sandbox.spy()
close: sandbox.spy()
send: sandbox.spy()
}
fakeStreamFilter = {
resetFilter: sandbox.stub().returnsThis()
addClause: sandbox.stub().returnsThis()
getFilter: sandbox.stub().returns({})
}
$provide.value 'annotationMapper', fakeAnnotationMapper
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value 'auth', fakeAuth
$provide.value 'crossframe', fakeCrossFrame
$provide.value 'store', fakeStore
$provide.value 'streamer', fakeStreamer
$provide.value 'streamfilter', fakeStreamFilter
return
beforeEach inject ($controller, $rootScope) ->
$scope = $rootScope.$new()
viewer = $controller 'ViewerController', {$scope}
afterEach ->
sandbox.restore()
describe 'loadAnnotations', ->
it 'loads all annotation for a provider', ->
fakeCrossFrame.providers.push {entities: ['http://example.com']}
$scope.$digest()
loadSpy = fakeAnnotationMapper.loadAnnotations
assert.callCount(loadSpy, 5)
assert.calledWith(loadSpy, [0..19])
assert.calledWith(loadSpy, [20..39])
assert.calledWith(loadSpy, [40..59])
assert.calledWith(loadSpy, [60..79])
assert.calledWith(loadSpy, [80..99])
describe 'AnnotationUIController', ->
$scope = null
$rootScope = null
annotationUI = null
sandbox = null
beforeEach module('h')
beforeEach inject ($controller, _$rootScope_) ->
sandbox = sinon.sandbox.create()
$rootScope = _$rootScope_
$scope = $rootScope.$new()
$scope.search = {}
annotationUI =
tool: 'comment'
selectedAnnotationMap: null
focusedAnnotationsMap: null
removeSelectedAnnotation: sandbox.stub()
$controller 'AnnotationUIController', {$scope, annotationUI}
afterEach ->
sandbox.restore()
it 'updates the view when the selection changes', ->
annotationUI.selectedAnnotationMap = {1: true, 2: true}
$rootScope.$digest()
assert.deepEqual($scope.selectedAnnotations, {1: true, 2: true})
it 'updates the selection counter when the selection changes', ->
annotationUI.selectedAnnotationMap = {1: true, 2: true}
$rootScope.$digest()
assert.deepEqual($scope.selectedAnnotationsCount, 2)
it 'clears the selection when no annotations are selected', ->
annotationUI.selectedAnnotationMap = {}
$rootScope.$digest()
assert.deepEqual($scope.selectedAnnotations, null)
assert.deepEqual($scope.selectedAnnotationsCount, 0)
it 'updates the focused annotations when the focus map changes', ->
annotationUI.focusedAnnotationMap = {1: true, 2: true}
$rootScope.$digest()
assert.deepEqual($scope.focusedAnnotations, {1: true, 2: true})
describe 'on annotationDeleted', ->
it 'removes the deleted annotation from the selection', ->
$rootScope.$emit('annotationDeleted', {id: 1})
assert.calledWith(annotationUI.removeSelectedAnnotation, {id: 1})
......@@ -4,7 +4,7 @@ assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'CrossFrameService', ->
describe 'CrossFrame', ->
sandbox = sinon.sandbox.create()
crossframe = null
$rootScope = null
......@@ -19,7 +19,7 @@ describe 'CrossFrameService', ->
before ->
angular.module('h', [])
require('../cross-frame-service')
.service('crossframe', require('../cross-frame'))
beforeEach module('h')
beforeEach module ($provide) ->
......
{module, inject} = require('angular-mock')
assert = chai.assert
describe 'h:directives', ->
before ->
angular.module('h', [])
require('../directives')
beforeEach module('h')
describe '.match', ->
$compile = null
$element = null
$isolateScope = null
$scope = null
beforeEach inject (_$compile_, _$rootScope_) ->
$compile = _$compile_
$scope = _$rootScope_.$new()
beforeEach ->
$scope.model = {a: 1, b: 1}
$element = $compile('<input name="confirmation" ng-model="model.b" match="model.a" />')($scope)
$isolateScope = $element.isolateScope()
$scope.$digest()
it 'is valid if both properties have the same value', ->
controller = $element.controller('ngModel')
assert.isFalse(controller.$error.match)
it 'is invalid if the local property differs', ->
$isolateScope.match = 2
$isolateScope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
it 'is invalid if the matched property differs', ->
$scope.model.a = 2
$scope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
it 'is invalid if the input itself is changed', ->
$element.val('2').trigger('input').keyup()
$scope.$digest()
controller = $element.controller('ngModel')
assert.isTrue(controller.$error.match)
......@@ -12,7 +12,7 @@ describe 'Discovery', ->
before ->
angular.module('h', [])
require('../discovery')
.value('Discovery', require('../discovery'))
beforeEach module('h')
beforeEach inject (Discovery) ->
......
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'h:filters', ->
before ->
angular.module('h', [])
require('../filters')
describe 'persona', ->
filter = null
term = 'acct:hacker@example.com'
beforeEach module('h')
beforeEach inject ($filter) ->
filter = $filter('persona')
it 'should return the whole term by request', ->
result = filter('acct:hacker@example.com', 'term')
assert.equal result, 'acct:hacker@example.com'
it 'should return the requested part', ->
assert.equal filter(term), 'hacker'
assert.equal filter(term, 'term'), term,
assert.equal filter(term, 'username'), 'hacker'
assert.equal filter(term, 'provider'), 'example.com'
it 'should pass through unrecognized terms as username or term', ->
assert.equal filter('bogus'), 'bogus'
assert.equal filter('bogus', 'username'), 'bogus'
it 'should handle error cases', ->
assert.notOk filter()
assert.notOk filter('bogus', 'provider')
describe 'urlencode', ->
filter = null
beforeEach module('h')
beforeEach inject ($filter) ->
filter = $filter('urlencode')
it 'encodes reserved characters in the term', ->
assert.equal(filter('#hello world'), '%23hello%20world')
{module, inject} = require('angular-mock')
assert = chai.assert
angular = require('angular')
describe 'form-respond', ->
$scope = null
formRespond = null
form = null
before ->
angular.module('h', [])
.service('formRespond', require('../form-respond'))
beforeEach module('h')
beforeEach inject (_$rootScope_, _formRespond_) ->
$scope = _$rootScope_.$new()
formRespond = _formRespond_
form =
$setValidity: sinon.spy()
username: {$setValidity: sinon.spy()}
password: {$setValidity: sinon.spy()}
it 'sets the "response" error key for each field with errors', ->
formRespond form,
username: 'must be at least 3 characters'
password: 'must be present'
assert.calledWith(form.username.$setValidity, 'response', false)
assert.calledWith(form.password.$setValidity, 'response', false)
it 'adds an error message to each input controller', ->
formRespond form,
username: 'must be at least 3 characters'
password: 'must be present'
assert.equal(form.username.responseErrorMessage, 'must be at least 3 characters')
assert.equal(form.password.responseErrorMessage, 'must be present')
it 'sets the "response" error key if the form has a failure reason', ->
formRespond form, null, 'fail'
assert.calledWith(form.$setValidity, 'response', false)
it 'adds an reason message as the response error', ->
formRespond form, null, 'fail'
assert.equal(form.responseErrorMessage, 'fail')
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'Host service', ->
sandbox = null
host = null
createChannel = -> notify: sandbox.stub()
fakeBridge = null
$digest = null
publish = null
PARENT_WINDOW = 'PARENT_WINDOW'
dumpListeners = null
before ->
require('../host-service')
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeWindow = parent: PARENT_WINDOW
listeners = {}
publish = ({method, params}) ->
listeners[method]('ctx', params)
fakeBridge =
ls: listeners
on: sandbox.spy (method, fn) -> listeners[method] = fn
notify: sandbox.stub()
onConnect: sandbox.stub()
links: [
{window: PARENT_WINDOW, channel: createChannel()}
{window: 'ANOTHER_WINDOW', channel: createChannel()}
{window: 'THIRD_WINDOW', channel: createChannel()}
]
$provide.value 'bridge', fakeBridge
$provide.value '$window', fakeWindow
return
afterEach ->
sandbox.restore()
beforeEach inject ($rootScope, _host_) ->
host = _host_
$digest = sandbox.stub($rootScope, '$digest')
describe 'the public API', ->
describe 'showSidebar()', ->
it 'sends the "showFrame" message to the host only', ->
host.showSidebar()
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'showFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
describe 'hideSidebar()', ->
it 'sends the "hideFrame" message to the host only', ->
host.hideSidebar()
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'hideFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
describe 'reacting to the bridge', ->
describe 'on "back" event', ->
it 'triggers the hideSidebar() API', ->
sandbox.spy host, "hideSidebar"
publish method: 'back'
assert.called host.hideSidebar
describe 'on "open" event', ->
it 'triggers the showSidebar() API', ->
sandbox.spy host, "showSidebar"
publish method: 'open'
assert.called host.showSidebar
Annotator = require('annotator')
Host = require('../host')
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose(assert, prefix: '')
describe 'Annotator.Host', ->
sandbox = sinon.sandbox.create()
fakeCrossFrame = null
createHost = (options={}) ->
element = document.createElement('div')
return new Host(element, options)
beforeEach ->
# Disable Annotator's ridiculous logging.
sandbox.stub(console, 'log')
fakeCrossFrame = {}
fakeCrossFrame.onConnect = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.on = sandbox.stub().returns(fakeCrossFrame)
fakeCrossFrame.notify = sandbox.stub().returns(fakeCrossFrame)
sandbox.stub(Annotator.Plugin, 'CrossFrame').returns(fakeCrossFrame)
afterEach -> sandbox.restore()
describe 'options', ->
it 'enables highlighting when showHighlights option is provided', (done) ->
host = createHost(showHighlights: true)
host.on 'panelReady', ->
assert.isTrue(host.visibleHighlights)
done()
host.publish('panelReady')
it 'does not enable highlighting when no showHighlights option is provided', (done) ->
host = createHost({})
host.on 'panelReady', ->
assert.isFalse(host.visibleHighlights)
done()
host.publish('panelReady')
describe 'crossframe listeners', ->
emitHostEvent = (event, args...) ->
fn(args...) for [evt, fn] in fakeCrossFrame.on.args when event == evt
describe 'on "showFrame" event', ->
it 'shows the frame', ->
target = sandbox.stub(Annotator.Host.prototype, 'showFrame')
host = createHost()
emitHostEvent('showFrame')
assert.called(target)
describe 'on "hideFrame" event', ->
it 'hides the frame', ->
target = sandbox.stub(Annotator.Host.prototype, 'hideFrame')
host = createHost()
emitHostEvent('hideFrame')
assert.called(target)
sinon.assert.expose assert, prefix: null
describe 'host', ->
sandbox = null
host = null
createChannel = -> notify: sandbox.stub()
fakeBridge = null
$digest = null
publish = null
PARENT_WINDOW = 'PARENT_WINDOW'
dumpListeners = null
before ->
angular.module('h', [])
.service('host', require('../host'))
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeWindow = parent: PARENT_WINDOW
listeners = {}
publish = ({method, params}) ->
listeners[method]('ctx', params)
fakeBridge =
ls: listeners
on: sandbox.spy (method, fn) -> listeners[method] = fn
notify: sandbox.stub()
onConnect: sandbox.stub()
links: [
{window: PARENT_WINDOW, channel: createChannel()}
{window: 'ANOTHER_WINDOW', channel: createChannel()}
{window: 'THIRD_WINDOW', channel: createChannel()}
]
$provide.value 'bridge', fakeBridge
$provide.value '$window', fakeWindow
return
afterEach ->
sandbox.restore()
beforeEach inject ($rootScope, _host_) ->
host = _host_
$digest = sandbox.stub($rootScope, '$digest')
describe 'the public API', ->
describe 'showSidebar()', ->
it 'sends the "showFrame" message to the host only', ->
host.showSidebar()
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'showFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
describe 'hideSidebar()', ->
it 'sends the "hideFrame" message to the host only', ->
host.hideSidebar()
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'hideFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
describe 'reacting to the bridge', ->
describe 'on "back" event', ->
it 'triggers the hideSidebar() API', ->
sandbox.spy host, "hideSidebar"
publish method: 'back'
assert.called host.hideSidebar
describe 'on "open" event', ->
it 'triggers the showSidebar() API', ->
sandbox.spy host, "showSidebar"
publish method: 'open'
assert.called host.showSidebar
......@@ -5,14 +5,15 @@ sinon.assert.expose assert, prefix: null
sandbox = sinon.sandbox.create()
describe 'h.identity', ->
describe 'identityProvider', ->
provider = null
mockInjectable = {}
before ->
require('../identity-service')
angular.module('h')
.provider('identity', require('../identity'))
beforeEach module('h.identity')
beforeEach module('h')
beforeEach module ($provide, identityProvider) ->
$provide.value('foo', mockInjectable)
......@@ -22,7 +23,7 @@ describe 'h.identity', ->
afterEach ->
sandbox.restore()
describe 'identityService', ->
describe 'identity', ->
scope = null
service = null
......
......@@ -4,18 +4,18 @@ assert = chai.assert
sinon.assert.expose assert, prefix: null
describe 'h:localstorage', ->
describe 'localStorage', ->
fakeWindow = null
sandbox = null
before ->
angular.module('h', [])
require('../local-storage-service')
.service('localStorage', require('../local-storage'))
beforeEach module('h')
describe 'memory fallback', ->
localstorage = null
localStorage = null
key = null
beforeEach module ($provide) ->
......@@ -30,33 +30,33 @@ describe 'h:localstorage', ->
afterEach ->
sandbox.restore()
beforeEach inject (_localstorage_) ->
localstorage = _localstorage_
beforeEach inject (_localStorage_) ->
localStorage = _localStorage_
key = 'test.memory.key'
it 'sets/gets Item', ->
value = 'What shall we do with a drunken sailor?'
localstorage.setItem key, value
actual = localstorage.getItem key
localStorage.setItem key, value
actual = localStorage.getItem key
assert.equal value, actual
it 'removes item', ->
localstorage.setItem key, ''
localstorage.removeItem key
result = localstorage.getItem key
localStorage.setItem key, ''
localStorage.removeItem key
result = localStorage.getItem key
assert.isNull result
it 'sets/gets Object', ->
data = {'foo': 'bar'}
localstorage.setObject key, data
stringified = localstorage.getItem key
localStorage.setObject key, data
stringified = localStorage.getItem key
assert.equal stringified, JSON.stringify data
actual = localstorage.getObject key
actual = localStorage.getObject key
assert.deepEqual actual, data
describe 'browser localStorage', ->
localstorage = null
localStorage = null
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
......@@ -74,12 +74,12 @@ describe 'h:localstorage', ->
afterEach ->
sandbox.restore()
beforeEach inject (_localstorage_) ->
localstorage = _localstorage_
beforeEach inject (_localStorage_) ->
localStorage = _localStorage_
it 'uses window.localStorage functions to handle data', ->
key = 'test.storage.key'
data = 'test data'
localstorage.setItem key, data
localStorage.setItem key, data
assert.calledWith fakeWindow.localStorage.setItem, key, data
......@@ -10,7 +10,7 @@ describe 'h:permissions', ->
before ->
angular.module('h', [])
require('../permissions-service')
.service('permissions', require('../permissions'))
beforeEach module('h')
......
{module, inject} = require('angular-mock')
assert = chai.assert
sinon.assert.expose assert, prefix: null
poem =
tiger: 'Tiger! Tiger! burning bright
In the forest of the night
What immortal hand or eye
Could frame thy fearful symmetry?'
raven: 'Once upon a midnight dreary, while I pondered, weak and weary,
Over many a quaint and curious volume of forgotten lore—
While I nodded, nearly napping, suddenly there came a tapping,
As of some one gently rapping, rapping at my chamber door.
“’Tis some visitor,” I muttered, “tapping at my chamber door—
Only this and nothing more.”'
describe 'h:services', ->
sandbox = null
fakeStringHelpers = null
before ->
angular.module('h', [])
require('../services')
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeStringHelpers = {
uniFold: sinon.stub().returnsArg(0)
}
$provide.value('stringHelpers', fakeStringHelpers)
return
afterEach ->
sandbox.restore()
describe 'viewFilter service', ->
viewFilter = null
beforeEach inject (_viewFilter_) ->
viewFilter = _viewFilter_
describe 'filter', ->
it 'normalizes the filter terms', ->
filters =
text:
terms: ['Tiger']
operator: 'and'
viewFilter.filter [], filters
assert.calledWith fakeStringHelpers.uniFold, 'tiger'
describe 'filter operators', ->
annotations = null
beforeEach ->
annotations = [
{id: 1, text: poem.tiger},
{id: 2, text: poem.raven}
]
it 'all terms must match for "and" operator', ->
filters =
text:
terms: ['Tiger', 'burning', 'bright']
operator: 'and'
result = viewFilter.filter annotations, filters
assert.equal result.length, 1
assert.equal result[0], 1
it 'only one term must match for "or" operator', ->
filters =
text:
terms: ['Tiger', 'quaint']
operator: 'or'
result = viewFilter.filter annotations, filters
assert.equal result.length, 2
describe 'checkers', ->
describe 'autofalse', ->
it 'consider auto false function', ->
viewFilter.checkers =
test:
autofalse: sandbox.stub().returns(true)
value: (annotation) -> return annotation.test
match: (term, value) -> return value.indexOf(term) > -1
filters =
test:
terms: ['Tiger']
operator: 'and'
annotations = [{id: 1, test: poem.tiger}]
result = viewFilter.filter annotations, filters
assert.called viewFilter.checkers.test.autofalse
assert.equal result.length, 0
it 'uses the value function to extract data from the annotation', ->
viewFilter.checkers =
test:
autofalse: (annotation) -> return false
value: sandbox.stub().returns('test')
match: (term, value) -> return value.indexOf(term) > -1
filters =
test:
terms: ['test']
operator: 'and'
annotations = [{id: 1, test: poem.tiger}]
result = viewFilter.filter annotations, filters
assert.called viewFilter.checkers.test.value
assert.equal result.length, 1
it 'the match function determines the matching', ->
viewFilter.checkers =
test:
autofalse: (annotation) -> return false
value: (annotation) -> return annotation.test
match: sandbox.stub().returns(false)
filters =
test:
terms: ['Tiger']
operator: 'and'
annotations = [{id: 1, test: poem.tiger}]
result = viewFilter.filter annotations, filters
assert.called viewFilter.checkers.test.match
assert.equal result.length, 0
viewFilter.checkers.test.match.returns(true)
result = viewFilter.filter annotations, filters
assert.called viewFilter.checkers.test.match
assert.equal result.length, 1
describe 'any field', ->
it 'finds matches across many fields', ->
annotation1 = {id: 1, text: poem.tiger}
annotation2 = {id: 2, user: poem.tiger}
annotation3 = {id: 3, tags: ['Tiger']}
annotations = [annotation1, annotation2, annotation3]
filters =
any:
terms: ['Tiger']
operator: 'and'
result = viewFilter.filter annotations, filters
assert.equal result.length, 3
it 'can find terms across different fields', ->
annotation =
id:1
text: poem.tiger
target: [
selector: [{
"type": "TextQuoteSelector",
"exact": "The Tiger by William Blake",
}]
user: "acct:poe@edgar.com"
tags: ["poem", "Blake", "Tiger"]
]
filters =
any:
terms: ['burning', 'William', 'poem', 'bright']
operator: 'and'
result = viewFilter.filter [annotation], filters
assert.equal result.length, 1
assert.equal result[0], 1
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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