Implement full suite of Evernote keyboard shortcuts and wire KeyboardShortcutsModal
This commit is contained in:
1 parent
49ec3bc645
commit
99522b3e09
1 file changed
+217
-23
@@ -47,6 +47,7 @@ import {
|
||||
GitMerge,
|
||||
History,
|
||||
Users,
|
||||
Keyboard,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
SidebarData,
|
||||
@@ -61,6 +62,7 @@ import TriageModal from './TriageModal';
|
||||
import MergeNotebookModal from './MergeNotebookModal';
|
||||
import ShareNotebookModal from './ShareNotebookModal';
|
||||
import NoteHistoryModal from './NoteHistoryModal';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import { PdfAutoThumbnail, InlinePdfViewerCard } from './PdfNoteComponents';
|
||||
|
||||
export default function EvercloneWorkspace() {
|
||||
@@ -268,18 +270,6 @@ export default function EvercloneWorkspace() {
|
||||
return () => clearTimeout(t);
|
||||
}, [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(
|
||||
(targetNoteId: string) => {
|
||||
if (!targetNoteId || targetNoteId === selectedNoteId) return;
|
||||
@@ -841,6 +831,192 @@ export default function EvercloneWorkspace() {
|
||||
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(
|
||||
async (
|
||||
sourceName: string,
|
||||
@@ -1935,17 +2111,29 @@ export default function EvercloneWorkspace() {
|
||||
|
||||
<div className="flex items-center justify-between text-[11px] text-gray-500 px-1">
|
||||
<span>Everclone v1.2</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 className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShortcutsModalOpen(true)}
|
||||
className="hover:text-gray-300 flex items-center gap-1 transition"
|
||||
title="Keyboard Shortcuts Cheat Sheet (? or Ctrl+/)"
|
||||
>
|
||||
<Keyboard className="w-3 h-3 text-gray-400" />
|
||||
<span>Shortcuts</span>
|
||||
</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>
|
||||
)}
|
||||
@@ -3467,6 +3655,7 @@ export default function EvercloneWorkspace() {
|
||||
className="inline-flex items-center"
|
||||
>
|
||||
<input
|
||||
id="everclone-add-tag-input"
|
||||
type="text"
|
||||
list="everclone-tags-datalist"
|
||||
value={tagInput}
|
||||
@@ -3547,6 +3736,11 @@ export default function EvercloneWorkspace() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<KeyboardShortcutsModal
|
||||
isOpen={shortcutsModalOpen}
|
||||
onClose={() => setShortcutsModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/* Floating PWA Install Banner on Mobile */}
|
||||
|
||||
Reference in new issue
Block a user