import { type IBlock } from './wysiwyg.types.js'; import { WysiwygBlocks } from './wysiwyg.blocks.js'; import { WysiwygBlockOperations } from './wysiwyg.blockoperations.js'; export class WysiwygKeyboardHandler { private component: any; constructor(component: any) { this.component = component; } /** * Handles keyboard events for blocks */ handleBlockKeyDown(e: KeyboardEvent, block: IBlock): void { // Handle slash menu navigation if (this.component.showSlashMenu && this.isSlashMenuKey(e.key)) { this.handleSlashMenuKeyboard(e); return; } // Handle formatting shortcuts if (this.handleFormattingShortcuts(e)) { return; } // Handle special keys switch (e.key) { case 'Tab': this.handleTab(e, block); break; case 'Enter': this.handleEnter(e, block); break; case 'Backspace': this.handleBackspace(e, block); break; } } /** * Checks if key is for slash menu navigation */ private isSlashMenuKey(key: string): boolean { return ['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(key); } /** * Handles formatting keyboard shortcuts */ private handleFormattingShortcuts(e: KeyboardEvent): boolean { if (!(e.metaKey || e.ctrlKey)) return false; switch (e.key.toLowerCase()) { case 'b': e.preventDefault(); this.component.applyFormat('bold'); return true; case 'i': e.preventDefault(); this.component.applyFormat('italic'); return true; case 'u': e.preventDefault(); this.component.applyFormat('underline'); return true; case 'k': e.preventDefault(); this.component.applyFormat('link'); return true; } return false; } /** * Handles Tab key */ private handleTab(e: KeyboardEvent, block: IBlock): void { if (block.type === 'code') { // Allow tab in code blocks e.preventDefault(); document.execCommand('insertText', false, ' '); } else if (block.type === 'list') { // Future: implement list indentation e.preventDefault(); } } /** * Handles Enter key */ private handleEnter(e: KeyboardEvent, block: IBlock): void { const blockOps = this.component.blockOperations; if (block.type === 'code') { if (e.shiftKey) { // Shift+Enter in code blocks creates a new block e.preventDefault(); const newBlock = blockOps.createBlock(); blockOps.insertBlockAfter(block, newBlock); } // Normal Enter in code blocks creates new line (let browser handle it) return; } if (!e.shiftKey) { if (block.type === 'list') { this.handleEnterInList(e, block); } else { // Create new paragraph block e.preventDefault(); const newBlock = blockOps.createBlock(); blockOps.insertBlockAfter(block, newBlock); } } // Shift+Enter creates line break (let browser handle it) } /** * Handles Enter key in list blocks */ private handleEnterInList(e: KeyboardEvent, block: IBlock): void { const target = e.target as HTMLDivElement; const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); const currentLi = range.startContainer.parentElement?.closest('li'); if (currentLi && currentLi.textContent === '') { // Empty list item - exit list mode e.preventDefault(); const blockOps = this.component.blockOperations; const newBlock = blockOps.createBlock(); blockOps.insertBlockAfter(block, newBlock); } // Otherwise, let browser create new list item } } /** * Handles Backspace key */ private handleBackspace(e: KeyboardEvent, block: IBlock): void { if (block.content === '' && this.component.blocks.length > 1) { e.preventDefault(); const blockOps = this.component.blockOperations; const prevBlock = blockOps.getPreviousBlock(block.id); if (prevBlock) { blockOps.removeBlock(block.id); setTimeout(() => { if (prevBlock.type !== 'divider') { blockOps.focusBlock(prevBlock.id, 'end'); } }); } } } /** * Handles slash menu keyboard navigation */ private handleSlashMenuKeyboard(e: KeyboardEvent): void { const menuItems = this.component.getFilteredMenuItems(); switch(e.key) { case 'ArrowDown': e.preventDefault(); this.component.slashMenuSelectedIndex = (this.component.slashMenuSelectedIndex + 1) % menuItems.length; break; case 'ArrowUp': e.preventDefault(); this.component.slashMenuSelectedIndex = this.component.slashMenuSelectedIndex === 0 ? menuItems.length - 1 : this.component.slashMenuSelectedIndex - 1; break; case 'Enter': e.preventDefault(); if (menuItems[this.component.slashMenuSelectedIndex]) { this.component.insertBlock( menuItems[this.component.slashMenuSelectedIndex].type as IBlock['type'] ); } break; case 'Escape': e.preventDefault(); this.component.closeSlashMenu(); break; } } }