148 lines
4.9 KiB
TypeScript
148 lines
4.9 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useSitesStore } from '@/store/sites.ts';
|
|
import { useCoverageStore } from '@/store/coverage.ts';
|
|
import { useSettingsStore } from '@/store/settings.ts';
|
|
import { useToolStore } from '@/store/tools.ts';
|
|
import { useToastStore } from '@/components/ui/Toast.tsx';
|
|
|
|
interface ShortcutHandlers {
|
|
onCalculate: () => void;
|
|
onCloseForm: () => void;
|
|
onShowShortcuts?: () => void;
|
|
onDeleteRequest?: (siteId: string, siteName: string) => void;
|
|
onUndo?: () => void;
|
|
onRedo?: () => void;
|
|
}
|
|
|
|
function isInputActive(): boolean {
|
|
const el = document.activeElement;
|
|
if (!el) return false;
|
|
const tag = el.tagName;
|
|
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
|
}
|
|
|
|
export function useKeyboardShortcuts({
|
|
onCalculate,
|
|
onCloseForm,
|
|
onShowShortcuts,
|
|
onDeleteRequest,
|
|
onUndo,
|
|
onRedo,
|
|
}: ShortcutHandlers) {
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
const isMac = navigator.platform.toLowerCase().includes('mac');
|
|
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
|
|
|
// === Modifier shortcuts (work even in inputs) ===
|
|
if (modKey && e.key === 'Enter') {
|
|
e.preventDefault();
|
|
onCalculate();
|
|
return;
|
|
}
|
|
|
|
// Undo/Redo: use e.code (layout-independent) as primary, e.key as fallback.
|
|
// e.key can be uppercase 'Z' with Shift, or a control char on some layouts.
|
|
// e.code is always 'KeyZ' / 'KeyY' regardless of modifier or layout.
|
|
const isZ = e.code === 'KeyZ' || e.key.toLowerCase() === 'z';
|
|
const isY = e.code === 'KeyY' || e.key.toLowerCase() === 'y';
|
|
|
|
// Undo: Ctrl+Z (or Cmd+Z on Mac)
|
|
if (modKey && isZ && !e.shiftKey) {
|
|
e.preventDefault();
|
|
onUndo?.();
|
|
return;
|
|
}
|
|
|
|
// Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
|
|
if (modKey && ((isZ && e.shiftKey) || isY)) {
|
|
e.preventDefault();
|
|
onRedo?.();
|
|
return;
|
|
}
|
|
|
|
// Escape always works
|
|
if (e.key === 'Escape') {
|
|
useSitesStore.getState().selectSite(null);
|
|
useToolStore.getState().clearTool();
|
|
onCloseForm();
|
|
return;
|
|
}
|
|
|
|
// === Skip remaining shortcuts if typing in an input ===
|
|
if (isInputActive()) return;
|
|
|
|
// Shift combos (no browser conflicts)
|
|
if (e.shiftKey && !modKey && !e.altKey) {
|
|
switch (e.key.toUpperCase()) {
|
|
case 'S': // Shift+S: New site (place mode)
|
|
e.preventDefault();
|
|
useToolStore.getState().setActiveTool('site-placement');
|
|
useToastStore.getState().addToast('Click on map to place new site', 'info');
|
|
return;
|
|
case 'C': // Shift+C: Clear coverage
|
|
e.preventDefault();
|
|
useCoverageStore.getState().clearCoverage();
|
|
useToastStore.getState().addToast('Coverage cleared', 'info');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Single letter shortcuts (no modifiers)
|
|
if (!modKey && !e.altKey && !e.shiftKey) {
|
|
switch (e.key.toLowerCase()) {
|
|
case 'h': // H: Toggle heatmap
|
|
useCoverageStore.getState().toggleHeatmap();
|
|
return;
|
|
case 'g': // G: Toggle grid
|
|
useSettingsStore.getState().setShowGrid(
|
|
!useSettingsStore.getState().showGrid
|
|
);
|
|
return;
|
|
case 't': // T: Toggle terrain
|
|
useSettingsStore.getState().setShowElevationOverlay(
|
|
!useSettingsStore.getState().showElevationOverlay
|
|
);
|
|
return;
|
|
case 'r': // R: Toggle ruler / measurement
|
|
useSettingsStore.getState().setMeasurementMode(
|
|
!useSettingsStore.getState().measurementMode
|
|
);
|
|
return;
|
|
case 'f': // F: Fit to coverage — dispatch custom event for Map.tsx to handle
|
|
window.dispatchEvent(new CustomEvent('rfcp:fit-bounds'));
|
|
return;
|
|
case 'delete': // Delete key: show confirmation dialog
|
|
case 'backspace':
|
|
{
|
|
const selectedId = useSitesStore.getState().selectedSiteId;
|
|
if (selectedId && onDeleteRequest) {
|
|
const site = useSitesStore.getState().sites.find(s => s.id === selectedId);
|
|
if (site) {
|
|
e.preventDefault();
|
|
onDeleteRequest(site.id, site.name);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ? key for help (not shift on some layouts)
|
|
if (e.key === '?') {
|
|
onShowShortcuts?.();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// ? with shift (most layouts: shift+/)
|
|
if (e.shiftKey && !modKey && !e.altKey && e.key === '?') {
|
|
onShowShortcuts?.();
|
|
return;
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [onCalculate, onCloseForm, onShowShortcuts, onDeleteRequest, onUndo, onRedo]);
|
|
}
|