Commit 992efe84 authored by Randall Leeds's avatar Randall Leeds Committed by Nick Stenning

Improve packaging, bundling and module boilerplate

Prevent negative interactions with module systems present in injected
pages and refactor browserify interactions with angular to increase
CommonJS accessibility / legibility and isolate angular module system
boilerplate.

- Avoid issues due to mis-detected module environments

  - In the injection script, load only Annotator and jQuery in the
    global scope and then both removed with `noConflict()`. Both are
    loaded with browserify-shim, which prevents jQuery from registering
    itself against require.js, if present (fix #2026).

  - Be explicit about registration of common cross-frame components
    (fix #2090).

    - Lift angular glue into app.coffee. Individual components are
      exported as CommonJS modules.

    - Lift Annotator glue into bootstrap.js (renamed hypothesis.js).

- Break up all modules that contained multiple services/directives/etc,
  such as the formHelpers and uiHelpers modules, as well as catch-all
  files like services.coffee and directives.coffee. For a summary of
  these movements see the bottom of this commit message.

- Rename all services, dropping the -service suffix, to match their
  injected names. This is consistent with how directives are being
  treated, and controllers as well (controllers are registered and
  injected done with the "Controller" suffix).

- Move Annotator subclasses into the annotator subdirectory. This
  change avoids having `Annotator.Host` clash with the `host`
  service and makes it easy to glob dependencies in assets.yaml to
  minimally rebuild either the app or the inject.

---

Summary of structural reorganisation. Modules that have been trivially
renamed (from "foo-service" to "foo") are not included in this list:

    controllers:AppController                   app-controller
    controllers:AnnotationUIController          annotation-ui-controller
    controllers:AnnotationViewerController      annotation-viewer-controller
    controllers:ViewerController                widget-controller

    directives:match                            directive/match
    directives:repeatAnim                       directive/repeat-anim
    directives:whenscrolled                     directive/whenscrolled

    directives/thread:pulse                     pulse

    filters:Converter                           filter/converter
    filters:momentFilter                        filter/moment
    filters:personaFilter                       filter/persona
    filters:urlEncodeFilter                     filter/urlencode

    guest                                       annotator/guest
    host                                        annotator/host

    helpers/form-helpers:createFormHelpers      form-respond
    helpers/form-helpers:formInput              directive/form-input
    helpers/form-helpers:formValidate           directive/form-validate
    helpers/string-helpers:createStringHelpers  unicode
    helpers/tag-helpers                         tags
    helpers/time-helpers                        time
    helpers/ui-helpers:tabbable                 directive/tabbable
    helpers/ui-helpers:tabReveal                directive/tab-reveal

    searchfilters:QueryParser                   query-parser
    searchfilters:SearchFilter                  search-filter
    searchfilters:StreamFilter                  stream-filter

    services:DraftProvider                      drafts
    services:ViewFilter                         view-filter
    services:renderFactory                      render

    session/session-service                     session

    streamsearch                                stream-controller
parent 7d19d654
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) ->
......@@ -19,10 +19,10 @@ class AuthController
failure = (form, response) ->
{errors, reason} = response.data
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
......@@ -38,7 +38,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_) ->
......@@ -81,7 +81,7 @@ describe 'h:AuthController', ->
auth.submit(form)
assert.calledWith mockFormHelpers.applyValidationErrors, form,
assert.calledWith mockFormRespond, form,
{username: 'taken'},
'registration error'
......
# 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