Implement full suite of Evernote keyboard shortcuts and wire KeyboardShortcutsModal

This commit is contained in:
chrisf committed 2026-10-23 07:59:39 -06:00
1 parent 49ec3bc645
commit 99522b3e09
1 file changed
+217 -23
+217 -23
View File
@@ -47,6 +47,7 @@ import {
GitMerge, GitMerge,
History, History,
Users, Users,
Keyboard,
} from 'lucide-react'; } from 'lucide-react';
import { import {
SidebarData, SidebarData,
@@ -61,6 +62,7 @@ import TriageModal from './TriageModal';
import MergeNotebookModal from './MergeNotebookModal'; import MergeNotebookModal from './MergeNotebookModal';
import ShareNotebookModal from './ShareNotebookModal'; import ShareNotebookModal from './ShareNotebookModal';
import NoteHistoryModal from './NoteHistoryModal'; import NoteHistoryModal from './NoteHistoryModal';
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
import { PdfAutoThumbnail, InlinePdfViewerCard } from './PdfNoteComponents'; import { PdfAutoThumbnail, InlinePdfViewerCard } from './PdfNoteComponents';
export default function EvercloneWorkspace() { export default function EvercloneWorkspace() {
@@ -268,18 +270,6 @@ export default function EvercloneWorkspace() {
return () => clearTimeout(t); return () => clearTimeout(t);
}, [searchQuery]); }, [searchQuery]);
// Keyboard shortcut Ctrl+K for search, Alt+N for new note
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
searchInputRef.current?.focus();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, []);
const handleNavigateToNote = useCallback( const handleNavigateToNote = useCallback(
(targetNoteId: string) => { (targetNoteId: string) => {
if (!targetNoteId || targetNoteId === selectedNoteId) return; if (!targetNoteId || targetNoteId === selectedNoteId) return;
@@ -841,6 +831,192 @@ export default function EvercloneWorkspace() {
await fetchNotesList(); await fetchNotesList();
}; };
// Stable refs for keyboard shortcut handlers
const activeNoteRef = useRef(activeNote);
activeNoteRef.current = activeNote;
const handleCreateNoteRef = useRef(handleCreateNote);
handleCreateNoteRef.current = handleCreateNote;
const handleImmediatePatchRef = useRef(handleImmediatePatch);
handleImmediatePatchRef.current = handleImmediatePatch;
const handleDeleteOrTrashNoteRef = useRef(handleDeleteOrTrashNote);
handleDeleteOrTrashNoteRef.current = handleDeleteOrTrashNote;
const handleCopyNoteLinkRef = useRef(handleCopyNoteLink);
handleCopyNoteLinkRef.current = handleCopyNoteLink;
const handleHistoryBackRef = useRef(handleHistoryBack);
handleHistoryBackRef.current = handleHistoryBack;
const handleHistoryForwardRef = useRef(handleHistoryForward);
handleHistoryForwardRef.current = handleHistoryForward;
// Global Keyboard Shortcuts Suite (Evernote compatible)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const target = e.target as HTMLElement | null;
const isInput =
target &&
(target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.isContentEditable ||
target.closest('.ProseMirror'));
// 1. Esc: Close open modals or blur focused inputs
if (e.key === 'Escape') {
if (shortcutsModalOpen) {
e.preventDefault();
setShortcutsModalOpen(false);
return;
}
if (mergeModalOpen) {
e.preventDefault();
setMergeModalOpen(false);
return;
}
if (shareModalOpen) {
e.preventDefault();
setShareModalOpen(false);
return;
}
if (historyModalOpen) {
e.preventDefault();
setHistoryModalOpen(false);
return;
}
if (triageModalOpen) {
e.preventDefault();
setTriageModalOpen(false);
return;
}
if (thumbnailEditorOpen) {
e.preventDefault();
setThumbnailEditorOpen(false);
return;
}
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) {
target.blur();
return;
}
}
// 2. Alt + N: Create New Note
if (e.altKey && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'n') {
e.preventDefault();
handleCreateNoteRef.current();
return;
}
// 3. Ctrl + K / Cmd + K: Focus Search (when not inside editor)
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'k') {
if (!target?.closest('.ProseMirror')) {
e.preventDefault();
searchInputRef.current?.focus();
searchInputRef.current?.select();
return;
}
}
// 4. "/" : Jump to search (only when NOT in an input/editor)
if (!isInput && e.key === '/' && !e.ctrlKey && !e.metaKey && !e.altKey) {
e.preventDefault();
searchInputRef.current?.focus();
searchInputRef.current?.select();
return;
}
// 5. "?" or Ctrl + /: Open Shortcuts Modal
if (
(!isInput && e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) ||
((e.ctrlKey || e.metaKey) && e.key === '/')
) {
e.preventDefault();
setShortcutsModalOpen((v) => !v);
return;
}
// 6. Ctrl + \ : Toggle Sidebar Collapse
if ((e.ctrlKey || e.metaKey) && e.key === '\\') {
e.preventDefault();
setSidebarCollapsed((v) => !v);
return;
}
// 7. Alt + Left / Alt + Right : Note Navigation History (Back / Forward)
if (e.altKey && !e.ctrlKey && !e.metaKey && (e.key === 'ArrowLeft' || e.key === 'Left')) {
e.preventDefault();
handleHistoryBackRef.current();
return;
}
if (e.altKey && !e.ctrlKey && !e.metaKey && (e.key === 'ArrowRight' || e.key === 'Right')) {
e.preventDefault();
handleHistoryForwardRef.current();
return;
}
// 8. Ctrl + Alt + L : Copy Internal Note Link
if ((e.ctrlKey || e.metaKey) && e.altKey && e.key.toLowerCase() === 'l') {
e.preventDefault();
handleCopyNoteLinkRef.current();
return;
}
// 9. Ctrl + Shift + P : Pin / Unpin Note
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'p') {
const cur = activeNoteRef.current;
if (cur) {
e.preventDefault();
handleImmediatePatchRef.current({ isPinned: !cur.isPinned });
}
return;
}
// 10. Ctrl + Shift + S : Star / Add to Shortcuts
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 's') {
const cur = activeNoteRef.current;
if (cur) {
e.preventDefault();
handleImmediatePatchRef.current({ isShortcut: !cur.isShortcut });
}
return;
}
// 11. Ctrl + Shift + T : Focus Tag Input
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 't') {
e.preventDefault();
const tagEl = document.getElementById('everclone-add-tag-input');
tagEl?.focus();
return;
}
// 12. Ctrl + Shift + Del / Backspace : Move Note to Trash
if (
(e.ctrlKey || e.metaKey) &&
e.shiftKey &&
(e.key === 'Delete' || e.key === 'Backspace')
) {
const cur = activeNoteRef.current;
if (cur && !cur.isTrashed) {
e.preventDefault();
handleDeleteOrTrashNoteRef.current(false);
}
return;
}
// 13. Ctrl + S : Prevent default browser webpage save
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 's') {
e.preventDefault();
return;
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [
shortcutsModalOpen,
mergeModalOpen,
shareModalOpen,
historyModalOpen,
triageModalOpen,
thumbnailEditorOpen,
]);
const handleMergedNotebook = useCallback( const handleMergedNotebook = useCallback(
async ( async (
sourceName: string, sourceName: string,
@@ -1935,17 +2111,29 @@ export default function EvercloneWorkspace() {
<div className="flex items-center justify-between text-[11px] text-gray-500 px-1"> <div className="flex items-center justify-between text-[11px] text-gray-500 px-1">
<span>Everclone v1.2</span> <span>Everclone v1.2</span>
<button <div className="flex items-center gap-2">
type="button" <button
onClick={() => { type="button"
setActiveView('settings'); onClick={() => setShortcutsModalOpen(true)}
setMobileMenuOpen(false); className="hover:text-gray-300 flex items-center gap-1 transition"
}} title="Keyboard Shortcuts Cheat Sheet (? or Ctrl+/)"
className="hover:text-gray-300 underline" >
title="Open Import, Export, Clipper & Email Settings" <Keyboard className="w-3 h-3 text-gray-400" />
> <span>Shortcuts</span>
Settings </button>
</button> <span>·</span>
<button
type="button"
onClick={() => {
setActiveView('settings');
setMobileMenuOpen(false);
}}
className="hover:text-gray-300 underline"
title="Open Import, Export, Clipper & Email Settings"
>
Settings
</button>
</div>
</div> </div>
</div> </div>
)} )}
@@ -3467,6 +3655,7 @@ export default function EvercloneWorkspace() {
className="inline-flex items-center" className="inline-flex items-center"
> >
<input <input
id="everclone-add-tag-input"
type="text" type="text"
list="everclone-tags-datalist" list="everclone-tags-datalist"
value={tagInput} value={tagInput}
@@ -3547,6 +3736,11 @@ export default function EvercloneWorkspace() {
}} }}
/> />
)} )}
<KeyboardShortcutsModal
isOpen={shortcutsModalOpen}
onClose={() => setShortcutsModalOpen(false)}
/>
</> </>
)} )}
{/* Floating PWA Install Banner on Mobile */} {/* Floating PWA Install Banner on Mobile */}