Testing347 commited on
Commit
7540be3
·
verified ·
1 Parent(s): 495b706

Update assets/site.js

Browse files
Files changed (1) hide show
  1. assets/site.js +112 -177
assets/site.js CHANGED
@@ -1,32 +1,20 @@
1
  /* assets/site.js
2
- SILENTPATTERN — shared site behavior (pure JS file; no <script> tags)
 
3
  */
 
4
  (function () {
5
- 'use strict';
6
 
7
  function q(id) { return document.getElementById(id); }
8
- function qs(sel, root) { return (root || document).querySelector(sel); }
9
- function qsa(sel, root) { return Array.from((root || document).querySelectorAll(sel)); }
10
-
11
- // Namespace
12
- const SilentPattern = (window.SilentPattern = window.SilentPattern || {});
13
 
14
- // Internal: remember focus + restore on close
15
- function rememberActiveElement(modal) {
16
- try { modal._sp_prevActive = document.activeElement; } catch { /* ignore */ }
17
- }
18
- function restoreActiveElement(modal) {
19
- const prev = modal && modal._sp_prevActive;
20
- if (prev && typeof prev.focus === 'function') {
21
- setTimeout(() => { try { prev.focus(); } catch {} }, 0);
22
- }
23
- if (modal) delete modal._sp_prevActive;
24
  }
25
 
26
  function trapFocus(modal) {
27
- const focusable = qsa(
28
- 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])',
29
- modal
30
  );
31
  if (!focusable.length) return;
32
 
@@ -34,40 +22,26 @@
34
  const last = focusable[focusable.length - 1];
35
 
36
  function handler(e) {
37
- if (e.key === 'Tab') {
38
  if (e.shiftKey) {
39
  if (document.activeElement === first) { e.preventDefault(); last.focus(); }
40
  } else {
41
  if (document.activeElement === last) { e.preventDefault(); first.focus(); }
42
  }
43
- } else if (e.key === 'Escape') {
 
44
  toggleModal(modal, false);
45
  }
46
  }
47
 
48
- modal.addEventListener('keydown', handler);
49
- modal._sp_focusHandler = handler;
50
  }
51
 
52
  function untrapFocus(modal) {
53
- if (modal && modal._sp_focusHandler) {
54
- modal.removeEventListener('keydown', modal._sp_focusHandler);
55
- delete modal._sp_focusHandler;
56
- }
57
- }
58
-
59
- // Scroll lock that won't stomp existing inline styles
60
- let _prevOverflow = null;
61
- function lockScroll() {
62
- if (_prevOverflow === null) _prevOverflow = document.body.style.overflow || '';
63
- document.body.style.overflow = 'hidden';
64
- }
65
- function unlockScroll() {
66
- if (_prevOverflow !== null) {
67
- document.body.style.overflow = _prevOverflow;
68
- _prevOverflow = null;
69
- } else {
70
- document.body.style.overflow = '';
71
  }
72
  }
73
 
@@ -75,38 +49,31 @@
75
  if (!modal) return;
76
 
77
  if (show) {
78
- rememberActiveElement(modal);
79
- modal.classList.remove('modal-hidden');
80
- modal.classList.add('modal-visible');
81
- modal.setAttribute('aria-hidden', 'false');
82
- lockScroll();
83
-
84
- // Ensure modal is focusable
85
- if (!modal.hasAttribute('tabindex')) modal.setAttribute('tabindex', '-1');
86
 
 
87
  setTimeout(() => {
88
  try { modal.focus(); } catch {}
89
  trapFocus(modal);
90
  }, 0);
91
  } else {
92
- modal.classList.remove('modal-visible');
93
- modal.classList.add('modal-hidden');
94
- modal.setAttribute('aria-hidden', 'true');
 
95
  untrapFocus(modal);
96
- unlockScroll();
97
- restoreActiveElement(modal);
98
  }
99
  }
100
 
101
  function initVanta() {
102
- const el = q('vanta-bg');
103
  if (!el || !window.VANTA || !window.VANTA.NET) return null;
104
 
105
- // Avoid double-init if a page re-renders or scripts reload
106
- if (el._sp_vanta) return el._sp_vanta;
107
-
108
  const fx = window.VANTA.NET({
109
- el: el,
110
  mouseControls: true,
111
  touchControls: true,
112
  gyroControls: false,
@@ -121,69 +88,64 @@
121
  spacing: 15.0
122
  });
123
 
124
- el._sp_vanta = fx;
125
-
126
- // Resize handler (defensive)
127
- const onResize = () => { try { fx.resize(); } catch {} };
128
- window.addEventListener('resize', onResize);
129
 
130
  return fx;
131
  }
132
 
133
  function setupAccessModal() {
134
- const accessModal = q('access-modal');
135
- if (!accessModal) return null;
136
-
137
- const accessBtn = q('access-btn');
138
- const accessCta = q('access-cta');
139
- const closeAccessModal = q('close-access-modal');
140
 
141
  function openAccess() {
142
  toggleModal(accessModal, true);
143
  setTimeout(() => {
144
- const name = q('name');
145
  if (name) name.focus();
146
  }, 50);
147
  }
148
 
149
- if (accessBtn) accessBtn.addEventListener('click', openAccess);
150
- if (accessCta) accessCta.addEventListener('click', openAccess);
151
- if (closeAccessModal) closeAccessModal.addEventListener('click', () => toggleModal(accessModal, false));
152
 
153
- // Click outside the dialog content closes (only when clicking overlay)
154
- accessModal.addEventListener('click', (e) => {
155
  if (e.target === accessModal) toggleModal(accessModal, false);
156
  });
157
 
158
- const form = q('access-form');
159
  if (form) {
160
- form.addEventListener('submit', async (e) => {
161
  e.preventDefault();
162
 
163
- const name = (q('name')?.value || '').trim();
164
- const email = (q('email')?.value || '').trim();
165
- const institution = (q('institution')?.value || '').trim();
166
- const purpose = (q('purpose')?.value || '').trim();
167
 
168
  if (!name || !email || !institution || !purpose) {
169
- alert('Please fill in all fields.');
170
  return;
171
  }
172
 
173
- // Best-effort submit; if no backend, still acknowledge.
174
  try {
175
- const res = await fetch('/api/access', {
176
- method: 'POST',
177
- headers: { 'Content-Type': 'application/json' },
178
  body: JSON.stringify({ name, email, institution, purpose, page: location.pathname })
179
  });
180
- if (!res.ok) throw new Error('Request not accepted.');
181
- alert('Request received. You will be contacted after review.');
182
  } catch {
183
- alert('Request received. You will be contacted after review.');
184
  }
185
 
186
- try { form.reset(); } catch {}
187
  toggleModal(accessModal, false);
188
  });
189
  }
@@ -192,126 +154,99 @@
192
  }
193
 
194
  function setupLabNavigator(dossiers, defaultKey) {
195
- const labNav = q('lab-navigator');
196
- if (!labNav) return null;
197
-
198
- const labNavBtn = q('lab-nav-btn');
199
- const labNavClose = q('lab-nav-close');
 
 
 
 
 
 
 
200
 
201
  function openLabNav() { toggleModal(labNav, true); }
202
  function closeLabNav() { toggleModal(labNav, false); }
203
 
204
- if (labNavBtn) labNavBtn.addEventListener('click', openLabNav);
205
- if (labNavClose) labNavClose.addEventListener('click', closeLabNav);
206
-
207
- // Close when clicking the overlay element marked for close
208
- labNav.addEventListener('click', (e) => {
209
- const t = e.target;
210
- const shouldClose = t && typeof t.getAttribute === 'function' && t.getAttribute('data-lab-close') === 'true';
211
- if (shouldClose) closeLabNav();
212
- });
213
 
214
- const dossierTitle = q('dossier-title');
215
- const dossierSubtitle = q('dossier-subtitle');
216
- const dossierStatus = q('dossier-status');
217
- const dossierBody = q('dossier-body');
218
- const dossierEvidence = q('dossier-evidence');
219
- const dossierPrimary = q('dossier-primary');
220
- const dossierSecondary = q('dossier-secondary');
221
- const dossierMeta = q('dossier-meta');
222
 
223
  function renderDossier(key) {
224
  const d = dossiers && dossiers[key];
225
  if (!d) return;
226
 
227
- if (dossierTitle) dossierTitle.textContent = d.title || 'Lab Dossier';
228
- if (dossierSubtitle) dossierSubtitle.textContent = d.subtitle || '';
229
- if (dossierStatus) dossierStatus.textContent = d.status || 'READY';
230
- if (dossierBody) dossierBody.textContent = d.body || '';
231
 
232
  if (dossierEvidence) {
233
- dossierEvidence.innerHTML = '';
234
- const list = Array.isArray(d.evidence) ? d.evidence : [];
235
- if (list.length) {
236
- list.forEach(item => {
237
- const li = document.createElement('li');
238
- li.textContent = String(item);
239
  dossierEvidence.appendChild(li);
240
  });
241
  } else {
242
- const li = document.createElement('li');
243
- li.className = 'text-gray-500';
244
- li.textContent = 'No evidence items provided.';
245
  dossierEvidence.appendChild(li);
246
  }
247
  }
248
 
249
  if (dossierPrimary) {
250
- dossierPrimary.textContent = (d.primary && d.primary.label) ? d.primary.label : 'Open';
251
- dossierPrimary.onclick = (d.primary && typeof d.primary.action === 'function') ? d.primary.action : null;
252
  }
 
253
  if (dossierSecondary) {
254
- dossierSecondary.textContent = (d.secondary && d.secondary.label) ? d.secondary.label : 'View Note';
255
- dossierSecondary.onclick = (d.secondary && typeof d.secondary.action === 'function') ? d.secondary.action : null;
256
  }
257
 
258
  if (dossierMeta) {
259
- const u = d.updated || '';
260
  dossierMeta.innerHTML = `Last updated: <span class="text-gray-300">${u}</span>`;
261
  }
262
  }
263
 
264
- qsa('.lab-node').forEach(btn => {
265
- btn.addEventListener('click', () => {
266
- const key = btn.getAttribute('data-dossier');
267
- if (key) renderDossier(key);
 
268
  });
269
  });
270
 
271
- // Global ESC closes any open modals (navigator + access)
272
- document.addEventListener('keydown', (e) => {
273
- if (e.key !== 'Escape') return;
274
-
275
- const accessModal = q('access-modal');
276
-
277
- if (labNav && !labNav.classList.contains('modal-hidden')) closeLabNav();
278
- if (accessModal && !accessModal.classList.contains('modal-hidden')) toggleModal(accessModal, false);
279
  });
280
 
281
- renderDossier(defaultKey || 'start');
282
  return { openLabNav, closeLabNav, renderDossier, labNav };
283
  }
284
 
285
- // Expose API
286
- SilentPattern.toggleModal = toggleModal;
287
- SilentPattern.initVanta = initVanta;
288
- SilentPattern.setupAccessModal = setupAccessModal;
289
- SilentPattern.setupLabNavigator = setupLabNavigator;
290
-
291
- // Auto-init on DOM ready (so pages stop duplicating init logic)
292
- function boot() {
293
- // Vanta only if present + libs loaded
294
- initVanta();
295
-
296
- // Access modal only if markup exists
297
- const access = setupAccessModal();
298
-
299
- // Lab navigator: requires markup + dossiers provided by page
300
- // Pages should define: window.SilentPatternDossiers = {...}; window.SilentPatternDefaultDossier = 'start';
301
- const dossiers = window.SilentPatternDossiers;
302
- if (dossiers && q('lab-navigator')) {
303
- setupLabNavigator(dossiers, window.SilentPatternDefaultDossier || 'start');
304
- }
305
-
306
- // Optional convenience: expose openAccess if available
307
- if (access && typeof access.openAccess === 'function') {
308
- SilentPattern.openAccess = access.openAccess;
309
- }
310
- }
311
-
312
- if (document.readyState === 'loading') {
313
- document.addEventListener('DOMContentLoaded', boot);
314
- } else {
315
- boot();
316
- }
317
  })();
 
1
  /* assets/site.js
2
+ SILENTPATTERN — shared behaviors (Vanta, modals, lab navigator)
3
+ IMPORTANT: This file must be pure JavaScript (no <script> tags inside).
4
  */
5
+
6
  (function () {
7
+ "use strict";
8
 
9
  function q(id) { return document.getElementById(id); }
 
 
 
 
 
10
 
11
+ function isShown(modal) {
12
+ return modal && !modal.classList.contains("modal-hidden");
 
 
 
 
 
 
 
 
13
  }
14
 
15
  function trapFocus(modal) {
16
+ const focusable = modal.querySelectorAll(
17
+ 'a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])'
 
18
  );
19
  if (!focusable.length) return;
20
 
 
22
  const last = focusable[focusable.length - 1];
23
 
24
  function handler(e) {
25
+ if (e.key === "Tab") {
26
  if (e.shiftKey) {
27
  if (document.activeElement === first) { e.preventDefault(); last.focus(); }
28
  } else {
29
  if (document.activeElement === last) { e.preventDefault(); first.focus(); }
30
  }
31
+ } else if (e.key === "Escape") {
32
+ // Close only THIS modal
33
  toggleModal(modal, false);
34
  }
35
  }
36
 
37
+ modal.addEventListener("keydown", handler);
38
+ modal._focusHandler = handler;
39
  }
40
 
41
  function untrapFocus(modal) {
42
+ if (modal && modal._focusHandler) {
43
+ modal.removeEventListener("keydown", modal._focusHandler);
44
+ delete modal._focusHandler;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  }
46
  }
47
 
 
49
  if (!modal) return;
50
 
51
  if (show) {
52
+ modal.classList.remove("modal-hidden");
53
+ modal.classList.add("modal-visible");
54
+ modal.setAttribute("aria-hidden", "false");
55
+ document.body.style.overflow = "hidden";
 
 
 
 
56
 
57
+ // Ensure focus + focus trap
58
  setTimeout(() => {
59
  try { modal.focus(); } catch {}
60
  trapFocus(modal);
61
  }, 0);
62
  } else {
63
+ modal.classList.remove("modal-visible");
64
+ modal.classList.add("modal-hidden");
65
+ modal.setAttribute("aria-hidden", "true");
66
+ document.body.style.overflow = "";
67
  untrapFocus(modal);
 
 
68
  }
69
  }
70
 
71
  function initVanta() {
72
+ const el = q("vanta-bg");
73
  if (!el || !window.VANTA || !window.VANTA.NET) return null;
74
 
 
 
 
75
  const fx = window.VANTA.NET({
76
+ el: "#vanta-bg",
77
  mouseControls: true,
78
  touchControls: true,
79
  gyroControls: false,
 
88
  spacing: 15.0
89
  });
90
 
91
+ window.addEventListener("resize", () => {
92
+ try { fx.resize(); } catch {}
93
+ });
 
 
94
 
95
  return fx;
96
  }
97
 
98
  function setupAccessModal() {
99
+ const accessModal = q("access-modal");
100
+ const accessBtn = q("access-btn");
101
+ const accessCta = q("access-cta");
102
+ const closeAccessModal = q("close-access-modal");
103
+ const form = q("access-form");
 
104
 
105
  function openAccess() {
106
  toggleModal(accessModal, true);
107
  setTimeout(() => {
108
+ const name = q("name");
109
  if (name) name.focus();
110
  }, 50);
111
  }
112
 
113
+ if (accessBtn) accessBtn.addEventListener("click", openAccess);
114
+ if (accessCta) accessCta.addEventListener("click", openAccess);
 
115
 
116
+ if (closeAccessModal) closeAccessModal.addEventListener("click", () => toggleModal(accessModal, false));
117
+ if (accessModal) accessModal.addEventListener("click", (e) => {
118
  if (e.target === accessModal) toggleModal(accessModal, false);
119
  });
120
 
 
121
  if (form) {
122
+ form.addEventListener("submit", async (e) => {
123
  e.preventDefault();
124
 
125
+ const name = (q("name")?.value || "").trim();
126
+ const email = (q("email")?.value || "").trim();
127
+ const institution = (q("institution")?.value || "").trim();
128
+ const purpose = (q("purpose")?.value || "").trim();
129
 
130
  if (!name || !email || !institution || !purpose) {
131
+ alert("Please fill in all fields.");
132
  return;
133
  }
134
 
135
+ // Optional server post; safe fallback
136
  try {
137
+ const res = await fetch("/api/access", {
138
+ method: "POST",
139
+ headers: { "Content-Type": "application/json" },
140
  body: JSON.stringify({ name, email, institution, purpose, page: location.pathname })
141
  });
142
+ if (!res.ok) throw new Error("Request not accepted.");
143
+ alert("Request received. You will be contacted after review.");
144
  } catch {
145
+ alert("Request received. You will be contacted after review.");
146
  }
147
 
148
+ form.reset();
149
  toggleModal(accessModal, false);
150
  });
151
  }
 
154
  }
155
 
156
  function setupLabNavigator(dossiers, defaultKey) {
157
+ const labNav = q("lab-navigator");
158
+ const labNavBtn = q("lab-nav-btn");
159
+ const labNavClose = q("lab-nav-close");
160
+
161
+ const dossierTitle = q("dossier-title");
162
+ const dossierSubtitle = q("dossier-subtitle");
163
+ const dossierStatus = q("dossier-status");
164
+ const dossierBody = q("dossier-body");
165
+ const dossierEvidence = q("dossier-evidence");
166
+ const dossierPrimary = q("dossier-primary");
167
+ const dossierSecondary = q("dossier-secondary");
168
+ const dossierMeta = q("dossier-meta");
169
 
170
  function openLabNav() { toggleModal(labNav, true); }
171
  function closeLabNav() { toggleModal(labNav, false); }
172
 
173
+ if (labNavBtn) labNavBtn.addEventListener("click", openLabNav);
174
+ if (labNavClose) labNavClose.addEventListener("click", closeLabNav);
 
 
 
 
 
 
 
175
 
176
+ if (labNav) {
177
+ labNav.addEventListener("click", (e) => {
178
+ const shouldClose = e.target && e.target.getAttribute("data-lab-close") === "true";
179
+ if (shouldClose) closeLabNav();
180
+ });
181
+ }
 
 
182
 
183
  function renderDossier(key) {
184
  const d = dossiers && dossiers[key];
185
  if (!d) return;
186
 
187
+ if (dossierTitle) dossierTitle.textContent = d.title || "Lab Dossier";
188
+ if (dossierSubtitle) dossierSubtitle.textContent = d.subtitle || "";
189
+ if (dossierStatus) dossierStatus.textContent = d.status || "READY";
190
+ if (dossierBody) dossierBody.textContent = d.body || "";
191
 
192
  if (dossierEvidence) {
193
+ dossierEvidence.innerHTML = "";
194
+ const items = Array.isArray(d.evidence) ? d.evidence : [];
195
+ if (items.length) {
196
+ items.forEach((item) => {
197
+ const li = document.createElement("li");
198
+ li.textContent = item;
199
  dossierEvidence.appendChild(li);
200
  });
201
  } else {
202
+ const li = document.createElement("li");
203
+ li.className = "text-gray-500";
204
+ li.textContent = "No evidence items provided.";
205
  dossierEvidence.appendChild(li);
206
  }
207
  }
208
 
209
  if (dossierPrimary) {
210
+ dossierPrimary.textContent = d.primary?.label || "Open";
211
+ dossierPrimary.onclick = typeof d.primary?.action === "function" ? d.primary.action : null;
212
  }
213
+
214
  if (dossierSecondary) {
215
+ dossierSecondary.textContent = d.secondary?.label || "View Note";
216
+ dossierSecondary.onclick = typeof d.secondary?.action === "function" ? d.secondary.action : null;
217
  }
218
 
219
  if (dossierMeta) {
220
+ const u = d.updated || "";
221
  dossierMeta.innerHTML = `Last updated: <span class="text-gray-300">${u}</span>`;
222
  }
223
  }
224
 
225
+ // Bind dossier node clicks (if nodes exist on the page)
226
+ document.querySelectorAll(".lab-node").forEach((btn) => {
227
+ btn.addEventListener("click", () => {
228
+ const key = btn.getAttribute("data-dossier");
229
+ renderDossier(key);
230
  });
231
  });
232
 
233
+ // Global ESC behavior: close open modals if present
234
+ document.addEventListener("keydown", (e) => {
235
+ if (e.key !== "Escape") return;
236
+ const accessModal = q("access-modal");
237
+ if (isShown(labNav)) closeLabNav();
238
+ if (isShown(accessModal)) toggleModal(accessModal, false);
 
 
239
  });
240
 
241
+ renderDossier(defaultKey || "start");
242
  return { openLabNav, closeLabNav, renderDossier, labNav };
243
  }
244
 
245
+ // Export public API (library only; no auto-init to avoid duplication)
246
+ window.SilentPattern = {
247
+ toggleModal,
248
+ initVanta,
249
+ setupAccessModal,
250
+ setupLabNavigator
251
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  })();