PageSourceSearch

https://bismikaallahuma.org/wp-content/themes/BMK/assets/js/theme.js?ver=2.15.0

js bismikaallahuma.org collected 2026-10-02 12:05:01 UTC 28,017 bytes, 504 lines download raw bytes

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.