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,
|
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 */}
|
||||||
|
|||||||
Reference in new issue
Block a user