- Integrated Unsplash API for image search functionality with environment configuration - Added new Nimbus Editor page component with navigation from sidebar and mobile drawer - Enhanced TOC with highlight animation for editor heading navigation - Improved CDK overlay z-index hierarchy for proper menu layering - Removed obsolete logging validation script
172 lines
5.1 KiB
TypeScript
172 lines
5.1 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|