In Angular können Sie benutzerdefinierte Tastenkombinationen (Key Bindings) implementieren, um die Benutzerfreundlichkeit Ihrer Anwendungen zu verbessern. Es gibt verschiedene Ansätze, je nachdem, wie umfangreich und komplex die Tastaturunterstützung sein soll. Ich werde Ihnen einen umfassenden Überblick geben und praktische Beispiele für die Implementierung in Angular-Versionen bis einschließlich Angular 19 zeigen.
Tastaturereignisse in Angular lassen sich auf mehrere Arten abfangen und verarbeiten:
Das direkte Binden von Tastaturereignissen ist der einfachste Weg, um auf Tastatureingaben zu reagieren:
<input (keydown.enter)="onEnterKey()" (keydown.control.s)="saveDocument($event)">Angular unterstützt die Kombination von Modifikatoren mit Tasten,
z.B. control, alt, shift und
meta (Windows-Taste oder Command-Taste auf macOS).
// search.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-search',
template: `
<div>
<input
[(ngModel)]="searchTerm"
(keydown.enter)="performSearch()"
placeholder="Suchen..."
>
<button (click)="performSearch()">Suchen</button>
</div>
`
})
export class SearchComponent {
searchTerm = '';
performSearch() {
console.log(`Suche nach: ${this.searchTerm}`);
// Implementieren Sie hier Ihre Suchlogik
}
}In diesem Beispiel wird die Suche sowohl durch Klicken auf den Suchbutton als auch durch Drücken der Enter-Taste ausgelöst.
Der @HostListener Decorator ermöglicht das Abfangen von
Ereignissen auf Komponentenebene:
import { Component, HostListener } from '@angular/core';
@Component({
selector: 'app-document-editor',
templateUrl: './document-editor.component.html'
})
export class DocumentEditorComponent {
documentContent = '';
@HostListener('window:keydown.control.s', ['$event'])
onSaveKeyDown(event: KeyboardEvent) {
event.preventDefault(); // Verhindert das Öffnen des Browser-Speicherdialogs
this.saveDocument();
}
@HostListener('window:keydown.control.z', ['$event'])
onUndoKeyDown(event: KeyboardEvent) {
event.preventDefault();
this.undoLastChange();
}
saveDocument() {
console.log('Dokument wird gespeichert:', this.documentContent);
// Implementieren Sie hier Ihre Speicherlogik
}
undoLastChange() {
console.log('Letzte Änderung rückgängig machen');
// Implementieren Sie hier Ihre Undo-Logik
}
}Beachten Sie, dass Sie mit window: als Präfix des
Ereignisnamens globale Tastenkombinationen erfassen können, die überall
in der Anwendung funktionieren, solange die Komponente aktiv ist.
Für wiederverwendbare Key Bindings ist eine benutzerdefinierte Direktive oft die beste Lösung:
// key-binding.directive.ts
import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { fromEvent, Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
@Directive({
selector: '[appKeyBinding]'
})
export class KeyBindingDirective implements OnInit, OnDestroy {
@Input() targetKey: string = '';
@Input() ctrlKey: boolean = false;
@Input() altKey: boolean = false;
@Input() shiftKey: boolean = false;
@Output() keyPressed = new EventEmitter<KeyboardEvent>();
private subscription: Subscription | null = null;
constructor(private el: ElementRef) {}
ngOnInit() {
this.subscription = fromEvent<KeyboardEvent>(this.el.nativeElement, 'keydown')
.pipe(
filter(event => {
return (
event.key === this.targetKey &&
event.ctrlKey === this.ctrlKey &&
event.altKey === this.altKey &&
event.shiftKey === this.shiftKey
);
})
)
.subscribe(event => {
event.preventDefault();
this.keyPressed.emit(event);
});
}
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}Verwendung der Direktive:
<textarea
appKeyBinding
targetKey="s"
[ctrlKey]="true"
(keyPressed)="saveDocument($event)"
></textarea>Für anwendungsweite Tastaturkürzel empfiehlt sich ein Service:
// keyboard.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { fromEvent, Observable, Subject, Subscription } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
export interface KeyBinding {
key: string;
ctrlKey?: boolean;
altKey?: boolean;
shiftKey?: boolean;
metaKey?: boolean;
callback: (event: KeyboardEvent) => void;
description?: string;
}
@Injectable({
providedIn: 'root'
})
export class KeyboardService implements OnDestroy {
private keyBindings: KeyBinding[] = [];
private keyDown$: Observable<KeyboardEvent>;
private subscription: Subscription;
private destroy$ = new Subject<void>();
constructor() {
// Globales Keyboard-Event-Observable
this.keyDown$ = fromEvent<KeyboardEvent>(document, 'keydown').pipe(
takeUntil(this.destroy$)
);
// Abonnieren von Tastaturereignissen
this.subscription = this.keyDown$.subscribe(event => {
this.handleKeyDown(event);
});
}
// Neue Tastenkombination registrieren
registerKeyBinding(binding: KeyBinding): void {
this.keyBindings.push(binding);
// Optionales Logging der registrierten Tastenkombination
const modifiers = [
binding.ctrlKey ? 'Ctrl' : '',
binding.altKey ? 'Alt' : '',
binding.shiftKey ? 'Shift' : '',
binding.metaKey ? 'Meta' : ''
].filter(Boolean).join('+');
const keyCombo = modifiers ? `${modifiers}+${binding.key}` : binding.key;
console.log(`Tastenkombination registriert: ${keyCombo} - ${binding.description || 'Keine Beschreibung'}`);
}
// Tastenkombination entfernen
unregisterKeyBinding(bindingToRemove: Partial<KeyBinding>): void {
this.keyBindings = this.keyBindings.filter(binding => {
// Prüft, ob die übergebenen Eigenschaften mit denen der registrierten Bindung übereinstimmen
return !(
(!bindingToRemove.key || binding.key === bindingToRemove.key) &&
(bindingToRemove.ctrlKey === undefined || binding.ctrlKey === bindingToRemove.ctrlKey) &&
(bindingToRemove.altKey === undefined || binding.altKey === bindingToRemove.altKey) &&
(bindingToRemove.shiftKey === undefined || binding.shiftKey === bindingToRemove.shiftKey) &&
(bindingToRemove.metaKey === undefined || binding.metaKey === bindingToRemove.metaKey)
);
});
}
// Alle registrierten Tastenkombinationen auflisten
getRegisteredKeyBindings(): KeyBinding[] {
return [...this.keyBindings];
}
// Verarbeitung der Tastatureingaben
private handleKeyDown(event: KeyboardEvent): void {
// Formularelemente ausschließen, es sei denn, es handelt sich um Tab-Taste oder Escape
if (
['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName) &&
event.key !== 'Tab' &&
event.key !== 'Escape'
) {
return;
}
// Passende Key Bindings finden und ausführen
for (const binding of this.keyBindings) {
if (
event.key === binding.key &&
(binding.ctrlKey === undefined || event.ctrlKey === binding.ctrlKey) &&
(binding.altKey === undefined || event.altKey === binding.altKey) &&
(binding.shiftKey === undefined || event.shiftKey === binding.shiftKey) &&
(binding.metaKey === undefined || event.metaKey === binding.metaKey)
) {
event.preventDefault();
binding.callback(event);
}
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}// app.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { KeyboardService } from './services/keyboard.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit, OnDestroy {
private subscriptions: Subscription[] = [];
constructor(private keyboardService: KeyboardService) {}
ngOnInit() {
// Tastenkombinationen registrieren
this.keyboardService.registerKeyBinding({
key: 's',
ctrlKey: true,
callback: (e) => this.saveDocument(),
description: 'Dokument speichern'
});
this.keyboardService.registerKeyBinding({
key: 'n',
ctrlKey: true,
callback: (e) => this.newDocument(),
description: 'Neues Dokument'
});
this.keyboardService.registerKeyBinding({
key: 'F1',
callback: (e) => this.showHelp(),
description: 'Hilfe anzeigen'
});
}
saveDocument() {
console.log('Dokument wird gespeichert...');
// Implementierung der Speicherfunktion
}
newDocument() {
console.log('Neues Dokument wird erstellt...');
// Implementierung für neues Dokument
}
showHelp() {
console.log('Hilfe wird angezeigt...');
// Implementierung der Hilfeanzeige
}
ngOnDestroy() {
// Alle Subscriptions kündigen
this.subscriptions.forEach(sub => sub.unsubscribe());
}
}Eine nützliche Ergänzung ist eine Komponente, die dem Benutzer die verfügbaren Tastenkombinationen anzeigt:
// keyboard-shortcuts-help.component.ts
import { Component, OnInit } from '@angular/core';
import { KeyBinding, KeyboardService } from '../services/keyboard.service';
@Component({
selector: 'app-keyboard-shortcuts-help',
template: `
<div class="shortcuts-overlay" *ngIf="showHelp">
<div class="shortcuts-dialog">
<h2>Verfügbare Tastenkombinationen</h2>
<table>
<thead>
<tr>
<th>Tastenkombination</th>
<th>Beschreibung</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let binding of keyBindings">
<td>{{ formatKeyBinding(binding) }}</td>
<td>{{ binding.description || 'Keine Beschreibung' }}</td>
</tr>
</tbody>
</table>
<button (click)="toggleHelp()">Schließen</button>
</div>
</div>
<button (click)="toggleHelp()">Tastaturkürzel anzeigen</button>
`,
styles: [`
.shortcuts-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.shortcuts-dialog {
background-color: white;
border-radius: 4px;
padding: 20px;
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 8px;
text-align: left;
border-bottom: 1px solid #ddd;
}
`]
})
export class KeyboardShortcutsHelpComponent implements OnInit {
keyBindings: KeyBinding[] = [];
showHelp = false;
constructor(private keyboardService: KeyboardService) {}
ngOnInit() {
// Tastenkombination zum Öffnen der Hilfe registrieren
this.keyboardService.registerKeyBinding({
key: '?',
shiftKey: true,
callback: () => this.toggleHelp(),
description: 'Tastaturkürzel-Hilfe anzeigen/ausblenden'
});
// Tastenkombinationen abrufen
this.keyBindings = this.keyboardService.getRegisteredKeyBindings();
}
toggleHelp() {
this.showHelp = !this.showHelp;
// Aktualisieren der angezeigten Tastenkombinationen
if (this.showHelp) {
this.keyBindings = this.keyboardService.getRegisteredKeyBindings();
}
}
formatKeyBinding(binding: KeyBinding): string {
const modifiers = [
binding.ctrlKey ? 'Ctrl' : '',
binding.altKey ? 'Alt' : '',
binding.shiftKey ? 'Shift' : '',
binding.metaKey ? 'Meta' : ''
].filter(Boolean);
const keyName = binding.key.length === 1 ? binding.key.toUpperCase() : binding.key;
return [...modifiers, keyName].join('+');
}
}Für komplexere Anwendungen können Sie kontextbasierte Tastenkombinationen implementieren:
// keyboard-context.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { KeyboardService, KeyBinding } from './keyboard.service';
// Definieren möglicher Anwendungskontexte
export type KeyboardContext = 'global' | 'editor' | 'viewer' | 'dialog';
@Injectable({
providedIn: 'root'
})
export class KeyboardContextService {
private activeContexts = new Set<KeyboardContext>(['global']);
private activeContexts$ = new BehaviorSubject<Set<KeyboardContext>>(this.activeContexts);
// Map zur Speicherung der kontextbezogenen Tastenkombinationen
private contextBindings = new Map<KeyboardContext, KeyBinding[]>();
constructor(private keyboardService: KeyboardService) {}
// Kontext aktivieren
activateContext(context: KeyboardContext): void {
if (!this.activeContexts.has(context)) {
this.activeContexts.add(context);
this.activeContexts$.next(this.activeContexts);
// Tastenkombinationen für den neuen Kontext registrieren
const bindings = this.contextBindings.get(context) || [];
bindings.forEach(binding => this.keyboardService.registerKeyBinding(binding));
}
}
// Kontext deaktivieren
deactivateContext(context: KeyboardContext): void {
if (context !== 'global' && this.activeContexts.has(context)) {
this.activeContexts.delete(context);
this.activeContexts$.next(this.activeContexts);
// Tastenkombinationen für den Kontext entfernen
const bindings = this.contextBindings.get(context) || [];
bindings.forEach(binding => this.keyboardService.unregisterKeyBinding(binding));
}
}
// Tastenkombination für einen bestimmten Kontext registrieren
registerContextKeyBinding(context: KeyboardContext, binding: KeyBinding): void {
if (!this.contextBindings.has(context)) {
this.contextBindings.set(context, []);
}
this.contextBindings.get(context)?.push(binding);
// Wenn der Kontext aktiv ist, sofort registrieren
if (this.activeContexts.has(context)) {
this.keyboardService.registerKeyBinding(binding);
}
}
// Aktive Kontexte beobachten
getActiveContexts(): Observable<Set<KeyboardContext>> {
return this.activeContexts$.asObservable();
}
// Prüfen, ob ein Kontext aktiv ist
isContextActive(context: KeyboardContext): boolean {
return this.activeContexts.has(context);
}
}// editor.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { KeyboardContextService } from '../services/keyboard-context.service';
@Component({
selector: 'app-editor',
template: `
<div class="editor-container">
<textarea [(ngModel)]="content" class="editor"></textarea>
<div class="status-bar">
<span>Editor-Modus aktiv</span>
<span>Verwenden Sie Ctrl+B für Fett, Ctrl+I für Kursiv</span>
</div>
</div>
`,
styles: [`
.editor-container {
display: flex;
flex-direction: column;
height: 100%;
}
.editor {
flex: 1;
padding: 10px;
font-family: monospace;
}
.status-bar {
padding: 5px;
background-color: #f5f5f5;
font-size: 12px;
display: flex;
justify-content: space-between;
}
`]
})
export class EditorComponent implements OnInit, OnDestroy {
content = '';
constructor(private keyboardContextService: KeyboardContextService) {}
ngOnInit() {
// Editor-Kontext aktivieren
this.keyboardContextService.activateContext('editor');
// Editor-spezifische Tastenkombinationen registrieren
this.keyboardContextService.registerContextKeyBinding('editor', {
key: 'b',
ctrlKey: true,
callback: () => this.applyFormatting('bold'),
description: 'Text fett formatieren'
});
this.keyboardContextService.registerContextKeyBinding('editor', {
key: 'i',
ctrlKey: true,
callback: () => this.applyFormatting('italic'),
description: 'Text kursiv formatieren'
});
this.keyboardContextService.registerContextKeyBinding('editor', {
key: 'u',
ctrlKey: true,
callback: () => this.applyFormatting('underline'),
description: 'Text unterstreichen'
});
}
applyFormatting(format: 'bold' | 'italic' | 'underline') {
console.log(`${format}-Formatierung anwenden`);
// Hier die eigentliche Formatierungslogik implementieren
}
ngOnDestroy() {
// Editor-Kontext deaktivieren, wenn die Komponente zerstört wird
this.keyboardContextService.deactivateContext('editor');
}
}Für besonders komplexe Anforderungen können Sie eine spezialisierte Bibliothek wie Mousetrap verwenden:
npm install mousetrap
npm install @types/mousetrap --save-devIntegration in Angular:
// mousetrap.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import * as Mousetrap from 'mousetrap';
@Injectable({
providedIn: 'root'
})
export class MousetrapService implements OnDestroy {
private mousetrap: Mousetrap.MousetrapInstance;
private boundShortcuts: string[] = [];
constructor() {
this.mousetrap = new Mousetrap();
}
bind(keys: string | string[], callback: (e: ExtendedKeyboardEvent, combo: string) => any, action?: string): void {
this.mousetrap.bind(keys, callback, action);
// Speichern der gebundenen Tastenkombinationen für die spätere Freigabe
if (Array.isArray(keys)) {
this.boundShortcuts.push(...keys);
} else {
this.boundShortcuts.push(keys);
}
}
unbind(keys: string | string[]): void {
this.mousetrap.unbind(keys);
// Entfernen der freigegebenen Tastenkombinationen aus der Liste
if (Array.isArray(keys)) {
this.boundShortcuts = this.boundShortcuts.filter(k => !keys.includes(k));
} else {
this.boundShortcuts = this.boundShortcuts.filter(k => k !== keys);
}
}
// Tastenkombination vorübergehend deaktivieren
pause(): void {
this.mousetrap.pause();
}
// Deaktivierte Tastenkombinationen wieder aktivieren
unpause(): void {
this.mousetrap.unpause();
}
// Alle registrierten Tastenkombinationen freigeben
reset(): void {
this.boundShortcuts.forEach(key => {
this.mousetrap.unbind(key);
});
this.boundShortcuts = [];
}
ngOnDestroy(): void {
this.reset();
}
}// app.component.ts
import { Component, OnInit } from '@angular/core';
import { MousetrapService } from './services/mousetrap.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
constructor(private mousetrapService: MousetrapService) {}
ngOnInit() {
// Einfache Tastenkombination
this.mousetrapService.bind('ctrl+s', (e) => {
e.preventDefault();
this.saveDocument();
return false; // Verhindert das Standardverhalten
});
// Mehrere Tastenkombinationen für die gleiche Aktion
this.mousetrapService.bind(['command+s', 'ctrl+s'], (e) => {
e.preventDefault();
this.saveDocument();
return false;
});
// Tastensequenz (nacheinander gedrückte Tasten)
this.mousetrapService.bind('g i', () => {
this.navigateToInbox();
return false;
});
// Globale Tastenkombination (funktioniert auch in Eingabefeldern)
this.mousetrapService.bind('esc', () => {
this.closeDialog();
return false;
}, 'keyup'); // 'keyup' als drittes Argument bedeutet, dass das Ereignis beim Loslassen der Taste ausgelöst wird
}
saveDocument() {
console.log('Dokument wird gespeichert...');
}
navigateToInbox() {
console.log('Navigation zum Posteingang...');
}
closeDialog() {
console.log('Dialog wird geschlossen...');
}
}Bei der Implementierung von Tastenkombinationen ist es wichtig, die Zugänglichkeit zu berücksichtigen:
// a11y-keyboard.service.ts
import { Injectable } from '@angular/core';
import { KeyboardService, KeyBinding } from './keyboard.service';
@Injectable({
providedIn: 'root'
})
export class A11yKeyboardService {
constructor(private keyboardService: KeyboardService) {}
setupAccessibleKeyBindings(): void {
// Navigations-Tastaturkürzel
this.keyboardService.registerKeyBinding({
key: 'h',
altKey: true,
callback: () => this.navigateTo('home'),
description: 'Zur Startseite'
});
this.keyboardService.registerKeyBinding({
key: 's',
altKey: true,
callback: () => this.skipToMainContent(),
description: 'Zum Hauptinhalt springen'
});
// Kontrasteinstellungen
this.keyboardService.registerKeyBinding({
key: 'c',
altKey: true,
callback: () => this.toggleHighContrast(),
description: 'Hoher Kontrast ein/aus'
});
// Textgröße ändern
this.keyboardService.registerKeyBinding({
key: '+',
altKey: true,
callback: () => this.changeFontSize(1),
description: 'Textgröße erhöhen'
});
this.keyboardService.registerKeyBinding({
key: '-',
altKey: true,
callback: () => this.changeFontSize(-1),
description: 'Textgröße verringern'
});
}
navigateTo(route: string): void {
console.log(`Navigation zu: ${route}`);
// Implementieren Sie hier die Navigation
}
skipToMainContent(): void {
const mainContent = document.querySelector('main') || document.getElementById('main-content');
if (mainContent) {
mainContent.focus();
// Fokussierbare Elemente hinzufügen, falls erforderlich
if (!mainContent.hasAttribute('tabindex')) {
mainContent.setAttribute('tabindex', '-1');
}
}
}
toggleHighContrast(): void {
document.body.classList.toggle('high-contrast');
console.log('Hoher Kontrast wurde umgeschaltet');
}
changeFontSize(delta: number): void {
const currentSize = parseInt(getComputedStyle(document.documentElement).fontSize, 10);
const newSize = Math.max(12, Math.min(24, currentSize + delta)); // Minimum 12px, Maximum 24px
document.documentElement.style.fontSize = `${newSize}px`;
console.log(`Textgröße geändert auf: ${newSize}px`);
}
}Standardtastenkombinationen berücksichtigen: Vermeiden Sie das Überschreiben von Browser-Standardtastenkombinationen, es sei denn, es ist absolut notwendig.
Konsistenz bewahren: Verwenden Sie in Ihrer gesamten Anwendung konsistente Tastenkombinationen.
Dokumentation bereitstellen: Stellen Sie eine Hilfeseite oder ein Overlay mit allen verfügbaren Tastenkombinationen zur Verfügung.
Kontextbezogene Tastenkombinationen: Aktivieren Sie nur die Tastenkombinationen, die im aktuellen Anwendungskontext sinnvoll sind.
Internationalisierung: Berücksichtigen Sie bei der Auswahl von Tasten unterschiedliche Tastaturlayouts.
Plattformspezifische Anpassungen: Passen Sie Tastenkombinationen an verschiedene Plattformen an (z. B. Command-Taste auf macOS vs. Strg-Taste auf Windows).
Zugänglichkeit: Stellen Sie sicher, dass Tastenkombinationen die Zugänglichkeit verbessern, nicht beeinträchtigen.
Performance: Vermeiden Sie zu viele globale Tastenkombinationen, da dies die Leistung beeinträchtigen kann.