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
text/typescript
/**
* @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');
}
}