Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
590 lines
24 KiB
JavaScript
Executable File
590 lines
24 KiB
JavaScript
Executable File
/**
|
||
* Flash_Alert - Temporary alert notification system with queue persistence
|
||
*
|
||
* Displays dismissable alert messages that auto-fade after a timeout.
|
||
* Messages are queued to prevent overwhelming the user with simultaneous alerts.
|
||
* Queue state persists across page navigations using sessionStorage (per-tab).
|
||
*
|
||
* Usage:
|
||
* Flash_Alert.success('Changes saved!')
|
||
* Flash_Alert.error('Something went wrong')
|
||
* Flash_Alert.info('FYI: New feature available')
|
||
* Flash_Alert.warning('Are you sure about that?')
|
||
*
|
||
* Features:
|
||
* - Queue system prevents alert spam (2.5s minimum between alerts)
|
||
* - Auto-dismiss after timeout (success: 4s, others: 6s)
|
||
* - Click to dismiss immediately
|
||
* - Smooth fade in/out animations
|
||
* - Bootstrap alert styling with icons
|
||
* - Persistent queue across page navigations (sessionStorage, tab-specific)
|
||
* - Stale message filtering (entire queue discarded if > 20 seconds old)
|
||
* - SPA-like experience: messages maintain timing state across navigation
|
||
*
|
||
* Queue Architecture:
|
||
* - Two-queue system: working queue (_queue) and persistence queue (_persistence_queue)
|
||
* - Working queue: Messages removed when displayed (controls display timing/spacing)
|
||
* - Persistence queue: Messages removed when fadeout starts (saved to sessionStorage)
|
||
* - Prevents duplicate displays while maintaining cross-navigation state
|
||
*
|
||
* Persistence System:
|
||
* - Queue state saved to sessionStorage when messages queued or state changes
|
||
* - State restored on page load via Server_Side_Flash._on_framework_core_init()
|
||
* - Entire queue discarded if last_updated timestamp > 20 seconds old
|
||
* - Individual messages filtered: discard if past their fadeout_start_time
|
||
* - Per-tab isolation (sessionStorage doesn't share across browser tabs)
|
||
* - Handles Ajax response + redirect scenario (message survives navigation)
|
||
*
|
||
* Message Lifecycle & Timing:
|
||
* - Queued → Display (fade-in 400ms) → Visible (4s success, 6s others) → Fadeout (1s) → Removed
|
||
* - fade_in_complete flag: Set after fade-in animation completes
|
||
* - fadeout_start_time: Calculated timestamp when fadeout should begin
|
||
* - Both saved to sessionStorage to maintain timing consistency across navigation
|
||
*
|
||
* Navigation Behavior:
|
||
* - beforeunload event: Hides alerts still fading in, keeps fully visible alerts (with scheduled fadeout)
|
||
* - On restore: Messages with fade_in_complete=true show immediately (no fade-in, no queue delay)
|
||
* - On restore: Messages with fade_in_complete=false added to queue (normal 2.5s spacing)
|
||
* - Restored messages honor original fadeout_start_time (not restarted)
|
||
* - Creates seamless SPA-like experience across full page navigations
|
||
*
|
||
* Common Scenarios:
|
||
* 1. Ajax + Redirect: Flash message queued during Ajax, page redirects immediately
|
||
* → Message saved to sessionStorage → Restored and displayed on new page
|
||
* 2. Mid-animation navigation: Alert fading in when user navigates away
|
||
* → Alert hidden during navigation → Restored on new page, continues fade-in
|
||
* 3. Visible alert navigation: Alert fully visible when user navigates
|
||
* → Alert stays visible → Appears immediately on new page, honors original fadeout timing
|
||
*/
|
||
class Flash_Alert {
|
||
// Queue and state tracking
|
||
static _queue = []; // Working queue - messages removed when displayed
|
||
static _persistence_queue = []; // Persistence queue - messages removed when fadeout starts
|
||
static _is_in_progress = false;
|
||
static _last_alert_time = 0;
|
||
static _container = null;
|
||
|
||
/**
|
||
* Initialize the flash alert container
|
||
* @private
|
||
*/
|
||
static _init() {
|
||
if (this._container) return;
|
||
|
||
// Create floating alert container
|
||
this._container = $('<div id="floating-alert-container" class="floating-alert-container"></div>');
|
||
$('body').append(this._container);
|
||
|
||
// Register page navigation handler (only once)
|
||
window.addEventListener('beforeunload', () => {
|
||
console.log('[Flash_Alert] Page navigation detected - cleaning up');
|
||
this._cleanup_for_navigation();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Cleanup alerts on page navigation
|
||
* Hides alerts still fading in, leaves fully faded-in alerts visible with scheduled fadeout
|
||
* @private
|
||
*/
|
||
static _cleanup_for_navigation() {
|
||
console.log('[Flash_Alert] Cleaning up for navigation:', {
|
||
working_queue_before: this._queue.length,
|
||
persistence_queue: this._persistence_queue.length,
|
||
});
|
||
|
||
if (this._container) {
|
||
const now = Date.now();
|
||
|
||
// Process each visible alert
|
||
this._container.find('.alert-wrapper').each((index, wrapper) => {
|
||
const $wrapper = $(wrapper);
|
||
const message_text = $wrapper.find('.alert').text().trim().replace(/×$/, '').trim();
|
||
|
||
// Find this message in persistence queue to check fade_in_complete
|
||
const msg = this._persistence_queue.find(m => m.message === message_text);
|
||
|
||
if (msg && msg.fade_in_complete) { // @JS-DEFENSIVE-01-EXCEPTION - Array.find() returns undefined when no match is found
|
||
// Alert has fully faded in - leave it visible
|
||
console.log('[Flash_Alert] Leaving fully faded-in alert visible:', message_text);
|
||
|
||
// Schedule fadeout based on fadeout_start_time
|
||
if (msg.fadeout_start_time) {
|
||
const time_until_fadeout = Math.max(0, msg.fadeout_start_time - now);
|
||
|
||
setTimeout(() => {
|
||
console.log('[Flash_Alert] Navigation fadeout starting for:', message_text);
|
||
// Fade to transparent, then slide up
|
||
$wrapper.animate({ opacity: 0 }, 1000, () => {
|
||
$wrapper.slideUp(250, () => {
|
||
$wrapper.remove();
|
||
});
|
||
});
|
||
}, time_until_fadeout);
|
||
|
||
console.log('[Flash_Alert] Scheduled fadeout for visible alert:', {
|
||
message: message_text,
|
||
time_until_fadeout,
|
||
fadeout_start_time: msg.fadeout_start_time
|
||
});
|
||
}
|
||
} else {
|
||
// Alert still fading in - remove it
|
||
console.log('[Flash_Alert] Removing alert still fading in:', message_text);
|
||
$wrapper.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Clear working queue (stop processing new alerts)
|
||
this._queue = [];
|
||
|
||
// Stop processing
|
||
this._is_in_progress = false;
|
||
|
||
// DO NOT touch _persistence_queue or sessionStorage
|
||
// They will be restored on the next page load
|
||
|
||
console.log('[Flash_Alert] Cleanup complete - persistence queue preserved');
|
||
}
|
||
|
||
/**
|
||
* Save persistence queue state to sessionStorage
|
||
* @private
|
||
*/
|
||
static _save_queue_state() {
|
||
const state = {
|
||
last_updated: Date.now(),
|
||
messages: this._persistence_queue.map((item) => ({
|
||
message: item.message,
|
||
level: item.level,
|
||
timeout: item.timeout,
|
||
position: item.position,
|
||
queued_at: item.queued_at,
|
||
fade_in_complete: item.fade_in_complete || false,
|
||
fadeout_start_time: item.fadeout_start_time || null,
|
||
})),
|
||
};
|
||
console.log('[Flash_Alert] Saving persistence queue to sessionStorage:', {
|
||
message_count: state.messages.length,
|
||
last_updated: state.last_updated,
|
||
messages: state.messages,
|
||
});
|
||
Rsx_Storage.session_set('rsx_flash_queue', state);
|
||
}
|
||
|
||
/**
|
||
* Load queue state from sessionStorage
|
||
* Discards entire queue if last_updated timestamp is > 20 seconds old
|
||
* @private
|
||
*/
|
||
static _restore_queue_state() {
|
||
const state = Rsx_Storage.session_get('rsx_flash_queue');
|
||
console.log('[Flash_Alert] Attempting to restore queue state from sessionStorage:', {
|
||
has_stored_data: !!state,
|
||
stored_data: state,
|
||
});
|
||
|
||
if (!state) {
|
||
console.log('[Flash_Alert] No stored queue data found');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const now = Date.now();
|
||
const MAX_AGE = 20000; // 20 seconds in milliseconds
|
||
|
||
console.log('[Flash_Alert] Parsed stored state:', {
|
||
message_count: state.messages.length,
|
||
last_updated: state.last_updated,
|
||
messages: state.messages,
|
||
current_time: now,
|
||
});
|
||
|
||
// Check if the stored queue is > 20 seconds old
|
||
// If so, throw out the entire queue
|
||
const queue_age = now - state.last_updated;
|
||
if (queue_age > MAX_AGE) {
|
||
console.log('[Flash_Alert] Stored queue is too old, discarding entire queue:', {
|
||
queue_age_ms: queue_age,
|
||
max_age_ms: MAX_AGE,
|
||
last_updated: state.last_updated,
|
||
});
|
||
Rsx_Storage.session_remove('rsx_flash_queue');
|
||
return;
|
||
}
|
||
|
||
// Filter messages: remove those past their fadeout time
|
||
const valid_messages = state.messages.filter((msg) => {
|
||
// If fadeout was scheduled and already passed, discard message
|
||
if (msg.fadeout_start_time && now >= msg.fadeout_start_time) {
|
||
console.log('[Flash_Alert] Discarding message past fadeout time:', {
|
||
message: msg.message,
|
||
fadeout_start_time: msg.fadeout_start_time,
|
||
current_time: now,
|
||
time_past_fadeout: now - msg.fadeout_start_time,
|
||
});
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
|
||
console.log('[Flash_Alert] Valid messages to restore:', {
|
||
count: valid_messages.length,
|
||
messages: valid_messages,
|
||
});
|
||
|
||
// Separate messages into two groups: fade-in complete vs not
|
||
const messages_to_show_immediately = [];
|
||
const messages_to_queue = [];
|
||
|
||
valid_messages.forEach((msg) => {
|
||
const message_data = {
|
||
message: msg.message,
|
||
level: msg.level,
|
||
timeout: msg.timeout,
|
||
position: msg.position,
|
||
queued_at: msg.queued_at,
|
||
fade_in_complete: msg.fade_in_complete || false,
|
||
fadeout_start_time: msg.fadeout_start_time || null,
|
||
};
|
||
|
||
if (msg.fade_in_complete) {
|
||
// Already completed fade-in - show immediately
|
||
messages_to_show_immediately.push(message_data);
|
||
} else {
|
||
// Still needs fade-in - add to queue
|
||
messages_to_queue.push(message_data);
|
||
}
|
||
|
||
// Add all to persistence queue
|
||
this._persistence_queue.push(message_data);
|
||
});
|
||
|
||
console.log('[Flash_Alert] Messages after restoration:', {
|
||
to_show_immediately: messages_to_show_immediately.length,
|
||
to_queue: messages_to_queue.length,
|
||
persistence_queue_length: this._persistence_queue.length,
|
||
});
|
||
|
||
// Re-save persistence queue to sessionStorage
|
||
this._save_queue_state();
|
||
|
||
// Ensure container is initialized before displaying
|
||
this._init();
|
||
|
||
// Display fade-in complete messages immediately (no delay, no fade-in)
|
||
// These are displayed outside the normal queue, so don't affect _is_in_progress
|
||
if (messages_to_show_immediately.length > 0) {
|
||
console.log('[Flash_Alert] Displaying fade-in complete messages immediately');
|
||
messages_to_show_immediately.forEach((msg) => {
|
||
this._display_alert(msg, true); // true = immediate display, don't block queue
|
||
});
|
||
}
|
||
|
||
// Add remaining messages to working queue for normal processing
|
||
messages_to_queue.forEach((msg) => {
|
||
this._queue.push(msg);
|
||
});
|
||
|
||
// Start queue processing for messages that still need fade-in
|
||
if (this._queue.length > 0) {
|
||
console.log('[Flash_Alert] Starting queue processing for messages needing fade-in');
|
||
if (!this._is_in_progress) {
|
||
this._process_queue();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('[Flash_Alert] Failed to restore flash queue state:', e);
|
||
sessionStorage.removeItem('rsx_flash_queue');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove a message from persistence queue when fadeout starts
|
||
* @private
|
||
*/
|
||
static _remove_from_persistence_queue(message, level) {
|
||
const original_count = this._persistence_queue.length;
|
||
this._persistence_queue = this._persistence_queue.filter((msg) => {
|
||
return !(msg.level === level && msg.message === message);
|
||
});
|
||
|
||
console.log('[Flash_Alert] Removing message from persistence queue:', {
|
||
message: message,
|
||
level: level,
|
||
original_count: original_count,
|
||
remaining_count: this._persistence_queue.length,
|
||
});
|
||
|
||
// Save updated persistence queue to sessionStorage
|
||
this._save_queue_state();
|
||
}
|
||
|
||
/**
|
||
* Mark fade-in complete for a message in persistence queue
|
||
* @private
|
||
*/
|
||
static _mark_fade_in_complete(message, level) {
|
||
const msg = this._persistence_queue.find((m) => m.level === level && m.message === message);
|
||
if (msg) {
|
||
msg.fade_in_complete = true;
|
||
console.log('[Flash_Alert] Marked fade-in complete:', { message, level });
|
||
this._save_queue_state();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Set fadeout start time for a message in persistence queue
|
||
* @private
|
||
*/
|
||
static _set_fadeout_start_time(message, level, fadeout_start_time) {
|
||
const msg = this._persistence_queue.find((m) => m.level === level && m.message === message);
|
||
if (msg) {
|
||
msg.fadeout_start_time = fadeout_start_time;
|
||
console.log('[Flash_Alert] Set fadeout start time:', { message, level, fadeout_start_time });
|
||
this._save_queue_state();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show a success alert
|
||
* @param {string} message - The message to display
|
||
* @param {number|null} timeout - Auto-dismiss timeout in ms (null = default 4000ms)
|
||
* @param {string} position - Position: 'top' or 'bottom' (default: 'top')
|
||
*/
|
||
static success(message, timeout = null, position = 'top') {
|
||
this._show(message, 'success', timeout, position);
|
||
}
|
||
|
||
/**
|
||
* Show an error alert
|
||
* @param {string} message - The message to display
|
||
* @param {number|null} timeout - Auto-dismiss timeout in ms (null = default 6000ms)
|
||
* @param {string} position - Position: 'top' or 'bottom' (default: 'top')
|
||
*/
|
||
static error(message, timeout = null, position = 'top') {
|
||
this._show(message, 'danger', timeout, position);
|
||
}
|
||
|
||
/**
|
||
* Show an info alert
|
||
* @param {string} message - The message to display
|
||
* @param {number|null} timeout - Auto-dismiss timeout in ms (null = default 6000ms)
|
||
* @param {string} position - Position: 'top' or 'bottom' (default: 'top')
|
||
*/
|
||
static info(message, timeout = null, position = 'top') {
|
||
this._show(message, 'info', timeout, position);
|
||
}
|
||
|
||
/**
|
||
* Show a warning alert
|
||
* @param {string} message - The message to display
|
||
* @param {number|null} timeout - Auto-dismiss timeout in ms (null = default 6000ms)
|
||
* @param {string} position - Position: 'top' or 'bottom' (default: 'top')
|
||
*/
|
||
static warning(message, timeout = null, position = 'top') {
|
||
this._show(message, 'warning', timeout, position);
|
||
}
|
||
|
||
/**
|
||
* Show an alert with custom level
|
||
* @param {string} message - The message to display
|
||
* @param {string} level - Alert level: 'success', 'danger', 'info', 'warning'
|
||
* @param {number|null} timeout - Auto-dismiss timeout in ms (null = use default)
|
||
* @param {string} position - Position: 'top' or 'bottom' (default: 'top')
|
||
* @private
|
||
*/
|
||
static _show(message, level = 'info', timeout = null, position = 'top') {
|
||
this._init();
|
||
|
||
// Add to BOTH queues with timestamp
|
||
const message_data = {
|
||
message,
|
||
level,
|
||
timeout,
|
||
position,
|
||
queued_at: Date.now(),
|
||
};
|
||
|
||
this._queue.push(message_data);
|
||
this._persistence_queue.push(message_data);
|
||
|
||
// Save persistence queue to sessionStorage
|
||
this._save_queue_state();
|
||
|
||
// Process queue if not already processing
|
||
if (!this._is_in_progress) {
|
||
this._process_queue();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Process the next alert in the queue
|
||
* @private
|
||
*/
|
||
static _process_queue() {
|
||
const now = Date.now();
|
||
|
||
console.log('[Flash_Alert] Processing queue:', {
|
||
working_queue_length: this._queue.length,
|
||
persistence_queue_length: this._persistence_queue.length,
|
||
is_in_progress: this._is_in_progress,
|
||
time_since_last: now - this._last_alert_time,
|
||
});
|
||
|
||
// Display next alert if enough time has passed (2.5s minimum between alerts)
|
||
if (now - this._last_alert_time >= 2500 && this._queue.length > 0) {
|
||
// Remove from working queue (shift) but stays in persistence queue until fadeout
|
||
const alert_data = this._queue.shift();
|
||
console.log('[Flash_Alert] Displaying alert from queue:', alert_data);
|
||
console.log('[Flash_Alert] Working queue after shift:', this._queue.length);
|
||
this._display_alert(alert_data);
|
||
this._last_alert_time = now;
|
||
}
|
||
|
||
// Schedule next queue check if more alerts pending
|
||
if (this._queue.length > 0) {
|
||
console.log('[Flash_Alert] Scheduling next queue check in 2.5s');
|
||
setTimeout(() => this._process_queue(), 2500);
|
||
} else {
|
||
console.log('[Flash_Alert] Working queue empty, stopping processing');
|
||
this._is_in_progress = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display a single alert
|
||
* @param {Object} alert_data - Alert configuration
|
||
* @param {boolean} is_immediate - If true, don't set _is_in_progress (for restored alerts displayed outside queue)
|
||
* @private
|
||
*/
|
||
static _display_alert({ message, level, timeout, position = 'top', fade_in_complete = false, fadeout_start_time = null }, is_immediate = false) {
|
||
// Only set in_progress if this is a queued display (not an immediate restored alert)
|
||
if (!is_immediate) {
|
||
this._is_in_progress = true;
|
||
}
|
||
|
||
console.log('[Flash_Alert] Displaying alert:', {
|
||
message,
|
||
level,
|
||
fade_in_complete,
|
||
fadeout_start_time,
|
||
current_time: Date.now(),
|
||
});
|
||
|
||
// Check if an alert with the same message is already displayed
|
||
let duplicate_found = false;
|
||
this._container.find('.alert-wrapper').each(function () {
|
||
const existing_text = $(this).find('.alert').text().trim();
|
||
// Remove the close button text (×) for comparison
|
||
const existing_message = existing_text.replace(/×$/, '').trim();
|
||
if (existing_message === message) {
|
||
duplicate_found = true;
|
||
return false; // Break loop
|
||
}
|
||
});
|
||
|
||
// Skip displaying if duplicate found
|
||
if (duplicate_found) {
|
||
console.log('[Flash_Alert] Duplicate found, skipping display');
|
||
return;
|
||
}
|
||
|
||
// Create alert element
|
||
const $alert = $(`<div class="alert alert-${level} alert-dismissible fade show" role="alert">`);
|
||
|
||
// Add icon based on level
|
||
if (level === 'danger') {
|
||
$alert.append('<i class="bi bi-exclamation-circle-fill me-2"></i>');
|
||
} else if (level === 'success') {
|
||
$alert.append('<i class="bi bi-check-circle-fill me-2"></i>');
|
||
} else if (level === 'info') {
|
||
$alert.append('<i class="bi bi-info-circle-fill me-2"></i>');
|
||
} else if (level === 'warning') {
|
||
$alert.append('<i class="bi bi-exclamation-triangle-fill me-2"></i>');
|
||
}
|
||
|
||
// Add close button
|
||
const $close_button = $('<button type="button" class="btn-close" aria-label="Close"></button>');
|
||
$alert.append($close_button).append(message);
|
||
|
||
// Wrap in container for animation
|
||
const $alert_container = $('<div class="alert-wrapper">').append($alert);
|
||
|
||
// Add to floating container based on position
|
||
if (position === 'top') {
|
||
// Top position - append (newer alerts below older ones)
|
||
this._container.css('justify-content', 'flex-start').append($alert_container);
|
||
} else {
|
||
// Bottom position - prepend (newer alerts above older ones)
|
||
this._container.css('justify-content', 'flex-end').prepend($alert_container);
|
||
}
|
||
|
||
// Fade in (skip if already completed fade-in on previous page)
|
||
if (fade_in_complete) {
|
||
console.log('[Flash_Alert] Skipping fade-in, showing immediately (restored from previous page)');
|
||
$alert_container.show();
|
||
} else {
|
||
$alert_container.hide().fadeIn(400, () => {
|
||
console.log('[Flash_Alert] Fade-in complete');
|
||
// Mark fade-in complete in persistence queue
|
||
this._mark_fade_in_complete(message, level);
|
||
});
|
||
}
|
||
|
||
// Close function - fade to transparent, then slide up
|
||
const close_alert = (speed) => {
|
||
$alert_container.animate({ opacity: 0 }, speed, () => {
|
||
$alert_container.slideUp(250, () => {
|
||
$alert_container.remove();
|
||
});
|
||
});
|
||
};
|
||
|
||
// Calculate when fadeout should start
|
||
const now = Date.now();
|
||
let time_until_fadeout;
|
||
|
||
if (fadeout_start_time) {
|
||
// Honor existing fadeout schedule from restored state
|
||
time_until_fadeout = Math.max(0, fadeout_start_time - now);
|
||
console.log('[Flash_Alert] Using restored fadeout schedule:', {
|
||
fadeout_start_time,
|
||
now,
|
||
time_until_fadeout,
|
||
});
|
||
} else {
|
||
// New message - calculate fadeout time (4s for success, 6s for others)
|
||
const display_duration = level === 'success' ? 4000 : 6000;
|
||
time_until_fadeout = display_duration;
|
||
const new_fadeout_start_time = now + display_duration;
|
||
|
||
console.log('[Flash_Alert] Scheduling new fadeout:', {
|
||
display_duration,
|
||
fadeout_start_time: new_fadeout_start_time,
|
||
});
|
||
|
||
// Store fadeout start time in persistence queue
|
||
this._set_fadeout_start_time(message, level, new_fadeout_start_time);
|
||
}
|
||
|
||
// Schedule fadeout
|
||
if (time_until_fadeout >= 0) {
|
||
setTimeout(() => {
|
||
console.log('[Flash_Alert] Fadeout starting for:', message);
|
||
// Remove from persistence queue when fadeout starts
|
||
this._remove_from_persistence_queue(message, level);
|
||
close_alert(1000);
|
||
}, time_until_fadeout);
|
||
}
|
||
|
||
// Click anywhere on alert to dismiss
|
||
$alert.click(() => {
|
||
// Remove from persistence queue when manually dismissed
|
||
this._remove_from_persistence_queue(message, level);
|
||
close_alert(300);
|
||
});
|
||
}
|
||
}
|