@mytec: iter9 ready for test
This commit is contained in:
@@ -1,61 +1,121 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useSitesStore } from '@/store/sites.ts';
|
||||
import { useCoverageStore } from '@/store/coverage.ts';
|
||||
import { useSettingsStore } from '@/store/settings.ts';
|
||||
import { useToastStore } from '@/components/ui/Toast.tsx';
|
||||
|
||||
interface ShortcutHandlers {
|
||||
onCalculate: () => void;
|
||||
onCloseForm: () => void;
|
||||
onShowShortcuts?: () => void;
|
||||
}
|
||||
|
||||
export function useKeyboardShortcuts({ onCalculate, onCloseForm }: ShortcutHandlers) {
|
||||
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,
|
||||
}: ShortcutHandlers) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Ignore if typing in input or textarea
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isMac = navigator.platform.toLowerCase().includes('mac');
|
||||
const modKey = isMac ? e.metaKey : e.ctrlKey;
|
||||
|
||||
if (modKey) {
|
||||
switch (e.key.toLowerCase()) {
|
||||
case 'enter': // Ctrl/Cmd+Enter: Calculate coverage
|
||||
e.preventDefault();
|
||||
onCalculate();
|
||||
break;
|
||||
// === Modifier shortcuts (work even in inputs) ===
|
||||
if (modKey && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onCalculate();
|
||||
return;
|
||||
}
|
||||
|
||||
case 'n': // Ctrl/Cmd+N: New site (enter placement mode)
|
||||
// Escape always works
|
||||
if (e.key === 'Escape') {
|
||||
useSitesStore.getState().selectSite(null);
|
||||
useSitesStore.getState().setPlacingMode(false);
|
||||
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();
|
||||
useSitesStore.getState().setPlacingMode(true);
|
||||
useToastStore.getState().addToast('Click on map to place new site', 'info');
|
||||
break;
|
||||
return;
|
||||
case 'C': // Shift+C: Clear coverage
|
||||
e.preventDefault();
|
||||
useCoverageStore.getState().clearCoverage();
|
||||
useToastStore.getState().addToast('Coverage cleared', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Non-modifier shortcuts
|
||||
switch (e.key) {
|
||||
case 'Escape': // Escape: Cancel/close
|
||||
useSitesStore.getState().selectSite(null);
|
||||
useSitesStore.getState().setPlacingMode(false);
|
||||
onCloseForm();
|
||||
break;
|
||||
|
||||
case 'h': // H: Toggle heatmap
|
||||
case 'H':
|
||||
if (!e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
// Single letter shortcuts (no modifiers)
|
||||
if (!modKey && !e.altKey && !e.shiftKey) {
|
||||
switch (e.key.toLowerCase()) {
|
||||
case 'h': // H: Toggle heatmap
|
||||
useCoverageStore.getState().toggleHeatmap();
|
||||
}
|
||||
break;
|
||||
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: delete selected sites
|
||||
case 'backspace':
|
||||
// Only if a site is selected (not batch)
|
||||
{
|
||||
const selectedId = useSitesStore.getState().selectedSiteId;
|
||||
if (selectedId) {
|
||||
const site = useSitesStore.getState().sites.find(s => s.id === selectedId);
|
||||
if (site) {
|
||||
useSitesStore.getState().deleteSite(selectedId);
|
||||
useToastStore.getState().addToast(`"${site.name}" deleted`, 'info');
|
||||
}
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}, [onCalculate, onCloseForm, onShowShortcuts]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user