🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
623 lines
25 KiB
JavaScript
Executable File
623 lines
25 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)
|
|
* - Hover to pause: Hovering an alert pauses the fadeout timer, resumes on mouse leave
|
|
* - Text selection: Alert text can be selected and copied
|
|
* - 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();
|
|
|
|
// 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_message = $(this).find('.alert').text().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 (no close button - auto-fades, click anywhere to dismiss)
|
|
const $alert = $(`<div class="alert alert-${level} 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>');
|
|
}
|
|
|
|
$alert.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);
|
|
}
|
|
|
|
// Track fadeout state on the wrapper element for hover pause/resume
|
|
$alert_container.data('fadeout_remaining', time_until_fadeout);
|
|
$alert_container.data('fadeout_paused', false);
|
|
|
|
// Schedule fadeout function (reusable for resume)
|
|
const schedule_fadeout = (delay) => {
|
|
const timeout_id = setTimeout(() => {
|
|
// Don't fade if paused (safety check)
|
|
if ($alert_container.data('fadeout_paused')) return;
|
|
|
|
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);
|
|
}, delay);
|
|
$alert_container.data('fadeout_timeout_id', timeout_id);
|
|
$alert_container.data('fadeout_scheduled_at', Date.now());
|
|
};
|
|
|
|
// Pause fadeout on hover
|
|
$alert_container.on('mouseenter', () => {
|
|
if ($alert_container.data('fadeout_paused')) return;
|
|
|
|
const timeout_id = $alert_container.data('fadeout_timeout_id');
|
|
const scheduled_at = $alert_container.data('fadeout_scheduled_at');
|
|
const remaining = $alert_container.data('fadeout_remaining');
|
|
|
|
if (timeout_id) {
|
|
clearTimeout(timeout_id);
|
|
// Calculate how much time was remaining when paused
|
|
const elapsed = Date.now() - scheduled_at;
|
|
const new_remaining = Math.max(0, remaining - elapsed);
|
|
$alert_container.data('fadeout_remaining', new_remaining);
|
|
$alert_container.data('fadeout_paused', true);
|
|
console.log('[Flash_Alert] Paused fadeout on hover:', { message, remaining: new_remaining });
|
|
}
|
|
});
|
|
|
|
// Resume fadeout on mouse leave
|
|
$alert_container.on('mouseleave', () => {
|
|
if (!$alert_container.data('fadeout_paused')) return;
|
|
|
|
const remaining = $alert_container.data('fadeout_remaining');
|
|
$alert_container.data('fadeout_paused', false);
|
|
console.log('[Flash_Alert] Resuming fadeout:', { message, remaining });
|
|
schedule_fadeout(remaining);
|
|
});
|
|
|
|
// Schedule initial fadeout
|
|
if (time_until_fadeout >= 0) {
|
|
schedule_fadeout(time_until_fadeout);
|
|
}
|
|
}
|
|
}
|