/** * Keyboard Shortcuts — shortcut definitions, cheatsheet modal, find bar UI, * and global keyboard event handlers. * * Extracted from main.ts for decomposition. */ // ── Types ─────────────────────────────────────────────────── export interface ShortcutCategory { category: string; shortcuts: Array<{ keys: string[]; label: string }>; } export interface KeyboardShortcutDeps { editor: any; provider: any; $: (id: string) => HTMLElement; exportHTML: () => void; doExportPdf: () => void; printDocument: () => void; mdToggle: { toggle: () => void }; showShortcutModal: () => void; } export interface FindBarDeps { editor: any; $: (id: string) => HTMLElement; } // ── Shortcut definitions ──────────────────────────────────── export const DOCS_SHORTCUTS: ShortcutCategory[] = [ { category: 'Formatting', shortcuts: [ { keys: ['\u2318', 'B'], label: 'Bold' }, { keys: ['\u2318', 'I'], label: 'Italic' }, { keys: ['\u2318', 'U'], label: 'Underline' }, { keys: ['\u2318', 'E'], label: 'Inline code' }, { keys: ['\u2318', '\u21e7', 'X'], label: 'Strikethrough' }, ]}, { category: 'Structure', shortcuts: [ { keys: ['\u2318', '\u21e7', '7'], label: 'Ordered list' }, { keys: ['\u2318', '\u21e7', '8'], label: 'Bullet list' }, { keys: ['\u2318', '\u21e7', '9'], label: 'Task list' }, { keys: ['\u2318', '\u21e7', 'B'], label: 'Blockquote' }, { keys: ['\u2318', '\u2325', 'C'], label: 'Code block' }, { keys: ['Tab'], label: 'Indent' }, { keys: ['\u21e7', 'Tab'], label: 'Outdent' }, { keys: ['\u2318', 'Enter'], label: 'Page break' }, ]}, { category: 'Markdown Autoformat', shortcuts: [ { keys: ['#', 'Space'], label: 'Heading 1' }, { keys: ['##', 'Space'], label: 'Heading 2' }, { keys: ['###', 'Space'], label: 'Heading 3' }, { keys: ['```', 'Space'], label: 'Code block' }, { keys: ['>', 'Space'], label: 'Blockquote' }, { keys: ['-', 'Space'], label: 'Bullet list' }, { keys: ['1.', 'Space'], label: 'Numbered list' }, { keys: ['[]', 'Space'], label: 'Task list' }, { keys: ['---'], label: 'Horizontal rule' }, { keys: ['**text**'], label: 'Bold' }, { keys: ['*text*'], label: 'Italic' }, { keys: ['~~text~~'], label: 'Strikethrough' }, { keys: ['`text`'], label: 'Inline code' }, { keys: ['[text](url)'], label: 'Link' }, ]}, { category: 'Navigation', shortcuts: [ { keys: ['\u2318', 'A'], label: 'Select all' }, { keys: ['\u2318', 'Z'], label: 'Undo' }, { keys: ['\u2318', '\u21e7', 'Z'], label: 'Redo' }, ]}, { category: 'Document', shortcuts: [ { keys: ['\u2318', 'S'], label: 'Save snapshot' }, { keys: ['\u2318', '\u21e7', 'S'], label: 'Export HTML' }, { keys: ['\u2318', '\u21e7', 'P'], label: 'Export PDF' }, { keys: ['\u2318', '\u21e7', 'M'], label: 'Toggle markdown source' }, { keys: ['\u2318', 'P'], label: 'Print' }, { keys: ['\u2318', '/'], label: 'Keyboard shortcuts' }, ]}, ]; // ── Shortcut Modal ────────────────────────────────────────── export function buildShortcutModal(shortcuts: ShortcutCategory[]): HTMLElement { const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; const modal = document.createElement('div'); modal.className = 'modal shortcuts-modal'; let html = '

Keyboard Shortcuts

'; for (const cat of shortcuts) { html += '
' + cat.category + '
'; for (const sc of cat.shortcuts) { html += '
' + sc.label + ''; html += sc.keys.map(k => '' + k + '').join(''); html += '
'; } html += '
'; } modal.innerHTML = html; overlay.appendChild(modal); const close = () => overlay.remove(); modal.querySelector('.shortcuts-modal-close')!.addEventListener('click', close); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', handler); } }; document.addEventListener('keydown', handler); return overlay; } export function showShortcutModal(): void { if (document.querySelector('.shortcuts-modal')) return; document.body.appendChild(buildShortcutModal(DOCS_SHORTCUTS)); } // ── Find Bar ──────────────────────────────────────────────── const FIND_BAR_HTML = ''; export function insertFindBar(): void { const editorContainerEl = document.querySelector('.editor-container'); if (editorContainerEl) { editorContainerEl.insertAdjacentHTML('afterbegin', FIND_BAR_HTML); } } export function wireFindBar(deps: FindBarDeps): { updateFindBar: () => void } { const { editor, $ } = deps; const findBar = $('find-bar'); const findInput = $('find-input') as HTMLInputElement; const replaceInput = $('replace-input') as HTMLInputElement; const findCount = $('find-count'); const findReplaceRow = $('find-replace-row'); function updateFindBar() { const storage = editor.storage.searchReplace; findBar.style.display = storage.isOpen ? '' : 'none'; findReplaceRow.style.display = storage.showReplace ? '' : 'none'; if (storage.matches.length > 0) { findCount.textContent = (storage.activeIndex + 1) + ' / ' + storage.matches.length; } else if (storage.searchTerm) { findCount.textContent = 'No results'; } else { findCount.textContent = ''; } $('find-case').classList.toggle('active', storage.caseSensitive); if (storage.isOpen && document.activeElement !== findInput && document.activeElement !== replaceInput) { findInput.focus(); findInput.select(); } } findInput.addEventListener('input', () => { editor.commands.setSearchTerm(findInput.value); }); findInput.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); if (e.shiftKey) { editor.commands.prevMatch(); } else { editor.commands.nextMatch(); } } else if (e.key === 'Escape') { e.preventDefault(); editor.commands.closeSearch(); editor.commands.focus(); } }); replaceInput.addEventListener('input', () => { editor.commands.setReplaceTerm(replaceInput.value); }); replaceInput.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); editor.commands.replaceCurrent(); } else if (e.key === 'Escape') { e.preventDefault(); editor.commands.closeSearch(); editor.commands.focus(); } }); $('find-next').addEventListener('click', () => editor.commands.nextMatch()); $('find-prev').addEventListener('click', () => editor.commands.prevMatch()); $('find-case').addEventListener('click', () => editor.commands.toggleCaseSensitive()); $('find-close').addEventListener('click', () => { editor.commands.closeSearch(); editor.commands.focus(); }); $('find-replace-toggle').addEventListener('click', () => { const storage = editor.storage.searchReplace; storage.showReplace = !storage.showReplace; updateFindBar(); }); $('replace-one').addEventListener('click', () => editor.commands.replaceCurrent()); $('replace-all').addEventListener('click', () => editor.commands.replaceAll()); return { updateFindBar }; } // ── Global keyboard shortcuts ─────────────────────────────── export function wireGlobalShortcuts(deps: KeyboardShortcutDeps): void { const { editor, provider, $, exportHTML, doExportPdf, printDocument, mdToggle, showShortcutModal: showModal } = deps; document.addEventListener('keydown', (e: KeyboardEvent) => { const mod = e.metaKey || e.ctrlKey; if (mod && e.key === '/') { e.preventDefault(); showModal(); return; } if (mod && !e.shiftKey && e.key.toLowerCase() === 'f') { e.preventDefault(); editor.commands.openSearch(); return; } if (mod && e.key.toLowerCase() === 'h') { e.preventDefault(); editor.commands.openSearchReplace(); return; } if (mod && e.shiftKey && e.key.toLowerCase() === 'g') { if (editor.storage.searchReplace.isOpen) { e.preventDefault(); editor.commands.prevMatch(); return; } } if (mod && e.key.toLowerCase() === 'g') { if (editor.storage.searchReplace.isOpen) { e.preventDefault(); editor.commands.nextMatch(); return; } } if (mod && e.shiftKey && e.key.toLowerCase() === 's') { e.preventDefault(); exportHTML(); } else if (mod && e.key.toLowerCase() === 's') { e.preventDefault(); provider._saveSnapshot(); } else if (mod && e.shiftKey && e.key.toLowerCase() === 'p') { e.preventDefault(); doExportPdf(); } else if (mod && e.key.toLowerCase() === 'p') { e.preventDefault(); printDocument(); } else if (mod && e.shiftKey && e.key.toLowerCase() === 'm') { e.preventDefault(); mdToggle.toggle(); } }); }