Improve mobile UI/UX.
This commit is contained in:
355
static/js/mobile-tables.js
Normal file
355
static/js/mobile-tables.js
Normal file
@@ -0,0 +1,355 @@
|
||||
// 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
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user