infinite-agents-public/legacy/src_enhanced/ui_enhanced_6.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>