Commit 833c203a authored by Nick Stenning's avatar Nick Stenning

Merge pull request #3053 from hypothesis/app-controller-decaf

Convert {app, app-controller, widget-controller}.coffee to JS
parents bed95d66 dcc41497
......@@ -67,7 +67,7 @@ var appBundles = [{
// The sidebar application for displaying and editing annotations
name: 'app',
transforms: ['coffee'],
entry: './h/static/scripts/app.coffee',
entry: './h/static/scripts/app',
},{
// The Annotator library which provides annotation controls on
// the page and sets up the sidebar
......
'use strict';
/**
* Utility functions for querying annotation metadata.
*/
......@@ -68,8 +70,30 @@ function isNew(annotation) {
return !annotation.id;
}
/** Return a numeric key that can be used to sort annotations by location.
*
* @return {number} - A key representing the location of the annotation in
* the document, where lower numbers mean closer to the
* start.
*/
function location(annotation) {
if (annotation) {
var targets = annotation.target || [];
for (var i=0; i < targets.length; i++) {
var selectors = targets[i].selector || [];
for (var k=0; k < selectors.length; k++) {
if (selectors[k].type === 'TextPositionSelector') {
return selectors[k].start;
}
}
}
}
return Number.POSITIVE_INFINITY;
}
module.exports = {
extractDocumentMetadata: extractDocumentMetadata,
isReply: isReply,
isNew: isNew,
location: location,
};
angular = require('angular')
events = require('./events')
parseAccountID = require('./filter/persona').parseAccountID
module.exports = class AppController
this.$inject = [
'$controller', '$document', '$location', '$rootScope', '$route', '$scope',
'$window', 'annotationUI', 'auth', 'drafts', 'features', 'groups',
'identity', 'session'
]
constructor: (
$controller, $document, $location, $rootScope, $route, $scope,
$window, annotationUI, auth, drafts, features, groups,
identity, session
) ->
$controller('AnnotationUIController', {$scope})
# This stores information about the current user's authentication status.
# When the controller instantiates we do not yet know if the user is
# logged-in or not, so it has an initial status of 'unknown'. This can be
# used by templates to show an intermediate or loading state.
$scope.auth = {status: 'unknown'}
# Allow all child scopes to look up feature flags as:
#
# if ($scope.feature('foo')) { ... }
$scope.feature = features.flagEnabled
# Allow all child scopes access to the session
$scope.session = session
isFirstRun = $location.search().hasOwnProperty('firstrun')
# App dialogs
$scope.accountDialog = visible: false
$scope.shareDialog = visible: false
# Check to see if we're in the sidebar, or on a standalone page such as
# the stream page or an individual annotation page.
$scope.isSidebar = $window.top isnt $window
# Default sort
$scope.sort = {
name: 'Location'
options: ['Newest', 'Oldest', 'Location']
}
# Reload the view when the user switches accounts
$scope.$on(events.USER_CHANGED, (event, data) ->
if !data || !data.initialLoad
$route.reload()
);
identity.watch({
onlogin: (identity) ->
# Hide the account dialog
$scope.accountDialog.visible = false
# Update the current logged-in user information
userid = auth.userid(identity)
parsed = parseAccountID(userid)
angular.copy({
status: 'signed-in',
userid: userid,
username: parsed.username,
provider: parsed.provider,
}, $scope.auth)
onlogout: ->
angular.copy({status: 'signed-out'}, $scope.auth)
onready: ->
# If their status is still 'unknown', then `onlogin` wasn't called and
# we know the current user isn't signed in.
if $scope.auth.status == 'unknown'
angular.copy({status: 'signed-out'}, $scope.auth)
if isFirstRun
$scope.login()
})
$scope.$watch 'sort.name', (name) ->
return unless name
predicate = switch name
when 'Newest' then ['-!!message', '-message.updated']
when 'Oldest' then ['-!!message', 'message.updated']
when 'Location' then (thread) ->
if thread.message?
for target in thread.message.target ? []
for selector in target.selector ? []
if selector.type is 'TextPositionSelector'
return selector.start
return Number.POSITIVE_INFINITY
$scope.sort = {
name,
predicate,
options: $scope.sort.options,
}
# Start the login flow. This will present the user with the login dialog.
$scope.login = ->
$scope.accountDialog.visible = true
identity.request({
oncancel: -> $scope.accountDialog.visible = false
})
# Prompt to discard any unsaved drafts.
promptToLogout = ->
# TODO - Replace this with a UI which doesn't look terrible.
if drafts.count() == 1
text = 'You have an unsaved annotation.\n
Do you really want to discard this draft?'
else if drafts.count() > 1
text = 'You have ' + drafts.count() + ' unsaved annotations.\n
Do you really want to discard these drafts?'
return (drafts.count() == 0 or $window.confirm(text))
# Log the user out.
$scope.logout = ->
if promptToLogout()
for draft in drafts.unsaved()
$rootScope.$emit("annotationDeleted", draft)
drafts.discard()
$scope.accountDialog.visible = false
identity.logout()
$scope.clearSelection = ->
$scope.search.query = ''
annotationUI.clearSelectedAnnotations()
$scope.search =
query: $location.search()['q']
clear: ->
$location.search('q', null)
update: (query) ->
unless angular.equals $location.search()['q'], query
$location.search('q', query or null)
annotationUI.clearSelectedAnnotations()
'use strict';
var angular = require('angular');
var annotationMetadata = require('./annotation-metadata');
var events = require('./events');
var parseAccountID = require('./filter/persona').parseAccountID;
// @ngInject
module.exports = function AppController(
$controller, $document, $location, $rootScope, $route, $scope,
$window, annotationUI, auth, drafts, features, groups,
identity, session
) {
$controller('AnnotationUIController', {$scope: $scope});
// This stores information about the current user's authentication status.
// When the controller instantiates we do not yet know if the user is
// logged-in or not, so it has an initial status of 'unknown'. This can be
// used by templates to show an intermediate or loading state.
$scope.auth = {status: 'unknown'};
// Allow all child scopes to look up feature flags as:
//
// if ($scope.feature('foo')) { ... }
$scope.feature = features.flagEnabled;
// Allow all child scopes access to the session
$scope.session = session;
var isFirstRun = $location.search().hasOwnProperty('firstrun');
// App dialogs
$scope.accountDialog = {visible: false};
$scope.shareDialog = {visible: false};
// Check to see if we're in the sidebar, or on a standalone page such as
// the stream page or an individual annotation page.
$scope.isSidebar = $window.top !== $window;
// Default sort
$scope.sort = {
name: 'Location',
options: ['Newest', 'Oldest', 'Location']
};
// Reload the view when the user switches accounts
$scope.$on(events.USER_CHANGED, function (event, data) {
if (!data || !data.initialLoad) {
$route.reload();
}
});
identity.watch({
onlogin: function (identity) {
// Hide the account dialog
$scope.accountDialog.visible = false;
// Update the current logged-in user information
var userid = auth.userid(identity);
var parsed = parseAccountID(userid);
angular.copy({
status: 'signed-in',
userid: userid,
username: parsed.username,
provider: parsed.provider,
}, $scope.auth);
},
onlogout: function () {
angular.copy({status: 'signed-out'}, $scope.auth);
},
onready: function () {
// If their status is still 'unknown', then `onlogin` wasn't called and
// we know the current user isn't signed in.
if ($scope.auth.status === 'unknown') {
angular.copy({status: 'signed-out'}, $scope.auth);
if (isFirstRun) {
$scope.login();
}
}
}
});
$scope.$watch('sort.name', function (name) {
if (!name) {
return;
}
var predicateFn;
switch (name) {
case 'Newest':
predicateFn = ['-!!message', '-message.updated'];
break;
case 'Oldest':
predicateFn = ['-!!message', 'message.updated'];
break;
case 'Location':
predicateFn = annotationMetadata.location;
break;
}
$scope.sort = {
name: name,
predicate: predicateFn,
options: $scope.sort.options,
};
});
// Start the login flow. This will present the user with the login dialog.
$scope.login = function () {
$scope.accountDialog.visible = true;
return identity.request({
oncancel: function () { $scope.accountDialog.visible = false; }
});
};
// Prompt to discard any unsaved drafts.
var promptToLogout = function () {
// TODO - Replace this with a UI which doesn't look terrible.
var text = '';
if (drafts.count() === 1) {
text = 'You have an unsaved annotation.\n' +
'Do you really want to discard this draft?';
} else if (drafts.count() > 1) {
text = 'You have ' + drafts.count() + ' unsaved annotations.\n' +
'Do you really want to discard these drafts?';
}
return (drafts.count() === 0 || $window.confirm(text));
};
// Log the user out.
$scope.logout = function () {
if (promptToLogout()) {
var iterable = drafts.unsaved();
for (var i = 0, draft; i < iterable.length; i++) {
draft = iterable[i];
$rootScope.$emit("annotationDeleted", draft);
}
drafts.discard();
$scope.accountDialog.visible = false;
return identity.logout();
}
};
$scope.clearSelection = function () {
$scope.search.query = '';
annotationUI.clearSelectedAnnotations();
};
$scope.search = {
query: $location.search().q,
clear: function () {
$location.search('q', null);
},
update: function (query) {
if (!angular.equals($location.search().q, query)) {
$location.search('q', query || null);
annotationUI.clearSelectedAnnotations();
}
}
};
};
require('./polyfills')
# Initialize Raven. This is required at the top of this file
# so that it happens early in the app's startup flow
settings = require('./settings')(document)
if settings.raven
raven = require('./raven')
raven.init(settings.raven)
angular = require('angular')
# autofill-event relies on the existence of window.angular so
# it must be require'd after angular is first require'd
require('autofill-event')
# Setup Angular integration for Raven
if settings.raven
raven.angularModule(angular)
else
angular.module('ngRaven', [])
streamer = require('./streamer')
resolve =
# Ensure that we have available a) the current authenticated userid, and b)
# the list of user groups.
sessionState: ['session', (session) -> session.load()]
store: ['store', (store) -> store.$promise]
streamer: streamer.connect
threading: [
'annotationMapper', 'drafts', 'threading'
(annotationMapper, drafts, threading) ->
# Unload all the annotations
annotationMapper.unloadAnnotations(threading.annotationList())
# Reset the threading root
threading.createIdTable([])
threading.root = mail.messageContainer()
# Reload all new, unsaved annotations
threading.thread(drafts.unsaved())
return threading
]
configureLocation = ['$locationProvider', ($locationProvider) ->
# Use HTML5 history
$locationProvider.html5Mode(true)
]
configureRoutes = ['$routeProvider', ($routeProvider) ->
$routeProvider.when '/a/:id',
controller: 'AnnotationViewerController'
templateUrl: 'viewer.html'
reloadOnSearch: false
resolve: resolve
$routeProvider.when '/viewer',
controller: 'WidgetController'
templateUrl: 'viewer.html'
reloadOnSearch: false
resolve: resolve
$routeProvider.when '/stream',
controller: 'StreamController'
templateUrl: 'viewer.html'
reloadOnSearch: false
resolve: resolve
$routeProvider.otherwise
redirectTo: '/viewer'
]
setupCrossFrame = ['crossframe', (crossframe) -> crossframe.connect()]
setupHttp = ['$http', ($http) ->
$http.defaults.headers.common['X-Client-Id'] = streamer.clientId
]
setupHost = ['host', (host) -> ]
module.exports = angular.module('h', [
# Angular addons which export the Angular module name
# via module.exports
require('angular-jwt')
require('angular-resource')
require('angular-route')
require('angular-sanitize')
require('angular-toastr')
# Angular addons which do not export the Angular module
# name via module.exports
['angulartics', require('angulartics')][0]
['angulartics.google.analytics', require('angulartics/src/angulartics-ga')][0]
['ngTagsInput', require('ng-tags-input')][0]
['ui.bootstrap', require('./vendor/ui-bootstrap-custom-tpls-0.13.4')][0]
# Local addons
'ngRaven'
])
.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)
.directive('deepCount', require('./directive/deep-count'))
.directive('excerpt', require('./directive/excerpt').directive)
.directive('formInput', require('./directive/form-input'))
.directive('formValidate', require('./directive/form-validate'))
.directive('groupList', require('./directive/group-list').directive)
.directive('hAutofocus', require('./directive/h-autofocus'))
.directive('loginForm', require('./directive/login-form').directive)
.directive('markdown', require('./directive/markdown'))
.directive('simpleSearch', require('./directive/simple-search'))
.directive('statusButton', require('./directive/status-button'))
.directive('thread', require('./directive/thread'))
.directive('threadFilter', require('./directive/thread-filter'))
.directive('spinner', require('./directive/spinner'))
.directive('shareDialog', require('./directive/share-dialog'))
.directive('windowScroll', require('./directive/window-scroll'))
.directive('dropdownMenuBtn', require('./directive/dropdown-menu-btn'))
.directive('publishAnnotationBtn', require('./directive/publish-annotation-btn'))
.directive('searchStatusBar', require('./directive/search-status-bar'))
.directive('sidebarTutorial', require('./directive/sidebar-tutorial').directive)
.directive('signinControl', require('./directive/signin-control'))
.directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('topBar', require('./directive/top-bar'))
.filter('converter', require('./filter/converter'))
.provider('identity', require('./identity'))
.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('features', require('./features'))
.service('flash', require('./flash'))
.service('formRespond', require('./form-respond'))
.service('groups', require('./groups'))
.service('host', require('./host'))
.service('localStorage', require('./local-storage'))
.service('permissions', require('./permissions'))
.service('queryParser', require('./query-parser'))
.service('render', require('./render'))
.service('searchFilter', require('./search-filter'))
.service('session', require('./session'))
.service('streamFilter', require('./stream-filter'))
.service('tags', require('./tags'))
.service('threading', require('./threading'))
.service('unicode', require('./unicode'))
.service('viewFilter', require('./view-filter'))
.factory('store', require('./store'))
.value('AnnotationSync', require('./annotation-sync'))
.value('AnnotationUISync', require('./annotation-ui-sync'))
.value('Discovery', require('./discovery'))
.value('raven', require('./raven'))
.value('settings', settings)
.value('time', require('./time'))
.config(configureLocation)
.config(configureRoutes)
.run(setupCrossFrame)
.run(setupHttp)
.run(setupHost)
require('./config/module')
'use strict';
require('./polyfills');
// Initialize Raven. This is required at the top of this file
// so that it happens early in the app's startup flow
var settings = require('./settings')(document);
if (settings.raven) {
var raven = require('./raven');
raven.init(settings.raven);
}
var angular = require('angular');
// autofill-event relies on the existence of window.angular so
// it must be require'd after angular is first require'd
require('autofill-event');
// Setup Angular integration for Raven
if (settings.raven) {
raven.angularModule(angular);
} else {
angular.module('ngRaven', []);
}
var mail = require('./vendor/jwz');
var streamer = require('./streamer');
var resolve =
// Ensure that we have available a) the current authenticated userid, and b)
// the list of user groups.
{
sessionState: ['session', function (session) { return session.load(); }],
store: ['store', function (store) { return store.$promise; }],
streamer: streamer.connect,
threading: [
'annotationMapper', 'drafts', 'threading',
function (annotationMapper, drafts, threading) {
// Unload all the annotations
annotationMapper.unloadAnnotations(threading.annotationList());
// Reset the threading root
threading.createIdTable([]);
threading.root = mail.messageContainer();
// Reload all new, unsaved annotations
threading.thread(drafts.unsaved());
return threading;
}
]
};
// @ngInject
function configureLocation($locationProvider) {
// Use HTML5 history
return $locationProvider.html5Mode(true);
}
// @ngInject
function configureRoutes($routeProvider) {
$routeProvider.when('/a/:id',
{
controller: 'AnnotationViewerController',
templateUrl: 'viewer.html',
reloadOnSearch: false,
resolve: resolve
});
$routeProvider.when('/viewer',
{
controller: 'WidgetController',
templateUrl: 'viewer.html',
reloadOnSearch: false,
resolve: resolve
});
$routeProvider.when('/stream',
{
controller: 'StreamController',
templateUrl: 'viewer.html',
reloadOnSearch: false,
resolve: resolve
});
return $routeProvider.otherwise({
redirectTo: '/viewer'
});
}
// @ngInject
function setupCrossFrame(crossframe) {
return crossframe.connect();
}
// @ngInject
function setupHttp($http) {
$http.defaults.headers.common['X-Client-Id'] = streamer.clientId;
}
module.exports = angular.module('h', [
// Angular addons which export the Angular module name
// via module.exports
require('angular-jwt'),
require('angular-resource'),
require('angular-route'),
require('angular-sanitize'),
require('angular-toastr'),
// Angular addons which do not export the Angular module
// name via module.exports
['angulartics', require('angulartics')][0],
['angulartics.google.analytics', require('angulartics/src/angulartics-ga')][0],
['ngTagsInput', require('ng-tags-input')][0],
['ui.bootstrap', require('./vendor/ui-bootstrap-custom-tpls-0.13.4')][0],
// Local addons
'ngRaven'
])
.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)
.directive('deepCount', require('./directive/deep-count'))
.directive('excerpt', require('./directive/excerpt').directive)
.directive('formInput', require('./directive/form-input'))
.directive('formValidate', require('./directive/form-validate'))
.directive('groupList', require('./directive/group-list').directive)
.directive('hAutofocus', require('./directive/h-autofocus'))
.directive('loginForm', require('./directive/login-form').directive)
.directive('markdown', require('./directive/markdown'))
.directive('simpleSearch', require('./directive/simple-search'))
.directive('statusButton', require('./directive/status-button'))
.directive('thread', require('./directive/thread'))
.directive('threadFilter', require('./directive/thread-filter'))
.directive('spinner', require('./directive/spinner'))
.directive('shareDialog', require('./directive/share-dialog'))
.directive('windowScroll', require('./directive/window-scroll'))
.directive('dropdownMenuBtn', require('./directive/dropdown-menu-btn'))
.directive('publishAnnotationBtn', require('./directive/publish-annotation-btn'))
.directive('searchStatusBar', require('./directive/search-status-bar'))
.directive('sidebarTutorial', require('./directive/sidebar-tutorial').directive)
.directive('signinControl', require('./directive/signin-control'))
.directive('sortDropdown', require('./directive/sort-dropdown'))
.directive('topBar', require('./directive/top-bar'))
.filter('converter', require('./filter/converter'))
.provider('identity', require('./identity'))
.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('features', require('./features'))
.service('flash', require('./flash'))
.service('formRespond', require('./form-respond'))
.service('groups', require('./groups'))
.service('host', require('./host'))
.service('localStorage', require('./local-storage'))
.service('permissions', require('./permissions'))
.service('queryParser', require('./query-parser'))
.service('render', require('./render'))
.service('searchFilter', require('./search-filter'))
.service('session', require('./session'))
.service('streamFilter', require('./stream-filter'))
.service('tags', require('./tags'))
.service('threading', require('./threading'))
.service('unicode', require('./unicode'))
.service('viewFilter', require('./view-filter'))
.factory('store', require('./store'))
.value('AnnotationSync', require('./annotation-sync'))
.value('AnnotationUISync', require('./annotation-ui-sync'))
.value('Discovery', require('./discovery'))
.value('raven', require('./raven'))
.value('settings', settings)
.value('time', require('./time'))
.config(configureLocation)
.config(configureRoutes)
.run(setupCrossFrame)
.run(setupHttp);
require('./config/module');
......@@ -4,123 +4,146 @@ var annotationMetadata = require('../annotation-metadata');
var extractDocumentMetadata = annotationMetadata.extractDocumentMetadata;
describe('extractDocumentMetadata()', function() {
describe('annotation-metadata', function () {
describe('.extractDocumentMetadata', function() {
context('when the model has a document property', function() {
it('returns the hostname from model.uri as the domain', function() {
var model = {
document: {},
uri: 'http://example.com/'
};
context('when the model has a document property', function() {
it('returns the hostname from model.uri as the domain', function() {
var model = {
document: {},
uri: 'http://example.com/'
};
assert.equal(extractDocumentMetadata(model).domain, 'example.com');
});
assert.equal(extractDocumentMetadata(model).domain, 'example.com');
});
context('when model.uri starts with "urn"', function() {
it(
'uses the first document.link uri that doesn\'t start with "urn"',
function() {
var model = {
uri: 'urn:isbn:0451450523',
document: {
link: [
{href: 'urn:isan:0000-0000-9E59-0000-O-0000-0000-2'},
{href: 'http://example.com/'}
]
}
};
assert.equal(
extractDocumentMetadata(model).uri, 'http://example.com/');
}
);
});
context('when model.uri starts with "urn"', function() {
it(
'uses the first document.link uri that doesn\'t start with "urn"',
function() {
context('when model.uri does not start with "urn"', function() {
it('uses model.uri as the uri', function() {
var model = {
uri: 'urn:isbn:0451450523',
document: {
link: [
{href: 'urn:isan:0000-0000-9E59-0000-O-0000-0000-2'},
{href: 'http://example.com/'}
]
}
document: {},
uri: 'http://example.com/'
};
assert.equal(
extractDocumentMetadata(model).uri, 'http://example.com/');
}
);
});
});
});
context('when model.uri does not start with "urn"', function() {
it('uses model.uri as the uri', function() {
var model = {
document: {},
uri: 'http://example.com/'
};
context('when document.title is a string', function() {
it('returns document.title as title', function() {
var model = {
uri: 'http://example.com/',
document: {
title: 'My Document'
}
};
assert.equal(
extractDocumentMetadata(model).uri, 'http://example.com/');
assert.equal(
extractDocumentMetadata(model).title, model.document.title);
});
});
});
context('when document.title is a string', function() {
it('returns document.title as title', function() {
var model = {
uri: 'http://example.com/',
document: {
title: 'My Document'
}
};
context('when document.title is an array', function() {
it('returns document.title[0] as title', function() {
var model = {
uri: 'http://example.com/',
document: {
title: ['My Document', 'My Other Document']
}
};
assert.equal(
extractDocumentMetadata(model).title, model.document.title);
assert.equal(
extractDocumentMetadata(model).title, model.document.title[0]);
});
});
});
context('when document.title is an array', function() {
it('returns document.title[0] as title', function() {
var model = {
uri: 'http://example.com/',
document: {
title: ['My Document', 'My Other Document']
}
};
context('when there is no document.title', function() {
it('returns the domain as the title', function() {
var model = {
document: {},
uri: 'http://example.com/',
};
assert.equal(
extractDocumentMetadata(model).title, model.document.title[0]);
assert.equal(extractDocumentMetadata(model).title, 'example.com');
});
});
});
context('when there is no document.title', function() {
it('returns the domain as the title', function() {
var model = {
document: {},
uri: 'http://example.com/',
};
context('when the model does not have a document property', function() {
it('returns model.uri for the uri', function() {
var model = {uri: 'http://example.com/'};
assert.equal(extractDocumentMetadata(model).title, 'example.com');
assert.equal(extractDocumentMetadata(model).uri, model.uri);
});
});
});
context('when the model does not have a document property', function() {
it('returns model.uri for the uri', function() {
var model = {uri: 'http://example.com/'};
it('returns the hostname of model.uri for the domain', function() {
var model = {uri: 'http://example.com/'};
assert.equal(extractDocumentMetadata(model).uri, model.uri);
});
assert.equal(extractDocumentMetadata(model).domain, 'example.com');
});
it('returns the hostname of model.uri for the domain', function() {
var model = {uri: 'http://example.com/'};
it('returns the hostname of model.uri for the title', function() {
var model = {uri: 'http://example.com/'};
assert.equal(extractDocumentMetadata(model).domain, 'example.com');
assert.equal(extractDocumentMetadata(model).title, 'example.com');
});
});
it('returns the hostname of model.uri for the title', function() {
var model = {uri: 'http://example.com/'};
context('when the title is longer than 30 characters', function() {
it('truncates the title with "…"', function() {
var model = {
uri: 'http://example.com/',
document: {
title: 'My Really Really Long Document Title'
}
};
assert.equal(extractDocumentMetadata(model).title, 'example.com');
assert.equal(
extractDocumentMetadata(model).title,
'My Really Really Long Document…'
);
});
});
});
context('when the title is longer than 30 characters', function() {
it('truncates the title with "…"', function() {
var model = {
uri: 'http://example.com/',
document: {
title: 'My Really Really Long Document Title'
}
};
assert.equal(
extractDocumentMetadata(model).title,
'My Really Really Long Document…'
);
describe('.location', function () {
it('returns the position for annotations with a text position', function () {
assert.equal(annotationMetadata.location({
target: [{
selector: [{
type: 'TextPositionSelector',
start: 100,
}]
}]
}), 100);
});
it('returns +ve infinity for annotations without a text position', function () {
assert.equal(annotationMetadata.location({
target: [{
selector: undefined,
}]
}), Number.POSITIVE_INFINITY);
});
});
});
{module, inject} = angular.mock
events = require('../events')
describe 'AppController', ->
$controller = null
$scope = null
$rootScope = null
fakeAnnotationUI = null
fakeAuth = null
fakeDrafts = null
fakeFeatures = null
fakeIdentity = null
fakeLocation = null
fakeParams = null
fakeSession = null
fakeGroups = null
fakeRoute = null
fakeWindow = null
sandbox = null
createController = (locals={}) ->
locals.$scope = $scope
$controller('AppController', locals)
before ->
angular.module('h', [])
.controller('AppController', require('../app-controller'))
.controller('AnnotationUIController', angular.noop)
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeAnnotationUI = {
tool: 'comment'
clearSelectedAnnotations: sandbox.spy()
}
fakeAuth = {
userid: sandbox.stub()
}
fakeDrafts = {
contains: sandbox.stub()
remove: sandbox.spy()
all: sandbox.stub().returns([])
discard: sandbox.spy()
count: sandbox.stub().returns(0)
unsaved: sandbox.stub().returns([])
}
fakeFeatures = {
fetch: sandbox.spy()
flagEnabled: sandbox.stub().returns(false)
}
fakeIdentity = {
watch: sandbox.spy()
request: sandbox.spy()
logout: sandbox.stub()
}
fakeLocation = {
search: sandbox.stub().returns({})
}
fakeParams = {id: 'test'}
fakeSession = {}
fakeGroups = {focus: sandbox.spy()}
fakeRoute = {reload: sandbox.spy()}
fakeWindow = {
top: {}
confirm: sandbox.stub()
}
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value 'auth', fakeAuth
$provide.value 'drafts', fakeDrafts
$provide.value 'features', fakeFeatures
$provide.value 'identity', fakeIdentity
$provide.value 'session', fakeSession
$provide.value 'groups', fakeGroups
$provide.value '$route', fakeRoute
$provide.value '$location', fakeLocation
$provide.value '$routeParams', fakeParams
$provide.value '$window', fakeWindow
return
beforeEach inject (_$controller_, _$rootScope_) ->
$controller = _$controller_
$rootScope = _$rootScope_
$scope = $rootScope.$new()
afterEach ->
sandbox.restore()
describe 'isSidebar property', ->
it 'is false if the window is the top window', ->
fakeWindow.top = fakeWindow
createController()
assert.isFalse($scope.isSidebar)
it 'is true if the window is not the top window', ->
fakeWindow.top = {}
createController()
assert.isTrue($scope.isSidebar)
it 'watches the identity service for identity change events', ->
createController()
assert.calledOnce(fakeIdentity.watch)
it 'auth.status is "unknown" on startup', ->
createController()
assert.equal($scope.auth.status, 'unknown')
it 'sets auth.status to "signed-out" when the identity has been checked but the user is not authenticated', ->
createController()
{onready} = fakeIdentity.watch.args[0][0]
onready()
assert.equal($scope.auth.status, 'signed-out')
it 'sets auth.status to "signed-in" when the identity has been checked and the user is authenticated', ->
createController()
fakeAuth.userid.withArgs('test-assertion').returns('acct:hey@joe')
{onlogin} = fakeIdentity.watch.args[0][0]
onlogin('test-assertion')
assert.equal($scope.auth.status, 'signed-in')
it 'sets userid, username, and provider properties at login', ->
createController()
fakeAuth.userid.withArgs('test-assertion').returns('acct:hey@joe')
{onlogin} = fakeIdentity.watch.args[0][0]
onlogin('test-assertion')
assert.equal($scope.auth.userid, 'acct:hey@joe')
assert.equal($scope.auth.username, 'hey')
assert.equal($scope.auth.provider, 'joe')
it 'sets auth.status to "signed-out" at logout', ->
createController()
{onlogout} = fakeIdentity.watch.args[0][0]
onlogout()
assert.equal($scope.auth.status, "signed-out")
it 'does not show login form for logged in users', ->
createController()
assert.isFalse($scope.accountDialog.visible)
it 'does not show the share dialog at start', ->
createController()
assert.isFalse($scope.shareDialog.visible)
it 'does not reload the view when the logged-in user changes on first load', ->
createController()
fakeRoute.reload = sinon.spy()
$scope.$broadcast(events.USER_CHANGED, {initialLoad: true})
assert.notCalled(fakeRoute.reload)
it 'reloads the view when the logged-in user changes after first load', ->
createController()
fakeRoute.reload = sinon.spy()
$scope.$broadcast(events.USER_CHANGED, {initialLoad: false})
assert.calledOnce(fakeRoute.reload)
describe 'logout()', ->
it 'prompts the user if there are drafts', ->
fakeDrafts.count.returns(1)
createController()
$scope.logout()
assert.equal(fakeWindow.confirm.callCount, 1)
it 'emits "annotationDeleted" for each unsaved draft annotation', ->
fakeDrafts.unsaved = sandbox.stub().returns(
["draftOne", "draftTwo", "draftThree"]
)
createController()
$rootScope.$emit = sandbox.stub()
$scope.logout()
assert($rootScope.$emit.calledThrice)
assert.deepEqual(
$rootScope.$emit.firstCall.args, ["annotationDeleted", "draftOne"])
assert.deepEqual(
$rootScope.$emit.secondCall.args, ["annotationDeleted", "draftTwo"])
assert.deepEqual(
$rootScope.$emit.thirdCall.args, ["annotationDeleted", "draftThree"])
it 'discards draft annotations', ->
createController()
$scope.logout()
assert(fakeDrafts.discard.calledOnce)
it 'does not emit "annotationDeleted" if the user cancels the prompt', ->
createController()
fakeDrafts.count.returns(1)
$rootScope.$emit = sandbox.stub()
fakeWindow.confirm.returns(false)
$scope.logout()
assert($rootScope.$emit.notCalled)
it 'does not discard drafts if the user cancels the prompt', ->
createController()
fakeDrafts.count.returns(1)
fakeWindow.confirm.returns(false)
$scope.logout()
assert(fakeDrafts.discard.notCalled)
it 'does not prompt if there are no drafts', ->
createController()
fakeDrafts.count.returns(0)
$scope.logout()
assert.equal(fakeWindow.confirm.callCount, 0)
'use strict';
var angular = require('angular');
var events = require('../events');
describe('AppController', function () {
var $controller = null;
var $scope = null;
var $rootScope = null;
var fakeAnnotationUI = null;
var fakeAuth = null;
var fakeDrafts = null;
var fakeFeatures = null;
var fakeIdentity = null;
var fakeLocation = null;
var fakeParams = null;
var fakeSession = null;
var fakeGroups = null;
var fakeRoute = null;
var fakeWindow = null;
var sandbox = null;
var createController = function (locals) {
locals = locals || {};
locals.$scope = $scope;
return $controller('AppController', locals);
};
before(function () {
return angular.module('h', [])
.controller('AppController', require('../app-controller'))
.controller('AnnotationUIController', angular.noop);
});
beforeEach(angular.mock.module('h'));
beforeEach(angular.mock.module(function ($provide) {
sandbox = sinon.sandbox.create();
fakeAnnotationUI = {
tool: 'comment',
clearSelectedAnnotations: sandbox.spy()
};
fakeAuth = {
userid: sandbox.stub()
};
fakeDrafts = {
contains: sandbox.stub(),
remove: sandbox.spy(),
all: sandbox.stub().returns([]),
discard: sandbox.spy(),
count: sandbox.stub().returns(0),
unsaved: sandbox.stub().returns([])
};
fakeFeatures = {
fetch: sandbox.spy(),
flagEnabled: sandbox.stub().returns(false)
};
fakeIdentity = {
watch: sandbox.spy(),
request: sandbox.spy(),
logout: sandbox.stub()
};
fakeLocation = {
search: sandbox.stub().returns({})
};
fakeParams = {id: 'test'};
fakeSession = {};
fakeGroups = {focus: sandbox.spy()};
fakeRoute = {reload: sandbox.spy()};
fakeWindow = {
top: {},
confirm: sandbox.stub()
};
$provide.value('annotationUI', fakeAnnotationUI);
$provide.value('auth', fakeAuth);
$provide.value('drafts', fakeDrafts);
$provide.value('features', fakeFeatures);
$provide.value('identity', fakeIdentity);
$provide.value('session', fakeSession);
$provide.value('groups', fakeGroups);
$provide.value('$route', fakeRoute);
$provide.value('$location', fakeLocation);
$provide.value('$routeParams', fakeParams);
$provide.value('$window', fakeWindow);
}));
beforeEach(angular.mock.inject(function (_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
}));
afterEach(function () {
sandbox.restore();
});
describe('isSidebar property', function () {
it('is false if the window is the top window', function () {
fakeWindow.top = fakeWindow;
createController();
assert.isFalse($scope.isSidebar);
});
it('is true if the window is not the top window', function () {
fakeWindow.top = {};
createController();
assert.isTrue($scope.isSidebar);
});
});
it('watches the identity service for identity change events', function () {
createController();
assert.calledOnce(fakeIdentity.watch);
});
it('auth.status is "unknown" on startup', function () {
createController();
assert.equal($scope.auth.status, 'unknown');
});
it('sets auth.status to "signed-out" when the identity has been checked but the user is not authenticated', function () {
createController();
var identityCallbackArgs = fakeIdentity.watch.args[0][0];
identityCallbackArgs.onready();
assert.equal($scope.auth.status, 'signed-out');
});
it('sets auth.status to "signed-in" when the identity has been checked and the user is authenticated', function () {
createController();
fakeAuth.userid.withArgs('test-assertion').returns('acct:hey@joe');
var identityCallbackArgs = fakeIdentity.watch.args[0][0];
identityCallbackArgs.onlogin('test-assertion');
assert.equal($scope.auth.status, 'signed-in');
});
it('sets userid, username, and provider properties at login', function () {
createController();
fakeAuth.userid.withArgs('test-assertion').returns('acct:hey@joe');
var identityCallbackArgs = fakeIdentity.watch.args[0][0];
identityCallbackArgs.onlogin('test-assertion');
assert.equal($scope.auth.userid, 'acct:hey@joe');
assert.equal($scope.auth.username, 'hey');
assert.equal($scope.auth.provider, 'joe');
});
it('sets auth.status to "signed-out" at logout', function () {
createController();
var identityCallbackArgs = fakeIdentity.watch.args[0][0];
identityCallbackArgs.onlogout();
assert.equal($scope.auth.status, "signed-out");
});
it('does not show login form for logged in users', function () {
createController();
assert.isFalse($scope.accountDialog.visible);
});
it('does not show the share dialog at start', function () {
createController();
assert.isFalse($scope.shareDialog.visible);
});
it('does not reload the view when the logged-in user changes on first load', function () {
createController();
fakeRoute.reload = sinon.spy();
$scope.$broadcast(events.USER_CHANGED, {initialLoad: true});
assert.notCalled(fakeRoute.reload);
});
it('reloads the view when the logged-in user changes after first load', function () {
createController();
fakeRoute.reload = sinon.spy();
$scope.$broadcast(events.USER_CHANGED, {initialLoad: false});
assert.calledOnce(fakeRoute.reload);
});
describe('logout()', function () {
it('prompts the user if there are drafts', function () {
fakeDrafts.count.returns(1);
createController();
$scope.logout();
assert.equal(fakeWindow.confirm.callCount, 1);
});
it('emits "annotationDeleted" for each unsaved draft annotation', function () {
fakeDrafts.unsaved = sandbox.stub().returns(
["draftOne", "draftTwo", "draftThree"]
);
createController();
$rootScope.$emit = sandbox.stub();
$scope.logout();
assert($rootScope.$emit.calledThrice);
assert.deepEqual(
$rootScope.$emit.firstCall.args, ["annotationDeleted", "draftOne"]);
assert.deepEqual(
$rootScope.$emit.secondCall.args, ["annotationDeleted", "draftTwo"]);
assert.deepEqual(
$rootScope.$emit.thirdCall.args, ["annotationDeleted", "draftThree"]);
});
it('discards draft annotations', function () {
createController();
$scope.logout();
assert(fakeDrafts.discard.calledOnce);
});
it('does not emit "annotationDeleted" if the user cancels the prompt', function () {
createController();
fakeDrafts.count.returns(1);
$rootScope.$emit = sandbox.stub();
fakeWindow.confirm.returns(false);
$scope.logout();
assert($rootScope.$emit.notCalled);
});
it('does not discard drafts if the user cancels the prompt', function () {
createController();
fakeDrafts.count.returns(1);
fakeWindow.confirm.returns(false);
$scope.logout();
assert(fakeDrafts.discard.notCalled);
});
it('does not prompt if there are no drafts', function () {
createController();
fakeDrafts.count.returns(0);
$scope.logout();
assert.equal(fakeWindow.confirm.callCount, 0);
});
});
});
{module, inject} = angular.mock
events = require('../events')
describe 'WidgetController', ->
$scope = null
$rootScope = null
fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeAuth = null
fakeCrossFrame = null
fakeDrafts = null
fakeStore = null
fakeStreamer = null
fakeStreamFilter = null
fakeThreading = null
fakeGroups = null
lastSearchResult = null
sandbox = null
viewer = null
before ->
angular.module('h', [])
.controller('WidgetController', require('../widget-controller.coffee'))
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeAnnotationMapper = {
loadAnnotations: sandbox.spy()
unloadAnnotations: sandbox.spy()
}
fakeAnnotationUI = {
tool: 'comment'
clearSelectedAnnotations: sandbox.spy()
}
fakeAuth = {user: null}
fakeCrossFrame = {frames: []}
fakeDrafts = {
unsaved: sandbox.stub()
}
fakeStore = {
SearchResource:
get: (query, callback) ->
offset = query.offset or 0
limit = query.limit or 20
result =
total: 100
rows: [offset..offset+limit-1]
replies: []
callback result
}
fakeStreamer = {
setConfig: sandbox.spy()
}
fakeStreamFilter = {
resetFilter: sandbox.stub().returnsThis()
addClause: sandbox.stub().returnsThis()
getFilter: sandbox.stub().returns({})
}
fakeThreading = {
root: {},
thread: sandbox.stub()
}
fakeGroups = {
focused: -> {id: 'foo'}
}
$provide.value 'annotationMapper', fakeAnnotationMapper
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value 'crossframe', fakeCrossFrame
$provide.value 'drafts', fakeDrafts
$provide.value 'store', fakeStore
$provide.value 'streamer', fakeStreamer
$provide.value 'streamFilter', fakeStreamFilter
$provide.value 'threading', fakeThreading
$provide.value 'groups', fakeGroups
return
beforeEach inject ($controller, _$rootScope_) ->
$rootScope = _$rootScope_
$scope = $rootScope.$new()
viewer = $controller 'WidgetController', {$scope}
afterEach ->
sandbox.restore()
describe 'loadAnnotations', ->
it 'loads all annotations for a frame', ->
viewer.chunkSize = 20
fakeCrossFrame.frames.push({uri: '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])
it 'passes _separate_replies: true to the search API', ->
fakeStore.SearchResource.get = sandbox.stub()
fakeCrossFrame.frames.push({uri: 'http://example.com'})
$scope.$digest()
assert.equal(
fakeStore.SearchResource.get.firstCall.args[0]._separate_replies, true)
it 'passes annotations and replies from search to loadAnnotations()', ->
fakeStore.SearchResource.get = (query, callback) ->
callback({
rows: ['annotation_1', 'annotation_2']
replies: ['reply_1', 'reply_2', 'reply_3']
})
fakeCrossFrame.frames.push({uri: 'http://example.com'})
$scope.$digest()
assert fakeAnnotationMapper.loadAnnotations.calledOnce
assert fakeAnnotationMapper.loadAnnotations.calledWith(
['annotation_1', 'annotation_2'], ['reply_1', 'reply_2', 'reply_3']
)
describe 'when the focused group changes', ->
it 'should load annotations for the new group', ->
fakeThreading.annotationList = sandbox.stub().returns([{id: '1'}])
fakeCrossFrame.frames.push({uri: 'http://example.com'})
searchResult = {total: 10, rows: [0..10], replies: []}
fakeStore.SearchResource.get = (query, callback) ->
callback(searchResult)
$scope.$broadcast(events.GROUP_FOCUSED)
assert.calledWith(fakeAnnotationMapper.unloadAnnotations,
[{id: '1'}])
$scope.$digest();
assert.calledWith(fakeAnnotationMapper.loadAnnotations,
searchResult.rows)
assert.calledWith(fakeThreading.thread, fakeDrafts.unsaved())
describe 'when a new annotation is created', ->
###*
# It should clear any selection that exists in the sidebar before
# creating a new annotation. Otherwise the new annotation with its
# form open for the user to type in won't be visible because it's
# not part of the selection.
###
it 'clears the selection', ->
$scope.clearSelection = sinon.stub()
$rootScope.$emit('beforeAnnotationCreated', {})
assert.called($scope.clearSelection)
it 'does not clear the selection if the new annotation is a highlight', ->
$scope.clearSelection = sinon.stub()
$rootScope.$emit('beforeAnnotationCreated', {$highlight: true})
assert.notCalled($scope.clearSelection)
it 'does not clear the selection if the new annotation is a reply', ->
$scope.clearSelection = sinon.stub()
$rootScope.$emit('beforeAnnotationCreated', {
references: ['parent-id']
})
assert.notCalled($scope.clearSelection)
'use strict';
var angular = require('angular');
var events = require('../events');
describe('WidgetController', function () {
var $scope = null;
var $rootScope = null;
var fakeAnnotationMapper = null;
var fakeAnnotationUI = null;
var fakeAuth = null;
var fakeCrossFrame = null;
var fakeDrafts = null;
var fakeStore = null;
var fakeStreamer = null;
var fakeStreamFilter = null;
var fakeThreading = null;
var fakeGroups = null;
var sandbox = null;
var viewer = null;
before(function () {
angular.module('h', [])
.controller('WidgetController', require('../widget-controller'));
});
beforeEach(angular.mock.module('h'));
beforeEach(angular.mock.module(function ($provide) {
sandbox = sinon.sandbox.create();
fakeAnnotationMapper = {
loadAnnotations: sandbox.spy(),
unloadAnnotations: sandbox.spy()
};
fakeAnnotationUI = {
tool: 'comment',
clearSelectedAnnotations: sandbox.spy()
};
fakeAuth = {user: null};
fakeCrossFrame = {frames: []};
fakeDrafts = {
unsaved: sandbox.stub()
};
fakeStore = {
SearchResource: {
get: function (query, callback) {
var offset = query.offset || 0;
var limit = query.limit || 20;
var result =
{
total: 100,
rows: ((function () {
var result1 = [];
var end = offset + limit - 1;
var i = offset;
if (offset <= end) {
while (i <= end) {
result1.push(i++);
}
} else {
while (i >= end) {
result1.push(i--);
}
}
return result1;
})()),
replies: []
};
return callback(result);
}
},
};
fakeStreamer = {
setConfig: sandbox.spy()
};
fakeStreamFilter = {
resetFilter: sandbox.stub().returnsThis(),
addClause: sandbox.stub().returnsThis(),
getFilter: sandbox.stub().returns({})
};
fakeThreading = {
root: {},
thread: sandbox.stub()
};
fakeGroups = {
focused: function () { return {id: 'foo'}; }
};
$provide.value('annotationMapper', fakeAnnotationMapper);
$provide.value('annotationUI', fakeAnnotationUI);
$provide.value('crossframe', fakeCrossFrame);
$provide.value('drafts', fakeDrafts);
$provide.value('store', fakeStore);
$provide.value('streamer', fakeStreamer);
$provide.value('streamFilter', fakeStreamFilter);
$provide.value('threading', fakeThreading);
$provide.value('groups', fakeGroups);
return;
}));
beforeEach(angular.mock.inject(function ($controller, _$rootScope_) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
viewer = $controller('WidgetController', {$scope: $scope});
}));
afterEach(function () {
return sandbox.restore();
});
describe('loadAnnotations', function () {
it('loads all annotations for a frame', function () {
$scope.chunkSize = 20;
fakeCrossFrame.frames.push({uri: 'http://example.com'});
$scope.$digest();
var loadSpy = fakeAnnotationMapper.loadAnnotations;
assert.callCount(loadSpy, 5);
assert.calledWith(loadSpy, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
assert.calledWith(loadSpy, [20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39]);
assert.calledWith(loadSpy, [40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59]);
assert.calledWith(loadSpy, [60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79]);
assert.calledWith(loadSpy, [80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99]);
});
it('passes _separate_replies: true to the search API', function () {
fakeStore.SearchResource.get = sandbox.stub();
fakeCrossFrame.frames.push({uri: 'http://example.com'});
$scope.$digest();
assert.equal(
fakeStore.SearchResource.get.firstCall.args[0]._separate_replies, true);
});
return it('passes annotations and replies from search to loadAnnotations()', function () {
fakeStore.SearchResource.get = function (query, callback) {
return callback({
rows: ['annotation_1', 'annotation_2'],
replies: ['reply_1', 'reply_2', 'reply_3']
});
};
fakeCrossFrame.frames.push({uri: 'http://example.com'});
$scope.$digest();
assert(fakeAnnotationMapper.loadAnnotations.calledOnce);
assert(fakeAnnotationMapper.loadAnnotations.calledWith(
['annotation_1', 'annotation_2'], ['reply_1', 'reply_2', 'reply_3']
));
});
});
describe('when the focused group changes', function () {
return it('should load annotations for the new group', function () {
fakeThreading.annotationList = sandbox.stub().returns([{id: '1'}]);
fakeCrossFrame.frames.push({uri: 'http://example.com'});
var searchResult = {total: 10, rows: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], replies: []};
fakeStore.SearchResource.get = function (query, callback) {
return callback(searchResult);
};
$scope.$broadcast(events.GROUP_FOCUSED);
assert.calledWith(fakeAnnotationMapper.unloadAnnotations, [{id: '1'}]);
$scope.$digest();
assert.calledWith(fakeAnnotationMapper.loadAnnotations, searchResult.rows);
assert.calledWith(fakeThreading.thread, fakeDrafts.unsaved());
});
});
describe('when a new annotation is created', function () {
/**
* It should clear any selection that exists in the sidebar before
* creating a new annotation. Otherwise the new annotation with its
* form open for the user to type in won't be visible because it's
* not part of the selection.
*/
it('clears the selection', function () {
$scope.clearSelection = sinon.stub();
$rootScope.$emit('beforeAnnotationCreated', {});
assert.called($scope.clearSelection);
});
it('does not clear the selection if the new annotation is a highlight', function () {
$scope.clearSelection = sinon.stub();
$rootScope.$emit('beforeAnnotationCreated', {$highlight: true});
assert.notCalled($scope.clearSelection);
});
it('does not clear the selection if the new annotation is a reply', function () {
$scope.clearSelection = sinon.stub();
$rootScope.$emit('beforeAnnotationCreated', {
references: ['parent-id']
});
assert.notCalled($scope.clearSelection);
});
});
});
angular = require('angular')
events = require('./events')
module.exports = class WidgetController
this.$inject = [
'$scope', '$rootScope', 'annotationUI', 'crossframe', 'annotationMapper',
'drafts', 'groups', 'streamer', 'streamFilter', 'store', 'threading'
]
constructor: (
$scope, $rootScope, annotationUI, crossframe, annotationMapper,
drafts, groups, streamer, streamFilter, store, threading
) ->
$scope.threadRoot = threading.root
$scope.sortOptions = ['Newest', 'Oldest', 'Location']
@chunkSize = 200
loaded = []
_resetAnnotations = ->
# Unload all the annotations
annotationMapper.unloadAnnotations(threading.annotationList())
# Reload all the drafts
threading.thread(drafts.unsaved())
_loadAnnotationsFrom = (query, offset) =>
queryCore =
limit: @chunkSize
offset: offset
sort: 'created'
order: 'asc'
group: groups.focused().id
q = angular.extend(queryCore, query)
q._separate_replies = true
store.SearchResource.get q, (results) ->
total = results.total
offset += results.rows.length
if offset < total
_loadAnnotationsFrom query, offset
annotationMapper.loadAnnotations(results.rows, results.replies)
loadAnnotations = (frames) ->
for f in frames
if f.uri in loaded
continue
loaded.push(f.uri)
_loadAnnotationsFrom({uri: f.uri}, 0)
if loaded.length > 0
streamFilter.resetFilter().addClause('/uri', 'one_of', loaded)
streamer.setConfig('filter', {filter: streamFilter.getFilter()})
$scope.$on events.GROUP_FOCUSED, ->
_resetAnnotations(annotationMapper, drafts, threading)
loaded = []
loadAnnotations crossframe.frames
$scope.$watchCollection (-> crossframe.frames), loadAnnotations
$scope.focus = (annotation) ->
if angular.isObject annotation
highlights = [annotation.$$tag]
else
highlights = []
crossframe.call('focusAnnotations', highlights)
$scope.scrollTo = (annotation) ->
if angular.isObject annotation
crossframe.call('scrollToAnnotation', annotation.$$tag)
$scope.hasFocus = (annotation) ->
!!($scope.focusedAnnotations ? {})[annotation?.$$tag]
$rootScope.$on('beforeAnnotationCreated', (event, data) ->
if data.$highlight || (data.references && data.references.length > 0)
return
$scope.clearSelection()
)
'use strict';
var angular = require('angular');
var events = require('./events');
// @ngInject
module.exports = function WidgetController(
$scope, $rootScope, annotationUI, crossframe, annotationMapper,
drafts, groups, streamer, streamFilter, store, threading
) {
$scope.threadRoot = threading.root;
$scope.sortOptions = ['Newest', 'Oldest', 'Location'];
var DEFAULT_CHUNK_SIZE = 200;
var loaded = [];
var _resetAnnotations = function () {
// Unload all the annotations
annotationMapper.unloadAnnotations(threading.annotationList());
// Reload all the drafts
threading.thread(drafts.unsaved());
};
var _loadAnnotationsFrom = function (query, offset) {
var queryCore = {
limit: $scope.chunkSize || DEFAULT_CHUNK_SIZE,
offset: offset,
sort: 'created',
order: 'asc',
group: groups.focused().id
};
var q = angular.extend(queryCore, query);
q._separate_replies = true;
store.SearchResource.get(q, function (results) {
var total = results.total;
offset += results.rows.length;
if (offset < total) {
_loadAnnotationsFrom(query, offset);
}
annotationMapper.loadAnnotations(results.rows, results.replies);
});
};
var loadAnnotations = function (frames) {
for (var i = 0, f; i < frames.length; i++) {
f = frames[i];
var ref;
if (ref = f.uri, loaded.indexOf(ref) >= 0) {
continue;
}
loaded.push(f.uri);
_loadAnnotationsFrom({uri: f.uri}, 0);
}
if (loaded.length > 0) {
streamFilter.resetFilter().addClause('/uri', 'one_of', loaded);
streamer.setConfig('filter', {filter: streamFilter.getFilter()});
}
};
$scope.$on(events.GROUP_FOCUSED, function () {
_resetAnnotations(annotationMapper, drafts, threading);
loaded = [];
return loadAnnotations(crossframe.frames);
});
$scope.$watchCollection(function () {
return crossframe.frames;
}, loadAnnotations);
$scope.focus = function (annotation) {
var highlights = [];
if (angular.isObject(annotation)) {
highlights = [annotation.$$tag];
}
return crossframe.call('focusAnnotations', highlights);
};
$scope.scrollTo = function (annotation) {
if (angular.isObject(annotation)) {
return crossframe.call('scrollToAnnotation', annotation.$$tag);
}
};
$scope.hasFocus = function (annotation) {
if (!annotation || !$scope.focusedAnnotations) {
return false;
}
return annotation.$$tag in $scope.focusedAnnotations;
};
$rootScope.$on('beforeAnnotationCreated', function (event, data) {
if (data.$highlight || (data.references && data.references.length > 0)) {
return;
}
return $scope.clearSelection();
});
};
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