UNPKG

mcp-quiz-server

Version:

๐Ÿง  AI-Powered Quiz Management via Model Context Protocol (MCP) - Create, manage, and take quizzes directly from VS Code, Claude, and other AI agents.

267 lines (226 loc) โ€ข 8.56 kB
/** * @moduleName: NavigationController * @version: 2.0.0 * @since: 2025-07-26 * @lastUpdated: 2025-07-26 * @projectSummary: Enhanced MCP Quiz Server - Navigation Management System * @techStack: TypeScript, DOM API, Event Management * @dependency: EventManagement, AppStore, SettingsService * @interModuleDependency: EventManagement for cleanup, AppStore for state management * @requirementsTraceability: * {@link Requirements.REQ_UI_002} (Dual View Mode System - Navigation Controls) * {@link Requirements.REQ_PERF_007} (Memory Management and Cleanup) * {@link Requirements.REQ_A11Y_002} (Keyboard Navigation and Accessibility) * {@link Requirements.REQ_LOG_001} (Frontend Component Logging) * @briefDescription: Handles quiz navigation including continue/complete buttons with infinite recursion fix * @methods: setupNavigation, handleNextQuestion, handleSubmit, bindContinueButton, bindCompleteQuizButton * @contributors: GitHub Copilot * @examples: * - const nav = new NavigationController(store, settings); nav.setupNavigation(); * @vulnerabilitiesAssessment: Memory leak prevention through proper event cleanup, no sensitive data */ import { SettingsService } from '../../services/SettingsService'; import { AppStore } from '../../store/AppStore'; import { EventManagement } from './EventManagement'; export class NavigationController { private store: AppStore; private settingsService: SettingsService; private currentQuestionIndex: number = 0; constructor(store: AppStore, settingsService: SettingsService) { this.store = store; this.settingsService = settingsService; // Listen for navigation events document.addEventListener('quiz:next-question', this.handleNextQuestion.bind(this)); } /** * Set up navigation buttons with proper event cleanup * CRITICAL: This fixes ISSUE_021 infinite recursion bug */ setupNavigation(): void { console.log('๐Ÿงญ Setting up navigation controls'); // Clean up any existing navigation listeners to prevent infinite recursion EventManagement.cleanup(['continue-button', 'complete-quiz-button']); // Set up continue button if it exists this.bindContinueButton(); // Set up complete quiz button if it exists this.bindCompleteQuizButton(); console.log('โœ… Navigation setup complete - infinite recursion prevented'); } /** * Bind continue button with proper cleanup * CRITICAL FIX: Prevents duplicate event listeners that caused infinite recursion */ private bindContinueButton(): void { const continueButton = document.getElementById('continue-btn') as HTMLButtonElement; if (continueButton) { // Use EventManagement to prevent duplicate listeners EventManagement.addListener( continueButton, 'click', () => this.handleNextQuestion(), 'continue-button-click' ); // Keyboard support EventManagement.addListener( continueButton, 'keydown', (e: Event) => { const keyEvent = e as KeyboardEvent; if (keyEvent.key === 'Enter' || keyEvent.key === ' ') { keyEvent.preventDefault(); this.handleNextQuestion(); } }, 'continue-button-keyboard' ); console.log('๐Ÿ”— Continue button bound successfully'); } } /** * Bind complete quiz button with proper cleanup * CRITICAL FIX: Prevents duplicate event listeners that caused infinite recursion */ private bindCompleteQuizButton(): void { const completeQuizButton = document.getElementById('complete-quiz-btn') as HTMLButtonElement; if (completeQuizButton) { // Use EventManagement to prevent duplicate listeners EventManagement.addListener( completeQuizButton, 'click', () => { console.log('๐Ÿ Complete Quiz button clicked from single mode'); this.handleSubmit(); }, 'complete-quiz-button-click' ); // Keyboard support EventManagement.addListener( completeQuizButton, 'keydown', (e: Event) => { const keyEvent = e as KeyboardEvent; if (keyEvent.key === 'Enter' || keyEvent.key === ' ') { keyEvent.preventDefault(); console.log('๐Ÿ Complete Quiz button (keyboard) from single mode'); this.handleSubmit(); } }, 'complete-quiz-button-keyboard' ); console.log('๐Ÿ”— Complete quiz button bound successfully'); } } /** * Handle next question navigation * CRITICAL: This method was part of the infinite recursion loop */ private handleNextQuestion(): void { console.log('โžก๏ธ Handling next question navigation'); const state = this.store.getState(); const quiz = state.currentQuiz; if (!quiz) { console.error('๐Ÿšจ No current quiz available for navigation'); return; } // Check if we're at the last question if (this.currentQuestionIndex >= quiz.questions.length - 1) { console.log('๐Ÿ At last question, triggering quiz completion'); this.handleSubmit(); return; } // Move to next question this.currentQuestionIndex++; console.log(`๐Ÿ“– Moving to question ${this.currentQuestionIndex + 1}/${quiz.questions.length}`); // Trigger re-render through custom event (breaks recursion cycle) const renderEvent = new CustomEvent('quiz:render-question', { detail: { questionIndex: this.currentQuestionIndex }, }); document.dispatchEvent(renderEvent); } /** * Handle quiz submission */ private async handleSubmit(): Promise<void> { console.log('๐Ÿ“ Handling quiz submission'); try { const result = await this.store.submitQuiz(); if (result) { console.log('โœ… Quiz submitted successfully'); // Trigger results display const resultsEvent = new CustomEvent('quiz:show-results'); document.dispatchEvent(resultsEvent); } else { console.warn('โš ๏ธ Quiz submission failed - may be incomplete'); } } catch (error) { console.error('๐Ÿšจ Error during quiz submission:', error); } } /** * Set current question index (used by parent component) */ setCurrentQuestionIndex(index: number): void { this.currentQuestionIndex = index; console.log(`๐Ÿ“ Question index set to: ${index}`); } /** * Get current question index */ getCurrentQuestionIndex(): number { return this.currentQuestionIndex; } /** * Check if we're at the last question */ isLastQuestion(): boolean { const state = this.store.getState(); const quiz = state.currentQuiz; return quiz ? this.currentQuestionIndex >= quiz.questions.length - 1 : false; } /** * Navigate to specific question */ goToQuestion(index: number): void { const state = this.store.getState(); const quiz = state.currentQuiz; if (!quiz || index < 0 || index >= quiz.questions.length) { console.error('๐Ÿšจ Invalid question index:', index); return; } this.currentQuestionIndex = index; console.log(`๐ŸŽฏ Navigating to question ${index + 1}/${quiz.questions.length}`); // Trigger re-render const renderEvent = new CustomEvent('quiz:render-question', { detail: { questionIndex: this.currentQuestionIndex }, }); document.dispatchEvent(renderEvent); } /** * Navigate to previous question */ goToPreviousQuestion(): void { if (this.currentQuestionIndex > 0) { this.goToQuestion(this.currentQuestionIndex - 1); } } /** * Navigate to next question */ goToNextQuestion(): void { const state = this.store.getState(); const quiz = state.currentQuiz; if (quiz && this.currentQuestionIndex < quiz.questions.length - 1) { this.goToQuestion(this.currentQuestionIndex + 1); } } /** * Clean up navigation controller */ cleanup(): void { EventManagement.componentCleanup('continue-'); EventManagement.componentCleanup('complete-quiz-'); // Remove custom event listeners document.removeEventListener('quiz:next-question', this.handleNextQuestion.bind(this)); console.log('๐Ÿงน NavigationController cleanup completed'); } }