/**
* 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();
}
});
}