Commit c0b7c75b authored by Nick Stenning's avatar Nick Stenning

Merge pull request #2057 from hypothesis/toastr

Replace flash service with angular-toastr
parents e53a7820 f3b7d065
......@@ -10,7 +10,8 @@ class AccountController
onSuccess = (form, response) ->
# Fire flash messages.
for type, msgs of response.flash
flash(type, msgs)
for m in msgs
flash[type](m)
form.$setPristine()
formModel = form.$name.slice(0, -4)
......@@ -26,9 +27,11 @@ class AccountController
formHelpers.applyValidationErrors(form, response.data.errors)
else
if response.data.flash
flash(type, msgs) for own type, msgs of response.data.flash
for own type, msgs of response.data.flash
for m in msgs
flash[type](m)
else
flash('error', 'Sorry, we were unable to perform your request')
flash.error('Sorry, we were unable to perform your request')
$scope.$broadcast 'formState', form.$name, '' # Update status btn
......
......@@ -89,7 +89,7 @@ configure = [
authCheck.reject 'no session'
return null
.catch (err) ->
flash 'error', 'Sign out failed!'
flash.error('Sign out failed!')
throw err
]
......
......@@ -48,8 +48,9 @@ class AuthController
timeout = $timeout ->
angular.copy {}, $scope.model
$scope.form?.$setPristine()
flash 'info',
flash.info(
'For your security, the forms have been reset due to inactivity.'
)
, 300000
......
......@@ -26,7 +26,11 @@ describe 'h:AccountController', ->
beforeEach module ($provide, $filterProvider) ->
sandbox = sinon.sandbox.create()
fakeSession = {}
fakeFlash = sandbox.spy()
fakeFlash =
success: sandbox.spy()
info: sandbox.spy()
warning: sandbox.spy()
error: sandbox.spy()
fakeIdentity =
logout: sandbox.spy()
fakeFormHelpers =
......@@ -120,9 +124,7 @@ describe 'h:AccountController', ->
controller = createController()
$scope.submit(fakeForm)
assert.calledWith(fakeFlash, 'success', [
'Your profile has been updated.'
])
assert.calledWith(fakeFlash.success, 'Your profile has been updated.')
it 'displays a flash message if a server error occurs', ->
fakeForm = createFakeForm()
......@@ -136,7 +138,7 @@ describe 'h:AccountController', ->
flash:
error: ['Something bad happened']
assert.calledWith(fakeFlash, 'error', ['Something bad happened'])
assert.calledWith(fakeFlash.error, 'Something bad happened')
it 'displays a fallback flash message if none are present', ->
fakeForm = createFakeForm()
......@@ -148,7 +150,8 @@ describe 'h:AccountController', ->
status: 500
data: {}
assert.calledWith(fakeFlash, 'error', 'Sorry, we were unable to perform your request')
assert.calledWith(fakeFlash.error,
'Sorry, we were unable to perform your request')
describe '.delete', ->
createFakeForm = (overrides={}) ->
......@@ -217,9 +220,9 @@ describe 'h:AccountController', ->
flash:
error: ['Something bad happened']
assert.calledWith(fakeFlash, 'error', ['Something bad happened'])
assert.calledWith(fakeFlash.error, 'Something bad happened')
it 'displays a fallback flash message if none are present', ->
it 'displays a fallback toast message if none are present', ->
fakeForm = createFakeForm()
controller = createController()
$scope.delete(fakeForm)
......@@ -229,4 +232,5 @@ describe 'h:AccountController', ->
status: 500
data: {}
assert.calledWith(fakeFlash, 'error', 'Sorry, we were unable to perform your request')
assert.calledWith(fakeFlash.error,
'Sorry, we were unable to perform your request')
......@@ -18,7 +18,7 @@ class MockSession
$promise:
finally: sandbox.stub()
mockFlash = sandbox.spy()
mockFlash = info: sandbox.spy()
mockFormHelpers = applyValidationErrors: sandbox.spy()
describe 'h:AuthController', ->
......@@ -117,7 +117,7 @@ describe 'h:AuthController', ->
$timeout.lastCall.args[0]()
assert.called $scope.form.$setPristine, 'the form is pristine'
assert.deepEqual $scope.model, {}, 'the model is erased'
assert.called mockFlash, 'a notification is flashed'
assert.called mockFlash.info, 'a flash notification is shown'
it 'should not happen if the model is empty', ->
$scope.model = undefined
......
......@@ -11,6 +11,7 @@ imports = [
'ngRoute'
'ngSanitize'
'ngTagsInput'
'h.flash'
'h.helpers'
'h.identity'
'h.session'
......
......@@ -151,9 +151,9 @@ AnnotationController = [
###
this.save = ->
unless model.user or model.deleted
return flash 'info', 'Please sign in to save your annotations.'
return flash.info('Please sign in to save your annotations.')
unless validate(@annotation)
return flash 'info', 'Please add text or a tag before publishing.'
return flash.info('Please add text or a tag before publishing.')
# Update stored tags with the new tags of this annotation
tags = @annotation.tags.filter (tag) ->
......
escape = (html) ->
html
.replace(/&(?!\w+;)/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
class Notification
# Default options.
options:
html: "<div class='annotator-notice'></div>"
classes:
show: "annotator-notice-show"
info: "annotator-notice-info"
success: "annotator-notice-success"
error: "annotator-notice-error"
@INFO: 'info'
@ERROR: 'error'
@SUCCESS: 'success'
constructor: (options) ->
element = $(@options.html).hide()[0]
@element = $(element)
@options = $.extend(true, {}, @options, options)
# Retain the fat arrow binding despite skipping the super-class constructor
# XXX: replace with _appendElement override when we move to Annotator v2.
show: (message, status = Notification.INFO) =>
@currentStatus = status
$(@element)
.addClass(@options.classes.show)
.addClass(@options.classes[@currentStatus])
.html(escape(message || ""))
setTimeout @hide, 5000
@element.prependTo(document.body).slideDown()
hide: =>
@currentStatus ?= Annotator.Notification.INFO
$(@element)
.removeClass(@options.classes.show)
.removeClass(@options.classes[@currentStatus])
@element.slideUp => @element.remove()
class FlashProvider
queues:
'': []
info: []
error: []
success: []
notice: null
$get: ->
angular.bind this, this._flash
_process: ->
for q, msgs of @queues
if msgs.length
msg = msgs.shift()
unless q then [q, msg] = msg
notice = new Notification()
notice.show(msg, q)
break
_flash: (queue, messages) ->
if @queues[queue]?
@queues[queue] = @queues[queue]?.concat messages
this._process()
angular.module('h')
.provider('flash', FlashProvider)
angular.module('h.flash', ['toastr']).factory('flash', [
'toastr', (toastr) ->
info: angular.bind(toastr, toastr.info)
success: angular.bind(toastr, toastr.success)
warning: angular.bind(toastr, toastr.warning)
error: angular.bind(toastr, toastr.error)
])
......@@ -60,7 +60,8 @@ class SessionProvider
# Fire flash messages.
for q, msgs of data.flash
flash q, msgs
for m in msgs
flash[q](m)
xsrf.token = model.csrf
......
......@@ -19,8 +19,8 @@ describe 'h.session', ->
beforeEach module ($provide, sessionProvider) ->
sandbox = sinon.sandbox.create()
fakeFlash = sandbox.spy()
fakeDocument = {prop: -> '/session'}
fakeFlash = error: sandbox.spy()
fakeXsrf = {token: 'faketoken'}
$provide.value '$document', fakeDocument
......@@ -56,11 +56,11 @@ describe 'h.session', ->
it 'should invoke the flash service with any flash messages', ->
response =
flash:
error: 'fail'
error: ['fail']
$httpBackend.expectPOST(url).respond(response)
result = session.login({})
$httpBackend.flush()
assert.calledWith fakeFlash, 'error', 'fail'
assert.calledWith fakeFlash.error, 'fail'
it 'should assign errors and status reasons to the model', ->
response =
......
.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;
}
}
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>"
);
}]);
......@@ -29,47 +29,11 @@
background: url("../images/noise_1.png");
}
//FLASH/TOAST/ALERTS///////////////////////////////
.annotator-notice {
@include box-shadow(inset 2px 1px 1px hsla(0, 0%, 0%, .1));
@include single-transition(opacity, .2s);
direction: ltr;
font-family: $sans-font-family;
font-weight: 300;
line-height: 29px;
opacity: 0;
position: relative;
text-align: center;
z-index: 5;
&.show, &.annotator-notice-show {
opacity: 1;
}
}
.annotator-hide {
display: none;
visibility: hidden;
}
.annotator-notice-info {
color: #3a87ad;
background-color: #d9edf7;
border-color: #98BED1;
}
.annotator-notice-success {
color: #468847;
background-color: #dff0d8;
border-color: #8DC98E;
}
.annotator-notice-error {
color: #b94a48;
background-color: #f2dede;
border-color: #F5A1A0;
}
// Icons
[class^="h-icon-"], [class*=" h-icon-"] {
vertical-align: middle;
......
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