Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 274 lines 11 kB view raw
1/** 2 * Keyboard Shortcuts — shortcut definitions, cheatsheet modal, find bar UI, 3 * and global keyboard event handlers. 4 * 5 * Extracted from main.ts for decomposition. 6 */ 7 8// ── Types ─────────────────────────────────────────────────── 9 10export interface ShortcutCategory { 11 category: string; 12 shortcuts: Array<{ keys: string[]; label: string }>; 13} 14 15export interface KeyboardShortcutDeps { 16 editor: any; 17 provider: any; 18 $: (id: string) => HTMLElement; 19 exportHTML: () => void; 20 doExportPdf: () => void; 21 printDocument: () => void; 22 mdToggle: { toggle: () => void }; 23 showShortcutModal: () => void; 24} 25 26export interface FindBarDeps { 27 editor: any; 28 $: (id: string) => HTMLElement; 29} 30 31// ── Shortcut definitions ──────────────────────────────────── 32 33export const DOCS_SHORTCUTS: ShortcutCategory[] = [ 34 { category: 'Formatting', shortcuts: [ 35 { keys: ['\u2318', 'B'], label: 'Bold' }, 36 { keys: ['\u2318', 'I'], label: 'Italic' }, 37 { keys: ['\u2318', 'U'], label: 'Underline' }, 38 { keys: ['\u2318', 'E'], label: 'Inline code' }, 39 { keys: ['\u2318', '\u21e7', 'X'], label: 'Strikethrough' }, 40 ]}, 41 { category: 'Structure', shortcuts: [ 42 { keys: ['\u2318', '\u21e7', '7'], label: 'Ordered list' }, 43 { keys: ['\u2318', '\u21e7', '8'], label: 'Bullet list' }, 44 { keys: ['\u2318', '\u21e7', '9'], label: 'Task list' }, 45 { keys: ['\u2318', '\u21e7', 'B'], label: 'Blockquote' }, 46 { keys: ['\u2318', '\u2325', 'C'], label: 'Code block' }, 47 { keys: ['Tab'], label: 'Indent' }, 48 { keys: ['\u21e7', 'Tab'], label: 'Outdent' }, 49 { keys: ['\u2318', 'Enter'], label: 'Page break' }, 50 ]}, 51 { category: 'Markdown Autoformat', shortcuts: [ 52 { keys: ['#', 'Space'], label: 'Heading 1' }, 53 { keys: ['##', 'Space'], label: 'Heading 2' }, 54 { keys: ['###', 'Space'], label: 'Heading 3' }, 55 { keys: ['```', 'Space'], label: 'Code block' }, 56 { keys: ['>', 'Space'], label: 'Blockquote' }, 57 { keys: ['-', 'Space'], label: 'Bullet list' }, 58 { keys: ['1.', 'Space'], label: 'Numbered list' }, 59 { keys: ['[]', 'Space'], label: 'Task list' }, 60 { keys: ['---'], label: 'Horizontal rule' }, 61 { keys: ['**text**'], label: 'Bold' }, 62 { keys: ['*text*'], label: 'Italic' }, 63 { keys: ['~~text~~'], label: 'Strikethrough' }, 64 { keys: ['`text`'], label: 'Inline code' }, 65 { keys: ['[text](url)'], label: 'Link' }, 66 ]}, 67 { category: 'Navigation', shortcuts: [ 68 { keys: ['\u2318', 'A'], label: 'Select all' }, 69 { keys: ['\u2318', 'Z'], label: 'Undo' }, 70 { keys: ['\u2318', '\u21e7', 'Z'], label: 'Redo' }, 71 ]}, 72 { category: 'Document', shortcuts: [ 73 { keys: ['\u2318', 'S'], label: 'Save snapshot' }, 74 { keys: ['\u2318', '\u21e7', 'S'], label: 'Export HTML' }, 75 { keys: ['\u2318', '\u21e7', 'P'], label: 'Export PDF' }, 76 { keys: ['\u2318', '\u21e7', 'M'], label: 'Toggle markdown source' }, 77 { keys: ['\u2318', 'P'], label: 'Print' }, 78 { keys: ['\u2318', '/'], label: 'Keyboard shortcuts' }, 79 ]}, 80]; 81 82// ── Shortcut Modal ────────────────────────────────────────── 83 84export function buildShortcutModal(shortcuts: ShortcutCategory[]): HTMLElement { 85 const overlay = document.createElement('div'); 86 overlay.className = 'modal-overlay'; 87 const modal = document.createElement('div'); 88 modal.className = 'modal shortcuts-modal'; 89 let html = '<h2>Keyboard Shortcuts <button class="shortcuts-modal-close" title="Close (Escape)">\u2715</button></h2>'; 90 for (const cat of shortcuts) { 91 html += '<div class="shortcut-category"><div class="shortcut-category-title">' + cat.category + '</div>'; 92 for (const sc of cat.shortcuts) { 93 html += '<div class="shortcut-row"><span class="shortcut-label">' + sc.label + '</span><span class="shortcut-keys">'; 94 html += sc.keys.map(k => '<span class="shortcut-key">' + k + '</span>').join(''); 95 html += '</span></div>'; 96 } 97 html += '</div>'; 98 } 99 modal.innerHTML = html; 100 overlay.appendChild(modal); 101 const close = () => overlay.remove(); 102 modal.querySelector('.shortcuts-modal-close')!.addEventListener('click', close); 103 overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); 104 const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', handler); } }; 105 document.addEventListener('keydown', handler); 106 return overlay; 107} 108 109export function showShortcutModal(): void { 110 if (document.querySelector('.shortcuts-modal')) return; 111 document.body.appendChild(buildShortcutModal(DOCS_SHORTCUTS)); 112} 113 114// ── Find Bar ──────────────────────────────────────────────── 115 116const FIND_BAR_HTML = '<div class="find-bar" id="find-bar" style="display:none">' + 117 '<div class="find-bar-row">' + 118 '<input class="find-bar-input" id="find-input" type="text" placeholder="Find..." spellcheck="false">' + 119 '<span class="find-bar-count" id="find-count"></span>' + 120 '<button class="btn-icon find-bar-btn" id="find-prev" title="Previous">\u25b2</button>' + 121 '<button class="btn-icon find-bar-btn" id="find-next" title="Next (Enter)">\u25bc</button>' + 122 '<button class="btn-icon find-bar-btn" id="find-case" title="Case sensitive">Aa</button>' + 123 '<button class="btn-icon find-bar-btn" id="find-replace-toggle" title="Toggle replace">\u21c4</button>' + 124 '<button class="btn-icon find-bar-btn" id="find-close" title="Close (Esc)">\u00d7</button>' + 125 '</div>' + 126 '<div class="find-bar-row find-bar-replace" id="find-replace-row" style="display:none">' + 127 '<input class="find-bar-input" id="replace-input" type="text" placeholder="Replace..." spellcheck="false">' + 128 '<button class="btn-icon find-bar-btn" id="replace-one" title="Replace">Replace</button>' + 129 '<button class="btn-icon find-bar-btn" id="replace-all" title="Replace all">All</button>' + 130 '</div>' + 131'</div>'; 132 133export function insertFindBar(): void { 134 const editorContainerEl = document.querySelector('.editor-container'); 135 if (editorContainerEl) { 136 editorContainerEl.insertAdjacentHTML('afterbegin', FIND_BAR_HTML); 137 } 138} 139 140export function wireFindBar(deps: FindBarDeps): { updateFindBar: () => void } { 141 const { editor, $ } = deps; 142 const findBar = $('find-bar'); 143 const findInput = $('find-input') as HTMLInputElement; 144 const replaceInput = $('replace-input') as HTMLInputElement; 145 const findCount = $('find-count'); 146 const findReplaceRow = $('find-replace-row'); 147 148 function updateFindBar() { 149 const storage = editor.storage.searchReplace; 150 findBar.style.display = storage.isOpen ? '' : 'none'; 151 findReplaceRow.style.display = storage.showReplace ? '' : 'none'; 152 153 if (storage.matches.length > 0) { 154 findCount.textContent = (storage.activeIndex + 1) + ' / ' + storage.matches.length; 155 } else if (storage.searchTerm) { 156 findCount.textContent = 'No results'; 157 } else { 158 findCount.textContent = ''; 159 } 160 161 $('find-case').classList.toggle('active', storage.caseSensitive); 162 163 if (storage.isOpen && document.activeElement !== findInput && document.activeElement !== replaceInput) { 164 findInput.focus(); 165 findInput.select(); 166 } 167 } 168 169 findInput.addEventListener('input', () => { 170 editor.commands.setSearchTerm(findInput.value); 171 }); 172 173 findInput.addEventListener('keydown', (e: KeyboardEvent) => { 174 if (e.key === 'Enter') { 175 e.preventDefault(); 176 if (e.shiftKey) { 177 editor.commands.prevMatch(); 178 } else { 179 editor.commands.nextMatch(); 180 } 181 } else if (e.key === 'Escape') { 182 e.preventDefault(); 183 editor.commands.closeSearch(); 184 editor.commands.focus(); 185 } 186 }); 187 188 replaceInput.addEventListener('input', () => { 189 editor.commands.setReplaceTerm(replaceInput.value); 190 }); 191 192 replaceInput.addEventListener('keydown', (e: KeyboardEvent) => { 193 if (e.key === 'Enter') { 194 e.preventDefault(); 195 editor.commands.replaceCurrent(); 196 } else if (e.key === 'Escape') { 197 e.preventDefault(); 198 editor.commands.closeSearch(); 199 editor.commands.focus(); 200 } 201 }); 202 203 $('find-next').addEventListener('click', () => editor.commands.nextMatch()); 204 $('find-prev').addEventListener('click', () => editor.commands.prevMatch()); 205 $('find-case').addEventListener('click', () => editor.commands.toggleCaseSensitive()); 206 $('find-close').addEventListener('click', () => { 207 editor.commands.closeSearch(); 208 editor.commands.focus(); 209 }); 210 $('find-replace-toggle').addEventListener('click', () => { 211 const storage = editor.storage.searchReplace; 212 storage.showReplace = !storage.showReplace; 213 updateFindBar(); 214 }); 215 $('replace-one').addEventListener('click', () => editor.commands.replaceCurrent()); 216 $('replace-all').addEventListener('click', () => editor.commands.replaceAll()); 217 218 return { updateFindBar }; 219} 220 221// ── Global keyboard shortcuts ─────────────────────────────── 222 223export function wireGlobalShortcuts(deps: KeyboardShortcutDeps): void { 224 const { editor, provider, $, exportHTML, doExportPdf, printDocument, mdToggle, showShortcutModal: showModal } = deps; 225 226 document.addEventListener('keydown', (e: KeyboardEvent) => { 227 const mod = e.metaKey || e.ctrlKey; 228 if (mod && e.key === '/') { 229 e.preventDefault(); 230 showModal(); 231 return; 232 } 233 if (mod && !e.shiftKey && e.key.toLowerCase() === 'f') { 234 e.preventDefault(); 235 editor.commands.openSearch(); 236 return; 237 } 238 if (mod && e.key.toLowerCase() === 'h') { 239 e.preventDefault(); 240 editor.commands.openSearchReplace(); 241 return; 242 } 243 if (mod && e.shiftKey && e.key.toLowerCase() === 'g') { 244 if (editor.storage.searchReplace.isOpen) { 245 e.preventDefault(); 246 editor.commands.prevMatch(); 247 return; 248 } 249 } 250 if (mod && e.key.toLowerCase() === 'g') { 251 if (editor.storage.searchReplace.isOpen) { 252 e.preventDefault(); 253 editor.commands.nextMatch(); 254 return; 255 } 256 } 257 if (mod && e.shiftKey && e.key.toLowerCase() === 's') { 258 e.preventDefault(); 259 exportHTML(); 260 } else if (mod && e.key.toLowerCase() === 's') { 261 e.preventDefault(); 262 provider._saveSnapshot(); 263 } else if (mod && e.shiftKey && e.key.toLowerCase() === 'p') { 264 e.preventDefault(); 265 doExportPdf(); 266 } else if (mod && e.key.toLowerCase() === 'p') { 267 e.preventDefault(); 268 printDocument(); 269 } else if (mod && e.shiftKey && e.key.toLowerCase() === 'm') { 270 e.preventDefault(); 271 mdToggle.toggle(); 272 } 273 }); 274}