@@ -8,6 +8,96 @@ import { uiState, updateUiState, dataState, cacheState, updateCacheState, search
88import { escapeHtml , escapeCSV } from './utils.js' ;
99import { markFiltersChanged , getFilterValues } from './filters.js' ;
1010
11+ // ============================================================================
12+ // UTILITY FUNCTIONS
13+ // ============================================================================
14+
15+ /**
16+ * Debounce utility function
17+ * Delays function execution until after a specified wait time has elapsed
18+ * @param {Function } func - Function to debounce
19+ * @param {number } wait - Wait time in milliseconds
20+ * @returns {Function } Debounced function
21+ */
22+ function debounce ( func , wait ) {
23+ let timeout ;
24+ return function executedFunction ( ...args ) {
25+ const later = ( ) => {
26+ clearTimeout ( timeout ) ;
27+ func ( ...args ) ;
28+ } ;
29+ clearTimeout ( timeout ) ;
30+ timeout = setTimeout ( later , wait ) ;
31+ } ;
32+ }
33+
34+ /**
35+ * Show loading overlay on data table
36+ */
37+ function showTableLoading ( ) {
38+ const overlay = document . getElementById ( 'dtLoadingOverlay' ) ;
39+ if ( overlay ) {
40+ overlay . style . display = 'flex' ;
41+ }
42+ }
43+
44+ /**
45+ * Hide loading overlay on data table
46+ */
47+ function hideTableLoading ( ) {
48+ const overlay = document . getElementById ( 'dtLoadingOverlay' ) ;
49+ if ( overlay ) {
50+ overlay . style . display = 'none' ;
51+ }
52+ }
53+
54+ /**
55+ * Show a notification message to the user
56+ * @param {string } message - Notification message
57+ * @param {string } type - Type of notification ('info', 'warning', 'error', 'success')
58+ */
59+ function showNotification ( message , type = 'info' ) {
60+ // Create notification element
61+ const notification = document . createElement ( 'div' ) ;
62+ notification . className = `user-notification user-notification-${ type } ` ;
63+ notification . textContent = message ;
64+ notification . style . cssText = `
65+ position: fixed;
66+ top: 80px;
67+ right: 20px;
68+ padding: 12px 20px;
69+ border-radius: 8px;
70+ background: var(--bg-secondary);
71+ border: 1px solid var(--border);
72+ box-shadow: var(--shadow-lg);
73+ z-index: 10002;
74+ font-size: 14px;
75+ max-width: 350px;
76+ animation: slideInFromRight 0.3s ease-out;
77+ ` ;
78+
79+ // Add type-specific styling
80+ if ( type === 'warning' ) {
81+ notification . style . borderLeft = '4px solid #ff9800' ;
82+ } else if ( type === 'error' ) {
83+ notification . style . borderLeft = '4px solid #f44336' ;
84+ } else if ( type === 'success' ) {
85+ notification . style . borderLeft = '4px solid #4caf50' ;
86+ }
87+
88+ document . body . appendChild ( notification ) ;
89+
90+ // Auto-remove after 5 seconds
91+ setTimeout ( ( ) => {
92+ notification . style . animation = 'slideOutToRight 0.3s ease-out' ;
93+ setTimeout ( ( ) => {
94+ if ( notification . parentNode ) {
95+ document . body . removeChild ( notification ) ;
96+ }
97+ } , 300 ) ;
98+ } , 5000 ) ;
99+ }
100+
11101// ============================================================================
12102// DISCLAIMER & FIRST VISIT
13103// ============================================================================
@@ -261,16 +351,23 @@ export function toggleDataTable() {
261351
262352 if ( panel . style . display === 'none' || panel . style . display === '' ) {
263353 panel . style . display = 'flex' ;
264- renderDataTable ( ) ;
265- // Restore maximized state if it was saved
266- if ( uiState . dtMaximized ) {
267- panel . classList . add ( 'dt-maximized' ) ;
268- const btn = document . getElementById ( 'dtMaximizeBtn' ) ;
269- if ( btn ) {
270- btn . innerHTML = '▣' ; // Minimize icon
271- btn . title = 'Minimize table' ;
354+ showTableLoading ( ) ;
355+
356+ // Use setTimeout to allow loading indicator to display
357+ setTimeout ( ( ) => {
358+ renderDataTable ( ) ;
359+ hideTableLoading ( ) ;
360+
361+ // Restore maximized state if it was saved
362+ if ( uiState . dtMaximized ) {
363+ panel . classList . add ( 'dt-maximized' ) ;
364+ const btn = document . getElementById ( 'dtMaximizeBtn' ) ;
365+ if ( btn ) {
366+ btn . innerHTML = '▣' ; // Minimize icon
367+ btn . title = 'Minimize table' ;
368+ }
272369 }
273- }
370+ } , 50 ) ;
274371 } else {
275372 panel . style . display = 'none' ;
276373 }
@@ -378,17 +475,28 @@ export function handlePageJump(event) {
378475}
379476
380477/**
381- * Search data table
478+ * Internal function to perform the actual search and render
382479 * @param {string } searchTerm - Search term
383480 */
384- export function searchDataTable ( searchTerm ) {
481+ function performSearch ( searchTerm ) {
385482 updateUiState ( {
386483 dtSearchTerm : searchTerm . toLowerCase ( ) ,
387484 dtCurrentPage : 0 // Reset to first page
388485 } ) ;
389486 renderDataTable ( ) ;
390487}
391488
489+ // Create debounced version (300ms delay)
490+ const debouncedSearch = debounce ( performSearch , 300 ) ;
491+
492+ /**
493+ * Search data table (debounced)
494+ * @param {string } searchTerm - Search term
495+ */
496+ export function searchDataTable ( searchTerm ) {
497+ debouncedSearch ( searchTerm ) ;
498+ }
499+
392500/**
393501 * Toggle column picker visibility
394502 */
@@ -683,6 +791,10 @@ export function saveTablePreferences() {
683791 } ) ) ;
684792 } catch ( e ) {
685793 console . warn ( 'Failed to save table preferences:' , e ) ;
794+ if ( e . name === 'QuotaExceededError' ) {
795+ // Show user-friendly notification for storage quota errors
796+ showNotification ( 'Unable to save table preferences. Browser storage is full.' , 'warning' ) ;
797+ }
686798 }
687799}
688800
@@ -765,6 +877,15 @@ export function initTableKeyboardNav() {
765877 const searchInput = document . getElementById ( 'dtSearch' ) ;
766878 if ( searchInput ) searchInput . focus ( ) ;
767879 }
880+
881+ // Enter or Space to activate focused table row
882+ if ( e . key === 'Enter' || e . key === ' ' ) {
883+ const focusedRow = document . activeElement ;
884+ if ( focusedRow && focusedRow . classList . contains ( 'dt-row-clickable' ) ) {
885+ e . preventDefault ( ) ;
886+ focusedRow . click ( ) ;
887+ }
888+ }
768889 } ) ;
769890}
770891
@@ -834,8 +955,9 @@ export function exportFilteredData(exportAll = true) {
834955 return ;
835956 }
836957
837- // Determine which data to export
838- let dataToExport ;
958+ try {
959+ // Determine which data to export
960+ let dataToExport ;
839961 if ( exportAll ) {
840962 dataToExport = dataState . filteredData ;
841963 } else {
@@ -995,9 +1117,13 @@ export function exportFilteredData(exportAll = true) {
9951117 link . setAttribute ( 'download' , `SA_Crash_Data_Export_${ timestamp } .csv` ) ;
9961118 link . style . visibility = 'hidden' ;
9971119
998- document . body . appendChild ( link ) ;
999- link . click ( ) ;
1000- document . body . removeChild ( link ) ;
1120+ document . body . appendChild ( link ) ;
1121+ link . click ( ) ;
1122+ document . body . removeChild ( link ) ;
1123+ } catch ( e ) {
1124+ console . error ( 'Failed to export data:' , e ) ;
1125+ showNotification ( 'Failed to export data. Please try again or reduce the dataset size.' , 'error' ) ;
1126+ }
10011127}
10021128
10031129// ============================================================================
0 commit comments