Skip to content

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.vue
  • UebungVokabelmosaikVokabelAbfrageComponent.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;
}