PageSourceSearch

https://classaction.cv/app/js/ui/modals.js

js classaction.cv collected 2026-10-07 13:18:31 UTC 5,226 bytes, 171 lines download raw bytes

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.