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.
468 lines (393 loc) • 19.5 kB
text/typescript
/**
* @moduleName: Settings Menu Component - User Preferences & Configuration UI
* @version: 1.0.0
* @since: 2025-07-23
* @lastUpdated: 2025-07-27
* @projectSummary: Enhanced MCP Quiz Server - Settings dropdown modal with user preferences and configuration management
* @techStack: TypeScript, Component Architecture, DOM Events, LocalStorage Integration
* @dependency: Component base class, SettingsService, AppSettings interface
* @interModuleDependency: SettingsService for persistence, header dropdown UI, z-index management
* @requirementsTraceability:
* {@link Requirements.REQ_UI_003} (Advanced Settings & Configuration Interface)
* @briefDescription: Component managing comprehensive settings dropdown with UI preferences, quiz defaults, and reset functionality
* @methods: render, bindEvents, updateSettingsUI, toggleDropdown, resetSettings, saveSettings
* @contributors: Claude Code Agent, GitHub Copilot, Original Architecture Team
* @examples:
* - const settingsMenu = new SettingsMenu()
* - settingsMenu.render() // Initializes settings modal functionality
* @vulnerabilitiesAssessment: Settings validation, XSS prevention via controlled inputs, secure localStorage usage
*/
import { AppSettings, SettingsService } from '../services/SettingsService';
import { Component } from './Component';
export class SettingsMenu extends Component {
private settingsService: SettingsService;
private menuButton: HTMLButtonElement;
private dropdown: HTMLElement;
private closeButton: HTMLButtonElement;
private unsubscribe: (() => void) | null = null;
constructor() {
super('#settings-dropdown');
this.settingsService = SettingsService.getInstance();
this.menuButton = document.querySelector('#settings-menu-btn') as HTMLButtonElement;
this.dropdown = document.querySelector('#settings-dropdown') as HTMLElement;
this.closeButton = document.querySelector('#close-settings-menu') as HTMLButtonElement;
}
protected onMount(): void {
this.unsubscribe = this.settingsService.subscribe(settings => this.onSettingsChange(settings));
this.updateUI();
}
protected onUnmount(): void {
this.unsubscribe?.();
}
protected bindEvents(): void {
// Menu toggle
this.menuButton?.addEventListener('click', this.toggleMenu.bind(this));
this.closeButton?.addEventListener('click', this.closeMenu.bind(this));
// Escape key to close
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && !this.dropdown.classList.contains('hidden')) {
this.closeMenu();
}
});
// Click outside to close
document.addEventListener('click', e => {
if (!this.element.contains(e.target as Node) && !this.menuButton.contains(e.target as Node)) {
this.closeMenu();
}
});
// Settings controls
this.bindSettingsControls();
}
private bindSettingsControls(): void {
// Hide disabled nav buttons
const hideNavButtons = document.querySelector('#hide-nav-buttons') as HTMLInputElement;
hideNavButtons?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateUISettings({ hideDisabledNavButtons: target.checked });
});
// Hide all navigation buttons
const hideAllNavButtons = document.querySelector('#hide-all-nav-buttons') as HTMLInputElement;
hideAllNavButtons?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateUISettings({ hideAllNavButtons: target.checked });
});
// Click to advance
const clickToAdvance = document.querySelector('#click-to-advance') as HTMLInputElement;
clickToAdvance?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateUISettings({ clickToAdvance: target.checked });
});
// Auto-advance questions
const autoAdvance = document.querySelector('#auto-advance') as HTMLInputElement;
autoAdvance?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateUISettings({ autoAdvanceQuestions: target.checked });
});
// Enhanced auto-advance (FIX: Critical Gap 5 - Settings UI)
const autoAdvanceEnabled = document.querySelector('#auto-advance-enabled') as HTMLInputElement;
autoAdvanceEnabled?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateQuizSettings({ autoAdvanceEnabled: target.checked });
});
// Auto-advance delay
const autoAdvanceDelay = document.querySelector('#auto-advance-delay') as HTMLSelectElement;
autoAdvanceDelay?.addEventListener('change', e => {
const target = e.target as HTMLSelectElement;
this.settingsService.updateQuizSettings({ autoAdvanceDelay: parseInt(target.value) });
});
// Note: Auto-start timer moved to QuizStartModal for better UX
// Smart submit logic
const smartSubmitLogic = document.querySelector('#smart-submit-logic') as HTMLInputElement;
smartSubmitLogic?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateQuizSettings({ smartSubmitLogic: target.checked });
});
// Show question numbers
const showQuestionNumbers = document.querySelector(
'#show-question-numbers'
) as HTMLInputElement;
showQuestionNumbers?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateUISettings({ showQuestionNumbers: target.checked });
});
// Default view mode
const defaultViewMode = document.querySelector('#default-view-mode') as HTMLSelectElement;
defaultViewMode?.addEventListener('change', e => {
const target = e.target as HTMLSelectElement;
this.settingsService.updateUISettings({
defaultViewMode: target.value as 'single' | 'list',
});
});
// Enable animations
const enableAnimations = document.querySelector('#enable-animations') as HTMLInputElement;
enableAnimations?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateUISettings({ enableAnimations: target.checked });
});
// ISSUE_020: Click-to-select answer cards
const clickToSelectCards = document.querySelector('#click-to-select-cards') as HTMLInputElement;
clickToSelectCards?.addEventListener('change', e => {
const target = e.target as HTMLInputElement;
this.settingsService.updateUISettings({ clickToSelectCards: target.checked });
console.log('🎯 Click-to-select cards setting updated:', target.checked);
});
// NEW: Unified Quiz Mode Control
const quizMode = document.querySelector('#quiz-mode') as HTMLSelectElement;
quizMode?.addEventListener('change', e => {
const target = e.target as HTMLSelectElement;
const mode = target.value as 'traditional' | 'cards' | 'instant';
// Update unified setting
this.settingsService.updateUISettings({ quizMode: mode });
// Update legacy settings for compatibility
this.settingsService.updateUISettings({
clickToSelectCards: mode === 'cards',
});
this.settingsService.updateQuizSettings({
instantSubmission: mode === 'instant',
});
console.log('🎮 Quiz mode updated:', mode);
});
// NEW: Unified Auto-advance Control
const autoAdvanceMode = document.querySelector('#auto-advance-mode') as HTMLSelectElement;
autoAdvanceMode?.addEventListener('change', e => {
const target = e.target as HTMLSelectElement;
const mode = target.value as 'off' | 'basic' | 'enhanced' | 'custom';
// Update unified setting
this.settingsService.updateQuizSettings({ autoAdvanceMode: mode });
// Show/hide custom delay control
const customDelayRow = document.querySelector('#custom-delay-row') as HTMLElement;
if (customDelayRow) {
customDelayRow.style.display = mode === 'custom' ? 'flex' : 'none';
}
// Update legacy settings for compatibility
const autoAdvanceEnabled = mode !== 'off';
this.settingsService.updateQuizSettings({ autoAdvanceEnabled });
this.settingsService.updateUISettings({ autoAdvanceQuestions: autoAdvanceEnabled });
// Set appropriate delay
let delay = 3000; // default
if (mode === 'basic') delay = 2000;
else if (mode === 'enhanced') delay = 3000;
this.settingsService.updateQuizSettings({ autoAdvanceDelay: delay });
console.log('⚡ Auto-advance mode updated:', mode, 'delay:', delay);
});
// NEW: Custom delay slider
const autoAdvanceDelaySlider = document.querySelector(
'#auto-advance-delay-slider'
) as HTMLInputElement;
const delayValue = document.querySelector('#delay-value') as HTMLElement;
autoAdvanceDelaySlider?.addEventListener('input', e => {
const target = e.target as HTMLInputElement;
const seconds = parseInt(target.value);
const delay = seconds * 1000;
// Update display
if (delayValue) {
delayValue.textContent = `${seconds} second${seconds !== 1 ? 's' : ''}`;
}
// Update setting
this.settingsService.updateQuizSettings({ autoAdvanceDelay: delay });
console.log('⏱️ Custom auto-advance delay updated:', delay);
});
// Reset settings
const resetSettings = document.querySelector('#reset-settings') as HTMLButtonElement;
resetSettings?.addEventListener('click', () => {
if (confirm('Reset all settings to defaults?')) {
this.settingsService.resetToDefaults();
}
});
}
private toggleMenu(): void {
if (this.dropdown.classList.contains('hidden')) {
this.openMenu();
} else {
this.closeMenu();
}
}
/**
* Opens the settings dropdown menu with navigation button visibility fix.
*
* @description Opens the settings modal while ensuring navigation buttons remain
* visible and accessible. Fixes ISSUE_008_MODAL by properly managing
* z-index layering between modal and navigation controls.
*
* @example
* ```typescript
* // Called when settings button is clicked
* this.openMenu();
* // Settings modal opens, navigation buttons stay visible
* ```
*
* @since 2025-07-24 (Enhanced from original implementation)
* @author Claude Code Agent
* @requirements REQ-008 (Settings Modal Navigation Bug Fix)
* @bugfix Resolves navigation buttons disappearing when settings modal opens
*/
private openMenu(): void {
this.dropdown.classList.remove('hidden');
// ISSUE_008_MODAL FIX: Ensure navigation buttons remain accessible
// Target the specific navigation container more precisely
const navigationContainer = document
.querySelector('#prev-btn')
?.closest('div.bg-white.border-t') as HTMLElement;
if (navigationContainer) {
// Settings dropdown is z-50, so navigation needs to be higher
navigationContainer.style.zIndex = '60';
navigationContainer.style.position = 'relative';
console.log('✅ Settings modal opened - navigation buttons z-index preserved');
} else {
console.warn('⚠️ Navigation container not found - buttons may be hidden');
}
// Also ensure individual navigation buttons are accessible
const prevBtn = document.querySelector('#prev-btn') as HTMLElement;
const nextBtn = document.querySelector('#next-btn') as HTMLElement;
const submitBtn = document.querySelector('#submit-button') as HTMLElement;
[prevBtn, nextBtn, submitBtn].forEach(btn => {
if (btn) {
btn.style.zIndex = '65'; // Even higher priority
btn.style.position = 'relative';
}
});
this.updateUI();
}
/**
* Closes the settings dropdown menu and resets navigation button styling.
*
* @description Hides the settings modal and restores normal z-index values
* for navigation buttons and their container. Also refreshes
* document title to restore progress tracking if quiz is active.
*
* @example
* ```typescript
* // Called when close button clicked or escape pressed
* this.closeMenu();
* // Settings modal closes, navigation buttons return to normal z-index
* ```
*
* @since 2025-07-24 (Enhanced from original implementation)
* @author Claude Code Agent
* @requirements REQ-008 (Settings Modal Navigation Bug Fix)
*/
private closeMenu(): void {
this.dropdown.classList.add('hidden');
// ISSUE_008_MODAL FIX: Reset navigation container z-index
const navigationContainer = document
.querySelector('#prev-btn')
?.closest('div.bg-white.border-t') as HTMLElement;
if (navigationContainer) {
navigationContainer.style.zIndex = '';
navigationContainer.style.position = '';
console.log('✅ Settings modal closed - navigation z-index reset');
}
// Reset individual navigation button z-index
const prevBtn = document.querySelector('#prev-btn') as HTMLElement;
const nextBtn = document.querySelector('#next-btn') as HTMLElement;
const submitBtn = document.querySelector('#submit-button') as HTMLElement;
[prevBtn, nextBtn, submitBtn].forEach(btn => {
if (btn) {
btn.style.zIndex = '';
btn.style.position = '';
}
});
// PROGRESS_TRACKING_FIX: Refresh document title when modal closes
// This allows progress tracking to resume updating the browser title
setTimeout(() => {
this.refreshProgressTitle();
}, 100);
}
/**
* Refresh progress title by triggering ProgressTracker update.
* This is called when settings modal closes to restore quiz progress in title.
*/
private refreshProgressTitle(): void {
// Check if QuizApp exists in global context and has QuizContent component
const quizApp = (window as any).__quizApp;
if (quizApp && quizApp.components && quizApp.components.quizContent) {
const quizContent = quizApp.components.quizContent;
// Access ProgressTracker through QuizContent's getProgressTracker method
const progressTracker = quizContent.getProgressTracker && quizContent.getProgressTracker();
if (progressTracker && typeof progressTracker.refreshDocumentTitle === 'function') {
progressTracker.refreshDocumentTitle();
console.log('🔄 Progress title refreshed after settings modal close');
}
}
}
private onSettingsChange(settings: AppSettings): void {
console.log('🔄 SettingsMenu: Settings changed, updating UI...');
this.updateUI();
}
private updateUI(): void {
const settings = this.settingsService.getSettings();
// Update checkbox states
const hideNavButtons = document.querySelector('#hide-nav-buttons') as HTMLInputElement;
if (hideNavButtons) hideNavButtons.checked = settings.ui.hideDisabledNavButtons;
const hideAllNavButtons = document.querySelector('#hide-all-nav-buttons') as HTMLInputElement;
if (hideAllNavButtons) hideAllNavButtons.checked = settings.ui.hideAllNavButtons;
const clickToAdvance = document.querySelector('#click-to-advance') as HTMLInputElement;
if (clickToAdvance) clickToAdvance.checked = settings.ui.clickToAdvance;
const autoAdvance = document.querySelector('#auto-advance') as HTMLInputElement;
if (autoAdvance) autoAdvance.checked = settings.ui.autoAdvanceQuestions;
// Enhanced auto-advance settings (FIX: Critical Gap 5 - Settings UI)
const autoAdvanceEnabled = document.querySelector('#auto-advance-enabled') as HTMLInputElement;
if (autoAdvanceEnabled) autoAdvanceEnabled.checked = settings.quiz.autoAdvanceEnabled;
const autoAdvanceDelay = document.querySelector('#auto-advance-delay') as HTMLSelectElement;
if (autoAdvanceDelay) autoAdvanceDelay.value = settings.quiz.autoAdvanceDelay.toString();
// Note: Auto-start timer settings now handled in QuizStartModal
const smartSubmitLogic = document.querySelector('#smart-submit-logic') as HTMLInputElement;
if (smartSubmitLogic) smartSubmitLogic.checked = settings.quiz.smartSubmitLogic;
const showQuestionNumbers = document.querySelector(
'#show-question-numbers'
) as HTMLInputElement;
if (showQuestionNumbers) showQuestionNumbers.checked = settings.ui.showQuestionNumbers;
const enableAnimations = document.querySelector('#enable-animations') as HTMLInputElement;
if (enableAnimations) enableAnimations.checked = settings.ui.enableAnimations;
// ISSUE_020: Click-to-select answer cards
const clickToSelectCards = document.querySelector('#click-to-select-cards') as HTMLInputElement;
if (clickToSelectCards) clickToSelectCards.checked = settings.ui.clickToSelectCards;
// NEW: Load unified controls
const quizMode = document.querySelector('#quiz-mode') as HTMLSelectElement;
if (quizMode) {
// Set quiz mode based on current settings (with fallback logic)
let mode = settings.ui.quizMode || 'cards'; // default fallback
if (settings.quiz.instantSubmission) mode = 'instant';
else if (settings.ui.clickToSelectCards) mode = 'cards';
else mode = 'traditional';
quizMode.value = mode;
}
const autoAdvanceMode = document.querySelector('#auto-advance-mode') as HTMLSelectElement;
const customDelayRow = document.querySelector('#custom-delay-row') as HTMLElement;
const autoAdvanceDelaySlider = document.querySelector(
'#auto-advance-delay-slider'
) as HTMLInputElement;
const delayValue = document.querySelector('#delay-value') as HTMLElement;
if (autoAdvanceMode) {
// Determine auto-advance mode based on current settings
let mode = settings.quiz.autoAdvanceMode || 'enhanced'; // default fallback
if (!settings.quiz.autoAdvanceEnabled) {
mode = 'off';
} else if (settings.quiz.autoAdvanceDelay === 2000) {
mode = 'basic';
} else if (settings.quiz.autoAdvanceDelay === 3000) {
mode = 'enhanced';
} else {
mode = 'custom';
}
autoAdvanceMode.value = mode;
// Show/hide custom delay control
if (customDelayRow) {
customDelayRow.style.display = mode === 'custom' ? 'flex' : 'none';
}
// Update custom delay slider and display
if (autoAdvanceDelaySlider && delayValue) {
const seconds = Math.round(settings.quiz.autoAdvanceDelay / 1000);
autoAdvanceDelaySlider.value = seconds.toString();
delayValue.textContent = `${seconds} second${seconds !== 1 ? 's' : ''}`;
}
}
// Update select values
const defaultViewMode = document.querySelector('#default-view-mode') as HTMLSelectElement;
if (defaultViewMode) defaultViewMode.value = settings.ui.defaultViewMode;
}
protected render(): void {
// UI is rendered in HTML, just update state
this.updateUI();
}
}