1/* TheIQRA Ghost Theme - Main JavaScript */ 2 3// ============================================ 4// DARK MODE TOGGLE 5// ============================================ 6 7(function() { 8 const STORAGE_KEY = 'theiqra-theme'; 9 const DARK_CLASS = 'dark'; 10 11 // Detect system preference 12 function getSystemPreference() { 13 return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 14 } 15 16 // Get theme from localStorage or system preference 17 function getTheme() { 18 const stored = localStorage.getItem(STORAGE_KEY); 19 if (stored) { 20 return stored; 21 } 22 return getSystemPreference(); 23 } 24 25 // Apply theme to document 26 function applyTheme(theme) { 27 const htmlElement = document.documentElement; 28 if (theme === 'dark') { 29 htmlElement.classList.add(DARK_CLASS); 30 htmlElement.setAttribute('data-theme', 'dark'); 31 } else { 32 htmlElement.classList.remove(DARK_CLASS); 33 htmlElement.removeAttribute('data-theme'); 34 } 35 } 36 37 // Initialize dark mode on page load 38 function initDarkMode() { 39 const theme = getTheme(); 40 applyTheme(theme); 41 42 const toggle = document.getElementById('dark-mode-toggle'); 43 if (toggle) { 44 toggle.addEventListener('click', function() { 45 const currentTheme = getTheme(); 46 const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; 47 localStorage.setItem(STORAGE_KEY, newTheme); 48 applyTheme(newTheme); 49 }); 50 } 51 } 52 53 // Listen for system preference changes 54 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { 55 const stored = localStorage.getItem(STORAGE_KEY); 56 if (!stored) { 57 applyTheme(e.matches ? 'dark' : 'light'); 58 } 59 }); 60 61 // Initialize when DOM is ready 62 if (document.readyState === 'loading') { 63 document.addEventListener('DOMContentLoaded', initDarkMode); 64 } else { 65 initDarkMode(); 66 } 67})(); 68 69// ============================================ 70// MOBILE MENU TOGGLE 71// ============================================ 72 73(function() { 74 function initMobileMenu() { 75 const mobileMenuBtn = document.getElementById('mobile-menu-btn'); 76 const navMenu = document.querySelector('.nav-menu'); 77 78 if (mobileMenuBtn && navMenu) { 79 mobileMenuBtn.addEventListener('click', function() { 80 mobileMenuBtn.classList.toggle('active'); 81 navMenu.classList.toggle('visible'); 82 }); 83 84 // Close menu when clicking on a link 85 const navLinks = navMenu.querySelectorAll('a'); 86 navLinks.forEach(link => { 87 link.addEventListener('click', function() { 88 mobileMenuBtn.classList.remove('active'); 89 navMenu.classList.remove('visible'); 90 }); 91 }); 92 93 // Close menu when clicking outside 94 document.addEventListener('click', function(e) { 95 if (!e.target.closest('.main-nav')) { 96 mobileMenuBtn.classList.remove('active'); 97 navMenu.classList.remove('visible'); 98 } 99 }); 100 } 101 } 102 103 if (document.readyState === 'loading') { 104 document.addEventListener('DOMContentLoaded', initMobileMenu); 105 } else { 106 initMobileMenu(); 107 } 108})(); 109 110// ============================================ 111// COPY CODE BUTTON 112// ============================================ 113 114(function() { 115 function initCopyButtons() { 116 const codeBlocks = document.querySelectorAll('pre'); 117 118 codeBlocks.forEach(block => { 119 const code = block.querySelector('code'); 120 if (!code) return; 121 122 // Create copy button 123 const button = document.createElement('button'); 124 button.className = 'copy-button'; 125 button.textContent = 'Copy'; 126 button.type = 'button'; 127 button.setAttribute('aria-label', 'Copy code'); 128 129 // Make pre position relative for button positioning 130 block.style.position = 'relative'; 131 block.appendChild(button); 132 133 // Copy functionality 134 button.addEventListener('click', function() { 135 const text = code.textContent; 136 navigator.clipboard.writeText(text).then(function() { 137 button.textContent = 'Copied!'; 138 button.classList.add('copied'); 139 140 // Reset button after 2 seconds 141 setTimeout(function() { 142 button.textContent = 'Copy'; 143 button.classList.remove('copied'); 144 }, 2000); 145 }).catch(function(err) { 146 console.error('Failed to copy: ', err); 147 button.textContent = 'Failed'; 148 setTimeout(function() { 149 button.textContent = 'Copy'; 150 }, 2000); 151 }); 152 }); 153 }); 154 } 155 156 if (document.readyState === 'loading') { 157 document.addEventListener('DOMContentLoaded', initCopyButtons); 158 } else { 159 initCopyButtons(); 160 } 161})(); 162 163// ============================================ 164// READING PROGRESS BAR 165// ============================================ 166 167(function() { 168 function initReadingProgress() { 169 const progressBar = document.getElementById('reading-progress-bar'); 170 if (!progressBar) return; 171 172 window.addEventListener('scroll', function() { 173 const docHeight = document.documentElement.scrollHeight - window.innerHeight; 174 const scrolled = window.scrollY; 175 const progress = (scrolled / docHeight) * 100; 176 progressBar.style.width = progress + '%'; 177 }); 178 } 179 180 if (document.readyState === 'loading') { 181 document.addEventListener('DOMContentLoaded', initReadingProgress); 182 } else { 183 initReadingProgress(); 184 } 185})(); 186 187// ============================================ 188// BACK TO TOP BUTTON 189// ============================================ 190 191(function() { 192 function initBackToTop() { 193 const backToTopBtn = document.getElementById('back-to-top'); 194 if (!backToTopBtn) return; 195 196 const SCROLL_THRESHOLD = 300; 197 198 window.addEventListener('scroll', function() { 199 if (window.scrollY > SCROLL_THRESHOLD) { 200 backToTopBtn.classList.add('visible'); 201 } else { 202 backToTopBtn.classList.remove('visible'); 203 } 204 }); 205 206 backToTopBtn.addEventListener('click', function() { 207 window.scrollTo({ 208 top: 0, 209 behavior: 'smooth' 210 }); 211 }); 212 } 213 214 if (document.readyState === 'loading') { 215 document.addEventListener('DOMContentLoaded', initBackToTop); 216 } else { 217 initBackToTop(); 218 } 219})(); 220 221// ============================================ 222// SMOOTH SCROLL FOR ANCHOR LINKS 223// ============================================ 224 225(function() { 226 function initSmoothScroll() { 227 document.addEventListener('click', function(e) { 228 const target = e.target; 229 230 // Check if it's an anchor link 231 if (target.tagName === 'A' && target.getAttribute('href').startsWith('#')) { 232 const id = target.getAttribute('href'); 233 const element = document.querySelector(id); 234 235 if (element) { 236 e.preventDefault(); 237 element.scrollIntoView({ 238 behavior: 'smooth', 239 block: 'start' 240 }); 241 242 // Update URL without jumping 243 window.history.pushState(null, null, id); 244 } 245 } 246 }); 247 } 248 249 if (document.readyState === 'loading') { 250 document.addEventListener('DOMContentLoaded', initSmoothScroll); 251 } else { 252 initSmoothScroll(); 253 } 254})(); 255 256// ============================================ 257// ACTIVE TOC HIGHLIGHTING 258// ============================================ 259 260(function() { 261 function initTOCHighlight() { 262 const tocLinks = document.querySelectorAll('.toc a'); 263 if (tocLinks.length === 0) return; 264 265 window.addEventListener('scroll', function() { 266 let current = ''; 267 268 // Find the heading currently in view 269 const headings = document.querySelectorAll('article h2, article h3, article h4');
270 headings.forEach(heading => { 271 const sectionTop = heading.offsetTop; 272 if (pageYOffset >= sectionTop - 200) { 273 current = heading.getAttribute('id'); 274 } 275 }); 276 277 // Remove active class from all links 278 tocLinks.forEach(link => { 279 link.classList.remove('active'); 280 if (link.getAttribute('href') === '#' + current) { 281 link.classList.add('active'); 282 } 283 }); 284 }); 285 } 286 287 if (document.readyState === 'loading') { 288 document.addEventListener('DOMContentLoaded', initTOCHighlight); 289 } else { 290 initTOCHighlight(); 291 } 292})(); 293 294// ============================================ 295// COPY LINK BUTTON 296// ============================================ 297 298(function() { 299 function initCopyLinkButton() { 300 const copyLinkBtn = document.getElementById('copy-link-btn'); 301 if (!copyLinkBtn) return; 302 303 copyLinkBtn.addEventListener('click', function() { 304 const url = window.location.href; 305 navigator.clipboard.writeText(url).then(function() { 306 const originalText = copyLinkBtn.textContent; 307 copyLinkBtn.textContent = 'Link Copied!'; 308 copyLinkBtn.classList.add('copied'); 309 310 setTimeout(function() { 311 copyLinkBtn.textContent = originalText; 312 copyLinkBtn.classList.remove('copied'); 313 }, 2000); 314 }).catch(function(err) { 315 console.error('Failed to copy link: ', err); 316 }); 317 }); 318 } 319 320 if (document.readyState === 'loading') { 321 document.addEventListener('DOMContentLoaded', initCopyLinkButton); 322 } else { 323 initCopyLinkButton(); 324 } 325})(); 326 327// ============================================ 328// SEARCH FUNCTIONALITY (BASIC) 329// ============================================ 330 331(function() { 332 function initSearch() { 333 const searchInputs = document.querySelectorAll('#site-search, #search-input'); 334 const searchModal = document.getElementById('search-modal'); 335 const searchResults = document.getElementById('search-results'); 336 337 if (!searchInputs.length) return; 338 339 searchInputs.forEach(input => { 340 input.addEventListener('input', function(e) { 341 const query = e.target.value.toLowerCase(); 342 343 if (query.length < 2) { 344 if (searchResults) searchResults.innerHTML = ''; 345 return; 346 } 347 348 // Basic client-side search (searches headings and paragraphs) 349 const results = []; 350 const headings = document.querySelectorAll('h1, h2, h3'); 351 const paragraphs = document.querySelectorAll('p'); 352
353 headings.forEach(h => { 354 if (h.textContent.toLowerCase().includes(query)) { 355 results.push({ 356 title: h.textContent, 357 link: '#' + (h.id || ''), 358 type: 'Heading' 359 }); 360 } 361 }); 362 363 if (results.length > 0 && searchResults) { 364 searchResults.innerHTML = results.map(r => 365 `<div class="search-result"> 366 <a href="${r.link}">${r.title}</a> 367 <span class="result-type">${r.type}</span> 368 </div>` 369 ).join(''); 370 } else if (searchResults) { 371 searchResults.innerHTML = '<p class="no-results">No results found</p>'; 372 } 373 }); 374 }); 375 } 376 377 if (document.readyState === 'loading') { 378 document.addEventListener('DOMContentLoaded', initSearch); 379 } else { 380 initSearch(); 381 } 382})(); 383 384// ============================================ 385// TABLE OF CONTENTS AUTO-ID ASSIGNMENT 386// ============================================ 387 388(function() { 389 function initHeadingIDs() { 390 const article = document.querySelector('article'); 391 if (!article) return; 392 393 const headings = article.querySelectorAll('h2, h3, h4'); 394 let headingCount = {}; 395
396 headings.forEach(heading => { 397 if (!heading.id) { 398 let id = heading.textContent 399 .toLowerCase() 400 .replace(/[^\w\s-]/g, '') 401 .replace(/\s+/g, '-') 402 .replace(/-+/g, '-') 403 .replace(/^-+|-+$/g, ''); 404 405 // Handle duplicate IDs 406 if (headingCount[id]) { 407 headingCount[id]++; 408 id += '-' + headingCount[id]; 409 } else { 410 headingCount[id] = 0; 411 } 412 413 heading.id = id; 414 } 415 }); 416 } 417 418 if (document.readyState === 'loading') { 419 document.addEventListener('DOMContentLoaded', initHeadingIDs); 420 } else { 421 initHeadingIDs(); 422 } 423})(); 424 425// ============================================ 426// KEYBOARD SHORTCUTS 427// ============================================ 428 429(function() { 430 function initKeyboardShortcuts() { 431 document.addEventListener('keydown', function(e) { 432 // Ctrl+K or Cmd+K to open search 433 if ((e.ctrlKey || e.metaKey) && e.key === 'k') { 434 e.preventDefault(); 435 const searchModal = document.getElementById('search-modal'); 436 const searchInput = document.getElementById('search-input'); 437 if (searchModal) { 438 searchModal.hidden = false; 439 if (searchInput) searchInput.focus(); 440 } 441 } 442 443 // Escape to close search modal 444 if (e.key === 'Escape') { 445 const searchModal = document.getElementById('search-modal'); 446 if (searchModal && !searchModal.hidden) { 447 searchModal.hidden = true; 448 } 449 } 450 }); 451 } 452 453 if (document.readyState === 'loading') { 454 document.addEventListener('DOMContentLoaded', initKeyboardShortcuts); 455 } else { 456 initKeyboardShortcuts(); 457 } 458})(); 459 460// ============================================ 461// PERFORMANCE: LAZY LOADING FOR IMAGES 462// ============================================ 463 464(function() { 465 function initLazyLoading() { 466 // Modern browsers support native lazy loading 467 const images = document.querySelectorAll('img[loading="lazy"]'); 468 469 // Fallback for older browsers using Intersection Observer 470 if ('IntersectionObserver' in window) { 471 const observer = new IntersectionObserver((entries, observer) => { 472 entries.forEach(entry => { 473 if (entry.isIntersecting) { 474 const img = entry.target; 475 if (img.dataset.src) { 476 img.src = img.dataset.src; 477 img.removeAttribute('data-src'); 478 observer.unobserve(img); 479 } 480 } 481 }); 482 }); 483 484 images.forEach(img => { 485 if (img.dataset.src) { 486 observer.observe(img); 487 } 488 }); 489 } 490 } 491 492 if (document.readyState === 'loading') { 493 document.addEventListener('DOMContentLoaded', initLazyLoading); 494 } else { 495 initLazyLoading(); 496 } 497})(); 498 499console.log('TheIQRA Ghost Theme initialized');
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.