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.

468 lines (393 loc) 19.5 kB
/** * @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(); } }