var app = angular.module('plunker', ['toastr']);
app.config(function(toastrConfig) {
angular.extend(toastrConfig, {
autoDismiss: false,
containerId: 'toast-container',
maxOpened: 0,
newestOnTop: true,
positionClass: 'toast-top-right',
preventDuplicates: false,
preventOpenDuplicates: false,
target: 'body'
});
});
app.controller('MainCtrl', function($scope, toastr) {
$scope.name = 'Angular Toastr Test';
$scope.showDefaultToast = function() {
toastr.success('This toast uses default settings', 'Default Settings!')
};
$scope.showCustomToast = function() {
toastr.error('This toast should be in the top-left', 'Custom Position!', {
positionClass: 'toast-top-left'
})
};
});
<!DOCTYPE html>
<html ng-app="plunker">
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link rel="stylesheet" href="angular-toastr.css" />
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js@1.4.x" src="https://code.angularjs.org/1.4.7/angular.js" data-semver="1.4.7"></script>
<script type="text/javascript" src="angular-toastr.tpls.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<h1>{{ name }}</h1>
<button ng-click="showDefaultToast()">Default Toast</button>
<button ng-click="showCustomToast()">Custom Toast</button>
</body>
</html>
(function() {
'use strict';
angular.module('toastr', [])
.factory('toastr', toastr);
toastr.$inject = ['$animate', '$injector', '$document', '$rootScope', '$sce', 'toastrConfig', '$q'];
function toastr($animate, $injector, $document, $rootScope, $sce, toastrConfig, $q) {
var container;
var index = 0;
var toasts = [];
var previousToastMessage = '';
var openToasts = {};
var containerDefer = $q.defer();
var toast = {
clear: clear,
error: error,
info: info,
remove: remove,
success: success,
warning: warning
};
return toast;
/* Public API */
function clear(toast) {
// Bit of a hack, I will remove this soon with a BC
if (arguments.length === 1 && !toast) { return; }
if (toast) {
remove(toast.toastId);
} else {
for (var i = 0; i < toasts.length; i++) {
remove(toasts[i].toastId);
}
}
}
function error(message, title, optionsOverride) {
var type = _getOptions().iconClasses.error;
return _buildNotification(type, message, title, optionsOverride);
}
function info(message, title, optionsOverride) {
var type = _getOptions().iconClasses.info;
return _buildNotification(type, message, title, optionsOverride);
}
function success(message, title, optionsOverride) {
var type = _getOptions().iconClasses.success;
return _buildNotification(type, message, title, optionsOverride);
}
function warning(message, title, optionsOverride) {
var type = _getOptions().iconClasses.warning;
return _buildNotification(type, message, title, optionsOverride);
}
function remove(toastId, wasClicked) {
var toast = findToast(toastId);
if (toast && ! toast.deleting) { // Avoid clicking when fading out
toast.deleting = true;
toast.isOpened = false;
$animate.leave(toast.el).then(function() {
if (toast.scope.options.onHidden) {
toast.scope.options.onHidden(!!wasClicked, toast);
}
toast.scope.$destroy();
var index = toasts.indexOf(toast);
delete openToasts[toast.scope.message];
toasts.splice(index, 1);
var maxOpened = toastrConfig.maxOpened;
if (maxOpened && toasts.length >= maxOpened) {
toasts[maxOpened - 1].open.resolve();
}
if (lastToast()) {
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];
}
}
}
function lastToast() {
return !toasts.length;
}
}
/* Internal functions */
function _buildNotification(type, message, title, optionsOverride) {
if (angular.isObject(title)) {
optionsOverride = title;
title = null;
}
return _notify({
iconClass: type,
message: message,
optionsOverride: optionsOverride,
title: title
});
}
function _getOptions() {
return angular.extend({}, toastrConfig);
}
function _createOrGetContainer(options) {
if(container) { return containerDefer.promise; }
container = angular.element('<div></div>');
container.attr('id', options.containerId);
container.addClass(options.positionClass);
container.css({'pointer-events': 'auto'});
var target = angular.element(document.querySelector(options.target));
if ( ! target || ! target.length) {
throw 'Target for toasts doesn\'t exist';
}
$animate.enter(container, target).then(function() {
containerDefer.resolve();
});
return containerDefer.promise;
}
function _notify(map) {
var options = _getOptions();
if (shouldExit()) { return; }
var newToast = createToast();
toasts.push(newToast);
if (ifMaxOpenedAndAutoDismiss()) {
var oldToasts = toasts.slice(0, (toasts.length - options.maxOpened));
for (var i = 0, len = oldToasts.length; i < len; i++) {
remove(oldToasts[i].toastId);
}
}
if (maxOpenedNotReached()) {
newToast.open.resolve();
}
newToast.open.promise.then(function() {
_createOrGetContainer(options).then(function() {
newToast.isOpened = true;
if (options.newestOnTop) {
$animate.enter(newToast.el, container).then(function() {
newToast.scope.init();
});
} else {
var sibling = container[0].lastChild ? angular.element(container[0].lastChild) : null;
$animate.enter(newToast.el, container, sibling).then(function() {
newToast.scope.init();
});
}
});
});
return newToast;
function ifMaxOpenedAndAutoDismiss() {
return options.autoDismiss && options.maxOpened && toasts.length > options.maxOpened;
}
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.extraData = options.extraData;
toast.scope.options = {
extendedTimeOut: options.extendedTimeOut,
messageClass: options.messageClass,
onHidden: options.onHidden,
onShown: generateEvent('onShown'),
onTap: generateEvent('onTap'),
progressBar: options.progressBar,
tapToDismiss: options.tapToDismiss,
timeOut: options.timeOut,
titleClass: options.titleClass,
toastClass: options.toastClass
};
if (options.closeButton) {
toast.scope.options.closeHtml = options.closeHtml;
}
function generateEvent(event) {
if (options[event]) {
return function() {
options[event](toast);
};
}
}
}
function createToast() {
var newToast = {
toastId: index++,
isOpened: false,
scope: $rootScope.$new(),
open: $q.defer()
};
newToast.iconClass = map.iconClass;
if (map.optionsOverride) {
angular.extend(options, cleanOptionsOverride(map.optionsOverride));
newToast.iconClass = map.optionsOverride.iconClass || newToast.iconClass;
}
createScope(newToast, map, options);
newToast.el = createToastEl(newToast.scope);
return newToast;
function cleanOptionsOverride(options) {
var badOptions = ['containerId', 'iconClasses', 'maxOpened', 'newestOnTop',
'positionClass', 'preventDuplicates', 'preventOpenDuplicates', 'templates'];
for (var i = 0, l = badOptions.length; i < l; i++) {
delete options[badOptions[i]];
}
return options;
}
}
function createToastEl(scope) {
var angularDomEl = angular.element('<div toast></div>'),
$compile = $injector.get('$compile');
return $compile(angularDomEl)(scope);
}
function maxOpenedNotReached() {
return options.maxOpened && toasts.length <= options.maxOpened || !options.maxOpened;
}
function shouldExit() {
var isDuplicateOfLast = options.preventDuplicates && map.message === previousToastMessage;
var isDuplicateOpen = options.preventOpenDuplicates && openToasts[map.message];
if (isDuplicateOfLast || isDuplicateOpen) {
return true;
}
previousToastMessage = map.message;
openToasts[map.message] = true;
return false;
}
}
}
}());
(function() {
'use strict';
angular.module('toastr')
.constant('toastrConfig', {
allowHtml: false,
autoDismiss: false,
closeButton: false,
closeHtml: '<button>×</button>',
containerId: 'toast-container',
extendedTimeOut: 1000,
iconClasses: {
error: 'toast-error',
info: 'toast-info',
success: 'toast-success',
warning: 'toast-warning'
},
maxOpened: 0,
messageClass: 'toast-message',
newestOnTop: true,
onHidden: null,
onShown: null,
onTap: null,
positionClass: 'toast-top-right',
preventDuplicates: false,
preventOpenDuplicates: false,
progressBar: false,
tapToDismiss: true,
target: 'body',
templates: {
toast: 'directives/toast/toast.html',
progressbar: 'directives/progressbar/progressbar.html'
},
timeOut: 5000,
titleClass: 'toast-title',
toastClass: 'toast'
});
}());
(function() {
'use strict';
angular.module('toastr')
.directive('progressBar', progressBar);
progressBar.$inject = ['toastrConfig'];
function progressBar(toastrConfig) {
return {
replace: true,
require: '^toast',
templateUrl: function() {
return toastrConfig.templates.progressbar;
},
link: linkFunction
};
function linkFunction(scope, element, attrs, toastCtrl) {
var intervalId, currentTimeOut, hideTime;
toastCtrl.progressBar = scope;
scope.start = function(duration) {
if (intervalId) {
clearInterval(intervalId);
}
currentTimeOut = parseFloat(duration);
hideTime = new Date().getTime() + currentTimeOut;
intervalId = setInterval(updateProgress, 10);
};
scope.stop = function() {
if (intervalId) {
clearInterval(intervalId);
}
};
function updateProgress() {
var percentage = ((hideTime - (new Date().getTime())) / currentTimeOut) * 100;
element.css('width', percentage + '%');
}
scope.$on('$destroy', function() {
// Failsafe stop
clearInterval(intervalId);
});
}
}
}());
(function() {
'use strict';
angular.module('toastr')
.controller('ToastController', ToastController);
function ToastController() {
this.progressBar = null;
this.startProgressBar = function(duration) {
if (this.progressBar) {
this.progressBar.start(duration);
}
};
this.stopProgressBar = function() {
if (this.progressBar) {
this.progressBar.stop();
}
};
}
}());
(function() {
'use strict';
angular.module('toastr')
.directive('toast', toast);
toast.$inject = ['$injector', '$interval', 'toastrConfig', 'toastr'];
function toast($injector, $interval, toastrConfig, toastr) {
return {
replace: true,
templateUrl: function() {
return toastrConfig.templates.toast;
},
controller: 'ToastController',
link: toastLinkFunction
};
function toastLinkFunction(scope, element, attrs, toastCtrl) {
var timeout;
scope.toastClass = scope.options.toastClass;
scope.titleClass = scope.options.titleClass;
scope.messageClass = scope.options.messageClass;
scope.progressBar = scope.options.progressBar;
if (wantsCloseButton()) {
var button = angular.element(scope.options.closeHtml),
$compile = $injector.get('$compile');
button.addClass('toast-close-button');
button.attr('ng-click', 'close(true, $event)');
$compile(button)(scope);
element.prepend(button);
}
scope.init = function() {
if (scope.options.timeOut) {
timeout = createTimeout(scope.options.timeOut);
}
if (scope.options.onShown) {
scope.options.onShown();
}
};
element.on('mouseenter', function() {
hideAndStopProgressBar();
if (timeout) {
$interval.cancel(timeout);
}
});
scope.tapToast = function () {
if (angular.isFunction(scope.options.onTap)) {
scope.options.onTap();
}
if (scope.options.tapToDismiss) {
scope.close(true);
}
};
scope.close = function (wasClicked, $event) {
if ($event && angular.isFunction($event.stopPropagation)) {
$event.stopPropagation();
}
toastr.remove(scope.toastId, wasClicked);
};
element.on('mouseleave', function() {
if (scope.options.timeOut === 0 && scope.options.extendedTimeOut === 0) { return; }
scope.$apply(function() {
scope.progressBar = scope.options.progressBar;
});
timeout = createTimeout(scope.options.extendedTimeOut);
});
function createTimeout(time) {
toastCtrl.startProgressBar(time);
return $interval(function() {
toastCtrl.stopProgressBar();
toastr.remove(scope.toastId);
}, time, 1);
}
function hideAndStopProgressBar() {
scope.progressBar = false;
toastCtrl.stopProgressBar();
}
function wantsCloseButton() {
return scope.options.closeHtml;
}
}
}
}());
angular.module("toastr").run(["$templateCache", function($templateCache) {$templateCache.put("directives/progressbar/progressbar.html","<div class=\"toast-progress\"></div>\n");
$templateCache.put("directives/toast/toast.html","<div class=\"{{toastClass}} {{toastType}}\" ng-click=\"tapToast()\">\n <div ng-switch on=\"allowHtml\">\n <div ng-switch-default ng-if=\"title\" class=\"{{titleClass}}\" aria-label=\"{{title}}\">{{title}}</div>\n <div ng-switch-default class=\"{{messageClass}}\" aria-label=\"{{message}}\">{{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 <progress-bar ng-if=\"progressBar\"></progress-bar>\n</div>\n");}]);
.toast-title {
font-weight: bold;
}
.toast-message {
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;
}
.toast-close-button:hover,
.toast-close-button:focus {
color: #000000;
text-decoration: none;
cursor: pointer;
opacity: 0.4;
}
/*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-center {
top: 0;
right: 0;
width: 100%;
}
.toast-bottom-center {
bottom: 0;
right: 0;
width: 100%;
}
.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 {
position: relative;
overflow: hidden;
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;
}
#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;
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-center > div,
#toast-container.toast-bottom-center > div {
width: 300px;
margin: auto;
}
#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;
}
.toast-progress {
position: absolute;
left: 0;
bottom: 0;
height: 4px;
background-color: #000000;
opacity: 0.4;
}
/*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;
}
}