/**
* 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 = `
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
}
}
});
})();