Files
dashcaddy/status/js/keyboard-shortcuts.js
Sami f61e85d9a7 Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
2026-03-05 02:26:12 -08:00

623 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* DashCaddy Keyboard Shortcuts System
* Provides global keyboard shortcuts for improved navigation
*/
(function() {
'use strict';
// All modal selectors that can be closed with Escape
const MODAL_SELECTORS = [
'#app-selector-modal',
'#app-deploy-modal',
'#weather-modal',
'#token-management-modal',
'#service-edit-modal',
'#notifications-modal',
'#backup-modal',
'#stats-modal',
'#arr-setup-modal',
'#add-service-modal',
'#error-log-modal',
'#logs-modal',
'#dns-template-modal'
];
// Quick search state
let quickSearchModal = null;
let quickSearchInput = null;
let quickSearchResults = null;
/**
* Initialize the keyboard shortcuts system
*/
function init() {
try {
// Create quick search modal
createQuickSearchModal();
// Add global keyboard listener
document.addEventListener('keydown', handleKeyDown);
console.log('[Keyboard Shortcuts] Initialized');
console.log('[Keyboard Shortcuts] Press Ctrl+K to open quick search');
console.log('[Keyboard Shortcuts] Press Escape to close modals');
} catch (e) {
console.warn('[Keyboard Shortcuts] Failed to initialize:', e.message);
}
}
/**
* Create the quick search modal
*/
function createQuickSearchModal() {
quickSearchModal = document.createElement('div');
quickSearchModal.id = 'quick-search-modal';
quickSearchModal.className = 'quick-search-modal';
quickSearchModal.innerHTML = `
<div class="quick-search-content">
<div class="quick-search-input-wrapper">
<span class="quick-search-icon">🔍</span>
<input type="text" id="quick-search-input" placeholder="Search services, apps, or actions..." autocomplete="off">
<span class="quick-search-shortcut">ESC</span>
</div>
<div id="quick-search-results" class="quick-search-results"></div>
<div class="quick-search-footer">
<span><kbd>↑↓</kbd> Navigate</span>
<span><kbd>Enter</kbd> Select</span>
<span><kbd>Esc</kbd> Close</span>
</div>
</div>
`;
// Add styles
const style = document.createElement('style');
style.textContent = `
.quick-search-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 100000;
justify-content: center;
align-items: flex-start;
padding-top: 15vh;
backdrop-filter: blur(4px);
}
.quick-search-modal.show {
display: flex;
}
.quick-search-content {
background: var(--card-base, #1a1a2e);
border: 1px solid var(--border, #333);
border-radius: 12px;
width: 90%;
max-width: 600px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
.quick-search-input-wrapper {
display: flex;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--border, #333);
gap: 12px;
}
.quick-search-icon {
font-size: 20px;
opacity: 0.6;
}
#quick-search-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--fg, #fff);
font-size: 18px;
font-family: inherit;
}
#quick-search-input::placeholder {
color: var(--muted, #666);
}
.quick-search-shortcut {
background: var(--card-hover, #2a2a4e);
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
color: var(--muted, #666);
font-family: monospace;
}
.quick-search-results {
max-height: 400px;
overflow-y: auto;
}
.quick-search-category {
padding: 8px 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--muted, #666);
background: var(--card-hover, #2a2a4e);
letter-spacing: 0.5px;
}
.quick-search-item {
display: flex;
align-items: center;
padding: 12px 20px;
cursor: pointer;
transition: background 0.15s;
gap: 12px;
}
.quick-search-item:hover,
.quick-search-item.selected {
background: var(--accent, #3498db);
}
.quick-search-item-icon {
font-size: 24px;
width: 32px;
text-align: center;
}
.quick-search-item-content {
flex: 1;
}
.quick-search-item-title {
font-weight: 500;
color: var(--fg, #fff);
}
.quick-search-item-description {
font-size: 12px;
color: var(--muted, #aaa);
margin-top: 2px;
}
.quick-search-item-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
background: var(--card-hover, #2a2a4e);
color: var(--muted, #888);
}
.quick-search-empty {
padding: 40px 20px;
text-align: center;
color: var(--muted, #666);
}
.quick-search-footer {
display: flex;
justify-content: center;
gap: 24px;
padding: 12px 20px;
border-top: 1px solid var(--border, #333);
background: var(--card-hover, #2a2a4e);
font-size: 12px;
color: var(--muted, #666);
}
.quick-search-footer kbd {
background: var(--card-base, #1a1a2e);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
margin-right: 4px;
}
`;
document.head.appendChild(style);
document.body.appendChild(quickSearchModal);
quickSearchInput = document.getElementById('quick-search-input');
quickSearchResults = document.getElementById('quick-search-results');
// Input handling
quickSearchInput.addEventListener('input', handleSearchInput);
quickSearchInput.addEventListener('keydown', handleSearchKeyDown);
// Close on backdrop click
quickSearchModal.addEventListener('click', (e) => {
if (e.target === quickSearchModal) {
closeQuickSearch();
}
});
}
/**
* Handle global keydown events
*/
function handleKeyDown(e) {
try {
// Ctrl+K or Cmd+K to open quick search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
openQuickSearch();
return;
}
// Escape to close modals
if (e.key === 'Escape') {
// First check if quick search is open
if (quickSearchModal && quickSearchModal.classList.contains('show')) {
closeQuickSearch();
return;
}
// Then close any other open modal
closeTopModal();
}
} catch (e2) {
console.warn('[Keyboard Shortcuts] Error handling keydown:', e2.message);
}
}
/**
* Open quick search modal
*/
function openQuickSearch() {
try {
quickSearchModal.classList.add('show');
quickSearchInput.value = '';
quickSearchInput.focus();
showDefaultResults();
} catch (e) {
console.warn('[Keyboard Shortcuts] Error opening quick search:', e.message);
}
}
/**
* Close quick search modal
*/
function closeQuickSearch() {
try {
quickSearchModal.classList.remove('show');
quickSearchInput.value = '';
quickSearchResults.innerHTML = '';
} catch (e) {
console.warn('[Keyboard Shortcuts] Error closing quick search:', e.message);
}
}
/**
* Close the topmost open modal
*/
function closeTopModal() {
for (const selector of MODAL_SELECTORS) {
const modal = document.querySelector(selector);
if (modal && (modal.classList.contains('show') || modal.style.display === 'flex')) {
modal.classList.remove('show');
modal.style.display = 'none';
return true;
}
}
return false;
}
/**
* Show default/suggested results
*/
function showDefaultResults() {
const html = `
<div class="quick-search-category">Quick Actions</div>
<div class="quick-search-item" data-action="refresh">
<span class="quick-search-item-icon">🔄</span>
<div class="quick-search-item-content">
<div class="quick-search-item-title">Refresh Dashboard</div>
<div class="quick-search-item-description">Refresh all service statuses</div>
</div>
</div>
<div class="quick-search-item" data-action="reload-caddy">
<span class="quick-search-item-icon">⚡</span>
<div class="quick-search-item-content">
<div class="quick-search-item-title">Reload Caddy</div>
<div class="quick-search-item-description">Reload Caddy configuration</div>
</div>
</div>
<div class="quick-search-item" data-action="add-service">
<span class="quick-search-item-icon"></span>
<div class="quick-search-item-content">
<div class="quick-search-item-title">Add Service</div>
<div class="quick-search-item-description">Open service configuration modal</div>
</div>
</div>
<div class="quick-search-item" data-action="app-selector">
<span class="quick-search-item-icon">📱</span>
<div class="quick-search-item-content">
<div class="quick-search-item-title">App Selector</div>
<div class="quick-search-item-description">Deploy new applications</div>
</div>
</div>
<div class="quick-search-category">Services</div>
${getServiceItems()}
`;
quickSearchResults.innerHTML = html;
attachResultListeners();
}
/**
* Get service items from the dashboard
*/
function getServiceItems() {
const cards = document.querySelectorAll('.card[data-app], #cards .card');
let html = '';
cards.forEach(card => {
const name = card.querySelector('.name')?.textContent || 'Unknown';
const status = card.dataset.status || 'unknown';
const app = card.dataset.app || '';
if (name && name !== '--') {
html += `
<div class="quick-search-item" data-action="open-service" data-service="${app}">
<span class="quick-search-item-icon">${status === 'on' ? '🟢' : '🔴'}</span>
<div class="quick-search-item-content">
<div class="quick-search-item-title">${name}</div>
<div class="quick-search-item-description">Click to open service</div>
</div>
<span class="quick-search-item-badge">${status.toUpperCase()}</span>
</div>
`;
}
});
return html || '<div class="quick-search-empty">No services found</div>';
}
/**
* Handle search input
*/
function handleSearchInput(e) {
try {
const query = e.target.value.toLowerCase().trim();
if (!query) {
showDefaultResults();
return;
}
// Search through services and actions
const results = searchAll(query);
displaySearchResults(results);
} catch (e2) {
console.warn('[Keyboard Shortcuts] Error handling search input:', e2.message);
}
}
/**
* Search all items
*/
function searchAll(query) {
const results = {
actions: [],
services: []
};
// Search actions
const actions = [
{ id: 'refresh', title: 'Refresh Dashboard', icon: '🔄', keywords: 'refresh reload update status' },
{ id: 'reload-caddy', title: 'Reload Caddy', icon: '⚡', keywords: 'reload caddy proxy config' },
{ id: 'add-service', title: 'Add Service', icon: '', keywords: 'add new service create' },
{ id: 'app-selector', title: 'App Selector', icon: '📱', keywords: 'app deploy install docker container' },
{ id: 'backup', title: 'Backup & Restore', icon: '💾', keywords: 'backup restore export import' },
{ id: 'stats', title: 'Container Stats', icon: '📊', keywords: 'stats resources cpu memory' },
{ id: 'logs', title: 'View Logs', icon: '📋', keywords: 'logs error debug' },
{ id: 'tokens', title: 'Manage Tokens', icon: '🔑', keywords: 'tokens api keys credentials' },
{ id: 'notifications', title: 'Notifications', icon: '🔔', keywords: 'alerts notifications discord telegram' },
{ id: 'theme', title: 'Change Theme', icon: '🎨', keywords: 'theme dark light appearance' },
{ id: 'tour', title: 'Help Tour', icon: '🎓', keywords: 'help tour guide onboarding' }
];
actions.forEach(action => {
if (action.title.toLowerCase().includes(query) || action.keywords.includes(query)) {
results.actions.push(action);
}
});
// Search services
const cards = document.querySelectorAll('.card[data-app], #cards .card');
cards.forEach(card => {
const name = card.querySelector('.name')?.textContent || '';
const app = card.dataset.app || '';
const status = card.dataset.status || 'unknown';
if (name.toLowerCase().includes(query) || app.toLowerCase().includes(query)) {
results.services.push({
id: app,
title: name,
status: status,
icon: status === 'on' ? '🟢' : '🔴'
});
}
});
return results;
}
/**
* Display search results
*/
function displaySearchResults(results) {
let html = '';
if (results.actions.length > 0) {
html += '<div class="quick-search-category">Actions</div>';
results.actions.forEach(action => {
html += `
<div class="quick-search-item" data-action="${action.id}">
<span class="quick-search-item-icon">${action.icon}</span>
<div class="quick-search-item-content">
<div class="quick-search-item-title">${action.title}</div>
</div>
</div>
`;
});
}
if (results.services.length > 0) {
html += '<div class="quick-search-category">Services</div>';
results.services.forEach(service => {
html += `
<div class="quick-search-item" data-action="open-service" data-service="${service.id}">
<span class="quick-search-item-icon">${service.icon}</span>
<div class="quick-search-item-content">
<div class="quick-search-item-title">${service.title}</div>
</div>
<span class="quick-search-item-badge">${service.status.toUpperCase()}</span>
</div>
`;
});
}
if (!html) {
html = '<div class="quick-search-empty">No results found</div>';
}
quickSearchResults.innerHTML = html;
attachResultListeners();
}
/**
* Attach click listeners to result items
*/
function attachResultListeners() {
const items = quickSearchResults.querySelectorAll('.quick-search-item');
items.forEach((item, index) => {
item.addEventListener('click', () => executeAction(item));
// Select first item by default
if (index === 0) {
item.classList.add('selected');
}
});
}
/**
* Handle keyboard navigation in search
*/
function handleSearchKeyDown(e) {
try {
const items = quickSearchResults.querySelectorAll('.quick-search-item');
const selected = quickSearchResults.querySelector('.quick-search-item.selected');
const selectedIndex = Array.from(items).indexOf(selected);
if (e.key === 'ArrowDown') {
e.preventDefault();
if (selected) selected.classList.remove('selected');
const nextIndex = (selectedIndex + 1) % items.length;
items[nextIndex]?.classList.add('selected');
items[nextIndex]?.scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (selected) selected.classList.remove('selected');
const prevIndex = selectedIndex <= 0 ? items.length - 1 : selectedIndex - 1;
items[prevIndex]?.classList.add('selected');
items[prevIndex]?.scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Enter') {
e.preventDefault();
if (selected) {
executeAction(selected);
}
}
} catch (e2) {
console.warn('[Keyboard Shortcuts] Error handling search navigation:', e2.message);
}
}
/**
* Execute an action from quick search
*/
function executeAction(item) {
try {
const action = item.dataset.action;
const service = item.dataset.service;
closeQuickSearch();
switch (action) {
case 'refresh':
document.getElementById('refresh')?.click();
break;
case 'reload-caddy':
document.getElementById('reload-caddy-top')?.click();
break;
case 'add-service':
document.getElementById('add-service')?.click();
break;
case 'app-selector':
document.getElementById('add-service-btn')?.click();
break;
case 'backup':
document.getElementById('backup-restore-btn')?.click();
break;
case 'stats':
document.getElementById('container-stats-btn')?.click();
break;
case 'logs':
document.getElementById('view-error-logs')?.click();
break;
case 'tokens':
document.getElementById('manage-tokens')?.click();
break;
case 'notifications':
document.getElementById('manage-notifications')?.click();
break;
case 'theme':
document.getElementById('theme')?.click();
break;
case 'tour':
document.getElementById('restart-tour-btn')?.click();
break;
case 'open-service':
if (service) {
const openBtn = document.querySelector(`[data-app="${service}"] [id$="-open"], [data-app="${service}"] button:not(.restart-btn):not(.logs-btn):not(.settings-btn)`);
if (openBtn) {
openBtn.click();
} else {
// Try to find the card and click it
const card = document.querySelector(`[data-app="${service}"]`);
if (card) card.click();
}
}
break;
default:
console.log('[Keyboard Shortcuts] Unknown action:', action);
}
} catch (e) {
console.warn('[Keyboard Shortcuts] Error executing action:', e.message);
}
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Expose to global scope
window.DashCaddyKeyboardShortcuts = {
openQuickSearch,
closeQuickSearch
};
})();