Files
tinker_tickets/assets/js/keyboard-shortcuts.js
T
jared 2e450dc01d Apply web_template gap analysis improvements (P1-P3)
P1-A: Fix CSP - add fonts.googleapis.com to style-src, fonts.gstatic.com to font-src
P1-B: CSRF token rotation - add rotateToken() to CsrfMiddleware; bootstrap.php rotates
      after successful validation and stores in $GLOBALS['_new_csrf_token']; add
      apiRespond() helper to append token to responses; lt.api interceptor in
      layout_footer.php auto-updates window.CSRF_TOKEN from responses
P1-C: Styled 403/404 error views with TDS layout instead of raw text; index.php now
      uses requireAdmin() helper eliminating 7 duplicated guard blocks (P3-D)
P2-A: Remove duplicate JS-generated keyboard help modal from keyboard-shortcuts.js;
      '?' key now routes to static #lt-keys-help modal in footer
P2-B: Asset versioning driven by config ASSET_VERSION key; base.css and base.js get
      ?v= cache-busting in layout_header.php
P2-C: Add data-theme="dark" to <html> tag to prevent FOUC on light-mode users
P2-E: Escape status value in dashboard.js hover preview class attribute via lt.escHtml()
P2-F: Replace bespoke showLoadingOverlay() with lt-spinner / lt-loading-text from
      base.css; add .lt-loading-overlay wrapper CSS to dashboard.css
P2-G: Add keyboard-shortcuts.js to all 7 admin views so J/K nav and ? help work
P3-A: APP_NAME, APP_SUBTITLE, APP_VERSION driven from config.php; layout header/footer
      use config values instead of hardcoded strings
P3-G: Replace custom initTableSorting() with lt.sortTable.init() which manages aria-sort

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 17:02:40 -04:00

113 lines
3.9 KiB
JavaScript

/**
* Keyboard shortcuts for power users.
* App-specific shortcuts registered via lt.keys.on() from web_template/base.js.
* ESC, Ctrl+K, and ? are handled by lt.keys.initDefaults().
*/
// Track currently selected row for J/K navigation
let currentSelectedRowIndex = -1;
function navigateTableRow(direction) {
const rows = document.querySelectorAll('tbody tr');
if (rows.length === 0) return;
rows.forEach(row => row.classList.remove('keyboard-selected'));
if (direction === 'next') {
currentSelectedRowIndex = Math.min(currentSelectedRowIndex + 1, rows.length - 1);
} else {
currentSelectedRowIndex = Math.max(currentSelectedRowIndex - 1, 0);
}
const selectedRow = rows[currentSelectedRowIndex];
if (selectedRow) {
selectedRow.classList.add('keyboard-selected');
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
document.addEventListener('DOMContentLoaded', function() {
if (!window.lt) return;
// Ctrl+E: Toggle edit mode (ticket pages)
lt.keys.on('ctrl+e', function() {
const editButton = document.getElementById('editButton');
if (editButton) {
editButton.click();
lt.toast.info('Edit mode ' + (editButton.classList.contains('active') ? 'enabled' : 'disabled'));
}
});
// Ctrl+S: Save ticket (ticket pages)
lt.keys.on('ctrl+s', function() {
const editButton = document.getElementById('editButton');
if (editButton && editButton.classList.contains('active')) {
editButton.click();
lt.toast.success('Saving ticket...');
}
});
// ?: Show keyboard shortcuts help — use the static #lt-keys-help modal in the footer
lt.keys.on('?', function() {
if (window.lt) lt.modal.open('lt-keys-help');
});
// J: Next row
lt.keys.on('j', () => navigateTableRow('next'));
// K: Previous row
lt.keys.on('k', () => navigateTableRow('prev'));
// Enter: Open selected ticket
lt.keys.on('enter', function() {
const selectedRow = document.querySelector('tbody tr.keyboard-selected');
if (selectedRow) {
const ticketLink = selectedRow.querySelector('a[href*="/ticket/"]');
if (ticketLink) window.location.href = ticketLink.href;
}
});
// N: New ticket
lt.keys.on('n', function() {
const newTicketBtn = document.querySelector('a[href*="/create"]');
if (newTicketBtn) window.location.href = newTicketBtn.href;
});
// C: Focus comment box
lt.keys.on('c', function() {
const commentBox = document.getElementById('newComment');
if (commentBox) {
commentBox.focus();
commentBox.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
// G then D: Go to Dashboard (vim-style)
lt.keys.on('g', function() {
window._pendingG = true;
setTimeout(() => { window._pendingG = false; }, 1000);
});
lt.keys.on('d', function() {
if (window._pendingG) {
window._pendingG = false;
window.location.href = '/';
}
});
// 1-4: Quick status change on ticket page
['1', '2', '3', '4'].forEach(key => {
lt.keys.on(key, function() {
const statusSelect = document.getElementById('statusSelect');
if (statusSelect && !document.querySelector('.lt-modal-overlay[aria-hidden="false"]')) {
const statusMap = { '1': 'Open', '2': 'Pending', '3': 'In Progress', '4': 'Closed' };
const targetStatus = statusMap[key];
const option = Array.from(statusSelect.options).find(opt => opt.value === targetStatus);
if (option && !option.disabled) {
statusSelect.value = targetStatus;
statusSelect.dispatchEvent(new Event('change'));
}
}
});
});
});