Files
TimeTrack/static/js/mobile-tables.js
2025-07-13 10:52:20 +02:00

355 lines
12 KiB
JavaScript

// Mobile Table Enhancements for TimeTrack
document.addEventListener('DOMContentLoaded', function() {
// Configuration
const MOBILE_BREAKPOINT = 768;
const CARD_VIEW_BREAKPOINT = 576;
// Initialize all data tables
function initMobileTables() {
const tables = document.querySelectorAll('.data-table, table');
tables.forEach(table => {
// Wrap tables in responsive container
if (!table.closest('.table-responsive')) {
const wrapper = document.createElement('div');
wrapper.className = 'table-responsive';
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
// Check if table is scrollable
checkTableScroll(wrapper);
}
// Add mobile-specific attributes
if (window.innerWidth <= CARD_VIEW_BREAKPOINT) {
convertTableToCards(table);
}
});
}
// Check if table needs horizontal scroll
function checkTableScroll(wrapper) {
const table = wrapper.querySelector('table');
if (table.scrollWidth > wrapper.clientWidth) {
wrapper.classList.add('scrollable');
addScrollIndicator(wrapper);
} else {
wrapper.classList.remove('scrollable');
}
}
// Add visual scroll indicator
function addScrollIndicator(wrapper) {
if (!wrapper.querySelector('.scroll-indicator')) {
const indicator = document.createElement('div');
indicator.className = 'scroll-indicator';
indicator.innerHTML = '<i class="ti ti-arrow-right"></i> Scroll for more';
wrapper.appendChild(indicator);
// Hide indicator when scrolled to end
wrapper.addEventListener('scroll', function() {
const maxScroll = this.scrollWidth - this.clientWidth;
if (this.scrollLeft >= maxScroll - 10) {
indicator.style.opacity = '0';
} else {
indicator.style.opacity = '1';
}
});
}
}
// Convert table to card layout for mobile
function convertTableToCards(table) {
// Skip if already converted or marked to skip
if (table.classList.contains('no-card-view') || table.dataset.mobileCards === 'true') {
return;
}
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
const rows = table.querySelectorAll('tbody tr');
// Create card container
const cardContainer = document.createElement('div');
cardContainer.className = 'mobile-card-view';
cardContainer.setAttribute('role', 'list');
rows.forEach((row, rowIndex) => {
const card = createCardFromRow(row, headers);
cardContainer.appendChild(card);
});
// Insert card view before table
table.parentNode.insertBefore(cardContainer, table);
// Add classes for toggling
table.classList.add('desktop-table-view');
table.dataset.mobileCards = 'true';
}
// Create a card element from table row
function createCardFromRow(row, headers) {
const cells = row.querySelectorAll('td');
const card = document.createElement('div');
card.className = 'table-card';
card.setAttribute('role', 'listitem');
// Check for special data attributes
const primaryField = row.dataset.primaryField || 0;
const secondaryField = row.dataset.secondaryField || 1;
// Create card header with primary info
if (cells[primaryField]) {
const cardHeader = document.createElement('div');
cardHeader.className = 'table-card-header';
cardHeader.innerHTML = cells[primaryField].innerHTML;
card.appendChild(cardHeader);
}
// Create card body with other fields
const cardBody = document.createElement('div');
cardBody.className = 'table-card-body';
cells.forEach((cell, index) => {
// Skip primary field as it's in header
if (index === primaryField) return;
const field = document.createElement('div');
field.className = 'table-card-field';
const label = document.createElement('span');
label.className = 'table-card-label';
label.textContent = headers[index] || '';
const value = document.createElement('span');
value.className = 'table-card-value';
value.innerHTML = cell.innerHTML;
field.appendChild(label);
field.appendChild(value);
cardBody.appendChild(field);
});
card.appendChild(cardBody);
// Copy any data attributes from row
Array.from(row.attributes).forEach(attr => {
if (attr.name.startsWith('data-')) {
card.setAttribute(attr.name, attr.value);
}
});
// Copy click handlers if any
if (row.onclick) {
card.onclick = row.onclick;
card.style.cursor = 'pointer';
}
return card;
}
// Time entry table specific enhancements
function enhanceTimeEntryTable() {
const timeTable = document.querySelector('.time-entries-table');
if (!timeTable) return;
if (window.innerWidth <= MOBILE_BREAKPOINT) {
// Add swipe actions for time entries
addSwipeActions(timeTable);
// Compact view for mobile
timeTable.classList.add('mobile-compact');
}
}
// Add swipe gestures to table rows
function addSwipeActions(table) {
const rows = table.querySelectorAll('tbody tr');
rows.forEach(row => {
let startX = 0;
let currentX = 0;
let isDragging = false;
row.addEventListener('touchstart', handleTouchStart, { passive: true });
row.addEventListener('touchmove', handleTouchMove, { passive: true });
row.addEventListener('touchend', handleTouchEnd);
function handleTouchStart(e) {
startX = e.touches[0].clientX;
isDragging = true;
row.style.transition = 'none';
}
function handleTouchMove(e) {
if (!isDragging) return;
currentX = e.touches[0].clientX;
const diffX = currentX - startX;
// Limit swipe distance
const maxSwipe = 100;
const swipeX = Math.max(-maxSwipe, Math.min(maxSwipe, diffX));
row.style.transform = `translateX(${swipeX}px)`;
// Show action indicators
if (swipeX < -50) {
row.classList.add('swipe-delete');
} else if (swipeX > 50) {
row.classList.add('swipe-edit');
} else {
row.classList.remove('swipe-delete', 'swipe-edit');
}
}
function handleTouchEnd(e) {
if (!isDragging) return;
const diffX = currentX - startX;
row.style.transition = 'transform 0.3s ease';
row.style.transform = '';
// Trigger actions based on swipe distance
if (diffX < -80) {
// Delete action
const deleteBtn = row.querySelector('.delete-btn');
if (deleteBtn) deleteBtn.click();
} else if (diffX > 80) {
// Edit action
const editBtn = row.querySelector('.edit-btn');
if (editBtn) editBtn.click();
}
row.classList.remove('swipe-delete', 'swipe-edit');
isDragging = false;
}
});
}
// Handle responsive table on window resize
function handleResize() {
const tables = document.querySelectorAll('.table-responsive');
tables.forEach(wrapper => {
checkTableScroll(wrapper);
});
// Re-initialize tables if crossing breakpoint
if (window.innerWidth <= CARD_VIEW_BREAKPOINT) {
initMobileTables();
}
// Update time entry table
enhanceTimeEntryTable();
}
// Add CSS for card view
function injectCardStyles() {
if (document.getElementById('mobile-table-styles')) return;
const styles = `
<style id="mobile-table-styles">
.table-card {
background: var(--bg-light);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.table-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.table-card-header {
font-weight: 600;
font-size: 16px;
margin-bottom: 12px;
color: var(--text-primary);
}
.table-card-body {
display: grid;
gap: 8px;
}
.table-card-field {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.table-card-label {
font-size: 13px;
color: var(--text-muted);
flex-shrink: 0;
margin-right: 12px;
}
.table-card-value {
text-align: right;
font-size: 14px;
color: var(--text-secondary);
}
.scroll-indicator {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
background: linear-gradient(to right, transparent, rgba(255,255,255,0.9));
padding: 8px 16px 8px 32px;
pointer-events: none;
transition: opacity 0.3s ease;
font-size: 13px;
color: var(--text-muted);
}
/* Swipe action styles */
.swipe-delete {
background-color: rgba(255, 59, 48, 0.1);
}
.swipe-edit {
background-color: rgba(52, 199, 89, 0.1);
}
/* Mobile compact view */
.mobile-compact td {
padding: 8px 6px;
font-size: 13px;
}
.mobile-compact .btn-sm {
padding: 4px 8px;
font-size: 12px;
}
</style>
`;
document.head.insertAdjacentHTML('beforeend', styles);
}
// Initialize on load
injectCardStyles();
initMobileTables();
enhanceTimeEntryTable();
// Handle window resize
let resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(handleResize, 250);
});
// Export functions for external use
window.MobileTables = {
init: initMobileTables,
convertToCards: convertTableToCards,
enhanceTimeEntry: enhanceTimeEntryTable
};
});