2026-01-08 22:49:48 -05:00
|
|
|
/**
|
|
|
|
|
* Keyboard shortcuts for power users
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
document.addEventListener('keydown', function(e) {
|
2026-01-23 22:04:39 -05:00
|
|
|
// ESC: Close modals, cancel edit mode, blur inputs
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
// Close any open modals first
|
|
|
|
|
const openModals = document.querySelectorAll('.modal-overlay');
|
|
|
|
|
let closedModal = false;
|
|
|
|
|
openModals.forEach(modal => {
|
|
|
|
|
if (modal.style.display !== 'none' && modal.offsetParent !== null) {
|
|
|
|
|
modal.remove();
|
|
|
|
|
document.body.classList.remove('modal-open');
|
|
|
|
|
closedModal = true;
|
2026-01-08 22:49:48 -05:00
|
|
|
}
|
2026-01-23 22:04:39 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Close settings modal if open
|
|
|
|
|
const settingsModal = document.getElementById('settingsModal');
|
|
|
|
|
if (settingsModal && settingsModal.style.display !== 'none') {
|
|
|
|
|
settingsModal.style.display = 'none';
|
|
|
|
|
document.body.classList.remove('modal-open');
|
|
|
|
|
closedModal = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close advanced search modal if open
|
|
|
|
|
const searchModal = document.getElementById('advancedSearchModal');
|
|
|
|
|
if (searchModal && searchModal.style.display !== 'none') {
|
|
|
|
|
searchModal.style.display = 'none';
|
|
|
|
|
document.body.classList.remove('modal-open');
|
|
|
|
|
closedModal = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we closed a modal, stop here
|
|
|
|
|
if (closedModal) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Blur any focused input
|
|
|
|
|
if (e.target.tagName === 'INPUT' ||
|
|
|
|
|
e.target.tagName === 'TEXTAREA' ||
|
|
|
|
|
e.target.isContentEditable) {
|
|
|
|
|
e.target.blur();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel edit mode on ticket pages
|
|
|
|
|
const editButton = document.getElementById('editButton');
|
|
|
|
|
if (editButton && editButton.classList.contains('active')) {
|
|
|
|
|
window.location.reload();
|
2026-01-08 22:49:48 -05:00
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 22:04:39 -05:00
|
|
|
// Skip other shortcuts if user is typing in an input/textarea
|
|
|
|
|
if (e.target.tagName === 'INPUT' ||
|
|
|
|
|
e.target.tagName === 'TEXTAREA' ||
|
|
|
|
|
e.target.isContentEditable) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 22:49:48 -05:00
|
|
|
// Ctrl/Cmd + E: Toggle edit mode (on ticket pages)
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'e') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const editButton = document.getElementById('editButton');
|
|
|
|
|
if (editButton) {
|
|
|
|
|
editButton.click();
|
|
|
|
|
toast.info('Edit mode ' + (editButton.classList.contains('active') ? 'enabled' : 'disabled'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ctrl/Cmd + S: Save ticket (on ticket pages)
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const editButton = document.getElementById('editButton');
|
|
|
|
|
if (editButton && editButton.classList.contains('active')) {
|
|
|
|
|
editButton.click();
|
|
|
|
|
toast.success('Saving ticket...');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ctrl/Cmd + K: Focus search (on dashboard)
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const searchBox = document.querySelector('.search-box');
|
|
|
|
|
if (searchBox) {
|
|
|
|
|
searchBox.focus();
|
|
|
|
|
searchBox.select();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-23 22:04:39 -05:00
|
|
|
// ? : Show keyboard shortcuts help (requires Shift on most keyboards)
|
|
|
|
|
if (e.key === '?') {
|
|
|
|
|
e.preventDefault();
|
2026-01-08 22:49:48 -05:00
|
|
|
showKeyboardHelp();
|
|
|
|
|
}
|
2026-01-30 19:21:36 -05:00
|
|
|
|
|
|
|
|
// J: Move to next row in table (Gmail-style)
|
|
|
|
|
if (e.key === 'j') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
navigateTableRow('next');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// K: Move to previous row in table (Gmail-style)
|
|
|
|
|
if (e.key === 'k') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
navigateTableRow('prev');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enter: Open selected ticket
|
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
const selectedRow = document.querySelector('tbody tr.keyboard-selected');
|
|
|
|
|
if (selectedRow) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const ticketLink = selectedRow.querySelector('a[href*="/ticket/"]');
|
|
|
|
|
if (ticketLink) {
|
|
|
|
|
window.location.href = ticketLink.href;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// N: Create new ticket (on dashboard)
|
|
|
|
|
if (e.key === 'n') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const newTicketBtn = document.querySelector('a[href*="/create"]');
|
|
|
|
|
if (newTicketBtn) {
|
|
|
|
|
window.location.href = newTicketBtn.href;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// C: Focus comment textarea (on ticket page)
|
|
|
|
|
if (e.key === 'c') {
|
|
|
|
|
const commentBox = document.getElementById('newComment');
|
|
|
|
|
if (commentBox) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
commentBox.focus();
|
|
|
|
|
commentBox.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// G then D: Go to Dashboard (vim-style)
|
|
|
|
|
if (e.key === 'g') {
|
|
|
|
|
window._pendingG = true;
|
|
|
|
|
setTimeout(() => { window._pendingG = false; }, 1000);
|
|
|
|
|
}
|
|
|
|
|
if (e.key === 'd' && window._pendingG) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
window._pendingG = false;
|
|
|
|
|
window.location.href = '/';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1-4: Quick status change on ticket page
|
|
|
|
|
if (['1', '2', '3', '4'].includes(e.key)) {
|
|
|
|
|
const statusSelect = document.getElementById('statusSelect');
|
|
|
|
|
if (statusSelect && !document.querySelector('.modal-overlay')) {
|
|
|
|
|
const statusMap = { '1': 'Open', '2': 'Pending', '3': 'In Progress', '4': 'Closed' };
|
|
|
|
|
const targetStatus = statusMap[e.key];
|
|
|
|
|
const option = Array.from(statusSelect.options).find(opt => opt.value === targetStatus);
|
|
|
|
|
if (option && !option.disabled) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
statusSelect.value = targetStatus;
|
|
|
|
|
statusSelect.dispatchEvent(new Event('change'));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-08 22:49:48 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-30 19:21:36 -05:00
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
// Remove current selection
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add selection to new row
|
|
|
|
|
const selectedRow = rows[currentSelectedRowIndex];
|
|
|
|
|
if (selectedRow) {
|
|
|
|
|
selectedRow.classList.add('keyboard-selected');
|
|
|
|
|
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 22:49:48 -05:00
|
|
|
function showKeyboardHelp() {
|
2026-01-23 22:04:39 -05:00
|
|
|
// Check if help is already showing
|
|
|
|
|
if (document.getElementById('keyboardHelpModal')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const modal = document.createElement('div');
|
|
|
|
|
modal.id = 'keyboardHelpModal';
|
|
|
|
|
modal.className = 'modal-overlay';
|
|
|
|
|
modal.innerHTML = `
|
2026-01-30 19:21:36 -05:00
|
|
|
<div class="modal-content ascii-frame-outer" style="max-width: 500px;">
|
2026-01-23 22:04:39 -05:00
|
|
|
<div class="ascii-frame">
|
|
|
|
|
<div class="ascii-content">
|
|
|
|
|
<h3 style="margin: 0 0 1rem 0; color: var(--terminal-green);">KEYBOARD SHORTCUTS</h3>
|
|
|
|
|
<div class="modal-body" style="padding: 0;">
|
2026-01-30 19:21:36 -05:00
|
|
|
<h4 style="color: var(--terminal-amber); margin: 0.5rem 0; font-size: 0.9rem;">Navigation</h4>
|
|
|
|
|
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>J</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Next ticket in list</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>K</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Previous ticket in list</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Enter</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Open selected ticket</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem;"><kbd>G</kbd> then <kbd>D</kbd></td><td style="padding: 0.4rem;">Go to Dashboard</td></tr>
|
|
|
|
|
</table>
|
|
|
|
|
<h4 style="color: var(--terminal-amber); margin: 0.5rem 0; font-size: 0.9rem;">Actions</h4>
|
|
|
|
|
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>N</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">New ticket</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>C</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Focus comment box</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd + E</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Toggle Edit Mode</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem;"><kbd>Ctrl/Cmd + S</kbd></td><td style="padding: 0.4rem;">Save Changes</td></tr>
|
|
|
|
|
</table>
|
|
|
|
|
<h4 style="color: var(--terminal-amber); margin: 0.5rem 0; font-size: 0.9rem;">Quick Status (Ticket Page)</h4>
|
|
|
|
|
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>1</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set Open</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>2</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set Pending</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>3</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set In Progress</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem;"><kbd>4</kbd></td><td style="padding: 0.4rem;">Set Closed</td></tr>
|
|
|
|
|
</table>
|
|
|
|
|
<h4 style="color: var(--terminal-amber); margin: 0.5rem 0; font-size: 0.9rem;">Other</h4>
|
2026-01-23 22:04:39 -05:00
|
|
|
<table style="width: 100%; border-collapse: collapse;">
|
2026-01-30 19:21:36 -05:00
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd + K</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Focus Search</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>ESC</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Close Modal / Cancel</td></tr>
|
|
|
|
|
<tr><td style="padding: 0.4rem;"><kbd>?</kbd></td><td style="padding: 0.4rem;">Show This Help</td></tr>
|
2026-01-23 22:04:39 -05:00
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="modal-footer" style="margin-top: 1rem;">
|
2026-01-30 13:15:55 -05:00
|
|
|
<button class="btn btn-secondary" data-action="close-shortcuts-modal">Close</button>
|
2026-01-23 22:04:39 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-08 22:49:48 -05:00
|
|
|
`;
|
2026-01-23 22:04:39 -05:00
|
|
|
document.body.appendChild(modal);
|
2026-01-30 13:15:55 -05:00
|
|
|
|
|
|
|
|
// Add event listener for the close button
|
|
|
|
|
modal.querySelector('[data-action="close-shortcuts-modal"]').addEventListener('click', function() {
|
|
|
|
|
modal.remove();
|
|
|
|
|
});
|
2026-01-08 22:49:48 -05:00
|
|
|
}
|