1/** 2 * Bismika Allahuma â theme.js 3 * 4 * Core front-end JavaScript for the Bismika Allahuma child theme. 5 * This file handles all interactive functionality and user interface enhancements. 6 * 7 * Features: 8 * 0. DARK MODE TOGGLE 9 * - Reads saved preference from localStorage on load 10 * - Toggle button (.bismika-theme-toggle) switches between themes 11 * - Updates data-theme attribute on <html> element 12 * - Falls back to OS preference when no stored preference exists 13 * - Icon switching between sun/moon based on current state 14 * 15 * 1. STICKY HEADER 16 * - Shows sticky header when scrolling past main header 17 * - Smooth slide-down animation via CSS transitions 18 * - ARIA attributes for accessibility 19 * 20 * 2. MOBILE MENU TOGGLE 21 * - Hamburger menu button for mobile navigation 22 * - Toggle .is-open class on primary menu 23 * - Close on outside click and Escape key (WCAG 2.1) 24 * 25 * 3. NAV SEARCH DROPDOWN 26 * - Desktop search icon toggles dropdown 27 * - Close on outside click and Escape key 28 * - Focus management for accessibility 29 * 30 * 4. CATEGORY-INTO-META-ROW 31 * - Moves category labels inside post meta rows 32 * - Replicates Newspaper theme's layout pattern 33 * 34 * 5. SECTION TITLE ANIMATIONS 35 * - Adds ready class for CSS entrance animations 36 * 37 * 6. READING PROGRESS BAR (single posts only) 38 * - Thin bar at top showing scroll progress 39 * - Proportional filling based on article content 40 * - ARIA attributes for screen readers 41 * 42 * 7. BACK TO TOP BUTTON 43 * - Floating button appears after 1vh scroll 44 * - Smooth scroll to top animation 45 * 46 * 8. SHARE BAR COPY LINK 47 * - Copy article URL to clipboard 48 * - Visual feedback with success state 49 * 50 * 9. SHARE BAR MASTODON SHARE HANDLER 51 * - Prompt for Mastodon instance 52 * - Opens share dialog with custom URL 53 * 54 * 10. GLOSSARY MODAL 55 * @package Bismika_Allahuma 56 * @since 1.0.0 57 * @since 1.4.0 Added sticky header, mobile nav, search dropdown 58 * @since 1.5.7 Added progress bar, back-to-top, share copy 59 * @since 1.8.0 Added dark mode toggle, enhanced accessibility 60 * @since 1.8.3 Fixed dark mode default to light, removed OS preference auto-detection 61 */ 62 63 /* ââ DARK MODE: Theme preference is applied in header.php inline script 64 * (before first paint). The toggle handler below manages runtime switching. 65 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 66 67 (function () { 68 'use strict'; 69 70 /* ââ i18n helper â use PHP-localised strings when available ââ */ 71 var _i18n = (typeof bismikaI18n !== 'undefined') ? bismikaI18n : {}; 72 function _t(key, fallback) { return _i18n[key] || fallback; } 73 74 function bismikaInit() { 75 76 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 77 * 0. DARK MODE â handled by inline script in header.php 78 * (event delegation on document, immune to DOM changes) 79 * Update icons on load to match current theme state. 80 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 81 var isDark = document.documentElement.getAttribute('data-theme') === 'dark'; 82 document.querySelectorAll('.bismika-theme-toggle').forEach(function(toggle) { 83 toggle.innerHTML = isDark 84 ? '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>' 85 : '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'; 86 }); 87 88 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 89 * 1. STICKY HEADER 90 * The sticky header slides down from top: -80px to top: 0 91 * via CSS transition when .is-sticky is added. 92 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 93 var header = document.getElementById('masthead'); 94 var stickyHeader = document.getElementById('bismika-sticky-header'); 95 96 if (header && stickyHeader) { 97 var headerHeight = header.offsetHeight; 98 99 window.addEventListener('scroll', function () { 100 if (window.scrollY > headerHeight) { 101 stickyHeader.classList.add('is-sticky'); 102 stickyHeader.removeAttribute('aria-hidden'); 103 stickyHeader.removeAttribute('inert'); 104 } else { 105 stickyHeader.classList.remove('is-sticky'); 106 stickyHeader.setAttribute('aria-hidden', 'true'); 107 stickyHeader.setAttribute('inert', ''); 108 } 109 }, { passive: true }); 110 111 // Recalculate on resize (tablet rotation, window resize) 112 window.addEventListener('resize', function () { 113 headerHeight = header.offsetHeight; 114 }, { passive: true }); 115 } 116 117 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 118 * 2. MOBILE MENU TOGGLE 119 * Toggles .is-open on #primary-menu and updates 120 * aria-expanded on the button. Also closes on outside click. 121 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 122 var toggle = document.querySelector('.bismika-menu-toggle'); 123 var menu = document.getElementById('primary-menu'); 124 // The sticky header has a second .bismika-menu-toggle â keep both in sync. 125 var allToggles = document.querySelectorAll('.bismika-menu-toggle'); 126 127 function setMenuState( open ) {
128 allToggles.forEach(function(t) { 129 t.setAttribute('aria-expanded', String(open)); 130 }); 131 if (open) { 132 menu.classList.add('is-open'); 133 } else { 134 menu.classList.remove('is-open'); 135 } 136 } 137 138 if (toggle && menu) { 139 allToggles.forEach(function(t) { 140 t.addEventListener('click', function () { 141 var expanded = menu.classList.contains('is-open'); 142 var opening = !expanded; 143 setMenuState(opening); 144 145 // WCAG 2.1 SC 2.1.1 â move focus to first menu item on open 146 if (opening) { 147 var firstLink = menu.querySelector('a, button'); 148 if (firstLink) { setTimeout(function() { firstLink.focus(); }, 50); } 149 } 150 }); 151 }); 152 153 // Close mobile menu on outside click 154 document.addEventListener('click', function (e) { 155 var clickedToggle = Array.from(allToggles).some(function(t) { return t.contains(e.target); }); 156 if (!clickedToggle && !menu.contains(e.target)) { 157 setMenuState(false); 158 } 159 }); 160 161 // Close mobile menu on Escape key (WCAG 2.1) 162 document.addEventListener('keydown', function (e) { 163 if (e.key === 'Escape' && menu.classList.contains('is-open')) { 164 setMenuState(false); 165 toggle.focus(); // Return focus to main (non-sticky) toggle 166 } 167 }); 168 169 // Focus trap: keep Tab cycling within open menu on mobile 170 menu.addEventListener('keydown', function (e) { 171 if (e.key !== 'Tab' || !menu.classList.contains('is-open')) return; 172 var focusable = Array.from(menu.querySelectorAll('a, button, input, [tabindex="0"]')) 173 .filter(function(el) { return !el.disabled && el.offsetParent !== null; }); 174 if (!focusable.length) return; 175 var first = focusable[0]; 176 var last = focusable[focusable.length - 1]; 177 if (e.shiftKey && document.activeElement === first) { 178 e.preventDefault(); 179 last.focus(); 180 } else if (!e.shiftKey && document.activeElement === last) { 181 e.preventDefault(); 182 first.focus(); 183 } 184 }); 185 } 186 187 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 188 * 3. SEARCH OVERLAY â keyboard shortcuts only 189 * Click handlers are in header.php inline script (event delegation). 190 * This section handles Escape to close and '/' to open. 191 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 192 var searchOverlay = document.getElementById('bismika-search-overlay'); 193 var searchInput = document.querySelector('.bismika-search-input'); 194 195 function openSearch() { 196 if (searchOverlay) { 197 searchOverlay.classList.add('is-open'); 198 searchOverlay.setAttribute('aria-hidden', 'false'); 199 document.body.style.overflow = 'hidden'; 200 setTimeout(function() { if (searchInput) searchInput.focus(); }, 100); 201 } 202 } 203 204 function closeSearch() { 205 if (searchOverlay) { 206 searchOverlay.classList.remove('is-open'); 207 searchOverlay.setAttribute('aria-hidden', 'true'); 208 document.body.style.overflow = ''; 209 } 210 } 211 212 document.addEventListener('keydown', function (e) { 213 if (e.key === 'Escape' && searchOverlay && searchOverlay.classList.contains('is-open')) { 214 closeSearch(); 215 } 216 if (e.key === '/' && searchOverlay && !searchOverlay.classList.contains('is-open')) { 217 if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { 218 e.preventDefault(); 219 openSearch(); 220 } 221 } 222 }); 223 224 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 225 * 3B. NAV SUB-MENU TOUCH SUPPORT 226 * Standard :hover fails on mobile/tablets. This adds a 227 * tap-to-open toggle for parent menu items. 228 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 229 var parentItems = document.querySelectorAll('.bismika-menu .menu-item-has-children > a');
230 parentItems.forEach(function(item) { 231 item.addEventListener('click', function(e) { 232 if (window.innerWidth <= 1024) { // Touch/Mobile breakpoint 233 var parent = item.parentElement; 234 if (!parent.classList.contains('is-submenu-open')) { 235 e.preventDefault(); 236 // Close other open sub-menus first 237 document.querySelectorAll('.is-submenu-open').forEach(function(open) { 238 if (open !== parent) open.classList.remove('is-submenu-open'); 239 }); 240 parent.classList.add('is-submenu-open'); 241 } 242 } 243 }); 244 }); 245 246 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 247 * 4. CATEGORY-INTO-META-ROW 248 * Original script (decoded from vc_raw_html base64 in 249 * tdb_template_5173 of the Newspaper theme): 250 * 251 * document.querySelectorAll(".td-editor-date").forEach(function (container) { 252 * const category = container.querySelector(".td-post-category"); 253 * const authorDate = container.querySelector(".td-author-date"); 254 * if (category && authorDate) { 255 * authorDate.appendChild(category); 256 * } 257 * }); 258 * 259 * Adapted for classical PHP theme class names. 260 * Moves .bismika-cat-label inside .bismika-post-meta. 261 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 262 document.querySelectorAll('.bismika-post-card').forEach(function (card) { 263 var catLabel = card.querySelector('.bismika-cat-label'); 264 var metaRow = card.querySelector('.bismika-post-meta'); 265 266 if (catLabel && metaRow && !metaRow.contains(catLabel)) { 267 metaRow.insertBefore(catLabel, metaRow.firstChild); 268 } 269 }); 270 271 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 272 * 5. SECTION TITLE UNDERLINE â mark as ready 273 * Adds a class that CSS can use for entrance animations. 274 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 275 document.querySelectorAll('.bismika-section-title').forEach(function (el) { 276 el.classList.add('bismika-section-title--ready'); 277 }); 278 279 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 280 * 6. READING PROGRESS BAR (single posts only) 281 * Injects a thin bar at the top of the viewport that 282 * fills proportionally as the user scrolls the article. 283 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 284 if (document.body.classList.contains('bismika-single')) { 285 var progressBar = document.createElement('div'); 286 progressBar.className = 'bismika-progress-bar'; 287 progressBar.setAttribute('role', 'progressbar'); 288 progressBar.setAttribute('aria-valuemin', '0'); 289 progressBar.setAttribute('aria-valuemax', '100'); 290 progressBar.setAttribute('aria-label', 'Reading progress'); 291 document.body.appendChild(progressBar); 292 293 var articleContent = document.querySelector('.bismika-single-content'); 294 if (articleContent) { 295 window.addEventListener('scroll', function () { 296 var rect = articleContent.getBoundingClientRect(); 297 var articleTop = rect.top + window.scrollY; 298 var articleHeight = articleContent.offsetHeight; 299 var scrolled = window.scrollY - articleTop; 300 var pct = Math.min(100, Math.max(0, (scrolled / articleHeight) * 100)); 301 progressBar.style.width = pct + '%'; 302 progressBar.setAttribute('aria-valuenow', Math.round(pct)); 303 }, { passive: true }); 304 } 305 } 306 307 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 308 * 7. BACK TO TOP BUTTON 309 * Appears after scrolling past one viewport height. 310 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 311 var btt = document.createElement('button'); 312 btt.className = 'bismika-back-to-top'; 313 btt.setAttribute('aria-label', _t('backToTop', 'Back to top')); 314 btt.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="18 15 12 9 6 15"/></svg>'; 315 document.body.appendChild(btt); 316 317 window.addEventListener('scroll', function () { 318 if (window.scrollY > window.innerHeight) { 319 btt.classList.add('is-visible'); 320 } else { 321 btt.classList.remove('is-visible'); 322 } 323 }, { passive: true }); 324 325 btt.addEventListener('click', function () { 326 window.scrollTo({ top: 0, behavior: 'smooth' }); 327 }); 328 329 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 330 * 8. SHARE BAR â COPY LINK HANDLER 331 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 332 var copyBtn = document.querySelector('.bismika-share-btn--copy'); 333 if (copyBtn) { 334 copyBtn.addEventListener('click', function () { 335 var url = copyBtn.getAttribute('data-url'); 336 if (navigator.clipboard) { 337 navigator.clipboard.writeText(url).then(function () { 338 copyBtn.classList.add('is-copied'); 339 setTimeout(function () { copyBtn.classList.remove('is-copied'); }, 2000); 340 }); 341 } 342 }); 343 } 344 345 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 346 * 9. SHARE BAR â MASTODON SHARE HANDLER 347 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 348 var mastodonBtn = document.querySelector('.bismika-share-btn--mastodon'); 349 if (mastodonBtn) { 350 mastodonBtn.addEventListener('click', function (e) { 351 e.preventDefault(); 352 var url = mastodonBtn.getAttribute('data-mastodon-share'); 353 var title = mastodonBtn.getAttribute('data-mastodon-title'); 354 var instance = prompt(_t('mastodonPrompt', 'Enter your Mastodon instance (e.g., mastodon.social):')); 355 if (instance) { 356 // Strip anything that is not a valid hostname character 357 // (letters, digits, hyphens, dots) before constructing the URL. 358 var safeInstance = instance.trim().replace(/[^a-zA-Z0-9.\-]/g, ''); 359 if (!safeInstance) return; 360 var shareUrl = 'https://' + safeInstance + '/share?text=' + encodeURIComponent(title + ' ' + url); 361 window.open(shareUrl, '_blank', 'noopener,noreferrer'); 362 } 363 }); 364 } 365 366 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 367 * 10. GLOSSARY MODAL 368 * Opens glossary in modal overlay similar to search. 369 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 370 var glossaryTrigger = document.querySelector('.bismika-glossary-trigger'); 371 var glossaryUrl = glossaryTrigger ? glossaryTrigger.getAttribute('data-glossary-url') : ''; 372 373 if (glossaryTrigger && glossaryUrl) {
374 // Create modal HTML structure 375 var glossaryModal = document.createElement('div'); 376 glossaryModal.id = 'bismika-glossary-modal'; 377 glossaryModal.className = 'bismika-modal bismika-glossary-modal'; 378 glossaryModal.setAttribute('aria-hidden', 'true'); 379 glossaryModal.innerHTML = '<div class="bismika-modal__overlay"></div><div class="bismika-modal__content"><button type="button" class="bismika-modal__close" aria-label="Close glossary"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button><div class="bismika-modal__body"></div></div>'; 380 document.body.appendChild(glossaryModal); 381 382 var modalBody = glossaryModal.querySelector('.bismika-modal__body'); 383 var closeBtn = glossaryModal.querySelector('.bismika-modal__close'); 384 var overlay = glossaryModal.querySelector('.bismika-modal__overlay'); 385 386 function openGlossaryModal() { 387 glossaryModal.setAttribute('aria-hidden', 'false'); 388 document.body.style.overflow = 'hidden'; 389 if (!modalBody.hasChildNodes()) { 390 var loadingP = document.createElement('p'); 391 loadingP.className = 'loading'; 392 loadingP.textContent = 'Loading glossary\u2026'; 393 modalBody.appendChild(loadingP); 394 fetch(glossaryUrl + '?modal=1') 395 .then(function(response) { return response.text(); }) 396 .then(function(html) { 397 var parser = new DOMParser(); 398 var doc = parser.parseFromString(html, 'text/html'); 399 var source = doc.querySelector('.bismika-glossary-page'); 400 // Clear loading placeholder 401 while (modalBody.firstChild) { modalBody.removeChild(modalBody.firstChild); } 402 if (source) { 403 // Selectively import only known safe child nodes â 404 // avoids injecting arbitrary innerHTML from the fetched document. 405 var allowed = source.querySelectorAll('h1,h2,h3,h4,p,ul,ol,li,dl,dt,dd,a,strong,em,abbr,blockquote,table,thead,tbody,tr,th,td,div,span,section,article'); 406 allowed.forEach(function(node) { 407 if (node.parentNode === source) { 408 modalBody.appendChild(document.importNode(node, true)); 409 } 410 }); 411 if (!modalBody.hasChildNodes()) { 412 modalBody.appendChild(document.importNode(source, true)); 413 } 414 } else { 415 var errP = document.createElement('p'); 416 errP.textContent = _t('errorGlossary', 'Error loading glossary.'); 417 modalBody.appendChild(errP); 418 } 419 }) 420 .catch(function() { 421 while (modalBody.firstChild) { modalBody.removeChild(modalBody.firstChild); } 422 var errP = document.createElement('p'); 423 errP.textContent = 'Error loading glossary. '; 424 var link = document.createElement('a'); 425 link.href = glossaryUrl; 426 link.textContent = _t('viewFullPage', 'View full page'); 427 errP.appendChild(link); 428 modalBody.appendChild(errP); 429 }); 430 } 431 } 432 433 function closeGlossaryModal() { 434 glossaryModal.setAttribute('aria-hidden', 'true'); 435 document.body.style.overflow = ''; 436 } 437 438 glossaryTrigger.addEventListener('click', function(e) { 439 e.preventDefault(); 440 openGlossaryModal(); 441 }); 442 443 closeBtn.addEventListener('click', closeGlossaryModal); 444 overlay.addEventListener('click', closeGlossaryModal); 445 446 document.addEventListener('keydown', function(e) { 447 if (e.key === 'Escape' && glossaryModal.getAttribute('aria-hidden') === 'false') { 448 closeGlossaryModal(); 449 } 450 }); 451 } 452 453 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââ 454 * 11. TOC MOBILE COLLAPSE 455 * On screens â¤767px, the Table of Contents can occupy 456 * 30-40% of the viewport on long articles. Mark it as 457 * collapsible and collapsed by default; tapping the title 458 * expands it. The CSS in mobile-fixes.css handles the 459 * visual state via .is-collapsible and .is-open classes. 460 * On desktop nothing changes â is-collapsible has no effect 461 * above the 767px breakpoint in CSS. 462 * âââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 463 var toc = document.querySelector('.bismika-toc'); 464 if (toc) { 465 var tocTitle = toc.querySelector('.bismika-toc-title'); 466 var tocList = toc.querySelector('.bismika-toc-list'); 467 468 if (tocTitle && tocList && window.innerWidth <= 767) { 469 // Mark as collapsible and start collapsed 470 toc.classList.add('is-collapsible'); 471 472 // Accessibility: wire up ARIA on the title 473 var tocId = 'bismika-toc-list'; 474 tocList.id = tocId; 475 tocTitle.setAttribute('role', 'button'); 476 tocTitle.setAttribute('tabindex', '0'); 477 tocTitle.setAttribute('aria-expanded', 'false'); 478 tocTitle.setAttribute('aria-controls', tocId); 479 480 function toggleToc() { 481 var isOpen = toc.classList.toggle('is-open'); 482 tocTitle.setAttribute('aria-expanded', String(isOpen)); 483 } 484 485 tocTitle.addEventListener('click', toggleToc); 486 tocTitle.addEventListener('keydown', function(e) { 487 if (e.key === 'Enter' || e.key === ' ') { 488 e.preventDefault(); 489 toggleToc(); 490 } 491 }); 492 } 493 } 494 495 } // end bismikaInit 496 497 // Run immediately if DOM already ready, otherwise wait for it 498 if (document.readyState === 'loading') { 499 document.addEventListener('DOMContentLoaded', bismikaInit); 500 } else {
501 bismikaInit(); 502 } 503 504}());
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.