Logik-Guide für Vokabelabfrage-Komponenten
Dieser Leitfaden dokumentiert die technische Architektur und Implementierungsdetails für alle Vokabelabfrage-Komponenten im Hermeneus-System. Er basiert auf der Analyse bestehender Komponenten und definiert Standards für zukünftige Entwicklungen.
Ziel
Alle Vokabelabfrage-Komponenten sollen nach einheitlichen Patterns entwickelt werden, um Wartbarkeit, Testbarkeit und Konsistenz zu gewährleisten.
1. Komponenten-Architektur
1.1 Basis-Struktur
Jede Vokabelabfrage-Komponente folgt diesem grundlegenden Aufbau:
vue
<template>
<div class="[komponent-name] bg-white rounded-xl p-8 shadow-xl border border-grey-100">
<!-- Hauptvokabel-Anzeige -->
<section class="vokabel-header">
<!-- Gegebenes Wort & Info -->
</section>
<!-- Anweisungen -->
<section class="instructions mb-8">
<!-- Benutzer-Anweisungen -->
</section>
<!-- Interaktionsbereich -->
<section class="interaction-area" role="region" aria-label="Aufgabenbereich">
<!-- Spezifische Interaktionselemente -->
</section>
<!-- Feedback-Bereich -->
<section v-if="showFeedback" class="feedback-area mt-8" role="alert" aria-live="assertive">
<!-- Ergebnis-Feedback -->
</section>
<!-- Keyboard Hints -->
<section class="keyboard-hints mt-4" v-if="!isAnswered">
<!-- Tastatur-Navigation -->
</section>
<!-- Screen Reader Announcements -->
<div class="sr-only" aria-live="polite" aria-atomic="true">
<!-- Dynamische Ankündigungen -->
</div>
</div>
</template>1.2 Name Convention
Komponenten-Namen:
- Datei:
[UebungTyp]VokabelAbfrageComponent.vue - Vue Name:
[uebung-typ]-vokabel-abfrage-component
Beispiele:
UebungBedeutungenZuordnenVokabelAbfrageComponent.vueUebungVokabelmosaikVokabelAbfrageComponent.vue
2. Props-Definition
2.1 Pflicht-Props
Jede Vokabelabfrage-Komponente MUSS diese Props akzeptieren:
javascript
props: {
// Hauptdatenstruktur - zentrale Datenquelle
contentDataMixedElement: {
type: Object,
required: true,
validator(value) {
return value &&
typeof value.metadata === 'object' &&
typeof value.content_data === 'object' &&
value.metadata.hasOwnProperty('order') &&
value.metadata.hasOwnProperty('uebung_type');
}
},
// Index der aktuellen Vokabel (für Reset-Mechanismus)
vokabelIndex: {
type: Number,
required: true,
default: 0
},
// Gesamtanzahl der Vokabeln (für Kontext)
totalCount: {
type: Number,
required: false,
default: 1
},
// Optionale Konfiguration für Komponentenverhalten
config: {
type: Object,
required: false,
default: () => ({
autoSubmit: true, // Automatisch absenden nach Auswahl
showKeyboardHints: true, // Tastatur-Hinweise anzeigen
feedbackDuration: 2000, // Feedback-Anzeigedauer in ms
allowRetry: false // Erneute Versuche erlauben
})
}
}2.2 ContentDataMixedElement-Struktur
Die wichtigste Prop folgt diesem exakten Schema:
typescript
interface ContentDataMixedElement {
metadata: {
order: number; // Position in der Übungssequenz
uebung_type: string; // z.B. "uebung-bedeutungen-zuordnen"
};
content_data: {
value_given: string; // Die zu übersetzende/bearbeitende Vokabel
value_input: string; // Aktuelle Benutzereingabe (meist leer)
value_expected: string; // Die korrekte Antwort
value_given_info?: string; // Zusätzliche Informationen (optional)
// Typ-spezifische Felder:
value_available?: string[]; // Multiple-Choice Optionen
value_segments?: string[]; // Wort-Segmente zum Zusammensetzen
// ... weitere typ-spezifische Felder
};
evaluation_data: null | object; // Auswertungsergebnisse nach Submission
}3. Data Properties
3.1 Standard Data Structure
Jede Komponente sollte mindestens diese Datenstruktur haben:
javascript
data() {
return {
// === Zustandsmanagement ===
isAnswered: false, // Wurde bereits geantwortet?
showFeedback: false, // Feedback aktuell sichtbar?
feedbackMessage: '', // Feedback-Text
feedbackType: '', // 'success' | 'error'
// === Interaktions-Zustand ===
selectedOption: null, // Aktuell gewählte Option (Multiple Choice)
hoveredOption: null, // Gehoverte Option (für Keyboard Navigation)
userInput: '', // Benutzereingabe (bei Text-Input)
assembledWord: '', // Zusammengesetztes Wort (bei Mosaik)
// === UI-Zustand ===
isLoading: false, // Lade-Zustand
animationKey: 0, // Für Vue-Animationen (force re-render)
// === Tastatur-Navigation ===
keyboardSelectedIndex: -1, // Index für Keyboard-Navigation
availableOptions: [], // Cache für navigierbare Optionen
// === Feedback-Timer ===
feedbackTimer: null // Timer für Auto-Hide
}
}3.2 Computed Properties
Pflicht-Computed:
javascript
computed: {
// Extrahiert das gegebene Wort aus Props
valueGiven() {
return this.contentDataMixedElement?.content_data?.value_given || '';
},
// Extrahiert die erwartete Antwort
valueExpected() {
return this.contentDataMixedElement?.content_data?.value_expected || '';
},
// Extrahiert zusätzliche Wort-Informationen
valueGivenInfo() {
return this.contentDataMixedElement?.content_data?.value_given_info || '';
},
// Prüft ob zusätzliche Info verfügbar ist
hasInfo() {
return !!this.valueGivenInfo && this.valueGivenInfo.trim().length > 0;
},
// Aktueller Übungstyp aus Metadata
uebungType() {
return this.contentDataMixedElement?.metadata?.uebung_type || '';
},
// Ist Komponente bereit für Interaktion?
isReady() {
return !!this.valueGiven && !this.isLoading;
},
// CSS-Klassen für den Container
containerClasses() {
return {
[`${this.uebungType}-container`]: true,
'answered': this.isAnswered,
'loading': this.isLoading
};
}
}4. Lifecycle & Watchers
4.1 Mounted Hook
javascript
mounted() {
// Initialisierung nach DOM-Mount
this.initializeComponent();
// Event Listeners registrieren
this.registerEventListeners();
// Keyboard Navigation setup
if (this.config.showKeyboardHints) {
this.setupKeyboardNavigation();
}
},4.2 Kritische Watchers
ZWINGEND ERFORDERLICH:
javascript
watch: {
// Reset bei Vokabel-Index-Änderung
vokabelIndex: {
handler(newIndex, oldIndex) {
if (newIndex !== oldIndex) {
this.resetComponentState();
}
},
immediate: false
},
// Reset bei Content-Änderung (Double Safety)
valueGiven: {
handler(newValue, oldValue) {
if (newValue !== oldValue && oldValue !== undefined) {
this.resetComponentState();
}
},
immediate: false
},
// Feedback Auto-Hide
showFeedback: {
handler(show) {
if (show && this.config.feedbackDuration > 0) {
this.startFeedbackTimer();
}
}
}
}4.3 BeforeUnmount Hook
javascript
beforeUnmount() {
// Timer cleanup
if (this.feedbackTimer) {
clearTimeout(this.feedbackTimer);
}
// Event Listeners entfernen
this.removeEventListeners();
}5. Methods - Kernfunktionen
5.1 State Management
javascript
methods: {
/**
* Reset der kompletten Komponente für neue Vokabel
* KRITISCH: Muss alle Zustandsvariablen zurücksetzen
*/
resetComponentState() {
// Antwort-Zustand
this.isAnswered = false;
this.showFeedback = false;
this.feedbackMessage = '';
this.feedbackType = '';
// Interaktions-Zustand
this.selectedOption = null;
this.hoveredOption = null;
this.userInput = '';
this.assembledWord = '';
// UI-Zustand
this.isLoading = false;
this.animationKey++;
// Keyboard Navigation
this.keyboardSelectedIndex = -1;
this.resetKeyboardNavigation();
// Timer cleanup
if (this.feedbackTimer) {
clearTimeout(this.feedbackTimer);
this.feedbackTimer = null;
}
// Komponenten-spezifisches Reset
this.resetSpecificState();
},
/**
* Überschreibbar für komponenten-spezifisches Reset
*/
resetSpecificState() {
// Implementierung in Kind-Komponente
// z.B. Drag&Drop Bereiche zurücksetzen
}
}5.2 Antwort-Verarbeitung
javascript
methods: {
/**
* Hauptmethode für Antwort-Submission
* @param {any} answer - Die gegebene Antwort
*/
submitAnswer(answer) {
if (this.isAnswered || !this.isReady) return;
// Zustand setzen
this.isAnswered = true;
this.isLoading = true;
// Antwort evaluieren
const isCorrect = this.evaluateAnswer(answer);
// Feedback vorbereiten
this.prepareFeedback(isCorrect, answer);
// Event emittieren
this.$emit('answer-submitted', {
answer,
isCorrect,
valueExpected: this.valueExpected,
metadata: this.contentDataMixedElement.metadata
});
// Loading beenden
this.isLoading = false;
// Auto-progression
if (this.config.autoSubmit && isCorrect) {
this.scheduleNextVokabel();
}
},
/**
* Evaluiert die gegebene Antwort
* @param {any} answer
* @returns {boolean}
*/
evaluateAnswer(answer) {
// Normalisierung für String-Vergleich
const normalize = (str) =>
str.toString().trim().toLowerCase()
.replace(/[^\w\s]/g, '')
.replace(/\s+/g, ' ');
return normalize(answer) === normalize(this.valueExpected);
},
/**
* Bereitet Feedback basierend auf Antwort vor
* @param {boolean} isCorrect
* @param {any} userAnswer
*/
prepareFeedback(isCorrect, userAnswer) {
this.feedbackType = isCorrect ? 'success' : 'error';
if (isCorrect) {
this.feedbackMessage = 'Richtig!';
} else {
this.feedbackMessage = 'Falsch';
}
// Feedback anzeigen
this.showFeedback = true;
// Screen Reader Announcement
this.announceToScreenReader(
isCorrect
? `Richtig! ${this.valueExpected}`
: `Falsch. Die richtige Antwort ist ${this.valueExpected}. Du hattest ${userAnswer}.`
);
}
}5.3 Accessibility Support
javascript
methods: {
/**
* Screen Reader Announcement
* @param {string} message
*/
announceToScreenReader(message) {
// Temporäres Element für Screen Reader erstellen
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'assertive');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
},
/**
* Fokus setzen für Accessibility
* @param {string} elementSelector
*/
setAccessibleFocus(elementSelector) {
this.$nextTick(() => {
const element = this.$el.querySelector(elementSelector);
if (element) {
element.focus();
}
});
}
}6. Keyboard Navigation
6.1 Setup & Event Handling
javascript
methods: {
/**
* Keyboard Navigation initialisieren
*/
setupKeyboardNavigation() {
document.addEventListener('keydown', this.handleKeyPress);
this.updateNavigableElements();
},
/**
* Event Listeners entfernen
*/
removeEventListeners() {
document.removeEventListener('keydown', this.handleKeyPress);
},
/**
* Haupt-Keyboard Handler
* @param {KeyboardEvent} event
*/
handleKeyPress(event) {
if (this.isAnswered || !this.isReady) return;
switch(event.key) {
case 'ArrowUp':
case 'ArrowLeft':
event.preventDefault();
this.navigateUp();
break;
case 'ArrowDown':
case 'ArrowRight':
event.preventDefault();
this.navigateDown();
break;
case 'Enter':
case ' ':
event.preventDefault();
this.selectCurrentOption();
break;
case 'Escape':
event.preventDefault();
this.resetSelection();
break;
default:
// Zahlen-Navigation (1-4 für Multiple Choice)
this.handleNumberKey(event);
}
},
/**
* Navigation nach oben/links
*/
navigateUp() {
if (this.availableOptions.length === 0) return;
this.keyboardSelectedIndex = this.keyboardSelectedIndex <= 0
? this.availableOptions.length - 1
: this.keyboardSelectedIndex - 1;
this.updateHoveredOption();
},
/**
* Navigation nach unten/rechts
*/
navigateDown() {
if (this.availableOptions.length === 0) return;
this.keyboardSelectedIndex = this.keyboardSelectedIndex >= this.availableOptions.length - 1
? 0
: this.keyboardSelectedIndex + 1;
this.updateHoveredOption();
}
}7. Event System
7.1 Emitted Events
Jede Komponente MUSS diese Events emittieren:
javascript
// Bei Antwort-Submission
this.$emit('answer-submitted', {
answer: userAnswer,
isCorrect: boolean,
valueExpected: string,
metadata: object,
timeSpent: number // Zeit in Millisekunden
});
// Bei Komponenten-Reset
this.$emit('component-reset', {
vokabelIndex: number,
metadata: object
});
// Bei Zustandsänderungen
this.$emit('state-changed', {
oldState: string,
newState: string,
metadata: object
});
// Bei Fehlern
this.$emit('component-error', {
error: Error,
context: string,
metadata: object
});7.2 Event Listeners
javascript
// Globaler Event Bus (optional)
created() {
// Für komponentenübergreifende Kommunikation
this.$eventBus?.on('vokabel-skip', this.handleSkip);
this.$eventBus?.on('vokabel-hint', this.showHint);
},
beforeUnmount() {
this.$eventBus?.off('vokabel-skip', this.handleSkip);
this.$eventBus?.off('vokabel-hint', this.showHint);
}8. Error Handling
8.1 Error Boundaries
javascript
methods: {
/**
* Zentrale Error-Behandlung
* @param {Error} error
* @param {string} context
*/
handleComponentError(error, context) {
console.error(`[${this.$options.name}] Error in ${context}:`, error);
// Error Event emittieren
this.$emit('component-error', {
error,
context,
metadata: this.contentDataMixedElement?.metadata
});
// Fallback-Zustand
this.isLoading = false;
this.showFeedback = false;
// User-friendly Fehlermeldung
this.showErrorFeedback('Ein Fehler ist aufgetreten. Bitte versuche es erneut.');
},
/**
* Zeigt Benutzer-freundliche Fehlermeldung
* @param {string} message
*/
showErrorFeedback(message) {
this.feedbackType = 'error';
this.feedbackMessage = message;
this.showFeedback = true;
}
}8.2 Validation
javascript
methods: {
/**
* Validiert Props beim Mount/Update
* @returns {boolean}
*/
validateProps() {
if (!this.contentDataMixedElement) {
this.handleComponentError(
new Error('contentDataMixedElement is required'),
'prop-validation'
);
return false;
}
const { content_data } = this.contentDataMixedElement;
if (!content_data?.value_given) {
this.handleComponentError(
new Error('value_given is required in content_data'),
'prop-validation'
);
return false;
}
return true;
}
}9. Performance Optimierung
9.1 Debouncing & Throttling
javascript
// Import debounce utility
import { debounce } from 'lodash-es';
export default {
created() {
// Debounced methods für häufige Updates
this.debouncedUpdateUI = debounce(this.updateUI, 150);
this.debouncedValidateInput = debounce(this.validateInput, 300);
},
methods: {
/**
* Optimierte UI-Updates
*/
updateUI() {
// Schwere DOM-Operationen hier
},
/**
* Input-Validation mit Debouncing
*/
validateInput() {
// Validation logic
}
}
}9.2 Memory Management
javascript
beforeUnmount() {
// Timer cleanup
if (this.feedbackTimer) {
clearTimeout(this.feedbackTimer);
}
// Event Listeners
this.removeEventListeners();
// Debounced functions
this.debouncedUpdateUI?.cancel();
this.debouncedValidateInput?.cancel();
// Clear references
this.availableOptions = null;
this.feedbackTimer = null;
}