Skip to content

Commit 9123f42

Browse files
committed
Improve Data Table UX
1 parent 4e2a15a commit 9123f42

2 files changed

Lines changed: 197 additions & 18 deletions

File tree

src/js/ui.js

Lines changed: 142 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,96 @@ import { uiState, updateUiState, dataState, cacheState, updateCacheState, search
88
import { escapeHtml, escapeCSV } from './utils.js';
99
import { 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
// ============================================================================

styles.css

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3270,7 +3270,7 @@
32703270

32713271
@media (max-width: 768px) {
32723272
.data-table-panel {
3273-
height: 260px;
3273+
height: 320px;
32743274
}
32753275
}
32763276

@@ -3301,6 +3301,42 @@
33013301
color: var(--text-tertiary);
33023302
}
33033303

3304+
/* Loading overlay */
3305+
.dt-loading-overlay {
3306+
position: absolute;
3307+
top: 0;
3308+
left: 0;
3309+
right: 0;
3310+
bottom: 0;
3311+
background: rgba(0, 0, 0, 0.5);
3312+
display: flex;
3313+
flex-direction: column;
3314+
align-items: center;
3315+
justify-content: center;
3316+
z-index: 100;
3317+
backdrop-filter: blur(2px);
3318+
}
3319+
3320+
.dt-loading-spinner {
3321+
width: 40px;
3322+
height: 40px;
3323+
border: 4px solid var(--border);
3324+
border-top-color: var(--accent);
3325+
border-radius: 50%;
3326+
animation: spin 0.8s linear infinite;
3327+
}
3328+
3329+
.dt-loading-text {
3330+
margin-top: 12px;
3331+
color: var(--text-primary);
3332+
font-size: 14px;
3333+
font-weight: 500;
3334+
}
3335+
3336+
@keyframes spin {
3337+
to { transform: rotate(360deg); }
3338+
}
3339+
33043340
/* Page size selector */
33053341
.data-table-page-size {
33063342
padding: 4px 8px;
@@ -3606,7 +3642,24 @@
36063642

36073643
.data-table-column-btn,
36083644
.data-table-export-btn {
3609-
padding: 5px 8px;
3645+
padding: 10px 12px;
3646+
min-width: 44px;
3647+
min-height: 44px;
3648+
}
3649+
3650+
.data-table-nav-btn {
3651+
padding: 10px 14px;
3652+
min-width: 44px;
3653+
min-height: 44px;
3654+
font-size: 16px;
3655+
}
3656+
3657+
.data-table-maximize-btn,
3658+
.data-table-close-btn {
3659+
min-width: 44px;
3660+
min-height: 44px;
3661+
padding: 10px;
3662+
font-size: 18px;
36103663
}
36113664

36123665
.dt-pagination-label {

0 commit comments

Comments
 (0)