ObsiViewer/src/app/editor/services/shortcuts.service.ts
Bruno Charest ee3085ce38 feat: add Nimbus Editor with Unsplash integration
- 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
2025-11-11 11:38:27 -05:00

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);
}
}
}