View
@@ -0,0 +1,111 @@
{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')
View
@@ -0,0 +1,51 @@
{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)
View
@@ -0,0 +1,52 @@
{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)
View
@@ -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
View
@@ -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()">
View
@@ -10,7 +10,7 @@ describe 'h:directives.status-button', ->
before ->
angular.module('h', [])
require('../status-button')
.directive('statusButton', require('../status-button'))
beforeEach module('h')
View
@@ -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')
View
@@ -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)
View
@@ -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)
View
@@ -0,0 +1,6 @@
module.exports = ->
link: (scope, elem, attr) ->
elem.bind 'scroll', ->
{clientHeight, scrollHeight, scrollTop} = elem[0]
if scrollHeight - scrollTop <= clientHeight + 40
scope.$apply attr.whenscrolled
View
@@ -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
View
@@ -0,0 +1,41 @@
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
View
@@ -0,0 +1,15 @@
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;
};
View
@@ -0,0 +1,19 @@
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');
}
};
}];
View
@@ -0,0 +1,19 @@
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;
}
};
};
View
@@ -0,0 +1,43 @@
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'));
});
});
View
@@ -0,0 +1,24 @@
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');
});
});
View
@@ -0,0 +1,5 @@
module.exports = function () {
return function (value) {
return encodeURIComponent(value);
};
};
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View
@@ -0,0 +1,6 @@
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)
]
View
@@ -0,0 +1,12 @@
# 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
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View

This file was deleted.

Oops, something went wrong.
View
@@ -1,139 +1,31 @@
$ = 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('setTool', 'comment')
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
]
View
@@ -1,49 +1,53 @@
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'),
View
@@ -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)
View
@@ -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'],
},
View
@@ -1,4 +1,4 @@
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)
View
@@ -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)
]
View
@@ -0,0 +1,12 @@
###*
# @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')
]
View
@@ -0,0 +1,96 @@
# 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
View
@@ -0,0 +1,26 @@
###*
# @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)
]
View
@@ -1,7 +1,7 @@
# 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)
View
@@ -1,3 +1,5 @@
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)
View

This file was deleted.

Oops, something went wrong.
View
@@ -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
])
]
View
@@ -1,27 +1,30 @@
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)
View
@@ -0,0 +1,77 @@
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
View
@@ -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)
View
@@ -1,9 +1,9 @@
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)
View
@@ -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)
View
@@ -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) ->
@@ -28,11 +28,7 @@ describe 'AnnotationSync', ->
call: sandbox.stub()
notify: sandbox.stub()
onConnect: sandbox.stub()
links: [
{window: PARENT_WINDOW, channel: createChannel()}
{window: 'ANOTHER_WINDOW', channel: createChannel()}
{window: 'THIRD_WINDOW', channel: createChannel()}
]
links: []
# TODO: Fix this hack to remove pre-existing bound listeners.
$rootScope.$$listeners = []
View
@@ -0,0 +1,60 @@
{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})
View
@@ -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) ->
@@ -71,56 +71,7 @@ describe 'AnnotationUISync', ->
createAnnotationUISync()
assert.notCalled(channel.notify)
describe 'on "back" event', ->
it 'sends the "hideFrame" message to the host only', ->
createAnnotationUISync()
publish({method: 'back'})
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'hideFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
it 'triggers a digest', ->
createAnnotationUISync()
publish({method: 'back'})
assert.called($digest)
describe 'on "open" event', ->
it 'sends the "showFrame" message to the host only', ->
createAnnotationUISync()
publish({method: 'open'})
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'showFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
it 'triggers a digest', ->
createAnnotationUISync()
publish({method: 'open'})
assert.called($digest)
describe 'on "showEditor" event', ->
it 'sends the "showFrame" message to the host only', ->
createAnnotationUISync()
publish({method: 'showEditor'})
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'showFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
it 'triggers a digest', ->
createAnnotationUISync()
publish({method: 'showEditor'})
assert.called($digest)
describe 'on "showAnnotations" event', ->
it 'sends the "showFrame" message to the host only', ->
createAnnotationUISync()
publish({
method: 'showAnnotations',
params: ['tag1', 'tag2', 'tag3']
})
assert.calledWith(fakeBridge.links[0].channel.notify, method: 'showFrame')
assert.notCalled(fakeBridge.links[1].channel.notify)
assert.notCalled(fakeBridge.links[2].channel.notify)
it 'updates the annotationUI to include the shown annotations', ->
createAnnotationUISync()
publish({
View
@@ -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_) ->
View
@@ -0,0 +1,21 @@
{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)
View
@@ -0,0 +1,174 @@
{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
View
@@ -11,7 +11,7 @@ describe 'h', ->
before ->
angular.module('h', [])
require('../auth-service')
.factory('auth', require('../auth'))
beforeEach module('h')
View
@@ -12,7 +12,7 @@ describe 'Bridge', ->
before ->
angular.module('h', [])
require('../bridge')
.service('bridge', require('../bridge'))
beforeEach module('h')
beforeEach inject (_bridge_) ->
View

This file was deleted.

Oops, something went wrong.
View
@@ -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) ->
View

This file was deleted.

Oops, something went wrong.
View
@@ -12,7 +12,7 @@ describe 'Discovery', ->
before ->
angular.module('h', [])
require('../discovery')
.value('Discovery', require('../discovery'))
beforeEach module('h')
beforeEach inject (Discovery) ->
View

This file was deleted.

Oops, something went wrong.
View
@@ -0,0 +1,47 @@
{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')
Oops, something went wrong.