Skip to content

Commit 764c0f0

Browse files
Merge pull request steam-bell-92#946 from nishtha-agarwal-211/refactor/874-modular-mainjs
refactor(web): modularize main.js into focused ES6 modules (steam-bell-92#874)
2 parents 4a9ca31 + 73cb94e commit 764c0f0

10 files changed

Lines changed: 2870 additions & 1828 deletions

File tree

web-app/games.html

Lines changed: 571 additions & 228 deletions
Large diffs are not rendered by default.

web-app/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ <h3>Legal</h3>
820820
<!-- CodeMirror 6 – syntax-highlighted Python editor (local bundle) -->
821821
<script src="js/cm-editor.js"></script>
822822
<script src="js/playground.js"></script>
823-
<script src="js/main.js"></script>
823+
<script type="module" src="js/main.js"></script>
824824
<script src="js/hero-canvas.js"></script>
825825
<script>lucide.createIcons();</script>
826826
<script src="js/fix.js"></script>

web-app/js/main.js

Lines changed: 496 additions & 1247 deletions
Large diffs are not rendered by default.

web-app/js/modules/modal.js

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
modal.js - Project launcher modal dialogue and instructions helper.
3+
*/
4+
5+
import { safeRun } from "./utils.js";
6+
7+
let lastFocusedElement = null;
8+
let removeTrap = null;
9+
let currentProjectName = "";
10+
11+
function getFocusableElements(root) {
12+
const sel =
13+
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
14+
return Array.from(root.querySelectorAll(sel)).filter((el) => {
15+
return (
16+
!el.closest('[aria-hidden="true"]') &&
17+
!el.classList.contains("visually-hidden")
18+
);
19+
});
20+
}
21+
22+
function trapFocus(modalEl) {
23+
const handler = function (e) {
24+
if (e.key !== "Tab" || !modalEl.classList.contains("active")) return;
25+
const focusables = getFocusableElements(modalEl);
26+
if (!focusables.length) return;
27+
const first = focusables[0];
28+
const last = focusables[focusables.length - 1];
29+
if (e.shiftKey && document.activeElement === first) {
30+
e.preventDefault();
31+
last.focus({ preventScroll: true });
32+
} else if (!e.shiftKey && document.activeElement === last) {
33+
e.preventDefault();
34+
first.focus({ preventScroll: true });
35+
}
36+
};
37+
document.addEventListener("keydown", handler, true);
38+
return function () {
39+
document.removeEventListener("keydown", handler, true);
40+
};
41+
}
42+
43+
function setMainInert(isInert) {
44+
const main = document.getElementById("main-content");
45+
if (!main) return;
46+
if (isInert) main.setAttribute("inert", "");
47+
else main.removeAttribute("inert");
48+
}
49+
50+
export function showInfoModal(title, steps) {
51+
const overlay = document.getElementById("infoModalOverlay");
52+
const titleEl = document.getElementById("infoModalTitle");
53+
const listEl = document.getElementById("infoModalList");
54+
55+
if (!overlay || !titleEl || !listEl) return;
56+
57+
titleEl.textContent = title;
58+
listEl.innerHTML = steps.map((step) => "<li>" + step + "</li>").join("");
59+
60+
overlay.classList.add("active");
61+
62+
const closeBtn = document.getElementById("infoModalClose");
63+
const gotItBtn = document.getElementById("infoModalGotIt");
64+
65+
function closeModal() {
66+
overlay.classList.remove("active");
67+
closeBtn?.removeEventListener("click", closeModal);
68+
gotItBtn?.removeEventListener("click", closeModal);
69+
overlay.removeEventListener("click", overlayClick);
70+
}
71+
72+
function overlayClick(e) {
73+
if (e.target === overlay) closeModal();
74+
}
75+
76+
closeBtn?.addEventListener("click", closeModal);
77+
gotItBtn?.addEventListener("click", closeModal);
78+
overlay.addEventListener("click", overlayClick);
79+
}
80+
81+
export function setupModalInfoButton(projectName) {
82+
currentProjectName = projectName;
83+
const infoBtn = document.getElementById("modalInfoBtn");
84+
if (!infoBtn) return;
85+
86+
// Remove old listener by cloning
87+
const newBtn = infoBtn.cloneNode(true);
88+
infoBtn.parentNode.replaceChild(newBtn, infoBtn);
89+
90+
newBtn.addEventListener("click", function () {
91+
if (typeof window.getProjectInstructions === "function") {
92+
const info = window.getProjectInstructions(currentProjectName);
93+
showInfoModal(info.title, info.steps);
94+
}
95+
});
96+
}
97+
98+
export function openProjectSafe(name, trigger) {
99+
const modal = document.getElementById("projectModal");
100+
const modalBody = document.getElementById("modalBody");
101+
const modalClose = document.getElementById("modalClose");
102+
103+
if (!modal || !modalBody) return;
104+
lastFocusedElement = trigger || document.activeElement;
105+
modal.classList.add("active");
106+
modal.setAttribute("aria-hidden", "false");
107+
const scrollbarWidth =
108+
window.innerWidth - document.documentElement.clientWidth;
109+
document.body.style.paddingRight = scrollbarWidth + "px";
110+
document.body.style.overflow = "hidden";
111+
setMainInert(true);
112+
113+
safeRun(function () {
114+
if (typeof window.getProjectHTML === "function") {
115+
modalBody.innerHTML =
116+
window.getProjectHTML(name) ||
117+
'<div style="padding:1rem;color:var(--text-secondary)">Project content unavailable.</div>';
118+
} else {
119+
modalBody.innerHTML =
120+
'<div style="padding:1rem;color:var(--text-secondary)">Project content unavailable.</div>';
121+
}
122+
if (typeof window.initializeProject === "function")
123+
window.initializeProject(name);
124+
setupModalInfoButton(name);
125+
126+
// Inject info button next to the title (works for all projects)
127+
const projectContent = modalBody.querySelector(".project-content");
128+
if (projectContent) {
129+
let firstHeading = projectContent.querySelector(
130+
"h2, h3, .resume-analyzer-copy h2, .pet-title"
131+
);
132+
if (!firstHeading) {
133+
firstHeading = projectContent.querySelector(
134+
'[class*="title"], [class*="header"] h2'
135+
);
136+
}
137+
138+
if (firstHeading && !projectContent.querySelector(".inline-info-btn")) {
139+
const infoBtn = document.createElement("button");
140+
infoBtn.className = "inline-info-btn";
141+
infoBtn.innerHTML = "ⓘ";
142+
infoBtn.setAttribute("aria-label", "How to use this project");
143+
144+
infoBtn.style.marginLeft = "12px";
145+
infoBtn.style.background = "none";
146+
infoBtn.style.border = "none";
147+
infoBtn.style.fontSize = "1.3rem";
148+
infoBtn.style.cursor = "pointer";
149+
infoBtn.style.color = "var(--accent)";
150+
infoBtn.style.verticalAlign = "middle";
151+
152+
infoBtn.addEventListener("click", function (e) {
153+
e.stopPropagation();
154+
if (typeof window.getProjectInstructions === "function") {
155+
const info = window.getProjectInstructions(name);
156+
showInfoModal(info.title, info.steps);
157+
}
158+
});
159+
160+
if (firstHeading.style.display !== "inline-block") {
161+
firstHeading.style.display = "inline-block";
162+
}
163+
firstHeading.appendChild(infoBtn);
164+
}
165+
}
166+
});
167+
168+
removeTrap = trapFocus(modal);
169+
const focusables = getFocusableElements(modalBody);
170+
const firstFocusable = focusables[0] || modalClose;
171+
if (firstFocusable && typeof firstFocusable.focus === "function") {
172+
firstFocusable.focus({ preventScroll: true });
173+
}
174+
}
175+
176+
export function closeProjectSafe() {
177+
const modal = document.getElementById("projectModal");
178+
const modalBody = document.getElementById("modalBody");
179+
180+
if (!modal || !modal.classList.contains("active")) return;
181+
modal.classList.remove("active");
182+
modal.setAttribute("aria-hidden", "true");
183+
document.body.style.paddingRight = "";
184+
document.body.style.overflow = "";
185+
setMainInert(false);
186+
if (removeTrap) {
187+
removeTrap();
188+
removeTrap = null;
189+
}
190+
if (modalBody) {
191+
modalBody.innerHTML = "";
192+
}
193+
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
194+
lastFocusedElement.focus({ preventScroll: true });
195+
}
196+
lastFocusedElement = null;
197+
198+
// Dispatch custom event to notify orchestration that modal closed
199+
const event = new CustomEvent("projectModalClosed");
200+
document.dispatchEvent(event);
201+
}
202+
203+
export function initModal() {
204+
const modal = document.getElementById("projectModal");
205+
const modalClose = document.getElementById("modalClose");
206+
207+
if (modalClose) modalClose.addEventListener("click", closeProjectSafe);
208+
if (modal) {
209+
modal.addEventListener("click", function (e) {
210+
if (e.target === modal) closeProjectSafe();
211+
});
212+
}
213+
document.addEventListener("keydown", function (e) {
214+
if (e.key === "Escape") closeProjectSafe();
215+
});
216+
217+
// Expose openProjectSafe and closeProjectSafe on window object for legacy references
218+
window.openProjectSafe = openProjectSafe;
219+
window.closeProjectSafe = closeProjectSafe;
220+
}

0 commit comments

Comments
 (0)