1528 lines
54 KiB
HTML
1528 lines
54 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Table Enhanced - Data Explorer</title>
|
|
<style>
|
|
:root {
|
|
--primary: #3b82f6;
|
|
--primary-dark: #2563eb;
|
|
--primary-light: #dbeafe;
|
|
--secondary: #6366f1;
|
|
--gray-50: #f9fafb;
|
|
--gray-100: #f3f4f6;
|
|
--gray-200: #e5e7eb;
|
|
--gray-300: #d1d5db;
|
|
--gray-400: #9ca3af;
|
|
--gray-500: #6b7280;
|
|
--gray-600: #4b5563;
|
|
--gray-700: #374151;
|
|
--gray-800: #1f2937;
|
|
--gray-900: #111827;
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--danger: #ef4444;
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
--radius: 0.5rem;
|
|
--radius-sm: 0.375rem;
|
|
--radius-lg: 0.75rem;
|
|
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
|
background: var(--gray-50);
|
|
color: var(--gray-800);
|
|
line-height: 1.5;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
}
|
|
|
|
main {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 1rem;
|
|
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 1.125rem;
|
|
color: var(--gray-600);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
/* Table Container */
|
|
.table-container {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-md);
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
/* Table Controls */
|
|
.table-controls {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1.5rem;
|
|
border-bottom: 1px solid var(--gray-200);
|
|
gap: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.search-container {
|
|
position: relative;
|
|
flex: 1;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.search-icon {
|
|
position: absolute;
|
|
left: 0.75rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: var(--gray-400);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.search-input {
|
|
width: 100%;
|
|
padding: 0.625rem 1rem 0.625rem 2.5rem;
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: var(--radius);
|
|
font-size: 0.875rem;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.search-input:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.table-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.btn {
|
|
padding: 0.625rem 1rem;
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: var(--radius);
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
background: white;
|
|
color: var(--gray-700);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: var(--gray-50);
|
|
border-color: var(--gray-400);
|
|
}
|
|
|
|
.btn:active {
|
|
transform: translateY(1px);
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: var(--primary-dark);
|
|
border-color: var(--primary-dark);
|
|
}
|
|
|
|
.btn-icon {
|
|
padding: 0.625rem;
|
|
}
|
|
|
|
/* Table Wrapper */
|
|
.table-wrapper {
|
|
overflow-x: auto;
|
|
position: relative;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
/* Table Header */
|
|
thead {
|
|
background: var(--gray-50);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
box-shadow: inset 0 -1px 0 var(--gray-200);
|
|
}
|
|
|
|
th {
|
|
padding: 1rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
color: var(--gray-700);
|
|
white-space: nowrap;
|
|
user-select: none;
|
|
position: relative;
|
|
}
|
|
|
|
th.sortable {
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
th.sortable:hover {
|
|
background: var(--gray-100);
|
|
}
|
|
|
|
th.sortable .header-content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.sort-indicator {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
opacity: 0.3;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.sort-indicator .arrow {
|
|
width: 0;
|
|
height: 0;
|
|
border-style: solid;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.sort-indicator .arrow-up {
|
|
border-width: 0 4px 4px 4px;
|
|
border-color: transparent transparent var(--gray-600) transparent;
|
|
}
|
|
|
|
.sort-indicator .arrow-down {
|
|
border-width: 4px 4px 0 4px;
|
|
border-color: var(--gray-600) transparent transparent transparent;
|
|
}
|
|
|
|
th.sorted-asc .sort-indicator {
|
|
opacity: 1;
|
|
}
|
|
|
|
th.sorted-asc .arrow-up {
|
|
border-color: transparent transparent var(--primary) transparent;
|
|
}
|
|
|
|
th.sorted-desc .sort-indicator {
|
|
opacity: 1;
|
|
}
|
|
|
|
th.sorted-desc .arrow-down {
|
|
border-color: var(--primary) transparent transparent transparent;
|
|
}
|
|
|
|
/* Column Resize Handle */
|
|
.resize-handle {
|
|
position: absolute;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 4px;
|
|
cursor: col-resize;
|
|
background: transparent;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.resize-handle:hover,
|
|
.resize-handle.resizing {
|
|
background: var(--primary);
|
|
}
|
|
|
|
/* Table Body */
|
|
tbody tr {
|
|
border-bottom: 1px solid var(--gray-100);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
tbody tr:hover {
|
|
background: var(--gray-50);
|
|
}
|
|
|
|
tbody tr.selected {
|
|
background: var(--primary-light);
|
|
}
|
|
|
|
tbody tr.expanded {
|
|
background: var(--gray-50);
|
|
}
|
|
|
|
td {
|
|
padding: 1rem;
|
|
font-size: 0.875rem;
|
|
color: var(--gray-700);
|
|
}
|
|
|
|
/* Checkbox Column */
|
|
.checkbox-cell {
|
|
width: 40px;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.checkbox {
|
|
width: 1.125rem;
|
|
height: 1.125rem;
|
|
cursor: pointer;
|
|
accent-color: var(--primary);
|
|
}
|
|
|
|
/* Expand Column */
|
|
.expand-cell {
|
|
width: 40px;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.expand-btn {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
border: none;
|
|
background: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: var(--radius-sm);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.expand-btn:hover {
|
|
background: var(--gray-200);
|
|
}
|
|
|
|
.expand-icon {
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.expanded .expand-icon {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
/* Expanded Row */
|
|
.expanded-content {
|
|
grid-column: 1 / -1;
|
|
padding: 1rem;
|
|
background: var(--gray-50);
|
|
border-top: 1px solid var(--gray-200);
|
|
animation: slideDown 0.2s ease-out;
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* Status Badge */
|
|
.status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-active {
|
|
background: #dcfce7;
|
|
color: #16a34a;
|
|
}
|
|
|
|
.status-pending {
|
|
background: #fef3c7;
|
|
color: #d97706;
|
|
}
|
|
|
|
.status-inactive {
|
|
background: #f3f4f6;
|
|
color: #6b7280;
|
|
}
|
|
|
|
/* Pagination */
|
|
.table-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 1rem 1.5rem;
|
|
border-top: 1px solid var(--gray-200);
|
|
flex-wrap: wrap;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.pagination-info {
|
|
font-size: 0.875rem;
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
.pagination-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.rows-per-page {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.875rem;
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
.rows-per-page select {
|
|
padding: 0.375rem 2rem 0.375rem 0.75rem;
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.875rem;
|
|
background: white;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.pagination-buttons {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.pagination-btn {
|
|
padding: 0.5rem;
|
|
border: 1px solid var(--gray-300);
|
|
border-radius: var(--radius-sm);
|
|
background: white;
|
|
color: var(--gray-600);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
}
|
|
|
|
.pagination-btn:hover:not(:disabled) {
|
|
background: var(--gray-50);
|
|
border-color: var(--gray-400);
|
|
}
|
|
|
|
.pagination-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.pagination-btn.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
/* Loading State */
|
|
.loading-overlay {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(255, 255, 255, 0.8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 20;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.loading-overlay.active {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
}
|
|
|
|
.spinner {
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border: 3px solid var(--gray-200);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Mobile Responsive */
|
|
@media (max-width: 768px) {
|
|
main {
|
|
padding: 1rem;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.table-controls {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.search-container {
|
|
max-width: none;
|
|
order: -1;
|
|
flex-basis: 100%;
|
|
}
|
|
|
|
/* Card View on Mobile */
|
|
.mobile-cards .table-wrapper {
|
|
display: none;
|
|
}
|
|
|
|
.mobile-cards .cards-container {
|
|
display: block;
|
|
}
|
|
|
|
.cards-container {
|
|
display: none;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.card {
|
|
background: white;
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--radius);
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
box-shadow: var(--shadow-sm);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.card:hover {
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.card.selected {
|
|
background: var(--primary-light);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.75rem;
|
|
padding-bottom: 0.75rem;
|
|
border-bottom: 1px solid var(--gray-100);
|
|
}
|
|
|
|
.card-checkbox {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.card-field {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.5rem 0;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.card-field-label {
|
|
font-weight: 500;
|
|
color: var(--gray-600);
|
|
}
|
|
|
|
.card-field-value {
|
|
color: var(--gray-800);
|
|
text-align: right;
|
|
}
|
|
|
|
.table-footer {
|
|
padding: 1rem;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.pagination-info {
|
|
text-align: center;
|
|
}
|
|
|
|
.pagination-controls {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.rows-per-page {
|
|
justify-content: center;
|
|
}
|
|
|
|
.pagination-buttons {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
/* Accessibility */
|
|
.visually-hidden {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border: 0;
|
|
}
|
|
|
|
/* Focus Styles */
|
|
button:focus-visible,
|
|
input:focus-visible,
|
|
select:focus-visible {
|
|
outline: 2px solid var(--primary);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* No Data State */
|
|
.no-data {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: var(--gray-500);
|
|
}
|
|
|
|
.no-data-icon {
|
|
font-size: 3rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Column Filter Dropdown */
|
|
.column-filter {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
background: white;
|
|
border: 1px solid var(--gray-200);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-lg);
|
|
padding: 0.5rem;
|
|
min-width: 200px;
|
|
z-index: 100;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transform: translateY(-10px);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.column-filter.active {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.filter-item {
|
|
padding: 0.5rem;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
border-radius: var(--radius-sm);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.filter-item:hover {
|
|
background: var(--gray-50);
|
|
}
|
|
|
|
.filter-item label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Table - Enhanced</h1>
|
|
<p class="subtitle">Advanced data table with sorting, filtering, selection, and responsive design</p>
|
|
|
|
<div class="table-container">
|
|
<div class="table-controls">
|
|
<div class="search-container">
|
|
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<path d="m21 21-4.35-4.35"></path>
|
|
</svg>
|
|
<input type="text" class="search-input" placeholder="Search across all columns..." id="searchInput">
|
|
</div>
|
|
|
|
<div class="table-actions">
|
|
<button class="btn" id="exportBtn">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
<polyline points="7 10 12 15 17 10"></polyline>
|
|
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
</svg>
|
|
Export
|
|
</button>
|
|
<button class="btn" id="columnsBtn">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="20" x2="12" y2="10"></line>
|
|
<line x1="18" y1="20" x2="18" y2="4"></line>
|
|
<line x1="6" y1="20" x2="6" y2="16"></line>
|
|
</svg>
|
|
Columns
|
|
</button>
|
|
<button class="btn btn-icon" id="viewToggle" aria-label="Toggle view">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="3" width="7" height="7"></rect>
|
|
<rect x="14" y="3" width="7" height="7"></rect>
|
|
<rect x="14" y="14" width="7" height="7"></rect>
|
|
<rect x="3" y="14" width="7" height="7"></rect>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-wrapper">
|
|
<table id="dataTable" role="table">
|
|
<thead>
|
|
<tr role="row">
|
|
<th class="checkbox-cell">
|
|
<input type="checkbox" class="checkbox" id="selectAll" aria-label="Select all rows">
|
|
</th>
|
|
<th class="expand-cell"></th>
|
|
<th class="sortable" data-column="id" role="columnheader" aria-sort="none">
|
|
<div class="header-content">
|
|
<span>ID</span>
|
|
<div class="sort-indicator">
|
|
<div class="arrow arrow-up"></div>
|
|
<div class="arrow arrow-down"></div>
|
|
</div>
|
|
</div>
|
|
<div class="resize-handle" data-column="id"></div>
|
|
</th>
|
|
<th class="sortable" data-column="name" role="columnheader" aria-sort="none">
|
|
<div class="header-content">
|
|
<span>Name</span>
|
|
<div class="sort-indicator">
|
|
<div class="arrow arrow-up"></div>
|
|
<div class="arrow arrow-down"></div>
|
|
</div>
|
|
</div>
|
|
<div class="resize-handle" data-column="name"></div>
|
|
</th>
|
|
<th class="sortable" data-column="email" role="columnheader" aria-sort="none">
|
|
<div class="header-content">
|
|
<span>Email</span>
|
|
<div class="sort-indicator">
|
|
<div class="arrow arrow-up"></div>
|
|
<div class="arrow arrow-down"></div>
|
|
</div>
|
|
</div>
|
|
<div class="resize-handle" data-column="email"></div>
|
|
</th>
|
|
<th class="sortable" data-column="department" role="columnheader" aria-sort="none">
|
|
<div class="header-content">
|
|
<span>Department</span>
|
|
<div class="sort-indicator">
|
|
<div class="arrow arrow-up"></div>
|
|
<div class="arrow arrow-down"></div>
|
|
</div>
|
|
</div>
|
|
<div class="resize-handle" data-column="department"></div>
|
|
</th>
|
|
<th class="sortable" data-column="role" role="columnheader" aria-sort="none">
|
|
<div class="header-content">
|
|
<span>Role</span>
|
|
<div class="sort-indicator">
|
|
<div class="arrow arrow-up"></div>
|
|
<div class="arrow arrow-down"></div>
|
|
</div>
|
|
</div>
|
|
<div class="resize-handle" data-column="role"></div>
|
|
</th>
|
|
<th class="sortable" data-column="status" role="columnheader" aria-sort="none">
|
|
<div class="header-content">
|
|
<span>Status</span>
|
|
<div class="sort-indicator">
|
|
<div class="arrow arrow-up"></div>
|
|
<div class="arrow arrow-down"></div>
|
|
</div>
|
|
</div>
|
|
<div class="resize-handle" data-column="status"></div>
|
|
</th>
|
|
<th class="sortable" data-column="lastLogin" role="columnheader" aria-sort="none">
|
|
<div class="header-content">
|
|
<span>Last Login</span>
|
|
<div class="sort-indicator">
|
|
<div class="arrow arrow-up"></div>
|
|
<div class="arrow arrow-down"></div>
|
|
</div>
|
|
</div>
|
|
<div class="resize-handle" data-column="lastLogin"></div>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="tableBody">
|
|
<!-- Table rows will be inserted here -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="cards-container" id="cardsContainer">
|
|
<!-- Card view will be inserted here -->
|
|
</div>
|
|
|
|
<div class="table-footer">
|
|
<div class="pagination-info">
|
|
Showing <span id="startRecord">1</span> to <span id="endRecord">10</span> of <span id="totalRecords">0</span> entries
|
|
<span id="selectedInfo" style="display: none;">
|
|
(<span id="selectedCount">0</span> selected)
|
|
</span>
|
|
</div>
|
|
|
|
<div class="pagination-controls">
|
|
<div class="rows-per-page">
|
|
<label for="rowsPerPage">Rows per page:</label>
|
|
<select id="rowsPerPage">
|
|
<option value="10">10</option>
|
|
<option value="25">25</option>
|
|
<option value="50">50</option>
|
|
<option value="100">100</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="pagination-buttons">
|
|
<button class="pagination-btn" id="firstPage" aria-label="First page">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="11 17 6 12 11 7"></polyline>
|
|
<polyline points="18 17 13 12 18 7"></polyline>
|
|
</svg>
|
|
</button>
|
|
<button class="pagination-btn" id="prevPage" aria-label="Previous page">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="15 18 9 12 15 6"></polyline>
|
|
</svg>
|
|
</button>
|
|
<div id="pageNumbers"></div>
|
|
<button class="pagination-btn" id="nextPage" aria-label="Next page">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="9 18 15 12 9 6"></polyline>
|
|
</svg>
|
|
</button>
|
|
<button class="pagination-btn" id="lastPage" aria-label="Last page">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="13 17 18 12 13 7"></polyline>
|
|
<polyline points="6 17 11 12 6 7"></polyline>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="loading-overlay" id="loadingOverlay">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// Sample data generation
|
|
const departments = ['Engineering', 'Sales', 'Marketing', 'HR', 'Finance', 'Operations'];
|
|
const roles = ['Manager', 'Senior', 'Junior', 'Lead', 'Specialist', 'Analyst'];
|
|
const statuses = ['Active', 'Pending', 'Inactive'];
|
|
const firstNames = ['John', 'Jane', 'Michael', 'Sarah', 'David', 'Emma', 'James', 'Lisa', 'Robert', 'Maria'];
|
|
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez'];
|
|
|
|
function generateData(count) {
|
|
const data = [];
|
|
for (let i = 1; i <= count; i++) {
|
|
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
|
|
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
|
|
const name = `${firstName} ${lastName}`;
|
|
const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@company.com`;
|
|
|
|
data.push({
|
|
id: i,
|
|
name: name,
|
|
email: email,
|
|
department: departments[Math.floor(Math.random() * departments.length)],
|
|
role: roles[Math.floor(Math.random() * roles.length)],
|
|
status: statuses[Math.floor(Math.random() * statuses.length)],
|
|
lastLogin: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toLocaleDateString(),
|
|
details: {
|
|
phone: `+1 (555) ${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 9000) + 1000}`,
|
|
location: ['New York', 'San Francisco', 'London', 'Tokyo', 'Berlin'][Math.floor(Math.random() * 5)],
|
|
joined: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toLocaleDateString()
|
|
}
|
|
});
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// Initialize data
|
|
let allData = generateData(250);
|
|
let filteredData = [...allData];
|
|
let sortColumn = null;
|
|
let sortDirection = 'asc';
|
|
let currentPage = 1;
|
|
let rowsPerPage = 10;
|
|
let selectedRows = new Set();
|
|
let expandedRows = new Set();
|
|
let isMobileView = false;
|
|
|
|
// DOM elements
|
|
const tableBody = document.getElementById('tableBody');
|
|
const cardsContainer = document.getElementById('cardsContainer');
|
|
const searchInput = document.getElementById('searchInput');
|
|
const selectAllCheckbox = document.getElementById('selectAll');
|
|
const rowsPerPageSelect = document.getElementById('rowsPerPage');
|
|
const viewToggleBtn = document.getElementById('viewToggle');
|
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
|
|
|
// Initialize table
|
|
function init() {
|
|
setupEventListeners();
|
|
renderTable();
|
|
updatePaginationInfo();
|
|
checkMobileView();
|
|
}
|
|
|
|
// Setup event listeners
|
|
function setupEventListeners() {
|
|
// Search
|
|
searchInput.addEventListener('input', debounce(handleSearch, 300));
|
|
|
|
// Sorting
|
|
document.querySelectorAll('.sortable').forEach(th => {
|
|
th.addEventListener('click', (e) => {
|
|
if (!e.target.classList.contains('resize-handle')) {
|
|
handleSort(th.dataset.column);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Select all
|
|
selectAllCheckbox.addEventListener('change', handleSelectAll);
|
|
|
|
// Pagination
|
|
rowsPerPageSelect.addEventListener('change', handleRowsPerPageChange);
|
|
document.getElementById('firstPage').addEventListener('click', () => goToPage(1));
|
|
document.getElementById('prevPage').addEventListener('click', () => goToPage(currentPage - 1));
|
|
document.getElementById('nextPage').addEventListener('click', () => goToPage(currentPage + 1));
|
|
document.getElementById('lastPage').addEventListener('click', () => goToPage(Math.ceil(filteredData.length / rowsPerPage)));
|
|
|
|
// View toggle
|
|
viewToggleBtn.addEventListener('click', toggleView);
|
|
|
|
// Export
|
|
document.getElementById('exportBtn').addEventListener('click', handleExport);
|
|
|
|
// Column resize
|
|
setupColumnResize();
|
|
|
|
// Window resize
|
|
window.addEventListener('resize', debounce(checkMobileView, 150));
|
|
|
|
// Keyboard navigation
|
|
document.addEventListener('keydown', handleKeyboardNavigation);
|
|
}
|
|
|
|
// Render table
|
|
function renderTable() {
|
|
const start = (currentPage - 1) * rowsPerPage;
|
|
const end = start + rowsPerPage;
|
|
const pageData = filteredData.slice(start, end);
|
|
|
|
tableBody.innerHTML = '';
|
|
|
|
if (pageData.length === 0) {
|
|
tableBody.innerHTML = `
|
|
<tr>
|
|
<td colspan="9" class="no-data">
|
|
<div class="no-data-icon">📊</div>
|
|
<div>No data found</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
pageData.forEach(row => {
|
|
const tr = document.createElement('tr');
|
|
tr.dataset.id = row.id;
|
|
if (selectedRows.has(row.id)) {
|
|
tr.classList.add('selected');
|
|
}
|
|
|
|
tr.innerHTML = `
|
|
<td class="checkbox-cell">
|
|
<input type="checkbox" class="checkbox row-checkbox" data-id="${row.id}" ${selectedRows.has(row.id) ? 'checked' : ''}>
|
|
</td>
|
|
<td class="expand-cell">
|
|
<button class="expand-btn" aria-label="Expand row" data-id="${row.id}">
|
|
<svg class="expand-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="9 18 15 12 9 6"></polyline>
|
|
</svg>
|
|
</button>
|
|
</td>
|
|
<td>${row.id}</td>
|
|
<td>${row.name}</td>
|
|
<td>${row.email}</td>
|
|
<td>${row.department}</td>
|
|
<td>${row.role}</td>
|
|
<td><span class="status-badge status-${row.status.toLowerCase()}">${row.status}</span></td>
|
|
<td>${row.lastLogin}</td>
|
|
`;
|
|
|
|
tableBody.appendChild(tr);
|
|
|
|
// Add expanded content if row is expanded
|
|
if (expandedRows.has(row.id)) {
|
|
const expandedTr = document.createElement('tr');
|
|
expandedTr.classList.add('expanded');
|
|
expandedTr.innerHTML = `
|
|
<td colspan="9" class="expanded-content">
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
<div>
|
|
<strong>Phone:</strong> ${row.details.phone}
|
|
</div>
|
|
<div>
|
|
<strong>Location:</strong> ${row.details.location}
|
|
</div>
|
|
<div>
|
|
<strong>Joined:</strong> ${row.details.joined}
|
|
</div>
|
|
<div>
|
|
<strong>Employee ID:</strong> EMP${String(row.id).padStart(5, '0')}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
`;
|
|
tableBody.appendChild(expandedTr);
|
|
tr.classList.add('expanded');
|
|
}
|
|
});
|
|
|
|
// Setup row event listeners
|
|
setupRowEventListeners();
|
|
|
|
// Render cards view
|
|
renderCards(pageData);
|
|
}
|
|
|
|
// Render cards view
|
|
function renderCards(pageData) {
|
|
cardsContainer.innerHTML = '';
|
|
|
|
if (pageData.length === 0) {
|
|
cardsContainer.innerHTML = `
|
|
<div class="no-data">
|
|
<div class="no-data-icon">📊</div>
|
|
<div>No data found</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
pageData.forEach(row => {
|
|
const card = document.createElement('div');
|
|
card.className = 'card';
|
|
card.dataset.id = row.id;
|
|
if (selectedRows.has(row.id)) {
|
|
card.classList.add('selected');
|
|
}
|
|
|
|
card.innerHTML = `
|
|
<div class="card-header">
|
|
<div class="card-checkbox">
|
|
<input type="checkbox" class="checkbox row-checkbox" data-id="${row.id}" ${selectedRows.has(row.id) ? 'checked' : ''}>
|
|
<strong>${row.name}</strong>
|
|
</div>
|
|
<span class="status-badge status-${row.status.toLowerCase()}">${row.status}</span>
|
|
</div>
|
|
<div class="card-field">
|
|
<span class="card-field-label">ID</span>
|
|
<span class="card-field-value">${row.id}</span>
|
|
</div>
|
|
<div class="card-field">
|
|
<span class="card-field-label">Email</span>
|
|
<span class="card-field-value">${row.email}</span>
|
|
</div>
|
|
<div class="card-field">
|
|
<span class="card-field-label">Department</span>
|
|
<span class="card-field-value">${row.department}</span>
|
|
</div>
|
|
<div class="card-field">
|
|
<span class="card-field-label">Role</span>
|
|
<span class="card-field-value">${row.role}</span>
|
|
</div>
|
|
<div class="card-field">
|
|
<span class="card-field-label">Last Login</span>
|
|
<span class="card-field-value">${row.lastLogin}</span>
|
|
</div>
|
|
`;
|
|
|
|
cardsContainer.appendChild(card);
|
|
});
|
|
|
|
// Setup card event listeners
|
|
setupCardEventListeners();
|
|
}
|
|
|
|
// Setup row event listeners
|
|
function setupRowEventListeners() {
|
|
// Row checkboxes
|
|
document.querySelectorAll('.row-checkbox').forEach(checkbox => {
|
|
checkbox.addEventListener('change', (e) => {
|
|
handleRowSelection(parseInt(e.target.dataset.id), e.target.checked, e);
|
|
});
|
|
});
|
|
|
|
// Expand buttons
|
|
document.querySelectorAll('.expand-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
handleRowExpand(parseInt(e.currentTarget.dataset.id));
|
|
});
|
|
});
|
|
|
|
// Row click for selection
|
|
document.querySelectorAll('tbody tr').forEach(tr => {
|
|
tr.addEventListener('click', (e) => {
|
|
if (!e.target.classList.contains('checkbox') &&
|
|
!e.target.classList.contains('expand-btn') &&
|
|
!e.target.closest('.expand-btn')) {
|
|
const checkbox = tr.querySelector('.row-checkbox');
|
|
if (checkbox) {
|
|
checkbox.checked = !checkbox.checked;
|
|
handleRowSelection(parseInt(checkbox.dataset.id), checkbox.checked, e);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Setup card event listeners
|
|
function setupCardEventListeners() {
|
|
document.querySelectorAll('.cards-container .row-checkbox').forEach(checkbox => {
|
|
checkbox.addEventListener('change', (e) => {
|
|
handleRowSelection(parseInt(e.target.dataset.id), e.target.checked, e);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle search
|
|
function handleSearch() {
|
|
const searchTerm = searchInput.value.toLowerCase();
|
|
|
|
if (!searchTerm) {
|
|
filteredData = [...allData];
|
|
} else {
|
|
filteredData = allData.filter(row => {
|
|
return Object.values(row).some(value => {
|
|
if (typeof value === 'object') {
|
|
return Object.values(value).some(v =>
|
|
String(v).toLowerCase().includes(searchTerm)
|
|
);
|
|
}
|
|
return String(value).toLowerCase().includes(searchTerm);
|
|
});
|
|
});
|
|
}
|
|
|
|
currentPage = 1;
|
|
renderTable();
|
|
updatePaginationInfo();
|
|
}
|
|
|
|
// Handle sort
|
|
function handleSort(column) {
|
|
showLoading();
|
|
|
|
// Update sort direction
|
|
if (sortColumn === column) {
|
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortColumn = column;
|
|
sortDirection = 'asc';
|
|
}
|
|
|
|
// Update UI
|
|
document.querySelectorAll('.sortable').forEach(th => {
|
|
th.classList.remove('sorted-asc', 'sorted-desc');
|
|
th.setAttribute('aria-sort', 'none');
|
|
});
|
|
|
|
const currentTh = document.querySelector(`[data-column="${column}"]`);
|
|
currentTh.classList.add(`sorted-${sortDirection}`);
|
|
currentTh.setAttribute('aria-sort', sortDirection === 'asc' ? 'ascending' : 'descending');
|
|
|
|
// Sort data
|
|
filteredData.sort((a, b) => {
|
|
let aVal = a[column];
|
|
let bVal = b[column];
|
|
|
|
// Handle different data types
|
|
if (typeof aVal === 'number') {
|
|
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
|
} else if (column === 'lastLogin') {
|
|
aVal = new Date(aVal);
|
|
bVal = new Date(bVal);
|
|
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
|
} else {
|
|
aVal = String(aVal).toLowerCase();
|
|
bVal = String(bVal).toLowerCase();
|
|
if (sortDirection === 'asc') {
|
|
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
} else {
|
|
return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
|
|
}
|
|
}
|
|
});
|
|
|
|
setTimeout(() => {
|
|
renderTable();
|
|
hideLoading();
|
|
}, 300);
|
|
}
|
|
|
|
// Handle row selection
|
|
function handleRowSelection(id, checked, event) {
|
|
if (event.shiftKey && lastSelectedId !== null) {
|
|
// Range selection
|
|
const start = Math.min(id, lastSelectedId);
|
|
const end = Math.max(id, lastSelectedId);
|
|
|
|
filteredData.forEach(row => {
|
|
if (row.id >= start && row.id <= end) {
|
|
if (checked) {
|
|
selectedRows.add(row.id);
|
|
} else {
|
|
selectedRows.delete(row.id);
|
|
}
|
|
}
|
|
});
|
|
|
|
renderTable();
|
|
} else {
|
|
// Single selection
|
|
if (checked) {
|
|
selectedRows.add(id);
|
|
} else {
|
|
selectedRows.delete(id);
|
|
}
|
|
|
|
// Update UI
|
|
const tr = document.querySelector(`tr[data-id="${id}"]`);
|
|
const card = document.querySelector(`.card[data-id="${id}"]`);
|
|
|
|
if (tr) tr.classList.toggle('selected', checked);
|
|
if (card) card.classList.toggle('selected', checked);
|
|
}
|
|
|
|
lastSelectedId = id;
|
|
updateSelectionInfo();
|
|
updateSelectAllCheckbox();
|
|
}
|
|
|
|
let lastSelectedId = null;
|
|
|
|
// Handle select all
|
|
function handleSelectAll() {
|
|
const checked = selectAllCheckbox.checked;
|
|
const start = (currentPage - 1) * rowsPerPage;
|
|
const end = start + rowsPerPage;
|
|
const pageData = filteredData.slice(start, end);
|
|
|
|
pageData.forEach(row => {
|
|
if (checked) {
|
|
selectedRows.add(row.id);
|
|
} else {
|
|
selectedRows.delete(row.id);
|
|
}
|
|
});
|
|
|
|
renderTable();
|
|
updateSelectionInfo();
|
|
}
|
|
|
|
// Update select all checkbox
|
|
function updateSelectAllCheckbox() {
|
|
const start = (currentPage - 1) * rowsPerPage;
|
|
const end = start + rowsPerPage;
|
|
const pageData = filteredData.slice(start, end);
|
|
const pageIds = pageData.map(row => row.id);
|
|
const selectedPageIds = pageIds.filter(id => selectedRows.has(id));
|
|
|
|
selectAllCheckbox.checked = pageIds.length > 0 && selectedPageIds.length === pageIds.length;
|
|
selectAllCheckbox.indeterminate = selectedPageIds.length > 0 && selectedPageIds.length < pageIds.length;
|
|
}
|
|
|
|
// Handle row expand
|
|
function handleRowExpand(id) {
|
|
if (expandedRows.has(id)) {
|
|
expandedRows.delete(id);
|
|
} else {
|
|
expandedRows.add(id);
|
|
}
|
|
renderTable();
|
|
}
|
|
|
|
// Handle rows per page change
|
|
function handleRowsPerPageChange() {
|
|
rowsPerPage = parseInt(rowsPerPageSelect.value);
|
|
currentPage = 1;
|
|
renderTable();
|
|
updatePaginationInfo();
|
|
}
|
|
|
|
// Go to page
|
|
function goToPage(page) {
|
|
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
|
|
if (page < 1 || page > totalPages) return;
|
|
|
|
currentPage = page;
|
|
renderTable();
|
|
updatePaginationInfo();
|
|
}
|
|
|
|
// Update pagination info
|
|
function updatePaginationInfo() {
|
|
const totalPages = Math.ceil(filteredData.length / rowsPerPage);
|
|
const start = Math.min((currentPage - 1) * rowsPerPage + 1, filteredData.length);
|
|
const end = Math.min(currentPage * rowsPerPage, filteredData.length);
|
|
|
|
document.getElementById('startRecord').textContent = start;
|
|
document.getElementById('endRecord').textContent = end;
|
|
document.getElementById('totalRecords').textContent = filteredData.length;
|
|
|
|
// Update pagination buttons
|
|
document.getElementById('firstPage').disabled = currentPage === 1;
|
|
document.getElementById('prevPage').disabled = currentPage === 1;
|
|
document.getElementById('nextPage').disabled = currentPage === totalPages;
|
|
document.getElementById('lastPage').disabled = currentPage === totalPages;
|
|
|
|
// Update page numbers
|
|
const pageNumbers = document.getElementById('pageNumbers');
|
|
pageNumbers.innerHTML = '';
|
|
|
|
const maxButtons = 5;
|
|
let startPage = Math.max(1, currentPage - Math.floor(maxButtons / 2));
|
|
let endPage = Math.min(totalPages, startPage + maxButtons - 1);
|
|
|
|
if (endPage - startPage < maxButtons - 1) {
|
|
startPage = Math.max(1, endPage - maxButtons + 1);
|
|
}
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'pagination-btn';
|
|
btn.textContent = i;
|
|
if (i === currentPage) {
|
|
btn.classList.add('active');
|
|
}
|
|
btn.addEventListener('click', () => goToPage(i));
|
|
pageNumbers.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
// Update selection info
|
|
function updateSelectionInfo() {
|
|
const selectedInfo = document.getElementById('selectedInfo');
|
|
const selectedCount = document.getElementById('selectedCount');
|
|
|
|
if (selectedRows.size > 0) {
|
|
selectedInfo.style.display = 'inline';
|
|
selectedCount.textContent = selectedRows.size;
|
|
} else {
|
|
selectedInfo.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Toggle view
|
|
function toggleView() {
|
|
isMobileView = !isMobileView;
|
|
document.querySelector('.table-container').classList.toggle('mobile-cards', isMobileView);
|
|
|
|
// Update icon
|
|
viewToggleBtn.innerHTML = isMobileView ? `
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="8" y1="6" x2="21" y2="6"></line>
|
|
<line x1="8" y1="12" x2="21" y2="12"></line>
|
|
<line x1="8" y1="18" x2="21" y2="18"></line>
|
|
<line x1="3" y1="6" x2="3.01" y2="6"></line>
|
|
<line x1="3" y1="12" x2="3.01" y2="12"></line>
|
|
<line x1="3" y1="18" x2="3.01" y2="18"></line>
|
|
</svg>
|
|
` : `
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="3" width="7" height="7"></rect>
|
|
<rect x="14" y="3" width="7" height="7"></rect>
|
|
<rect x="14" y="14" width="7" height="7"></rect>
|
|
<rect x="3" y="14" width="7" height="7"></rect>
|
|
</svg>
|
|
`;
|
|
}
|
|
|
|
// Check mobile view
|
|
function checkMobileView() {
|
|
if (window.innerWidth <= 768 && !isMobileView) {
|
|
toggleView();
|
|
} else if (window.innerWidth > 768 && isMobileView) {
|
|
toggleView();
|
|
}
|
|
}
|
|
|
|
// Handle export
|
|
function handleExport() {
|
|
const dataToExport = selectedRows.size > 0
|
|
? filteredData.filter(row => selectedRows.has(row.id))
|
|
: filteredData;
|
|
|
|
// Convert to CSV
|
|
const headers = ['ID', 'Name', 'Email', 'Department', 'Role', 'Status', 'Last Login'];
|
|
const csv = [
|
|
headers.join(','),
|
|
...dataToExport.map(row =>
|
|
[row.id, row.name, row.email, row.department, row.role, row.status, row.lastLogin].join(',')
|
|
)
|
|
].join('\n');
|
|
|
|
// Download
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `table-export-${new Date().toISOString().split('T')[0]}.csv`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Setup column resize
|
|
function setupColumnResize() {
|
|
let isResizing = false;
|
|
let currentColumn = null;
|
|
let startX = 0;
|
|
let startWidth = 0;
|
|
|
|
document.querySelectorAll('.resize-handle').forEach(handle => {
|
|
handle.addEventListener('mousedown', (e) => {
|
|
isResizing = true;
|
|
currentColumn = handle.dataset.column;
|
|
startX = e.pageX;
|
|
const th = handle.parentElement;
|
|
startWidth = th.offsetWidth;
|
|
handle.classList.add('resizing');
|
|
document.body.style.cursor = 'col-resize';
|
|
e.preventDefault();
|
|
});
|
|
});
|
|
|
|
document.addEventListener('mousemove', (e) => {
|
|
if (!isResizing) return;
|
|
|
|
const diff = e.pageX - startX;
|
|
const newWidth = Math.max(100, startWidth + diff);
|
|
const th = document.querySelector(`[data-column="${currentColumn}"]`);
|
|
th.style.width = newWidth + 'px';
|
|
});
|
|
|
|
document.addEventListener('mouseup', () => {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
document.querySelector('.resize-handle.resizing')?.classList.remove('resizing');
|
|
document.body.style.cursor = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle keyboard navigation
|
|
function handleKeyboardNavigation(e) {
|
|
// Ctrl/Cmd + A to select all
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
|
e.preventDefault();
|
|
selectAllCheckbox.checked = true;
|
|
handleSelectAll();
|
|
}
|
|
|
|
// Arrow keys for navigation
|
|
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
|
|
const focused = document.activeElement;
|
|
const tr = focused.closest('tr');
|
|
if (tr) {
|
|
e.preventDefault();
|
|
const next = e.key === 'ArrowUp' ? tr.previousElementSibling : tr.nextElementSibling;
|
|
if (next && next.querySelector('.row-checkbox')) {
|
|
next.querySelector('.row-checkbox').focus();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show loading
|
|
function showLoading() {
|
|
loadingOverlay.classList.add('active');
|
|
}
|
|
|
|
// Hide loading
|
|
function hideLoading() {
|
|
loadingOverlay.classList.remove('active');
|
|
}
|
|
|
|
// Debounce function
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Initialize
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html> |