/** * Keyboard shortcuts for Cook CLI web interface * * Global shortcuts (available on all pages): * - / Focus search * - g h Go to home/recipes * - g s Go to shopping list * - g p Go to pantry * - g x Go to preferences * - ? Show keyboard shortcuts help * - Escape Close modals/dropdowns * - t Toggle theme (dark/light) * * Recipe page shortcuts: * - c Start cooking mode * - e Edit recipe * - a Add to shopping list * - p Print recipe * - +/- Increase/decrease scale * - [/] Decrease/increase scale by 0.5 */ (function() { 'use strict'; // Track pending key sequences (for multi-key shortcuts like "g h") let pendingKey = null; let pendingTimeout = null; // Check if user is typing in a text-entry input field function isTyping(event) { const target = event.target; const tagName = target.tagName.toLowerCase(); if (tagName === 'textarea' || tagName === 'select') { return true; } // Only block for text-entry inputs, not checkboxes/radios/etc. if (tagName === 'input') { const type = (target.type || 'text').toLowerCase(); const textTypes = ['text', 'password', 'email', 'number', 'search', 'url', 'tel', 'date', 'time', 'datetime-local']; return textTypes.includes(type); } // Check for contenteditable elements if (target.isContentEditable) { return true; } // Check for CodeMirror editor if (target.closest('.cm-editor')) { return true; } return false; } // Clear pending key sequence function clearPendingKey() { pendingKey = null; if (pendingTimeout) { clearTimeout(pendingTimeout); pendingTimeout = null; } } // Show keyboard shortcuts modal window.showShortcutsHelp = function() { const existingModal = document.getElementById('keyboard-shortcuts-modal'); if (existingModal) { existingModal.classList.remove('hidden'); return; } const staticMode = window.__STATIC_MODE__ === true; const kbd = 'class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-sm font-mono"'; const row = (label, keys) => `
${label} ${keys}
`; const k = (s) => `${s}`; const navRows = [ row('Focus search', k('/')), row('Navigate search results', `${k('↑')} ${k('↓')} ${k('Enter')}`), row('Go to recipes', `${k('g')} ${k('h')}`), ]; if (!staticMode) { navRows.push( row('Go to shopping list', `${k('g')} ${k('s')}`), row('Go to pantry', `${k('g')} ${k('p')}`), row('Go to preferences', `${k('g')} ${k('x')}`) ); } const recipeRows = [row('Start cooking mode', k('c'))]; if (!staticMode) { recipeRows.push( row('Edit recipe', k('e')), row('Add to shopping list', k('a')) ); } recipeRows.push(row('Print recipe', k('p'))); if (!staticMode) { recipeRows.push( row('Increase scale', k('+')), row('Decrease scale', k('-')) ); } const shoppingSection = staticMode ? '' : `

Shopping List

${row('Clear all items', k('c'))}
`; const modal = document.createElement('div'); modal.id = 'keyboard-shortcuts-modal'; modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'; modal.innerHTML = `

Keyboard Shortcuts

Navigation

${navRows.join('')}

General

${row('Toggle theme', k('t'))} ${row('Show shortcuts', k('?'))} ${row('Close modal', k('Esc'))}

Recipe Page

${recipeRows.join('')}
${shoppingSection}
Press Esc to close
`; document.body.appendChild(modal); // Close on backdrop click modal.addEventListener('click', function(e) { if (e.target === modal) { closeShortcutsHelp(); } }); } // Close keyboard shortcuts modal window.closeShortcutsHelp = function() { const modal = document.getElementById('keyboard-shortcuts-modal'); if (modal) { modal.classList.add('hidden'); } }; // Handle keyboard events function handleKeydown(event) { // Don't handle if user is typing if (isTyping(event)) { // But still allow Escape to blur inputs if (event.key === 'Escape') { event.target.blur(); } return; } // Don't handle if modifier keys are pressed (except Shift for ? and +) if (event.ctrlKey || event.metaKey || event.altKey) { return; } const key = event.key; // Handle pending key sequences (like "g h") if (pendingKey === 'g') { clearPendingKey(); const pfx = window.__PREFIX__ || ''; const staticMode = window.__STATIC_MODE__ === true; switch (key) { case 'h': case 'r': event.preventDefault(); window.location.href = pfx + '/'; return; case 's': if (staticMode) break; event.preventDefault(); window.location.href = pfx + '/shopping-list'; return; case 'p': if (staticMode) break; event.preventDefault(); window.location.href = pfx + '/pantry'; return; case 'x': if (staticMode) break; event.preventDefault(); window.location.href = pfx + '/preferences'; return; } // If no valid second key, fall through to handle as new key } // Global shortcuts switch (key) { case '/': event.preventDefault(); const searchInput = document.getElementById('search-input'); if (searchInput) { searchInput.focus(); searchInput.select(); } return; case 'g': event.preventDefault(); pendingKey = 'g'; // Clear pending after 1.5 seconds pendingTimeout = setTimeout(clearPendingKey, 1500); return; case '?': event.preventDefault(); showShortcutsHelp(); return; case 'Escape': event.preventDefault(); // Close shortcuts modal if open const shortcutsModal = document.getElementById('keyboard-shortcuts-modal'); if (shortcutsModal && !shortcutsModal.classList.contains('hidden')) { closeShortcutsHelp(); return; } // Close search results if open const searchResults = document.getElementById('search-results'); if (searchResults && !searchResults.classList.contains('hidden')) { searchResults.classList.add('hidden'); return; } // Close any other modals const modals = document.querySelectorAll('[data-modal]'); modals.forEach(modal => modal.classList.add('hidden')); return; case 't': event.preventDefault(); if (typeof toggleTheme === 'function') { toggleTheme(); } return; } // Page-specific shortcuts const path = window.location.pathname; const pfx2 = window.__PREFIX__ || ''; // Recipe page shortcuts if (path.startsWith(pfx2 + '/recipe/')) { handleRecipeShortcuts(event, key); } // Shopping list page shortcuts else if (path === pfx2 + '/shopping-list') { handleShoppingListShortcuts(event, key); } } // Recipe page specific shortcuts function handleRecipeShortcuts(event, key) { const staticMode = window.__STATIC_MODE__ === true; switch (key) { case 'c': event.preventDefault(); if (typeof startCookingMode === 'function') { startCookingMode(); } return; case 'e': if (staticMode) return; event.preventDefault(); // Find and click the edit link const editPfx = (window.__PREFIX__ || '') + '/edit/'; const editLink = document.querySelector(`a[href^="${editPfx}"]`); if (editLink) { editLink.click(); } return; case 'a': if (staticMode) return; event.preventDefault(); // Find and click the add to shopping list button const addButton = document.querySelector('button[onclick^="addToShoppingList"]'); if (addButton) { addButton.click(); } return; case 'p': event.preventDefault(); window.print(); return; case '+': case '=': // = is on the same key as + without shift if (staticMode) return; event.preventDefault(); adjustScale(0.5); return; case '-': case '_': if (staticMode) return; event.preventDefault(); adjustScale(-0.5); return; case ']': if (staticMode) return; event.preventDefault(); adjustScale(1); return; case '[': if (staticMode) return; event.preventDefault(); adjustScale(-1); return; } } // Adjust recipe scale function adjustScale(delta) { const scaleInput = document.getElementById('scale'); if (!scaleInput) return; let newValue = parseFloat(scaleInput.value) + delta; const min = parseFloat(scaleInput.min) || 0.5; const max = parseFloat(scaleInput.max) || 200; // Clamp to valid range newValue = Math.max(min, Math.min(max, newValue)); // Round to avoid floating point issues newValue = Math.round(newValue * 10) / 10; if (newValue !== parseFloat(scaleInput.value)) { scaleInput.value = newValue; // Trigger the onchange event scaleInput.dispatchEvent(new Event('change')); } } // Shopping list page specific shortcuts function handleShoppingListShortcuts(event, key) { switch (key) { case 'c': event.preventDefault(); // Clear the list (if the function exists) if (typeof clearList === 'function') { if (confirm('Clear all items from shopping list?')) { clearList(); } } return; } } // Initialize keyboard shortcuts document.addEventListener('keydown', handleKeydown); // Add visual indicator for keyboard navigation document.addEventListener('DOMContentLoaded', function() { // Add a small hint in the search placeholder about the shortcut const searchInput = document.getElementById('search-input'); if (searchInput) { const currentPlaceholder = searchInput.getAttribute('placeholder'); if (currentPlaceholder && !currentPlaceholder.includes('/')) { // Don't modify placeholder - keep it clean } } }); })();