Commit b6f14aea authored by Nick Stenning's avatar Nick Stenning

Merge pull request #2629 from hypothesis/angular-1.4

Upgrade to Angular 1.4.7
parents 2f101ab2 dbac8b0d
...@@ -22,7 +22,7 @@ module.exports = class AnnotationViewerController ...@@ -22,7 +22,7 @@ module.exports = class AnnotationViewerController
$scope.search.update = (query) -> $scope.search.update = (query) ->
$location.path('/stream').search('q', query) $location.path('/stream').search('q', query)
store.AnnotationResource.read id: id, (annotation) -> store.AnnotationResource.get id: id, (annotation) ->
annotationMapper.loadAnnotations([annotation]) annotationMapper.loadAnnotations([annotation])
$scope.threadRoot = {children: [threading.idTable[id]]} $scope.threadRoot = {children: [threading.idTable[id]]}
store.SearchResource.get references: id, ({rows}) -> store.SearchResource.get references: id, ({rows}) ->
......
...@@ -32,8 +32,9 @@ resolve = ...@@ -32,8 +32,9 @@ resolve =
configureDocument = ['$provide', ($provide) -> configureDocument = ['$provide', ($provide) ->
$provide.decorator '$document', ($delegate) -> $provide.decorator '$document', ['$delegate', ($delegate) ->
$delegate.prop('baseURI', baseURI) $delegate.prop('baseURI', baseURI)
]
] ]
...@@ -93,7 +94,7 @@ module.exports = angular.module('h', [ ...@@ -93,7 +94,7 @@ module.exports = angular.module('h', [
'ngTagsInput' 'ngTagsInput'
'ngWebSocket' 'ngWebSocket'
'toastr' 'toastr'
require('./ui-bootstrap-custom') 'ui.bootstrap'
]) ])
.controller('AppController', require('./app-controller')) .controller('AppController', require('./app-controller'))
...@@ -150,7 +151,6 @@ module.exports = angular.module('h', [ ...@@ -150,7 +151,6 @@ module.exports = angular.module('h', [
.service('render', require('./render')) .service('render', require('./render'))
.service('searchFilter', require('./search-filter')) .service('searchFilter', require('./search-filter'))
.service('session', require('./session')) .service('session', require('./session'))
.service('store', require('./store'))
.service('streamFilter', require('./stream-filter')) .service('streamFilter', require('./stream-filter'))
.service('tags', require('./tags')) .service('tags', require('./tags'))
.service('time', require('./time')) .service('time', require('./time'))
...@@ -159,6 +159,7 @@ module.exports = angular.module('h', [ ...@@ -159,6 +159,7 @@ module.exports = angular.module('h', [
.service('viewFilter', require('./view-filter')) .service('viewFilter', require('./view-filter'))
.factory('settings', require('./settings')) .factory('settings', require('./settings'))
.factory('store', require('./store'))
.value('AnnotationSync', require('./annotation-sync')) .value('AnnotationSync', require('./annotation-sync'))
.value('AnnotationUISync', require('./annotation-ui-sync')) .value('AnnotationUISync', require('./annotation-ui-sync'))
......
# Instantiates all objects used for cross frame discovery and communication. # Instantiates all objects used for cross frame discovery and communication.
module.exports = class CrossFrame module.exports = class CrossFrame
this.inject = [ this.$inject = [
'$rootScope', '$document', '$window', 'store', 'annotationUI' '$rootScope', '$document', '$window', 'store', 'annotationUI'
'Discovery', 'bridge', 'Discovery', 'bridge',
'AnnotationSync', 'AnnotationUISync' 'AnnotationSync', 'AnnotationUISync'
......
...@@ -263,7 +263,7 @@ module.exports = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitiz ...@@ -263,7 +263,7 @@ module.exports = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitiz
scope.preview = false scope.preview = false
scope.togglePreview = -> scope.togglePreview = ->
if !scope.readonly if !scope.readOnly
scope.preview = !scope.preview scope.preview = !scope.preview
if scope.preview if scope.preview
output.style.height = input.style.height output.style.height = input.style.height
...@@ -330,7 +330,7 @@ module.exports = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitiz ...@@ -330,7 +330,7 @@ module.exports = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitiz
# Re-render the markdown when the view needs updating. # Re-render the markdown when the view needs updating.
ctrl.$render = -> ctrl.$render = ->
if !scope.readonly and !scope.preview if !scope.readOnly and !scope.preview
inputEl.val (ctrl.$viewValue or '') inputEl.val (ctrl.$viewValue or '')
value = ctrl.$viewValue or '' value = ctrl.$viewValue or ''
rendered = renderMathAndMarkdown value rendered = renderMathAndMarkdown value
...@@ -345,16 +345,16 @@ module.exports = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitiz ...@@ -345,16 +345,16 @@ module.exports = ['$filter', '$sanitize', '$sce', '$timeout', ($filter, $sanitiz
# Reset height of output div incase it has been changed. # Reset height of output div incase it has been changed.
# Re-render when it becomes uneditable. # Re-render when it becomes uneditable.
# Auto-focus the input box when the widget becomes editable. # Auto-focus the input box when the widget becomes editable.
scope.$watch 'readonly', (readonly) -> scope.$watch 'readOnly', (readOnly) ->
scope.preview = false scope.preview = false
output.style.height = "" output.style.height = ""
ctrl.$render() ctrl.$render()
unless readonly then $timeout -> inputEl.focus() unless readOnly then $timeout -> inputEl.focus()
require: '?ngModel' require: '?ngModel'
restrict: 'A' restrict: 'E'
scope: scope:
readonly: '@' readOnly: '='
required: '@' required: '@'
templateUrl: 'markdown.html' templateUrl: 'markdown.html'
] ]
...@@ -4,6 +4,7 @@ describe 'annotation', -> ...@@ -4,6 +4,7 @@ describe 'annotation', ->
$compile = null $compile = null
$document = null $document = null
$element = null $element = null
$q = null
$rootScope = null $rootScope = null
$scope = null $scope = null
$timeout = null $timeout = null
...@@ -111,9 +112,10 @@ describe 'annotation', -> ...@@ -111,9 +112,10 @@ describe 'annotation', ->
$provide.value 'groups', fakeGroups $provide.value 'groups', fakeGroups
return return
beforeEach inject (_$compile_, _$document_, _$rootScope_, _$timeout_) -> beforeEach inject (_$compile_, _$document_, _$q_, _$rootScope_, _$timeout_) ->
$compile = _$compile_ $compile = _$compile_
$document = _$document_ $document = _$document_
$q = _$q_
$timeout = _$timeout_ $timeout = _$timeout_
$rootScope = _$rootScope_ $rootScope = _$rootScope_
$scope = $rootScope.$new() $scope = $rootScope.$new()
...@@ -463,7 +465,7 @@ describe 'annotation', -> ...@@ -463,7 +465,7 @@ describe 'annotation', ->
it "calls annotationMapper.delete() if the delete is confirmed", (done) -> it "calls annotationMapper.delete() if the delete is confirmed", (done) ->
window.confirm.returns(true) window.confirm.returns(true)
fakeAnnotationMapper.deleteAnnotation.returns(Promise.resolve()) fakeAnnotationMapper.deleteAnnotation.returns($q.resolve())
controller.delete().then -> controller.delete().then ->
assert fakeAnnotationMapper.deleteAnnotation.calledWith(annotation) assert fakeAnnotationMapper.deleteAnnotation.calledWith(annotation)
...@@ -476,7 +478,7 @@ describe 'annotation', -> ...@@ -476,7 +478,7 @@ describe 'annotation', ->
it "flashes a generic error if the server cannot be reached", (done) -> it "flashes a generic error if the server cannot be reached", (done) ->
window.confirm.returns(true) window.confirm.returns(true)
fakeAnnotationMapper.deleteAnnotation.returns(Promise.reject({status: 0})) fakeAnnotationMapper.deleteAnnotation.returns($q.reject({status: 0}))
controller.delete().then -> controller.delete().then ->
assert fakeFlash.error.calledWith( assert fakeFlash.error.calledWith(
...@@ -486,7 +488,7 @@ describe 'annotation', -> ...@@ -486,7 +488,7 @@ describe 'annotation', ->
it "flashes an error if the delete fails on the server", (done) -> it "flashes an error if the delete fails on the server", (done) ->
window.confirm.returns(true) window.confirm.returns(true)
fakeAnnotationMapper.deleteAnnotation.returns(Promise.reject({ fakeAnnotationMapper.deleteAnnotation.returns($q.reject({
status: 500, status: 500,
statusText: "Server Error", statusText: "Server Error",
data: {} data: {}
...@@ -501,7 +503,7 @@ describe 'annotation', -> ...@@ -501,7 +503,7 @@ describe 'annotation', ->
it "doesn't flash an error if the delete succeeds", (done) -> it "doesn't flash an error if the delete succeeds", (done) ->
window.confirm.returns(true) window.confirm.returns(true)
fakeAnnotationMapper.deleteAnnotation.returns(Promise.resolve()) fakeAnnotationMapper.deleteAnnotation.returns($q.resolve())
controller.delete().then -> controller.delete().then ->
assert fakeFlash.error.notCalled assert fakeFlash.error.notCalled
......
...@@ -80,7 +80,7 @@ describe 'form-input', -> ...@@ -80,7 +80,7 @@ describe 'form-input', ->
assert.include($field.prop('className'), 'form-field-error', 'Fail fast check') assert.include($field.prop('className'), 'form-field-error', 'Fail fast check')
controller.$setViewValue('abc') controller.$setViewValue('def')
$scope.$digest() $scope.$digest()
assert.notInclude($field.prop('className'), 'form-field-error') assert.notInclude($field.prop('className'), 'form-field-error')
......
...@@ -59,10 +59,12 @@ function sessionActions(options) { ...@@ -59,10 +59,12 @@ function sessionActions(options) {
* @ngInject * @ngInject
*/ */
function session($document, $http, $resource, $rootScope, flash) { function session($document, $http, $resource, $rootScope, flash) {
// TODO: Move accounts data management (e.g. profile, edit_profile, // Headers sent by every request made by the session service.
// disable_user, etc) into another module with another route. var headers = {};
// TODO: Move accounts data management (e.g. profile, edit_profile,
// disable_user, etc) into another module with another route.
var actions = sessionActions({ var actions = sessionActions({
transformRequest: prepare, headers: headers,
transformResponse: process, transformResponse: process,
withCredentials: true withCredentials: true
}); });
...@@ -117,6 +119,11 @@ function session($document, $http, $resource, $rootScope, flash) { ...@@ -117,6 +119,11 @@ function session($document, $http, $resource, $rootScope, flash) {
// Copy the model data (including the CSRF token) into `resource.state`. // Copy the model data (including the CSRF token) into `resource.state`.
angular.copy(model, resource.state); angular.copy(model, resource.state);
// Set up subsequent requests to send the CSRF token in the headers.
if (resource.state.csrf) {
headers[$http.defaults.xsrfHeaderName] = resource.state.csrf;
}
// Replace lastLoad with the latest data, and update lastLoadTime. // Replace lastLoad with the latest data, and update lastLoadTime.
lastLoad = {$promise: Promise.resolve(model), $resolved: true}; lastLoad = {$promise: Promise.resolve(model), $resolved: true};
lastLoadTime = Date.now(); lastLoadTime = Date.now();
...@@ -129,14 +136,6 @@ function session($document, $http, $resource, $rootScope, flash) { ...@@ -129,14 +136,6 @@ function session($document, $http, $resource, $rootScope, flash) {
return model; return model;
}; };
function prepare(data, headersGetter) {
var csrfTok = resource.state.csrf;
if (typeof csrfTok !== 'undefined') {
headersGetter()[$http.defaults.xsrfHeaderName] = csrfTok;
}
return angular.toJson(data);
}
function process(data, headersGetter) { function process(data, headersGetter) {
// Parse as json // Parse as json
data = angular.fromJson(data); data = angular.fromJson(data);
......
###*
# @ngdoc service
# @name store
#
# @description
# The `store` service handles the backend calls for the restful API. This is
# created dynamically from the API index as the angular $resource() method
# supports the same keys as the index document. This will make a resource
# constructor for each endpoint eg. store.AnnotationResource() and
# store.SearchResource().
###
module.exports = [
'$http', '$resource', 'settings'
($http, $resource, settings) ->
camelize = (string) ->
string.replace /(?:^|_)([a-z])/g, (_, char) -> char.toUpperCase()
store =
$resolved: false
# We call the API root and it gives back the actions it provides.
$promise: $http.get(settings.apiUrl)
.finally -> store.$resolved = true
.then (response) ->
for name, actions of response.data.links
# For each action name we configure an ng-resource.
# For the search resource, one URL is given for all actions.
# For the annotations, each action has its own URL.
prop = "#{camelize(name)}Resource"
store[prop] = $resource(actions.url or settings.apiUrl, {}, actions)
store
]
'use strict';
var angular = require('angular');
function prependTransform(defaults, transform) {
// We can't guarantee that the default transformation is an array
var result = angular.isArray(defaults) ? defaults.slice(0) : [defaults];
result.unshift(transform);
return result;
}
// stripInternalProperties returns a shallow clone of `obj`, lacking all
// properties that begin with a character that marks them as internal
// (currently '$' or '_');
function stripInternalProperties(obj) {
var result = {};
for (var k in obj) {
if (obj.hasOwnProperty(k) && k[0] !== '$') {
result[k] = obj[k];
}
}
return result;
}
/**
* @ngdoc factory
* @name store
* @description The `store` service handles the backend calls for the restful
* API. This is created dynamically from the document returned at
* API index so as to ensure that URL paths/methods do not go out
* of date.
*
* The service currently exposes two resources:
*
* store.SearchResource, for searching, and
* store.AnnotationResource, for CRUD operations on annotations.
*/
// @ngInject
function store($http, $resource, settings) {
var instance = {};
var defaultOptions = {
transformRequest: prependTransform(
$http.defaults.transformRequest,
stripInternalProperties
)
};
// We call the API root and it gives back the actions it provides.
instance.$resolved = false;
instance.$promise = $http.get(settings.apiUrl)
.finally(function () { instance.$resolved = true; })
.then(function (response) {
var links = response.data.links;
instance.SearchResource = $resource(links.search.url, {}, defaultOptions);
instance.AnnotationResource = $resource(links.annotation.read.url, {}, {
create: angular.extend(links.annotation.create, defaultOptions),
update: angular.extend(links.annotation.update, defaultOptions),
delete: angular.extend(links.annotation.delete, defaultOptions),
});
return instance;
});
return instance;
}
module.exports = store;
...@@ -3,7 +3,7 @@ mail = require('./vendor/jwz') ...@@ -3,7 +3,7 @@ mail = require('./vendor/jwz')
module.exports = class StreamController module.exports = class StreamController
this.inject = [ this.$inject = [
'$scope', '$route', '$rootScope', '$routeParams', '$scope', '$route', '$rootScope', '$routeParams',
'queryParser', 'searchFilter', 'store', 'queryParser', 'searchFilter', 'store',
'streamer', 'streamFilter', 'threading', 'annotationMapper' 'streamer', 'streamFilter', 'threading', 'annotationMapper'
......
...@@ -28,7 +28,7 @@ describe "AnnotationViewerController", -> ...@@ -28,7 +28,7 @@ describe "AnnotationViewerController", ->
$scope: $scope or {search: {}} $scope: $scope or {search: {}}
streamer: streamer or {send: ->} streamer: streamer or {send: ->}
store: store or { store: store or {
AnnotationResource: {read: sinon.spy()}, AnnotationResource: {get: sinon.spy()},
SearchResource: {get: ->}} SearchResource: {get: ->}}
streamFilter: streamFilter or { streamFilter: streamFilter or {
setMatchPolicyIncludeAny: -> {addClause: -> {addClause: ->}} setMatchPolicyIncludeAny: -> {addClause: -> {addClause: ->}}
...@@ -43,4 +43,4 @@ describe "AnnotationViewerController", -> ...@@ -43,4 +43,4 @@ describe "AnnotationViewerController", ->
it "calls the annotation API to get the annotation", -> it "calls the annotation API to get the annotation", ->
{store} = createAnnotationViewerController({}) {store} = createAnnotationViewerController({})
assert store.AnnotationResource.read.args[0][0].id == "test_annotation_id" assert store.AnnotationResource.get.args[0][0].id == "test_annotation_id"
{module, inject} = angular.mock
describe 'store', ->
$httpBackend = null
sandbox = null
store = null
before ->
angular.module('h', ['ngResource'])
.service('store', require('../store'))
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
$provide.value 'settings', {apiUrl: 'http://example.com/api'}
return
afterEach ->
sandbox.restore()
beforeEach inject ($q, _$httpBackend_, _store_) ->
$httpBackend = _$httpBackend_
store = _store_
$httpBackend.expectGET('http://example.com/api').respond
links:
annotation:
create: {
method: 'POST'
url: 'http://example.com/api/annotations'
}
delete: {}
read: {}
update: {}
search:
url: 'http://0.0.0.0:5000/api/search'
beware_dragons:
url: 'http://0.0.0.0:5000/api/roar'
$httpBackend.flush()
it 'reads the operations from the backend', ->
assert.isFunction(store.AnnotationResource, 'expected store.AnnotationResource to be a function')
assert.isFunction(store.BewareDragonsResource, 'expected store.BewareDragonsResource to be a function')
assert.isFunction(store.SearchResource, 'expected store.SearchResource to be a function')
it 'saves a new annotation', ->
annotation = { id: 'test'}
annotation = new store.AnnotationResource(annotation)
saved = {}
annotation.$create().then ->
assert.isNotNull(saved.id)
$httpBackend.expectPOST('http://example.com/api/annotations', annotation).respond ->
saved.id = annotation.id
return [201, {}, {}]
$httpBackend.flush()
'use strict';
var inject = angular.mock.inject;
var module = angular.mock.module;
describe('store', function () {
var $httpBackend = null;
var sandbox = null;
var store = null;
before(function () {
angular.module('h', ['ngResource'])
.service('store', require('../store'));
});
beforeEach(module('h'));
beforeEach(module(function ($provide) {
sandbox = sinon.sandbox.create();
$provide.value('settings', {apiUrl: 'http://example.com/api'});
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
sandbox.restore();
});
beforeEach(inject(function ($q, _$httpBackend_, _store_) {
$httpBackend = _$httpBackend_;
store = _store_;
$httpBackend.expectGET('http://example.com/api').respond({
links: {
annotation: {
create: {
method: 'POST',
url: 'http://example.com/api/annotations',
},
delete: {},
read: {},
update: {},
},
search: {
url: 'http://0.0.0.0:5000/api/search',
},
},
});
$httpBackend.flush();
}));
it('reads the operations from the backend', function () {
assert.isFunction(store.AnnotationResource, 'expected store.AnnotationResource to be a function')
assert.isFunction(store.SearchResource, 'expected store.SearchResource to be a function')
});
it('saves a new annotation', function () {
var annotation = new store.AnnotationResource({id: 'test'});
var saved = {};
annotation.$create().then(function () {
assert.isNotNull(saved.id);
});
$httpBackend.expectPOST('http://example.com/api/annotations', {id: 'test'})
.respond(function () {
saved.id = annotation.id;
return [201, {}, {}];
});
$httpBackend.flush();
});
it('removes internal properties before sending data to the server', function () {
var annotation = new store.AnnotationResource({
$highlight: true,
$notme: 'nooooo!',
allowed: 123
});
annotation.$create();
$httpBackend.expectPOST('http://example.com/api/annotations', {
allowed: 123
})
.respond(function () { return {id: 'test'}; });
$httpBackend.flush();
});
});
// this module is a wrapper around the 'ui.bootstrap' module which
// provides shims/stubs needed to use a current custom build of
// UI Bootstrap (which otherwise requires Angular 1.3.x) with Angular 1.2.x
//
// When/if the app is upgraded to Angular 1.3.x this module can
// just be removed.
var Promise = require('core-js/library/es6/promise');
module.exports = 'ui.bootstrap';
// import the upstream build of UI Bootstrap which defines the
// 'ui.bootstrap' module and sub-modules for the components we are using
require('./vendor/ui-bootstrap-custom-tpls-0.13.4');
angular.module('ui.bootstrap')
// stub implementation of $templateRequest, which is used
// by optional features that we are not using in the dropdown menu directive
.factory('$templateRequest', function() {
return function () {
throw new Error('$templateRequest service is not implemented');
}
})
.config(function ($provide) {
// wrap the $animate service's addClass() and removeClass() functions
// to return a Promise (as in Angular 1.3.x) instead of taking a done()
// callback as the last argument (as in Angular 1.2.x)
$provide.decorator('$animate', function($delegate) {
return angular.extend({}, $delegate, {
addClass: function (element, className) {
return new Promise(function (resolve) {
$delegate.addClass(element, className, resolve)
});
},
removeClass: function (element, className) {
return new Promise(function (resolve) {
$delegate.removeClass(element, className, resolve);
});
}
});
});
});
angular.module('toastr', [])
.directive('toast', ['$compile', '$timeout', 'toastr', function($compile, $timeout, toastr) {
return {
replace: true,
templateUrl: 'templates/toastr/toastr.html',
link: function(scope, element, attrs) {
var timeout;
scope.toastClass = scope.options.toastClass;
scope.titleClass = scope.options.titleClass;
scope.messageClass = scope.options.messageClass;
if (scope.options.closeHtml) {
var button = angular.element(scope.options.closeHtml);
button.addClass('toast-close-button');
button.attr('ng-click', 'close()');
$compile(button)(scope);
element.prepend(button);
}
scope.init = function() {
if (scope.options.timeOut) {
timeout = createTimeout(scope.options.timeOut);
}
};
element.on('mouseenter', function() {
if (timeout) {
$timeout.cancel(timeout);
}
});
scope.tapToast = function () {
if (scope.options.tapToDismiss) {
scope.close();
}
};
scope.close = function () {
toastr.remove(scope.toastId);
};
element.on('mouseleave', function() {
if (scope.options.timeOut === 0 && scope.options.extendedTimeOut === 0) { return; }
timeout = createTimeout(scope.options.extendedTimeOut);
});
function createTimeout(time) {
return $timeout(function() {
toastr.remove(scope.toastId);
}, time);
}
}
};
}])
.constant('toastrConfig', {
allowHtml: false,
closeButton: false,
closeHtml: '<button>&times;</button>',
containerId: 'toast-container',
extendedTimeOut: 1000,
iconClasses: {
error: 'toast-error',
info: 'toast-info',
success: 'toast-success',
warning: 'toast-warning'
},
messageClass: 'toast-message',
positionClass: 'toast-top-right',
tapToDismiss: true,
timeOut: 5000,
titleClass: 'toast-title',
toastClass: 'toast'
})
.factory('toastr', ['$animate', '$compile', '$document', '$rootScope', '$sce', 'toastrConfig', '$q', function($animate, $compile, $document, $rootScope, $sce, toastrConfig, $q) {
var container, index = 0, toasts = [];
var containerDefer = $q.defer();
var toastr = {
clear: clear,
error: error,
info: info,
remove: remove,
success: success,
warning: warning
};
return toastr;
/* Public API */
function clear(toast) {
if (toast) {
remove(toast.toastId);
} else {
for (var i = 0; i < toasts.length; i++) {
remove(toasts[i].toastId);
}
}
}
function error(message, title, optionsOverride) {
return _notify({
iconClass: _getOptions().iconClasses.error,
message: message,
optionsOverride: optionsOverride,
title: title
});
}
function info(message, title, optionsOverride) {
return _notify({
iconClass: _getOptions().iconClasses.info,
message: message,
optionsOverride: optionsOverride,
title: title
});
}
function success(message, title, optionsOverride) {
return _notify({
iconClass: _getOptions().iconClasses.success,
message: message,
optionsOverride: optionsOverride,
title: title
});
}
function warning(message, title, optionsOverride) {
return _notify({
iconClass: _getOptions().iconClasses.warning,
message: message,
optionsOverride: optionsOverride,
title: title
});
}
/* Internal functions */
function _getOptions() {
return angular.extend({}, toastrConfig);
}
function _setContainer(options) {
if(container) { return containerDefer.promise; } // If the container is there, don't create it.
container = angular.element('<div></div>');
container.attr('id', options.containerId);
container.addClass(options.positionClass);
container.css({'pointer-events': 'auto'});
var body = $document.find('body').eq(0);
$animate.enter(container, body, null, function() {
containerDefer.resolve();
});
return containerDefer.promise;
}
function _notify(map) {
var options = _getOptions();
var newToast = {
toastId: index++,
scope: $rootScope.$new()
};
newToast.iconClass = map.iconClass;
if (map.optionsOverride) {
options = angular.extend(options, map.optionsOverride);
newToast.iconClass = map.optionsOverride.iconClass || newToast.iconClass;
}
createScope(newToast, map, options);
newToast.el = createToast(newToast.scope);
toasts.push(newToast);
_setContainer(options).then(function() {
$animate.enter(newToast.el, container, null, function() {
newToast.scope.init();
});
});
return newToast;
function createScope(toast, map, options) {
if (options.allowHtml) {
toast.scope.allowHtml = true;
toast.scope.title = $sce.trustAsHtml(map.title);
toast.scope.message = $sce.trustAsHtml(map.message);
} else {
toast.scope.title = map.title;
toast.scope.message = map.message;
}
toast.scope.toastType = toast.iconClass;
toast.scope.toastId = toast.toastId;
toast.scope.options = {
extendedTimeOut: options.extendedTimeOut,
messageClass: options.messageClass,
tapToDismiss: options.tapToDismiss,
timeOut: options.timeOut,
titleClass: options.titleClass,
toastClass: options.toastClass
};
if (options.closeButton) {
toast.scope.options.closeHtml = options.closeHtml;
}
}
function createToast(scope) {
var angularDomEl = angular.element('<div toast></div>');
return $compile(angularDomEl)(scope);
}
}
function remove(toastIndex) {
var toast = findToast(toastIndex);
if (toast) { // Avoid clicking when fading out
$animate.leave(toast.el, function() {
toast.scope.$destroy();
if (container && container.children().length === 0) {
toasts = [];
container.remove();
container = null;
containerDefer = $q.defer();
}
});
}
function findToast(toastId) {
for (var i = 0; i < toasts.length; i++) {
if (toasts[i].toastId === toastId) {
return toasts[i];
}
}
}
}
}]);
angular.module('toastr').run(['$templateCache', function($templateCache) {
'use strict';
$templateCache.put('templates/toastr/toastr.html',
"<div class=\"{{toastClass}} {{toastType}}\" ng-click=\"tapToast()\">\n" +
" <div ng-switch on=\"allowHtml\">\n" +
" <div ng-switch-default ng-if=\"title\" class=\"{{titleClass}}\">{{title}}</div>\n" +
" <div ng-switch-default class=\"{{messageClass}}\">{{message}}</div>\n" +
" <div ng-switch-when=\"true\" ng-if=\"title\" class=\"{{titleClass}}\" ng-bind-html=\"title\"></div>\n" +
" <div ng-switch-when=\"true\" class=\"{{messageClass}}\" ng-bind-html=\"message\"></div>\n" +
" </div>\n" +
"</div>"
);
}]);
.toast-title {
font-weight: bold;
}
.toast-message {
-ms-word-wrap: break-word;
word-wrap: break-word;
}
.toast-message a,
.toast-message label {
color: #ffffff;
}
.toast-message a:hover {
color: #cccccc;
text-decoration: none;
}
.toast-close-button {
position: relative;
right: -0.3em;
top: -0.3em;
float: right;
font-size: 20px;
font-weight: bold;
color: #ffffff;
-webkit-text-shadow: 0 1px 0 #ffffff;
text-shadow: 0 1px 0 #ffffff;
opacity: 0.8;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
filter: alpha(opacity=80);
}
.toast-close-button:hover,
.toast-close-button:focus {
color: #000000;
text-decoration: none;
cursor: pointer;
opacity: 0.4;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
filter: alpha(opacity=40);
}
/*Additional properties for button version
iOS requires the button element instead of an anchor tag.
If you want the anchor version, it requires `href="#"`.*/
button.toast-close-button {
padding: 0;
cursor: pointer;
background: transparent;
border: 0;
-webkit-appearance: none;
}
.toast-top-full-width {
top: 0;
right: 0;
width: 100%;
}
.toast-bottom-full-width {
bottom: 0;
right: 0;
width: 100%;
}
.toast-top-left {
top: 12px;
left: 12px;
}
.toast-top-right {
top: 12px;
right: 12px;
}
.toast-bottom-right {
right: 12px;
bottom: 12px;
}
.toast-bottom-left {
bottom: 12px;
left: 12px;
}
#toast-container {
position: fixed;
z-index: 999999;
/*overrides*/
}
#toast-container * {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
#toast-container > div {
margin: 0 0 6px;
padding: 15px 15px 15px 50px;
width: 300px;
-moz-border-radius: 3px 3px 3px 3px;
-webkit-border-radius: 3px 3px 3px 3px;
border-radius: 3px 3px 3px 3px;
background-position: 15px center;
background-repeat: no-repeat;
-moz-box-shadow: 0 0 12px #999999;
-webkit-box-shadow: 0 0 12px #999999;
box-shadow: 0 0 12px #999999;
color: #ffffff;
opacity: 0.8;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
filter: alpha(opacity=80);
}
#toast-container > :hover {
-moz-box-shadow: 0 0 12px #000000;
-webkit-box-shadow: 0 0 12px #000000;
box-shadow: 0 0 12px #000000;
opacity: 1;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
filter: alpha(opacity=100);
cursor: pointer;
}
#toast-container > .toast-info {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important;
}
#toast-container > .toast-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important;
}
#toast-container > .toast-success {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important;
}
#toast-container > .toast-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important;
}
#toast-container.toast-top-full-width > div,
#toast-container.toast-bottom-full-width > div {
width: 96%;
margin: auto;
}
.toast {
background-color: #030303;
}
.toast-success {
background-color: #51a351;
}
.toast-error {
background-color: #bd362f;
}
.toast-info {
background-color: #2f96b4;
}
.toast-warning {
background-color: #f89406;
}
/*Animations*/
.toast {
opacity: 1 !important;
}
.toast.ng-enter {
opacity: 0 !important;
transition: opacity .3s linear;
}
.toast.ng-enter.ng-enter-active {
opacity: 1 !important;
}
.toast.ng-leave {
opacity: 1;
transition: opacity .3s linear;
}
.toast.ng-leave.ng-leave-active {
opacity: 0 !important;
}
/*Responsive Design*/
@media all and (max-width: 240px) {
#toast-container > div {
padding: 8px 8px 8px 50px;
width: 11em;
}
#toast-container .toast-close-button {
right: -0.2em;
top: -0.2em;
}
}
@media all and (min-width: 241px) and (max-width: 480px) {
#toast-container > div {
padding: 8px 8px 8px 50px;
width: 18em;
}
#toast-container .toast-close-button {
right: -0.2em;
top: -0.2em;
}
}
@media all and (min-width: 481px) and (max-width: 768px) {
#toast-container > div {
padding: 15px 15px 15px 50px;
width: 25em;
}
}
...@@ -80,9 +80,9 @@ ...@@ -80,9 +80,9 @@
<!-- Body --> <!-- Body -->
<section name="text" class="annotation-body"> <section name="text" class="annotation-body">
<excerpt enabled="feature('truncate_annotations') && !vm.editing"> <excerpt enabled="feature('truncate_annotations') && !vm.editing">
<div ng-model="vm.annotation.text" <markdown ng-model="vm.annotation.text"
ng-readonly="!vm.editing" read-only="!vm.editing"
markdown> ></markdown>
</excerpt> </excerpt>
</section> </section>
<!-- / Body --> <!-- / Body -->
......
<div ng-hide="readonly" class="markdown-tools" ng-class="preview && 'disable'"> <div ng-hide="readOnly" class="markdown-tools" ng-class="preview && 'disable'">
<span class="markdown-preview-toggle"> <span class="markdown-preview-toggle">
<a class="markdown-tools-badge h-icon-markdown" href="https://help.github.com/articles/markdown-basics" title="Parsed as Markdown" target="_blank"></a> <a class="markdown-tools-badge h-icon-markdown" href="https://help.github.com/articles/markdown-basics" title="Parsed as Markdown" target="_blank"></a>
<a href="" class="markdown-tools-toggle" ng-click="togglePreview()" ng-show="!preview">Preview</a> <a href="" class="markdown-tools-toggle" ng-click="togglePreview()" ng-show="!preview">Preview</a>
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
<i class="h-icon-format-list-bulleted markdown-tools-button" ng-click="insertList()" title="Insert list"></i> <i class="h-icon-format-list-bulleted markdown-tools-button" ng-click="insertList()" title="Insert list"></i>
</div> </div>
<textarea class="form-input form-textarea js-markdown-input" <textarea class="form-input form-textarea js-markdown-input"
ng-hide="readonly || preview" ng-hide="readOnly || preview"
ng-click="$event.stopPropagation()" ng-click="$event.stopPropagation()"
ng-required="required"></textarea> ng-required="required"></textarea>
<div class="styled-text js-markdown-preview" ng-class="preview && 'markdown-preview'" ng-dblclick="togglePreview()" ng-bind-html="rendered" ng-show="readonly || preview"></div> <div class="styled-text js-markdown-preview" ng-class="preview && 'markdown-preview'" ng-dblclick="togglePreview()" ng-bind-html="rendered" ng-show="readOnly || preview"></div>
...@@ -4,13 +4,14 @@ ...@@ -4,13 +4,14 @@
"version": "0.0.0", "version": "0.0.0",
"description": "The Internet, peer reviewed.", "description": "The Internet, peer reviewed.",
"dependencies": { "dependencies": {
"angular": "1.2.28", "angular": "1.4.7",
"angular-animate": "1.2.28", "angular-animate": "1.4.7",
"angular-jwt": "0.0.9", "angular-jwt": "0.0.9",
"angular-mocks": "^1.2.28", "angular-mocks": "1.4.7",
"angular-resource": "1.2.28", "angular-resource": "1.4.7",
"angular-route": "1.2.28", "angular-route": "1.4.7",
"angular-sanitize": "1.2.28", "angular-sanitize": "1.4.7",
"angular-toastr": "^1.5.0",
"angular-websocket": "^1.0.13", "angular-websocket": "^1.0.13",
"angulartics": "0.17.2", "angulartics": "0.17.2",
"autofill-event": "0.0.1", "autofill-event": "0.0.1",
......
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