|
/* |
|
* Toastr |
|
* Copyright 2025 |
|
* Authors: John Papa, Hans Fjällemark, and Tim Ferrell |
|
* All Rights Reserved. |
|
* Use, reproduction, distribution, and modification of this code is subject to the terms and |
|
* conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php |
|
* |
|
* ARIA Support: Greta Krafsig |
|
* |
|
* Updates and improvements: Rick van Hattem. |
|
* |
|
* Original project: https://github.com/CodeSeven/toastr |
|
* Current homepage: https://gist.github.com/wolph/b3e94d7549919cca4ee3c4513c24efd1/ |
|
*/ |
|
/* global define */ |
|
(function (define) { |
|
define(['jquery'], function ($) { |
|
return (function () { |
|
var $container; |
|
var listener; |
|
var toastId = 0; |
|
var toastType = { |
|
error: 'error', |
|
info: 'info', |
|
success: 'success', |
|
warning: 'warning' |
|
}; |
|
|
|
/** |
|
* Global mapping to hold toast notifications by tag. |
|
*/ |
|
var taggedToasts = {}; |
|
|
|
var toastr = { |
|
clear: clear, |
|
remove: remove, |
|
error: error, |
|
getContainer: getContainer, |
|
info: info, |
|
options: {}, |
|
subscribe: subscribe, |
|
success: success, |
|
version: '2.1.4', |
|
warning: warning |
|
}; |
|
|
|
var previousToast; |
|
|
|
// Helper to prevent duplicate toasts if configured. |
|
function shouldExit(options, map) { |
|
if (options.preventDuplicates) { |
|
if (map.message === previousToast) { |
|
return true; |
|
} else { |
|
previousToast = map.message; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
return toastr; |
|
|
|
///////////////////////////////////////////////////////////////////////// |
|
// Public API calls |
|
|
|
function error(message, title, optionsOverride) { |
|
return notify({ |
|
type: toastType.error, |
|
iconClass: getOptions().iconClasses.error, |
|
message: message, |
|
optionsOverride: optionsOverride, |
|
title: title |
|
}); |
|
} |
|
|
|
function info(message, title, optionsOverride) { |
|
return notify({ |
|
type: toastType.info, |
|
iconClass: getOptions().iconClasses.info, |
|
message: message, |
|
optionsOverride: optionsOverride, |
|
title: title |
|
}); |
|
} |
|
|
|
function success(message, title, optionsOverride) { |
|
return notify({ |
|
type: toastType.success, |
|
iconClass: getOptions().iconClasses.success, |
|
message: message, |
|
optionsOverride: optionsOverride, |
|
title: title |
|
}); |
|
} |
|
|
|
function warning(message, title, optionsOverride) { |
|
return notify({ |
|
type: toastType.warning, |
|
iconClass: getOptions().iconClasses.warning, |
|
message: message, |
|
optionsOverride: optionsOverride, |
|
title: title |
|
}); |
|
} |
|
|
|
function subscribe(callback) { |
|
listener = callback; |
|
} |
|
|
|
///////////////////////////////////////////////////////////////////////// |
|
// Main notify function with tag support and full replacement logic |
|
/** |
|
* Creates and displays a toast notification. |
|
* If options.tag is provided and an existing notification with that tag exists: |
|
* - If the new toast is the same type (based on iconClass), update it in place. |
|
* - Otherwise, immediately remove the old toast before creating the new one. |
|
* |
|
* @param {Object} map - Object containing type, title, message, iconClass, and optionsOverride. |
|
* @returns {Object} The notification API object for the toast. |
|
*/ |
|
function notify(map) { |
|
var options = getOptions(); |
|
if (typeof map.optionsOverride !== 'undefined') { |
|
options = $.extend(options, map.optionsOverride); |
|
} |
|
|
|
// Tag handling: |
|
if (options.tag) { |
|
var existingToast = taggedToasts[options.tag]; |
|
if (existingToast) { |
|
if (existingToast.map.iconClass === map.iconClass) { |
|
// Update in place. |
|
existingToast.replace(map.message, map.title, options); |
|
if (options.debug && console) { |
|
console.log("[DEBUG] Replaced existing toast in place with tag:", options.tag); |
|
} |
|
return existingToast; |
|
} else { |
|
// Immediately remove the existing toast (no animation). |
|
existingToast.immediateClose(); |
|
if (options.debug && console) { |
|
console.log("[DEBUG] Immediately removed toast with tag:", options.tag); |
|
} |
|
delete taggedToasts[options.tag]; |
|
} |
|
} |
|
} |
|
|
|
toastId++; |
|
$container = getContainer(options, true); |
|
|
|
var intervalId = null; |
|
var $toastElement = $('<div/>'); |
|
var $titleElement = $('<div/>'); |
|
var $messageElement = $('<div/>'); |
|
var $progressElement = $('<div/>'); |
|
var $closeElement = $(options.closeHtml); |
|
var progressBar = { |
|
intervalId: null, |
|
hideEta: null, |
|
maxHideTime: null |
|
}; |
|
var response = { |
|
toastId: toastId, |
|
state: 'visible', |
|
startTime: new Date(), |
|
options: options, |
|
map: map |
|
}; |
|
|
|
// Create the notification API object. |
|
var notificationAPI = { |
|
element: $toastElement, |
|
map: map, // expose underlying data |
|
|
|
setTitle: function (newTitle) { |
|
if ($titleElement.children().length === 0) { |
|
$titleElement.addClass(options.titleClass); |
|
$toastElement.prepend($titleElement); |
|
} |
|
$titleElement.html(newTitle); |
|
}, |
|
|
|
setMessage: function (newMessage) { |
|
if ($messageElement.children().length === 0) { |
|
$messageElement.addClass(options.messageClass); |
|
$toastElement.append($messageElement); |
|
} |
|
$messageElement.html(newMessage); |
|
}, |
|
|
|
update: function (newOptions) { |
|
options = $.extend(options, newOptions); |
|
if (newOptions.title !== undefined) { |
|
this.setTitle(newOptions.title); |
|
} |
|
if (newOptions.message !== undefined) { |
|
this.setMessage(newOptions.message); |
|
} |
|
if (newOptions.iconClass !== undefined) { |
|
$toastElement.removeClass(map.iconClass).addClass(newOptions.iconClass); |
|
map.iconClass = newOptions.iconClass; |
|
} |
|
}, |
|
|
|
/** |
|
* Fully replaces the current toast's DOM content. |
|
* Clears old event handlers, empties the DOM, updates underlying data, |
|
* and rebuilds the toast's content with new event bindings. |
|
* |
|
* @param {string} newMessage - The updated message. |
|
* @param {string} newTitle - The updated title. |
|
* @param {Object} newOptions - Updated options. |
|
*/ |
|
replace: function (newMessage, newTitle, newOptions) { |
|
// Clear timers and unbind events. |
|
clearTimeout(intervalId); |
|
if (progressBar.intervalId) clearInterval(progressBar.intervalId); |
|
$toastElement.off(); |
|
|
|
// Update underlying data. |
|
if (newTitle !== undefined) { |
|
map.title = newTitle; |
|
} |
|
if (newMessage !== undefined) { |
|
map.message = newMessage; |
|
} |
|
if (newOptions !== undefined) { |
|
options = $.extend(options, newOptions); |
|
} |
|
|
|
// Clear old content and rebuild completely. |
|
$toastElement.empty(); |
|
$toastElement.removeClass().addClass(options.toastClass).addClass(map.iconClass); |
|
|
|
// Build title. |
|
if (map.title) { |
|
var titleText = options.escapeHtml ? escapeHtml(map.title) : map.title; |
|
$titleElement = $('<div/>').addClass(options.titleClass).html(titleText); |
|
$toastElement.append($titleElement); |
|
} |
|
// Build message. |
|
if (map.message) { |
|
var messageText = options.escapeHtml ? escapeHtml(map.message) : map.message; |
|
$messageElement = $('<div/>').addClass(options.messageClass).html(messageText); |
|
$toastElement.append($messageElement); |
|
} |
|
// Build close button if needed. |
|
if (options.closeButton) { |
|
$closeElement = $(options.closeHtml).addClass(options.closeClass).attr('role', 'button'); |
|
$toastElement.prepend($closeElement); |
|
} |
|
// Build progress bar if enabled. |
|
if (options.progressBar) { |
|
$progressElement = $('<div/>').addClass(options.progressClass); |
|
$toastElement.prepend($progressElement); |
|
} |
|
setRTL(); |
|
setAria(); |
|
handleEvents(); |
|
resetTimeout(); |
|
}, |
|
|
|
/** |
|
* Closes the toast normally. |
|
* @returns {*} - Result of the hide animation. |
|
*/ |
|
close: function () { |
|
return hideToast(true); |
|
}, |
|
|
|
/** |
|
* Immediately closes the toast without any animation. |
|
* This ensures that no ghost notifications remain. |
|
*/ |
|
immediateClose: function () { |
|
clearTimeout(intervalId); |
|
if (progressBar.intervalId) clearInterval(progressBar.intervalId); |
|
$toastElement.stop(true, true).hide(0); |
|
removeToast($toastElement); |
|
} |
|
}; |
|
|
|
// Helper to reset auto-hide time and progress. |
|
function resetTimeout() { |
|
clearTimeout(intervalId); |
|
if (progressBar.intervalId) clearInterval(progressBar.intervalId); |
|
if (options.timeout > 0) { |
|
intervalId = setTimeout(hideToast, options.timeout); |
|
progressBar.maxHideTime = parseFloat(options.timeout); |
|
progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; |
|
if (options.progressBar) { |
|
progressBar.intervalId = setInterval(updateProgress, 10); |
|
} |
|
} |
|
} |
|
|
|
// Helper: escape HTML if enabled. |
|
function escapeHtml(source) { |
|
if (source == null) { |
|
source = ''; |
|
} |
|
return source |
|
.replace(/&/g, '&') |
|
.replace(/"/g, '"') |
|
.replace(/'/g, ''') |
|
.replace(/</g, '<') |
|
.replace(/>/g, '>'); |
|
} |
|
|
|
// Build the toast structure. |
|
function personalizeToast() { |
|
setIcon(); |
|
setTitle(); |
|
setMessage(); |
|
setCloseButton(); |
|
setProgressBar(); |
|
setRTL(); |
|
setSequence(); |
|
setAria(); |
|
} |
|
|
|
function setIcon() { |
|
if (map.iconClass) { |
|
$toastElement.addClass(options.toastClass).addClass(map.iconClass); |
|
} |
|
} |
|
|
|
function setSequence() { |
|
if (options.newestOnTop) { |
|
$container.prepend($toastElement); |
|
} else { |
|
$container.append($toastElement); |
|
} |
|
} |
|
|
|
function setTitle() { |
|
if (map.title) { |
|
var titleText = options.escapeHtml ? escapeHtml(map.title) : map.title; |
|
$titleElement.html(titleText).addClass(options.titleClass); |
|
$toastElement.append($titleElement); |
|
} |
|
} |
|
|
|
function setMessage() { |
|
if (map.message) { |
|
var messageText = options.escapeHtml ? escapeHtml(map.message) : map.message; |
|
$messageElement.html(messageText).addClass(options.messageClass); |
|
$toastElement.append($messageElement); |
|
} |
|
} |
|
|
|
function setCloseButton() { |
|
if (options.closeButton) { |
|
$closeElement.addClass(options.closeClass).attr('role', 'button'); |
|
$toastElement.prepend($closeElement); |
|
} |
|
} |
|
|
|
function setProgressBar() { |
|
if (options.progressBar) { |
|
$progressElement.addClass(options.progressClass); |
|
$toastElement.prepend($progressElement); |
|
} |
|
} |
|
|
|
function setRTL() { |
|
if (options.rtl) { |
|
$toastElement.addClass('rtl'); |
|
} |
|
} |
|
|
|
function setAria() { |
|
var ariaValue = ''; |
|
switch (map.iconClass) { |
|
case 'toast-success': |
|
case 'toast-info': |
|
ariaValue = 'polite'; |
|
break; |
|
default: |
|
ariaValue = 'assertive'; |
|
} |
|
$toastElement.attr('aria-live', ariaValue); |
|
} |
|
|
|
function displayToast() { |
|
$toastElement.hide(); |
|
if (options.oncreate) { |
|
options.oncreate.call(notificationAPI); |
|
} |
|
$toastElement[options.showMethod]({ |
|
duration: options.showDuration, |
|
easing: options.showEasing, |
|
complete: function () { |
|
if (options.onShown) { |
|
options.onShown.call(notificationAPI); |
|
} |
|
} |
|
}); |
|
if (options.timeout > 0) { |
|
intervalId = setTimeout(hideToast, options.timeout); |
|
progressBar.maxHideTime = parseFloat(options.timeout); |
|
progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; |
|
if (options.progressBar) { |
|
progressBar.intervalId = setInterval(updateProgress, 10); |
|
} |
|
} |
|
} |
|
|
|
function handleEvents() { |
|
if (options.closeOnhover) { |
|
$toastElement.on("mouseenter", stickAround).on("mouseleave", delayedHideToast); |
|
} |
|
if (options.onclick) { |
|
$toastElement.on("click", function (event) { |
|
options.onclick.call(notificationAPI, event); |
|
if (options.tapToDismiss) { |
|
hideToast(); |
|
} |
|
}); |
|
} else if (options.tapToDismiss) { |
|
$toastElement.on("click", hideToast); |
|
} |
|
if (options.closeButton && $closeElement) { |
|
$closeElement.on("click", function (event) { |
|
if (event.stopPropagation) { |
|
event.stopPropagation(); |
|
} else if (event.cancelBubble !== undefined && event.cancelBubble !== true) { |
|
event.cancelBubble = true; |
|
} |
|
if (options.closeOnclick) { |
|
options.closeOnclick.call(notificationAPI, event); |
|
} |
|
hideToast(true); |
|
}); |
|
} |
|
} |
|
|
|
function hideToast(override) { |
|
var method = override && options.closeMethod !== false ? options.closeMethod : options.hideMethod; |
|
var duration = override && options.closeDuration !== false ? options.closeDuration : options.hideDuration; |
|
var easing = override && options.closeEasing !== false ? options.closeEasing : options.hideEasing; |
|
if ($(':focus', $toastElement).length && !override) { |
|
return; |
|
} |
|
clearTimeout(progressBar.intervalId); |
|
return $toastElement[method]({ |
|
duration: duration, |
|
easing: easing, |
|
complete: function () { |
|
removeToast($toastElement); |
|
clearTimeout(intervalId); |
|
if (options.tag) { |
|
delete taggedToasts[options.tag]; |
|
} |
|
if (options.onhidden && response.state !== 'hidden') { |
|
options.onhidden.call(notificationAPI); |
|
} |
|
response.state = 'hidden'; |
|
response.endTime = new Date(); |
|
publish(response); |
|
} |
|
}); |
|
} |
|
|
|
function delayedHideToast() { |
|
if (options.timeout > 0 || options.extendedTimeout > 0) { |
|
intervalId = setTimeout(hideToast, options.extendedTimeout); |
|
progressBar.maxHideTime = parseFloat(options.extendedTimeout); |
|
progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; |
|
} |
|
} |
|
|
|
function stickAround() { |
|
clearTimeout(intervalId); |
|
progressBar.hideEta = 0; |
|
$toastElement.stop(true, true)[options.showMethod]({ |
|
duration: options.showDuration, |
|
easing: options.showEasing |
|
}); |
|
} |
|
|
|
function updateProgress() { |
|
var percentage = ((progressBar.hideEta - (new Date().getTime())) / progressBar.maxHideTime) * 100; |
|
$progressElement.width(percentage + '%'); |
|
} |
|
|
|
personalizeToast(); |
|
displayToast(); |
|
handleEvents(); |
|
publish(response); |
|
|
|
if (options.debug && console) { |
|
console.log("[DEBUG] Toast response:", response); |
|
} |
|
|
|
if (options.tag) { |
|
taggedToasts[options.tag] = notificationAPI; |
|
} |
|
|
|
return notificationAPI; |
|
} |
|
|
|
///////////////////////////////////////////////////////////////////////// |
|
// Default options and helper functions |
|
|
|
function getOptions() { |
|
return $.extend({}, getDefaults(), toastr.options); |
|
} |
|
|
|
function getDefaults() { |
|
return { |
|
tapToDismiss: true, |
|
toastClass: 'toast', |
|
containerId: 'toast-container', |
|
debug: false, |
|
oncreate: undefined, |
|
showMethod: 'fadeIn', |
|
showDuration: 300, |
|
showEasing: 'swing', |
|
onShown: undefined, |
|
hideMethod: 'fadeOut', |
|
hideDuration: 1000, |
|
hideEasing: 'swing', |
|
onhidden: undefined, |
|
closeMethod: false, |
|
closeDuration: false, |
|
closeEasing: false, |
|
closeOnhover: true, |
|
closeOnclick: undefined, |
|
extendedTimeout: 1000, |
|
iconClasses: { |
|
error: 'toast-error', |
|
info: 'toast-info', |
|
success: 'toast-success', |
|
warning: 'toast-warning' |
|
}, |
|
iconClass: 'toast-info', |
|
positionClass: 'toast-top-right', |
|
timeout: 5000, |
|
titleClass: 'toast-title', |
|
messageClass: 'toast-message', |
|
escapeHtml: false, |
|
target: 'body', |
|
closeHtml: '<button type="button">×</button>', |
|
closeClass: 'toast-close-button', |
|
newestOnTop: true, |
|
preventDuplicates: false, |
|
progressBar: false, |
|
progressClass: 'toast-progress', |
|
rtl: false, |
|
onclick: undefined, |
|
tag: undefined |
|
}; |
|
} |
|
|
|
function removeToast($toastElement) { |
|
if (!$container) { |
|
$container = getContainer(); |
|
} |
|
if ($toastElement.is(':visible')) { |
|
return; |
|
} |
|
$toastElement.remove(); |
|
$toastElement = null; |
|
if ($container.children().length === 0) { |
|
$container.remove(); |
|
previousToast = undefined; |
|
} |
|
} |
|
|
|
function clear($toastElement, clearOptions) { |
|
var options = getOptions(); |
|
if (!$container) { |
|
getContainer(options); |
|
} |
|
if (!clearToast($toastElement, options, clearOptions)) { |
|
clearContainer(options); |
|
} |
|
} |
|
|
|
function remove($toastElement) { |
|
var options = getOptions(); |
|
if (!$container) { |
|
getContainer(options); |
|
} |
|
if ($toastElement && $(':focus', $toastElement).length === 0) { |
|
removeToast($toastElement); |
|
return; |
|
} |
|
if ($container.children().length) { |
|
$container.remove(); |
|
} |
|
} |
|
|
|
function clearContainer(options) { |
|
var toastsToClear = $container.children(); |
|
for (var i = toastsToClear.length - 1; i >= 0; i--) { |
|
clearToast($(toastsToClear[i]), options); |
|
} |
|
} |
|
|
|
function clearToast($toastElement, options, clearOptions) { |
|
var force = clearOptions && clearOptions.force ? clearOptions.force : false; |
|
if ($toastElement && (force || $(':focus', $toastElement).length === 0)) { |
|
$toastElement[options.hideMethod]({ |
|
duration: options.hideDuration, |
|
easing: options.hideEasing, |
|
complete: function () { |
|
removeToast($toastElement); |
|
} |
|
}); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
function publish(args) { |
|
if (!listener) { |
|
return; |
|
} |
|
listener(args); |
|
} |
|
|
|
function getContainer(options, create) { |
|
if (!options) { |
|
options = getOptions(); |
|
} |
|
$container = $('#' + options.containerId + '[position=' + options.positionClass + ']'); |
|
if ($container.length) { |
|
return $container; |
|
} |
|
if (create) { |
|
$container = createContainer(options); |
|
} |
|
return $container; |
|
} |
|
|
|
function createContainer(options) { |
|
$container = $('<div/>') |
|
.attr('id', options.containerId) |
|
.attr('position', options.positionClass) |
|
.addClass(options.positionClass); |
|
$container.appendTo($(options.target)); |
|
return $container; |
|
} |
|
|
|
return toastr; |
|
})(); |
|
}); |
|
}(typeof define === 'function' && define.amd ? define : function (deps, factory) { |
|
if (typeof module !== 'undefined' && module.exports) { |
|
module.exports = factory(require('jquery')); |
|
} else { |
|
window.toastr = factory(window.jQuery); |
|
} |
|
})); |