import { Injectable, inject } from '@angular/core'; import { DocumentService } from './document.service'; import { SelectionService } from './selection.service'; import { PaletteService } from './palette.service'; import { SHORTCUTS, matchesShortcut } from '../core/constants/keyboard'; import { BlockType } from '../core/models/block.model'; /** * Keyboard shortcuts handler service */ @Injectable({ providedIn: 'root' }) export class ShortcutsService { private readonly documentService = inject(DocumentService); private readonly selectionService = inject(SelectionService); private readonly paletteService = inject(PaletteService); /** * Handle keyboard event */ handleKeyDown(event: KeyboardEvent): boolean { // Find matching shortcut for (const shortcut of SHORTCUTS) { if (matchesShortcut(event, shortcut)) { this.executeAction(shortcut.action, event); event.preventDefault(); return true; } } return false; } /** * Execute shortcut action */ private executeAction(action: string, event: KeyboardEvent): void { const activeBlockId = this.selectionService.getActive(); switch (action) { // Palette case 'open-palette': this.paletteService.open(activeBlockId); break; // Headings case 'heading-1': this.insertOrConvertBlock('heading', { level: 1, text: '' }); break; case 'heading-2': this.insertOrConvertBlock('heading', { level: 2, text: '' }); break; case 'heading-3': this.insertOrConvertBlock('heading', { level: 3, text: '' }); break; // Lists case 'bullet-list': this.insertOrConvertBlock('list', { kind: 'bullet' }); break; case 'numbered-list': this.insertOrConvertBlock('list', { kind: 'numbered' }); break; case 'checkbox-list': this.insertOrConvertBlock('list', { kind: 'check' }); break; // Blocks case 'toggle': this.insertOrConvertBlock('toggle', { title: 'Toggle', content: [], collapsed: true }); break; case 'code': this.insertOrConvertBlock('code', { code: '', lang: '' }); break; case 'quote': this.insertOrConvertBlock('quote', { text: '' }); break; case 'hint': this.insertOrConvertBlock('hint', { text: '', variant: 'info' }); break; case 'button': this.insertOrConvertBlock('button', { label: 'Button', url: '', variant: 'primary' }); break; // Block operations case 'delete-block': if (activeBlockId) { this.documentService.deleteBlock(activeBlockId); } break; case 'move-block-up': if (activeBlockId) { const blocks = this.documentService.blocks(); const index = blocks.findIndex(b => b.id === activeBlockId); if (index > 0) { this.documentService.moveBlock(activeBlockId, index - 1); } } break; case 'move-block-down': if (activeBlockId) { const blocks = this.documentService.blocks(); const index = blocks.findIndex(b => b.id === activeBlockId); if (index >= 0 && index < blocks.length - 1) { this.documentService.moveBlock(activeBlockId, index + 1); } } break; case 'duplicate-block': if (activeBlockId) { this.documentService.duplicateBlock(activeBlockId); } break; // Overlay case 'close-overlay': if (this.paletteService.isOpen()) { this.paletteService.close(); } break; // Save case 'save': // Save is automatic via effect console.log('Document auto-saved'); break; // Text formatting (handled by block components) case 'bold': case 'italic': case 'underline': case 'link': // These are handled by individual block components break; default: console.log('Unhandled action:', action); } } /** * Insert or convert block based on context */ private insertOrConvertBlock(type: BlockType, preset?: any): void { const activeBlockId = this.selectionService.getActive(); if (activeBlockId) { // Convert existing block this.documentService.convertBlock(activeBlockId, type, preset); } else { // Insert new block at end const block = this.documentService.createBlock(type, this.documentService.getDefaultProps(type)); if (preset) { block.props = { ...block.props, ...preset }; } // If it's a list created via shortcut, seed the first item's text for immediate visibility if (type === 'list') { const k = (block.props?.kind || '').toLowerCase(); const label = k === 'check' ? 'checkbox-list' : k === 'numbered' ? 'numbered-list' : 'bullet-list'; if (Array.isArray(block.props?.items) && block.props.items.length > 0) { block.props.items = [{ ...block.props.items[0], text: label }]; } } this.documentService.appendBlock(block); this.selectionService.setActive(block.id); } } }