1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 DORSKY LAB â App Logic 3 WCAG 2.1 AA Compliant 4âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 5 6/* ââ DATA is loaded from data.json at runtime (see DOMContentLoaded below) ââ */ 7 8/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 9 WEBGL HERO CANVAS â Zebrafish particle field 10âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 11function initHeroCanvas() { 12 const canvas = document.getElementById('hero-canvas'); 13 if (!canvas || !window.THREE) return; 14 15 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 16 canvas.style.display = 'none'; 17 return; 18 } 19 20 const renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true }); 21 renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); 22 renderer.setSize(canvas.clientWidth, canvas.clientHeight); 23 24 const scene = new THREE.Scene(); 25 const camera = new THREE.PerspectiveCamera(55, canvas.clientWidth / canvas.clientHeight, 0.1, 100); 26 camera.position.z = 2.5; 27 28 const W = 180, H = 130; 29 const count = W * H; 30 const positions = new Float32Array(count * 3); 31 const colors = new Float32Array(count * 3); 32 const scales = new Float32Array(count); 33 const randoms = new Float32Array(count); 34 35 for (let i = 0; i < W; i++) { 36 for (let j = 0; j < H; j++) { 37 const idx = i * H + j; 38 const u = (i / W - 0.5) * 3.0; 39 const v = (j / H - 0.5) * 2.2; 40 41 const bodyDist = Math.sqrt((u / 1.1) ** 2 + (v / 0.55) ** 2); 42 const tailX = u - 1.0; 43 const tailDist = Math.sqrt((tailX / 0.4) ** 2 + (v / 0.32) ** 2); 44 const mask = bodyDist < 1.0 || (tailDist < 1.0 && u > 0.55); 45 46 positions[idx * 3] = u + (Math.random() - 0.5) * 0.014; 47 positions[idx * 3 + 1] = v + (Math.random() - 0.5) * 0.014; 48 positions[idx * 3 + 2] = 0; 49 50 const t = (u + 1.5) / 3.0; 51 if (mask) { 52 colors[idx * 3] = 0.90 - t * 0.35; 53 colors[idx * 3 + 1] = t * 0.03; 54 colors[idx * 3 + 2] = t * 0.03; 55 } else { 56 colors[idx * 3] = 0.08 + t * 0.03; 57 colors[idx * 3 + 1] = 0.05; 58 colors[idx * 3 + 2] = 0.05; 59 } 60 61 scales[idx] = mask ? 1.2 + Math.random() * 1.0 : 0.1 + Math.random() * 0.08; 62 randoms[idx] = Math.random() * Math.PI * 2; 63 } 64 } 65 66 const geo = new THREE.BufferGeometry(); 67 geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 68 geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 69 geo.setAttribute('aScale', new THREE.BufferAttribute(scales, 1)); 70 geo.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1)); 71 72 const mat = new THREE.ShaderMaterial({ 73 vertexColors: true, transparent: true, depthWrite: false, 74 uniforms: { 75 uTime: { value: 0 }, 76 uMouse: { value: new THREE.Vector2(0, 0) }, 77 uPixelRatio: { value: renderer.getPixelRatio() }, 78 }, 79 vertexShader: ` 80 attribute float aScale; attribute float aRandom; 81 uniform float uTime; uniform vec2 uMouse; uniform float uPixelRatio; 82 varying vec3 vColor; 83 void main() { 84 vColor = color; 85 vec3 pos = position; 86 vec2 toMouse = pos.xy - uMouse * vec2(1.5, 1.1); 87 float dist = length(toMouse); 88 float wave = sin(dist * 5.5 - uTime * 2.2) * 0.05 * (1.0 / (dist * 4.0 + 1.0)); 89 pos.xy += normalize(toMouse + 0.001) * wave; 90 pos.x += sin(uTime * 0.55 + aRandom) * 0.0025; 91 pos.y += cos(uTime * 0.45 + aRandom * 1.2) * 0.0025; 92 gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0
92); 93 gl_PointSize = aScale * 3.2 * uPixelRatio; 94 } 95 `, 96 fragmentShader: ` 97 varying vec3 vColor; 98 void main() { 99 vec2 uv = gl_PointCoord - 0.5; 100 float d = length(uv); 101 float alpha = 1.0 - smoothstep(0.35, 0.5, d); 102 gl_FragColor = vec4(vColor, alpha * 0.94); 103 } 104 ` 105 }); 106 107 const points = new THREE.Points(geo, mat); 108 scene.add(points); 109 110 const mouse = { x: 0, y: 0, tx: 0, ty: 0 }; 111 window.addEventListener('mousemove', (e) => { 112 const rect = canvas.getBoundingClientRect(); 113 mouse.tx = ((e.clientX - rect.left) / rect.width) * 2 - 1; 114 mouse.ty = -((e.clientY - rect.top) / rect.height) * 2 + 1; 115 }); 116 117 window.addEventListener('resize', () => { 118 camera.aspect = canvas.clientWidth / canvas.clientHeight; 119 camera.updateProjectionMatrix(); 120 renderer.setSize(canvas.clientWidth, canvas.clientHeight); 121 }); 122 123 let t = 0, raf; 124 const animate = () => { 125 raf = requestAnimationFrame(animate); 126 t += 0.01; 127 mouse.x += (mouse.tx - mouse.x) * 0.06; 128 mouse.y += (mouse.ty - mouse.y) * 0.06; 129 mat.uniforms.uTime.value = t; 130 mat.uniforms.uMouse.value.set(mouse.x, mouse.y); 131 points.rotation.y = mouse.x * 0.07; 132 points.rotation.x = -mouse.y * 0.04; 133 renderer.render(scene, camera); 134 }; 135 animate(); 136 137 return () => { cancelAnimationFrame(raf); renderer.dispose(); }; 138} 139 140/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 141 LEAFLET MAP 142âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 143function initMap() { 144 if (window._mapInited) return; 145 window._mapInited = true; 146 147 const el = document.getElementById('leaflet-map'); 148 if (!el || !window.L) return; 149 150 const LAB = [40.7649, -111.8359]; 151 const map = L.map('leaflet-map', { center: LAB, zoom: 16, zoomControl: false }); 152 153 L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { 154 attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/">CARTO</a>', 155 maxZoom: 20 156 }).addTo(map); 157 158 const markerHtml = ` 159 <div style="width:40px;height:40px;background:rgba(190,0,0,0.2);border:2px solid #BE0000; 160 border-radius:50%;display:flex;align-items:center;justify-content:center; 161 box-shadow:0 0 24px rgba(190,0,0,0.4);" role="img" aria-label="Dorsky Lab location marker"> 162 <div style="width:10px;height:10px;background:#BE0000;border-radius:50%;"></div> 163 </div>`; 164 165 const icon = L.divIcon({ html: markerHtml, className: '', iconSize: [40, 40], iconAnchor: [20, 20] }); 166 L.marker(LAB, { icon, alt: 'Dorsky Lab, BPRB Building' }).addTo(map) 167 .bindPopup(` 168 <b style="font-size:12px">Dorsky Lab · BPRB</b><br/> 169 20 S 2030 E<br/> 170 Salt Lake City, UT 171 `); 172 L.control.zoom({ position: 'bottomright' }).addTo(map); 173} 174 175/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 176 INTERSECTION OBSERVER â animate-on-scroll 177âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 178function setupReveal() { 179 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 180 document.querySelectorAll('.reveal, .rq-item, .pub-card, .research-area-card, .pi-card') 181 .forEach(el => el.classList.add('visible')); 182 return; 183 } 184 185 const targets = document.querySelectorAll('.reveal, .rq-item, .pub-card, .research-area-card, .pi-card'); 186 const obs = new IntersectionObserver((entries) => {
187 entries.forEach((e, i) => { 188 if (e.isIntersecting) { 189 setTimeout(() => e.target.classList.add('visible'), i * 60); 190 obs.unobserve(e.target); 191 } 192 }); 193 }, { threshold: 0.1 }); 194 targets.forEach(t => obs.observe(t)); 195} 196 197/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 198 BREADCRUMB 199âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 200function renderBreadcrumb(pageName) { 201 if (pageName === 'Home') return ''; 202 return ` 203 <nav aria-label="Breadcrumb" class="breadcrumb"> 204 <ol class="breadcrumb-inner" style="list-style:none;padding:0;margin:0;display:flex;align-items:center;gap:8px;"> 205 <li> 206 <button class="breadcrumb-home-link" onclick="navigate('Home')">Home</button> 207 </li> 208 <li aria-hidden="true" class="breadcrumb-sep">âº</li> 209 <li> 210 <span class="breadcrumb-current" aria-current="page">${pageName}</span> 211 </li> 212 </ol> 213 </nav>`; 214} 215 216/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 217 EXTERNAL LINK HELPER 218âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 219function extLink(href, cls, text) { 220 return `<a href="${href}" target="_blank" rel="noopener noreferrer" class="${cls}">${text}<span class="sr-only"> (opens in new tab)</span></a>`; 221} 222 223/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 224 PAGE RENDERERS 225âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 226 227function renderHome() { 228 return ` 229 <section class="hero" aria-label="Lab introduction"> 230 <div class="hero-left"> 231 <p class="hero-eyebrow" aria-hidden="true">Dorsky Lab · Est. 2001</p> 232 <h1 class="hero-title"> 233 Wnt signaling 234 in the <strong>CNS</strong> 235 </h1> 236 <button class="hero-cta" onclick="navigate('Research')" type="button"> 237 Explore Research 238 <span class="hero-cta-arrow" aria-hidden="true">â</span> 239 </button> 240 </div> 241 <div class="hero-right" style="display:flex;align-items:center;justify-content:center;background:#000;"> 242 <img src="images/fig1.png" alt="Wnt signaling in the central nervous system" 243 style="width:70%;height:auto;max-height:80%;object-fit:contain;"/> 244 </div> 245 </section> 246 `; 247} 248 249function renderResearch() { 250 return ` 251 ${renderBreadcrumb('Research')} 252 <div class="page-banner" role="banner"> 253 <div class="page-banner-inner"> 254 <p class="page-banner-eyebrow" aria-hidden="true">Research Program</p> 255 <h1 class="page-banner-title">We study the function of Wnt-dependent transcription in the zebrafish central nervous system</h1> 256 </div> 257 </div> 258 259 <section class="section" style="padding-top:96px" aria-labelledby="active-areas-heading"> 260 <div class="section-inner"> 261 <div class="reveal"> 262 <span class="label" aria-hidden="true">Active Research Areas</span> 263 <div class="red-rule" aria-hidden="true"></div> 264 <h2 id="active-areas-heading" class="section-title">Current <em>projects</em></h2> 265 </div> 266 <div style="margin-top:48px"> 267 ${DATA.research.areas.map((a, i) => ` 268 <article class="research-area-card" style="transition-delay:${i * 150}ms"> 269 <div class="ra-tag">${a.tag}</div> 270 <h3 class="ra-title">${a.title}</h3> 271 <p class="ra-body">${a.body}</p> 272 <img src="${a.fig}" alt="${a.figAlt}" 273 style="margin-top:28px;max-width:420px;width:100%;border-radius:2px;display:block;" 274 onerror="this.style.display='none'"/>
274 275 </article>`).join('')} 276 </div> 277 </div> 278 </section> 279 `; 280} 281 282function renderMembers() { 283 const pi = DATA.members.pi; 284 return ` 285 ${renderBreadcrumb('Members')} 286 <div class="page-banner" role="banner"> 287 <div class="page-banner-inner" style="display:grid;grid-template-columns:1fr 1fr;align-items:center;"> 288 <div> 289 <p class="page-banner-eyebrow" aria-hidden="true">The Team</p> 290 <h1 class="page-banner-title">Lab <strong><em>Members</em></strong></h1> 291 </div> 292 <div style="display:flex;justify-content:center;align-items:center;"> 293 <img src="images/fig4.png" alt="Dorsky Lab group photo" 294 style="max-width:480px;width:100%;max-height:360px;object-fit:contain;display:block;" 295 onerror="this.style.display='none'"/> 296 </div> 297 </div> 298 </div> 299 300 <section class="section" aria-labelledby="pi-heading"> 301 <div class="section-inner"> 302 <span class="label" aria-hidden="true">Principal Investigator</span> 303 <div class="red-rule" aria-hidden="true"></div> 304 305 <article class="pi-card" aria-label="Principal Investigator profile"> 306 <div class="pi-photo-wrap"> 307 <img src="${pi.img}" alt="Portrait of ${pi.name}, ${pi.role}" 308 onerror="this.style.display='none';this.parentElement.style.background='var(--bg-dark-2)'"/> 309 <div class="pi-photo-overlay" aria-hidden="true"></div> 310 </div> 311 <div class="pi-info"> 312 <div class="pi-badge" aria-hidden="true">Principal Investigator</div> 313 <h2 id="pi-heading" class="pi-name">${pi.name}</h2> 314 <p class="pi-title">${pi.title}</p> 315 <a href="mailto:${pi.email}" class="pi-email">${pi.email}</a> 316 <p class="pi-bio">${pi.bio}</p> 317 </div> 318 </article> 319 320 <section style="margin-top:72px" aria-labelledby="current-members-heading"> 321 <h2 id="current-members-heading" class="label" style="margin-bottom:28px;font-size:inherit">Current Lab Members</h2> 322 <ul class="members-grid" style="list-style:none;padding:0;"> 323 ${DATA.members.current.map((m, i) => ` 324 <li class="member-card reveal" style="transition-delay:${i * 80}ms"> 325 <div class="member-photo-wrap"> 326 ${m.img 327 ? `<img src="${m.img}" alt="Portrait of ${m.name}" 328 onerror="this.style.display='none';this.nextElementSibling.style.display='flex'"/>` 329 : ''} 330 <div class="member-placeholder" style="display:${m.img ? 'none' : 'flex'}" aria-hidden="true"> 331 <span class="member-initials">${m.initials}</span> 332 </div> 333 </div> 334 <div class="member-info"> 335 <p class="member-name">${m.name}</p> 336 <p class="member-role">${m.role}</p> 337 </div> 338 </li>`).join('')} 339 </ul> 340 </section> 341 </div> 342 </section> 343 344 <section class="prev-section" aria-labelledby="prev-heading"> 345 <div class="prev-inner"> 346 <h2 id="prev-heading" class="prev-title reveal">Previous Trainees</h2> 347 <div class="prev-cols"> 348 <div class="reveal"> 349 <h3 class="prev-col-label">Ph.D. Students</h3> 350 <ul style="list-style:none;padding:0;"> 351 ${DATA.members.prevPhDs.map(n => `<li class="prev-name">${n}</li>`).join('')} 352 </ul> 353 </div> 354 <div class="reveal" style="transition-delay:100ms"> 355 <h3 class="prev-col-label">Postdoctoral Researchers</h3> 356 <ul style="list-style:none;padding:0;"> 357 ${DATA.members.prevPostdocs.map(n => `<li class="prev-name">${n}</li>`).join('')} 358 </ul> 359 </div> 360 </div> 361 </div> 362 </section> 363 `; 364} 365 366function renderPublications() { 367 return ` 368 ${renderBreadcrumb('Publications')} 369 <div class="page-banner" style="padding-right:48px;"> 370 <div style="display:flex;align-items:center;justify-content:space-between;gap:48px;flex-wrap:nowrap;width:100%;max-width:var(--max-w);margin:0 auto;"> 371 <div style="flex-shrink:0;"> 372 <p class="page-banner-eyebrow" aria-hidden="true">Scholarship</p> 373 <h1 class="page-banner-title">Selected<br/><strong><em>Publications</em></strong></h1> 374 </div> 375 <img src="images/fig5.png" alt="Publications figure" 376 style="width:480px;height:400px;object-fit:contain;display:block;flex-shrink:0;" 377 onerror="this.style.display='none'"/>
377 378 </div> 379 </div> 380 381 <section class="section" aria-labelledby="pubs-heading"> 382 <div class="section-inner"> 383 <h2 id="pubs-heading" class="sr-only">Publication list</h2> 384 385 <ul id="pub-list" style="list-style:none;padding:0;"> 386 ${DATA.publications.map((p, i) => ` 387 <li class="pub-card" style="transition-delay:${i * 80}ms"> 388 <div class="pub-year">${p.year}</div> 389 <div> 390 <p class="pub-authors">${p.authors}</p> 391 <p class="pub-title">${p.title}</p> 392 <p class="pub-journal"><strong>${p.journal}</strong> · <em>${p.volume}</em></p> 393 </div> 394 </li>`).join('')} 395 </ul> 396 397 <div class="reveal" style="transition-delay:500ms;margin-top:56px"> 398 ${extLink( 399 'http://www.ncbi.nlm.nih.gov/sites/myncbi/1Vk2bUQmb3DQF/bibliography/47575458/public/?sort=date&direction=descending', 400 'pubmed-link', 401 'Full Publication List on PubMed â' 402 )} 403 </div> 404 </div> 405 </section> 406 `; 407} 408 409function renderContact() { 410 return ` 411 ${renderBreadcrumb('Contact')} 412 <div class="page-banner"> 413 <div class="page-banner-inner"> 414 <p class="page-banner-eyebrow" aria-hidden="true">Get in Touch</p> 415 <h1 class="page-banner-title"><em>Contact</em><br/><strong>the Lab</strong></h1> 416 </div> 417 </div> 418 419 <div class="contact-grid"> 420 <section class="contact-info" aria-label="Contact information"> 421 <span class="label" style="color:var(--utah-red)" aria-hidden="true">Principal Investigator</span> 422 <div class="red-rule" aria-hidden="true"></div> 423 <p class="contact-pi-name">Richard Dorsky</p> 424 <a href="mailto:[email protected]" class="contact-email">[email protected]</a> 425 426 <div class="contact-rule" role="separator" aria-hidden="true"></div> 427 428 <div style="margin-bottom:32px"> 429 <p class="addr-label" id="mailing-label">Mailing Address</p> 430 <address class="addr-text" aria-labelledby="mailing-label"> 431 Department of Neurobiology<br/> 432 20 South 2030 East<br/> 433 Bldg. 570 BPRB, Rm. 490E<br/> 434 Salt Lake City, Utah 84112 435 </address> 436 </div> 437 438 <div style="margin-bottom:32px"> 439 <p class="contact-phone" style="margin-top:16px"> 440 Office: <a href="tel:+18015816073" style="color:inherit;text-decoration:underline">(801) 581-6073</a><br/> 441 Lab: <a href="tel:+18015814529" style="color:inherit;text-decoration:underline">(801) 581-4529</a> 442 </p> 443 </div> 444 </section> 445 446 <div class="contact-map-wrap"> 447 <div id="leaflet-map" 448 role="application" 449 aria-label="Interactive map showing Dorsky Lab location at 1795 E. South Campus Drive, Salt Lake City, UT"> 450 </div> 451 </div> 452 </div> 453 `; 454} 455 456/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 457 NAVIGATION & ROUTER 458âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 459function renderNav(activePage) { 460 const pages = ['Home', 'Research', 'Members', 'Publications', 'Contact']; 461 462 return ` 463 <header class="site-header"> 464 <div class="nav-top"> 465 <div class="nav-title-block"> 466 <button class="nav-site-title" onclick="navigate('Home')" type="button" 467 aria-label="Dorsky Lab â go to home page"> 468 Dorsky<span style="color: #d42020;" aria-hidden="true">Lab</span> 469 </button> 470 <span class="nav-title-pipe" aria-hidden="true">|</span> 471 <span class="nav-site-subtitle">Department of Neurobiology</span> 472 </div> 473 <button class="nav-mobile-toggle" type="button" 474 aria-label="Toggle navigation menu" 475 aria-expanded="false" 476 aria-controls="nav-menu" 477 onclick="toggleMobileNav(this)">
478 <span aria-hidden="true">â°</span> 479 </button> 480 </div> 481 <nav aria-label="Primary navigation" id="nav-menu" class="nav-bottom"> 482 ${pages.map(p => ` 483 <a class="nav-link${activePage === p ? ' active' : ''}" 484 href="#" 485 onclick="event.preventDefault(); navigate('${p}')" 486 ${activePage === p ? 'aria-current="page"' : ''} 487 >${p}</a> 488 `).join('')} 489 </nav> 490 </header>`; 491} 492 493function renderFooter() { 494 return ` 495 <footer class="site-footer" aria-label="Site footer"> 496 <div class="footer-top"> 497 <div> 498 <p class="footer-brand-name">Dorsky<span>Lab</span></p> 499 <p class="footer-brand-desc"> 500 Investigating Wnt-mediated CNS neurogenesis using zebrafish as a model organism. 501 </p> 502 </div> 503 <nav aria-label="Footer navigation"> 504 <p class="footer-col-title">Navigation</p> 505 <a class="footer-link" href="#" onclick="event.preventDefault(); navigate('Home')">Home</a> 506 <a class="footer-link" href="#" onclick="event.preventDefault(); navigate('Research')">Research</a> 507 <a class="footer-link" href="#" onclick="event.preventDefault(); navigate('Members')">Lab Members</a> 508 <a class="footer-link" href="#" onclick="event.preventDefault(); navigate('Publications')">Publications</a> 509 <a class="footer-link" href="#" onclick="event.preventDefault(); navigate('Contact')">Contact</a> 510 </nav> 511 <nav aria-label="Department links"> 512 <p class="footer-col-title">Department</p> 513 ${extLink('https://medicine.utah.edu/neurobiology', 'footer-link', 'Dept. of Neurobiology')} 514 ${extLink('http://neuroscience.med.utah.edu', 'footer-link', 'Neuroscience Graduate Program')} 515 ${extLink('https://bioscience.utah.edu', 'footer-link', 'Molecular Biology Program')} 516 </nav> 517 </div> 518 <div class="footer-bottom"> 519 <p class="footer-copy">© ${new Date().getFullYear()} Dorsky Laboratory · Department of Neurobiology</p> 520 </div> 521 </footer>`; 522} 523 524/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 525 MOBILE NAV TOGGLE 526âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 527function toggleMobileNav(btn) { 528 const nav = document.getElementById('nav-menu'); 529 if (!nav) return; 530 const isOpen = nav.classList.toggle('open'); 531 btn.setAttribute('aria-expanded', String(isOpen)); 532 btn.querySelector('span').textContent = isOpen ? 'â' : 'â°'; 533} 534 535/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 536 APP INIT 537âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 538let currentPage = 'Home'; 539 540window.navigate = function(page) { 541 currentPage = page; 542 render(); 543 window.scrollTo({ top: 0, behavior: 'smooth' }); 544 const announcer = document.getElementById('page-announcer'); 545 if (announcer) { 546 announcer.textContent = ''; 547 setTimeout(() => { announcer.textContent = `Navigated to ${page} page`; }, 100); 548 } 549 setTimeout(() => { 550 const main = document.getElementById('main'); 551 if (main) { main.focus(); } 552 }, 150); 553}; 554 555function getPageContent(page) { 556 switch (page) { 557 case 'Home': return renderHome(); 558 case 'Research': return renderResearch(); 559 case 'Members': return renderMembers(); 560 case 'Publications': return renderPublications(); 561 case 'Contact': return renderContact(); 562 default: return renderHome(); 563 } 564} 565 566function render() { 567 const page = currentPage; 568 document.getElementById('nav-container').innerHTML = renderNav(page); 569 document.getElementById('page-container').innerHTML = getPageContent(page); 570 document.getElementById('footer-container').innerHTML = renderFooter(); 571 572 document.title = page === 'Home' 573 ? 'Dorsky Lab â Neurobiology' 574 : `${page} â Dorsky Lab · Neurobiology`; 575 576 setTimeout(() => { 577 setupReveal(); 578 if (page === 'Home') initHeroCanvas(); 579 if (page === 'Contact') loadLeaflet(); 580 }, 50); 581} 582 583function loadLeaflet() { 584 if (window._leafletLoaded) { setTimeout(initMap, 100); return; } 585 586 const css = document.createElement('link'); 587 css.rel = 'stylesheet'; 588 css.href = 'https://unpkg.com/[email protected]/dist/leaflet.css'; 589 document.head.appendChild(css); 590 591 const js = document.createElement('script'); 592 js.src = 'https://unpkg.com/[email protected]/dist/leaflet.js'; 593 js.onload = () => { window._leafletLoaded = true; setTimeout(initMap, 100); }; 594 document.head.appendChild(js); 595} 596 597window.addEventListener('scroll', () => { 598 const h = document.querySelector('.site-header'); 599 if (h) h.style.boxShadow = window.scrollY > 40 600 ? '0 2px 24px rgba(0,0,0,0.12)' : '0 1px 12px rgba(0,0,0,0.06)'; 601}); 602 603document.addEventListener('keydown', (e) => { 604 if (e.key === 'Escape') { 605 const nav = document.getElementById('nav-menu'); 606 const btn = document.querySelector('.nav-mobile-toggle'); 607 if (nav && nav.classList.contains('open')) { 608 nav.classList.remove('open'); 609 if (btn) { 610 btn.setAttribute('aria-expanded', 'false'); 611 btn.querySelector('span').textContent = 'â°'; 612 btn.focus(); 613 } 614 } 615 } 616}); 617 618document.addEventListener('DOMContentLoaded', async () => { 619 try { 620 const res = await fetch('data.json'); 621 if (!res.ok) throw new Error('Failed to load data.json'); 622 window.DATA = await res.json(); 623 } catch (err) { 624 console.error('Could not load data.json:', err); 625 // Fallback: DATA remains undefined; page will show empty gracefully 626 window.DATA = { members: { pi: {}, current: [], prevPhDs: [], prevPostdocs: [] }, publications: [], research: { areas: [] } }; 627 } 628 render(); 629});
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.