1/** 2 * new/js/ui/modals.js 3 * 4 * Generic modal open/close helpers and the progress-modal overlay. 5 * 6 * Modals are simple visibility toggle patterns with backdrop-click handling. 7 * Keep this layer minimal â feature-specific logic delegated to callers. 8 * 9 * Source: 10 * online/index.html lines 11433â11460 (openNtfyModal, closeNtfyModal) 11 * online/index.html lines 13571â13710 (openProgressModal, closeProgressModal) 12 * online/index.html lines 9851â9920 (showCourseSelector, closeCourseSelector) 13 * online/index.html lines 3219â3345 (progress-modal-overlay, .progress-modal CSS) 14 * 15 * Step 28 â Phase 4 16 * Assigned to: Codex 17 * Status: Implemented 18 */ 19 20// --------------------------------------------------------------------------- 21// Generic modal primitives 22// --------------------------------------------------------------------------- 23 24/** 25 * Open a modal by ID (add 'visible' class). 26 * @param {string} modalId - Element ID of the overlay or modal to reveal 27 */ 28export function openModal(modalId) { 29 const el = document.getElementById(modalId); 30 if (el) { 31 el.classList.add('visible'); 32 lockBodyScroll(); 33 } 34} 35 36/** 37 * Close a modal by ID (remove 'visible' class). 38 * @param {string} modalId - Element ID of the overlay or modal to hide 39 */ 40export function closeModal(modalId) { 41 const el = document.getElementById(modalId); 42 if (el) { 43 el.classList.remove('visible'); 44 unlockBodyScroll(); 45 } 46} 47 48/** 49 * Check if a modal is currently visible. 50 * @param {string} modalId - Element ID to check 51 * @returns {boolean} 52 */ 53export function isModalOpen(modalId) { 54 const el = document.getElementById(modalId); 55 return el ? el.classList.contains('visible') : false; 56} 57 58/** 59 * Toggle modal visibility. 60 * @param {string} modalId - Element ID to toggle 61 */ 62export function toggleModal(modalId) { 63 if (isModalOpen(modalId)) { 64 closeModal(modalId); 65 } else { 66 openModal(modalId); 67 } 68} 69 70/** 71 * Handle backdrop click: close modal if click is on overlay itself, not inner modal. 72 * Intended for delegated click handlers or direct listener callbacks. 73 * @param {Event} event - Click event 74 * @param {string} modalId - Overlay/modal ID to close 75 */ 76export function handleBackdropClick(event, modalId) { 77 const overlay = document.getElementById(modalId); 78 if (overlay && event.target === overlay) { 79 closeModal(modalId); 80 } 81} 82 83// --------------------------------------------------------------------------- 84// Scroll lock helpers (for modal open/close) 85// Prevent body scroll when modals are active. 86// --------------------------------------------------------------------------- 87 88let scrollLockCount = 0; 89 90function lockBodyScroll() { 91 scrollLockCount++; 92 if (scrollLockCount === 1) { 93 document.body.style.overflow = 'hidden'; 94 } 95} 96 97function unlockBodyScroll() { 98 scrollLockCount = Math.max(0, scrollLockCount - 1); 99 if (scrollLockCount === 0) { 100 document.body.style.overflow = ''; 101 } 102} 103 104// --------------------------------------------------------------------------- 105// Progress modal specific controls 106// Separate from generic modal controls because it has additional state. 107// --------------------------------------------------------------------------- 108 109/** 110 * Open the progress modal (semester overview). 111 * Renders progress list and shows the overlay. 112 * Requires: lessonArchive and appConfig.classCount in global scope 113 * (this is called from existing shell code). 114 */ 115export function openProgressModal() { 116 const overlay = document.getElementById('progressModalOverlay'); 117 const body = document.getElementById('progressModalBody'); 118 if (!overlay || !body) return; 119 120 // This will be called by the existing shell code which handles rendering 121 openModal('progressModalOverlay'); 122} 123 124/** 125 * Close the progress modal. 126 * Safety check: only close if click is on the backdrop, not the modal window. 127 * @param {Event} [event] - Optional click event to validate backdrop click 128 */ 129export function closeProgressModal(event) { 130 const overlay = document.getElementById('progressModalOverlay'); 131 if (!overlay) return; 132 if (event && event.target !== overlay) return; 133 closeModal('progressModalOverlay'); 134 const saveBar = document.getElementById('progressSaveBar'); 135 if (saveBar) saveBar.classList.remove('visible'); 136} 137 138// --------------------------------------------------------------------------- 139// Module init (called once on app boot) 140// --------------------------------------------------------------------------- 141 142/**
143 * Initialize modal infrastructure. 144 * Set up modal event handlers and global state. 145 * @param {object} [options] - Initialization options (unused for now) 146 */ 147export function initModals(options = {}) { 148 if (initModals._bound) return; 149 initModals._bound = true; 150 151 // Install global ESC key handler for modals 152 document.addEventListener('keydown', (e) => { 153 if (e.key === 'Escape') { 154 const visibleModals = [ 155 'courseSelectorOverlay', 156 'progressModalOverlay', 157 'ntfyOverlay', 158 'profileOverlay', 159 ]; 160 for (const modalId of visibleModals) { 161 if (isModalOpen(modalId)) { 162 closeModal(modalId); 163 e.preventDefault(); 164 break; 165 } 166 } 167 } 168 }); 169 170 void options; 171}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.