329 lines
9.6 KiB
JavaScript
329 lines
9.6 KiB
JavaScript
(function (root) {
|
|
var pendingToastStorageKey = 'pulse:pending-toast';
|
|
|
|
function readPendingToast() {
|
|
try {
|
|
if (!root.sessionStorage) {
|
|
return null;
|
|
}
|
|
|
|
var rawValue = root.sessionStorage.getItem(pendingToastStorageKey);
|
|
if (!rawValue) {
|
|
return null;
|
|
}
|
|
|
|
root.sessionStorage.removeItem(pendingToastStorageKey);
|
|
var payload = JSON.parse(rawValue);
|
|
if (!payload || !payload.message) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
message: String(payload.message || '').trim(),
|
|
variant: String(payload.variant || 'info').trim().toLowerCase() || 'info'
|
|
};
|
|
} catch (_error) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writePendingToast(message, variant) {
|
|
try {
|
|
if (!root.sessionStorage) {
|
|
return false;
|
|
}
|
|
|
|
var text = String(message || '').trim();
|
|
if (!text) {
|
|
return false;
|
|
}
|
|
|
|
root.sessionStorage.setItem(pendingToastStorageKey, JSON.stringify({
|
|
message: text,
|
|
variant: String(variant || 'info').trim().toLowerCase() || 'info'
|
|
}));
|
|
return true;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function clearPendingToast() {
|
|
try {
|
|
if (root.sessionStorage) {
|
|
root.sessionStorage.removeItem(pendingToastStorageKey);
|
|
}
|
|
} catch (_error) {
|
|
// ignore storage cleanup failures
|
|
}
|
|
}
|
|
|
|
function getToastTitle(variant, message) {
|
|
var text = String(message || '').trim();
|
|
if (/\b(?:deleted|removed)\.?$/i.test(text)) {
|
|
return 'Deleted';
|
|
}
|
|
|
|
var textVariant = String(variant || '').trim().toLowerCase();
|
|
if (textVariant === 'danger') {
|
|
return 'Error';
|
|
}
|
|
if (textVariant === 'warning') {
|
|
return 'Warning';
|
|
}
|
|
if (textVariant === 'success') {
|
|
return 'Success';
|
|
}
|
|
return 'Pulse';
|
|
}
|
|
|
|
function getToastTimeLabel(createdAt) {
|
|
var timestamp = Number(createdAt);
|
|
if (!timestamp || Number.isNaN(timestamp)) {
|
|
return 'just now';
|
|
}
|
|
|
|
var elapsed = Date.now() - timestamp;
|
|
if (elapsed < 60 * 1000) {
|
|
return 'just now';
|
|
}
|
|
|
|
var minutes = Math.floor(elapsed / (60 * 1000));
|
|
if (minutes < 60) {
|
|
return minutes + ' min' + (minutes === 1 ? '' : 's') + ' ago';
|
|
}
|
|
|
|
var hours = Math.floor(minutes / 60);
|
|
if (hours < 24) {
|
|
return hours + ' hour' + (hours === 1 ? '' : 's') + ' ago';
|
|
}
|
|
|
|
var days = Math.floor(hours / 24);
|
|
if (days < 7) {
|
|
return days + ' day' + (days === 1 ? '' : 's') + ' ago';
|
|
}
|
|
|
|
return new Date(timestamp).toLocaleString();
|
|
}
|
|
|
|
function getToastButtonClass(variant) {
|
|
var textVariant = String(variant || '').trim().toLowerCase();
|
|
return textVariant === 'light' || textVariant === 'warning' ? 'btn-close' : 'btn-close-white';
|
|
}
|
|
|
|
function getToastIconClass(variant) {
|
|
var textVariant = String(variant || '').trim().toLowerCase();
|
|
if (textVariant === 'success') {
|
|
return 'bi-check-circle-fill';
|
|
}
|
|
if (textVariant === 'warning') {
|
|
return 'bi-exclamation-triangle-fill';
|
|
}
|
|
if (textVariant === 'danger') {
|
|
return 'bi-x-circle-fill';
|
|
}
|
|
if (textVariant === 'primary') {
|
|
return 'bi-bell-fill';
|
|
}
|
|
return 'bi-info-circle-fill';
|
|
}
|
|
|
|
function buildToastMarkup(title, timeLabel, buttonClass, iconClass) {
|
|
return '<div class="toast-header border-0">' +
|
|
'<i class="bi ' + iconClass + ' me-2"></i>' +
|
|
'<strong class="me-auto">' + title + '</strong>' +
|
|
'<small class="toast-time text-nowrap">' + timeLabel + '</small>' +
|
|
'<button type="button" class="btn-close ' + buttonClass + ' ms-2" data-bs-dismiss="toast" aria-label="Dismiss notification"></button>' +
|
|
'</div>' +
|
|
'<div class="toast-body"></div>';
|
|
}
|
|
|
|
function setToastHeader(toast, variant, createdAt) {
|
|
if (!toast) {
|
|
return;
|
|
}
|
|
|
|
var header = toast.querySelector('.toast-header');
|
|
var icon = toast.querySelector('.toast-header i');
|
|
var title = toast.querySelector('.toast-header .me-auto');
|
|
var time = toast.querySelector('.toast-time');
|
|
var closeButton = toast.querySelector('.toast-header .btn-close');
|
|
var nextVariant = getMessageVariant('', variant);
|
|
|
|
if (!header) {
|
|
return;
|
|
}
|
|
|
|
header.className = 'toast-header border-0 text-bg-' + nextVariant;
|
|
if (icon) {
|
|
icon.className = 'bi ' + getToastIconClass(nextVariant) + ' me-2';
|
|
}
|
|
if (title) {
|
|
title.textContent = getToastTitle(nextVariant);
|
|
}
|
|
if (time) {
|
|
time.textContent = getToastTimeLabel(createdAt);
|
|
}
|
|
if (closeButton) {
|
|
closeButton.className = 'btn-close ' + getToastButtonClass(nextVariant) + ' ms-2';
|
|
}
|
|
}
|
|
|
|
function getBootstrapToast(toast) {
|
|
if (!toast || !window.bootstrap || !window.bootstrap.Toast) {
|
|
return null;
|
|
}
|
|
|
|
return window.bootstrap.Toast.getOrCreateInstance(toast, {
|
|
autohide: true,
|
|
delay: 4000
|
|
});
|
|
}
|
|
|
|
function removeToast(toast) {
|
|
if (toast && toast.parentNode) {
|
|
toast.parentNode.removeChild(toast);
|
|
}
|
|
}
|
|
|
|
function dismissToast(toast) {
|
|
var instance = getBootstrapToast(toast);
|
|
if (instance) {
|
|
instance.hide();
|
|
return;
|
|
}
|
|
removeToast(toast);
|
|
}
|
|
|
|
function setToastVariant(toast, variant) {
|
|
if (!toast || !toast.classList) {
|
|
return;
|
|
}
|
|
|
|
setToastHeader(toast, variant, toast.getAttribute('data-toast-created-at') || Date.now());
|
|
}
|
|
|
|
function getMessageVariant(message, fallbackVariant) {
|
|
var text = String(message || '').trim();
|
|
var rules = [
|
|
{ variant: 'danger', patterns: [/\b(?:deleted|removed)\.?\s*$/i, /\b(?:deleted|removed)\b/i] },
|
|
{ variant: 'danger', patterns: [/\b(?:unable to|cannot|can't|could not|failed to)\s+delete\b/i, /\bdelete\b.*\b(?:before|first)\b/i, /\bstill (?:in use|linked|assigned|used)\b/i] },
|
|
{ variant: 'warning', patterns: [/\b(?:already exists|already exist|already taken|duplicate|must be unique|name already exists)\b/i] }
|
|
];
|
|
|
|
for (var i = 0; i < rules.length; i += 1) {
|
|
if (rules[i].patterns.some(function (pattern) { return pattern.test(text); })) {
|
|
return rules[i].variant;
|
|
}
|
|
}
|
|
|
|
return String(fallbackVariant || 'info').trim().toLowerCase() || 'info';
|
|
}
|
|
|
|
function showToast(message, variant) {
|
|
var text = String(message || '').trim();
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
var container = document.getElementById('app-toast-container');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
var existingToast = document.getElementById('app-toast');
|
|
if (existingToast) {
|
|
var existingBody = existingToast.querySelector('.toast-body');
|
|
if (existingBody) {
|
|
existingBody.textContent = text;
|
|
}
|
|
var nextVariant = getMessageVariant(text, variant);
|
|
existingToast.setAttribute('data-toast-variant', nextVariant);
|
|
existingToast.setAttribute('data-toast-created-at', String(Date.now()));
|
|
setToastVariant(existingToast, nextVariant);
|
|
setToastHeader(existingToast, nextVariant, Date.now());
|
|
var existingInstance = getBootstrapToast(existingToast);
|
|
if (existingInstance) {
|
|
existingInstance.show();
|
|
}
|
|
return;
|
|
}
|
|
|
|
var toast = document.createElement('div');
|
|
toast.className = 'toast shadow-lg border-0';
|
|
toast.id = 'app-toast';
|
|
toast.setAttribute('role', 'status');
|
|
toast.setAttribute('aria-live', 'polite');
|
|
toast.setAttribute('aria-atomic', 'true');
|
|
toast.setAttribute('data-bs-autohide', 'true');
|
|
toast.setAttribute('data-bs-delay', '4000');
|
|
toast.setAttribute('data-toast-created-at', String(Date.now()));
|
|
var toastVariant = getMessageVariant(text, variant);
|
|
toast.setAttribute('data-toast-variant', toastVariant);
|
|
toast.innerHTML = buildToastMarkup(getToastTitle(toastVariant, text), getToastTimeLabel(Date.now()), getToastButtonClass(toastVariant), getToastIconClass(toastVariant));
|
|
toast.querySelector('.toast-body').textContent = text;
|
|
setToastVariant(toast, toastVariant);
|
|
|
|
toast.addEventListener('hidden.bs.toast', function () {
|
|
removeToast(toast);
|
|
});
|
|
|
|
container.appendChild(toast);
|
|
var instance = getBootstrapToast(toast);
|
|
if (instance) {
|
|
instance.show();
|
|
}
|
|
}
|
|
|
|
function initToast() {
|
|
var toast = document.getElementById('app-toast');
|
|
if (!toast) {
|
|
var pendingToast = readPendingToast();
|
|
if (pendingToast) {
|
|
showToast(pendingToast.message, pendingToast.variant);
|
|
}
|
|
return;
|
|
}
|
|
|
|
clearPendingToast();
|
|
|
|
try {
|
|
var url = new URL(window.location.href);
|
|
if (url.searchParams.has('message')) {
|
|
url.searchParams.delete('message');
|
|
window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
|
|
}
|
|
} catch (_error) {
|
|
// ignore URL cleanup failures
|
|
}
|
|
|
|
var bodyText = (toast.querySelector('.toast-body') && toast.querySelector('.toast-body').textContent) || '';
|
|
var existingVariant = String(toast.getAttribute('data-toast-variant') || '').trim().toLowerCase() || getMessageVariant(bodyText, 'info');
|
|
setToastVariant(toast, existingVariant);
|
|
var title = toast.querySelector('.toast-header .me-auto');
|
|
if (title) {
|
|
title.textContent = getToastTitle(existingVariant, bodyText);
|
|
}
|
|
|
|
var instance = getBootstrapToast(toast);
|
|
if (instance) {
|
|
instance.show();
|
|
}
|
|
}
|
|
|
|
root.dismissToast = dismissToast;
|
|
root.showToast = showToast;
|
|
root.initToast = initToast;
|
|
root.getMessageVariant = getMessageVariant;
|
|
root.queueToastAfterRefresh = writePendingToast;
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
getMessageVariant: getMessageVariant
|
|
};
|
|
}
|
|
|
|
if (root.document) {
|
|
initToast();
|
|
}
|
|
}(typeof window !== 'undefined' ? window : globalThis)); |