Testing347 commited on
Commit
6b1bbc5
·
verified ·
1 Parent(s): 52247e1

Update assets/site.js

Browse files
Files changed (1) hide show
  1. assets/site.js +145 -41
assets/site.js CHANGED
@@ -1,13 +1,35 @@
1
- <!-- assets/site.js -->
2
- <script>
 
3
  (function () {
 
 
4
  function q(id) { return document.getElementById(id); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  function trapFocus(modal) {
7
- const focusable = modal.querySelectorAll(
8
- 'a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])'
 
9
  );
10
  if (!focusable.length) return;
 
11
  const first = focusable[0];
12
  const last = focusable[focusable.length - 1];
13
 
@@ -19,32 +41,60 @@
19
  if (document.activeElement === last) { e.preventDefault(); first.focus(); }
20
  }
21
  } else if (e.key === 'Escape') {
22
- window.SilentPattern.toggleModal(modal, false);
23
  }
24
  }
 
25
  modal.addEventListener('keydown', handler);
26
- modal._focusHandler = handler;
27
  }
28
 
29
  function untrapFocus(modal) {
30
- if (modal._focusHandler) {
31
- modal.removeEventListener('keydown', modal._focusHandler);
32
- delete modal._focusHandler;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
  }
35
 
36
  function toggleModal(modal, show) {
37
  if (!modal) return;
 
38
  if (show) {
 
39
  modal.classList.remove('modal-hidden');
40
  modal.classList.add('modal-visible');
41
- document.body.style.overflow = 'hidden';
42
- setTimeout(() => { modal.focus(); trapFocus(modal); }, 0);
 
 
 
 
 
 
 
 
43
  } else {
44
  modal.classList.remove('modal-visible');
45
  modal.classList.add('modal-hidden');
46
- document.body.style.overflow = '';
47
  untrapFocus(modal);
 
 
48
  }
49
  }
50
 
@@ -52,8 +102,11 @@
52
  const el = q('vanta-bg');
53
  if (!el || !window.VANTA || !window.VANTA.NET) return null;
54
 
 
 
 
55
  const fx = window.VANTA.NET({
56
- el: "#vanta-bg",
57
  mouseControls: true,
58
  touchControls: true,
59
  gyroControls: false,
@@ -68,12 +121,19 @@
68
  spacing: 15.0
69
  });
70
 
71
- window.addEventListener('resize', () => fx.resize());
 
 
 
 
 
72
  return fx;
73
  }
74
 
75
  function setupAccessModal() {
76
  const accessModal = q('access-modal');
 
 
77
  const accessBtn = q('access-btn');
78
  const accessCta = q('access-cta');
79
  const closeAccessModal = q('close-access-modal');
@@ -89,12 +149,17 @@
89
  if (accessBtn) accessBtn.addEventListener('click', openAccess);
90
  if (accessCta) accessCta.addEventListener('click', openAccess);
91
  if (closeAccessModal) closeAccessModal.addEventListener('click', () => toggleModal(accessModal, false));
92
- if (accessModal) accessModal.addEventListener('click', (e) => { if (e.target === accessModal) toggleModal(accessModal, false); });
 
 
 
 
93
 
94
  const form = q('access-form');
95
  if (form) {
96
  form.addEventListener('submit', async (e) => {
97
  e.preventDefault();
 
98
  const name = (q('name')?.value || '').trim();
99
  const email = (q('email')?.value || '').trim();
100
  const institution = (q('institution')?.value || '').trim();
@@ -105,7 +170,7 @@
105
  return;
106
  }
107
 
108
- // Optional: post to server if present; otherwise just acknowledge.
109
  try {
110
  const res = await fetch('/api/access', {
111
  method: 'POST',
@@ -118,26 +183,31 @@
118
  alert('Request received. You will be contacted after review.');
119
  }
120
 
121
- form.reset();
122
  toggleModal(accessModal, false);
123
  });
124
  }
125
 
126
- return { accessModal };
127
  }
128
 
129
  function setupLabNavigator(dossiers, defaultKey) {
130
  const labNav = q('lab-navigator');
 
 
131
  const labNavBtn = q('lab-nav-btn');
132
  const labNavClose = q('lab-nav-close');
133
 
134
- function openLabNav() { toggleModal(labNav, true); setTimeout(() => labNav?.focus(), 0); }
135
  function closeLabNav() { toggleModal(labNav, false); }
136
 
137
  if (labNavBtn) labNavBtn.addEventListener('click', openLabNav);
138
  if (labNavClose) labNavClose.addEventListener('click', closeLabNav);
139
- if (labNav) labNav.addEventListener('click', (e) => {
140
- const shouldClose = e.target && e.target.getAttribute('data-lab-close') === 'true';
 
 
 
141
  if (shouldClose) closeLabNav();
142
  });
143
 
@@ -151,7 +221,7 @@
151
  const dossierMeta = q('dossier-meta');
152
 
153
  function renderDossier(key) {
154
- const d = dossiers?.[key];
155
  if (!d) return;
156
 
157
  if (dossierTitle) dossierTitle.textContent = d.title || 'Lab Dossier';
@@ -161,12 +231,14 @@
161
 
162
  if (dossierEvidence) {
163
  dossierEvidence.innerHTML = '';
164
- (d.evidence || []).forEach(item => {
165
- const li = document.createElement('li');
166
- li.textContent = item;
167
- dossierEvidence.appendChild(li);
168
- });
169
- if (!(d.evidence || []).length) {
 
 
170
  const li = document.createElement('li');
171
  li.className = 'text-gray-500';
172
  li.textContent = 'No evidence items provided.';
@@ -175,26 +247,33 @@
175
  }
176
 
177
  if (dossierPrimary) {
178
- dossierPrimary.textContent = d.primary?.label || 'Open';
179
- dossierPrimary.onclick = d.primary?.action || null;
180
  }
181
  if (dossierSecondary) {
182
- dossierSecondary.textContent = d.secondary?.label || 'View Note';
183
- dossierSecondary.onclick = d.secondary?.action || null;
184
  }
 
185
  if (dossierMeta) {
186
  const u = d.updated || '—';
187
  dossierMeta.innerHTML = `Last updated: <span class="text-gray-300">${u}</span>`;
188
  }
189
  }
190
 
191
- document.querySelectorAll('.lab-node').forEach(btn => {
192
- btn.addEventListener('click', () => renderDossier(btn.getAttribute('data-dossier')));
 
 
 
193
  });
194
 
 
195
  document.addEventListener('keydown', (e) => {
196
  if (e.key !== 'Escape') return;
 
197
  const accessModal = q('access-modal');
 
198
  if (labNav && !labNav.classList.contains('modal-hidden')) closeLabNav();
199
  if (accessModal && !accessModal.classList.contains('modal-hidden')) toggleModal(accessModal, false);
200
  });
@@ -203,11 +282,36 @@
203
  return { openLabNav, closeLabNav, renderDossier, labNav };
204
  }
205
 
206
- window.SilentPattern = {
207
- toggleModal,
208
- initVanta,
209
- setupAccessModal,
210
- setupLabNavigator
211
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  })();
213
- </script>
 
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
+
33
  const first = focusable[0];
34
  const last = focusable[focusable.length - 1];
35
 
 
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
 
74
  function toggleModal(modal, show) {
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
 
 
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
  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');
 
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();
 
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',
 
183
  alert('Request received. You will be contacted after review.');
184
  }
185
 
186
+ try { form.reset(); } catch {}
187
  toggleModal(accessModal, false);
188
  });
189
  }
190
 
191
+ return { accessModal, openAccess };
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
 
 
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';
 
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.';
 
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
  });
 
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
  })();