| @@ -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 |
| @@ -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 |
| @@ -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; | ||
| }; |
| @@ -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'); | ||
| } | ||
| }; | ||
| }]; |
| @@ -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; | ||
| } | ||
| }; | ||
| }; |
| @@ -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')); | ||
| }); | ||
| }); |
| @@ -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'); | ||
| }); | ||
| }); |
| @@ -0,0 +1,5 @@ | ||
| module.exports = function () { | ||
| return function (value) { | ||
| return encodeURIComponent(value); | ||
| }; | ||
| }; |
| @@ -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) | ||
| ] |
| @@ -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 |
| @@ -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 | ||
| ] |
| @@ -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') | ||
| ] |
| @@ -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 |
| @@ -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) | ||
| ] |
| @@ -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 |
| @@ -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}) |
| @@ -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) |
| @@ -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 |
| @@ -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') |
| @@ -1,59 +1,84 @@ | ||
| 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 |