Agregar estructura completa del tema APUS con Bootstrap 5 y optimizaciones de rendimiento
Se implementa tema WordPress personalizado para Análisis de Precios Unitarios con funcionalidades avanzadas: - Sistema de templates (front-page, single, archive, page, 404, search) - Integración de Bootstrap 5.3.8 con estructura modular de assets - Panel de opciones del tema con Customizer API - Optimizaciones de rendimiento (Critical CSS, Image Optimization, Performance) - Funcionalidades SEO y compatibilidad con Rank Math - Sistema de posts relacionados y tabla de contenidos - Badge de categorías y manejo de AdSense diferido - Tipografías Google Fonts configurables - Documentación completa del tema y guías de uso 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
216
wp-content/themes/apus-theme/assets/js/adsense-loader.js
Normal file
216
wp-content/themes/apus-theme/assets/js/adsense-loader.js
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* AdSense Delayed Loader
|
||||
*
|
||||
* This script delays the loading of Google AdSense until user interaction
|
||||
* or a timeout occurs, improving initial page load performance.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuration
|
||||
const CONFIG = {
|
||||
timeout: 5000, // Fallback timeout in milliseconds
|
||||
loadedClass: 'adsense-loaded',
|
||||
debug: false // Set to true for console logging
|
||||
};
|
||||
|
||||
// State
|
||||
let adsenseLoaded = false;
|
||||
let loadTimeout = null;
|
||||
|
||||
/**
|
||||
* Log debug messages if debug mode is enabled
|
||||
* @param {string} message - The message to log
|
||||
*/
|
||||
function debugLog(message) {
|
||||
if (CONFIG.debug && typeof console !== 'undefined') {
|
||||
console.log('[AdSense Loader] ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load AdSense scripts and initialize ads
|
||||
*/
|
||||
function loadAdSense() {
|
||||
// Prevent multiple loads
|
||||
if (adsenseLoaded) {
|
||||
debugLog('AdSense already loaded, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
adsenseLoaded = true;
|
||||
debugLog('Loading AdSense scripts...');
|
||||
|
||||
// Clear the timeout if it exists
|
||||
if (loadTimeout) {
|
||||
clearTimeout(loadTimeout);
|
||||
loadTimeout = null;
|
||||
}
|
||||
|
||||
// Remove event listeners to prevent multiple triggers
|
||||
removeEventListeners();
|
||||
|
||||
// Load AdSense script tags
|
||||
loadAdSenseScripts();
|
||||
|
||||
// Execute AdSense push scripts
|
||||
executeAdSensePushScripts();
|
||||
|
||||
// Add loaded class to body
|
||||
document.body.classList.add(CONFIG.loadedClass);
|
||||
|
||||
debugLog('AdSense loading complete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and load all delayed AdSense script tags
|
||||
*/
|
||||
function loadAdSenseScripts() {
|
||||
const delayedScripts = document.querySelectorAll('script[data-adsense-script]');
|
||||
|
||||
if (delayedScripts.length === 0) {
|
||||
debugLog('No delayed AdSense scripts found');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Found ' + delayedScripts.length + ' delayed AdSense script(s)');
|
||||
|
||||
delayedScripts.forEach(function(oldScript) {
|
||||
const newScript = document.createElement('script');
|
||||
|
||||
// Copy attributes
|
||||
if (oldScript.src) {
|
||||
newScript.src = oldScript.src;
|
||||
}
|
||||
|
||||
// Set async attribute
|
||||
newScript.async = true;
|
||||
|
||||
// Copy crossorigin if present
|
||||
if (oldScript.getAttribute('crossorigin')) {
|
||||
newScript.crossorigin = oldScript.getAttribute('crossorigin');
|
||||
}
|
||||
|
||||
// Replace old script with new one
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute delayed AdSense push scripts
|
||||
*/
|
||||
function executeAdSensePushScripts() {
|
||||
const delayedPushScripts = document.querySelectorAll('script[data-adsense-push]');
|
||||
|
||||
if (delayedPushScripts.length === 0) {
|
||||
debugLog('No delayed AdSense push scripts found');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Found ' + delayedPushScripts.length + ' delayed push script(s)');
|
||||
|
||||
// Initialize adsbygoogle array if it doesn't exist
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
delayedPushScripts.forEach(function(oldScript) {
|
||||
const scriptContent = oldScript.innerHTML;
|
||||
|
||||
// Create and execute new script
|
||||
const newScript = document.createElement('script');
|
||||
newScript.innerHTML = scriptContent;
|
||||
newScript.type = 'text/javascript';
|
||||
|
||||
// Replace old script with new one
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for user interactions
|
||||
*/
|
||||
function handleUserInteraction() {
|
||||
debugLog('User interaction detected');
|
||||
loadAdSense();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all event listeners
|
||||
*/
|
||||
function removeEventListeners() {
|
||||
window.removeEventListener('scroll', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('mousemove', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('touchstart', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('click', handleUserInteraction, { passive: true });
|
||||
window.removeEventListener('keydown', handleUserInteraction, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event listeners for user interactions
|
||||
*/
|
||||
function addEventListeners() {
|
||||
debugLog('Adding event listeners for user interaction');
|
||||
|
||||
// Scroll event - load on first scroll
|
||||
window.addEventListener('scroll', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Mouse movement - load when user moves mouse
|
||||
window.addEventListener('mousemove', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Touch events - load on first touch (mobile)
|
||||
window.addEventListener('touchstart', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Click events - load on first click
|
||||
window.addEventListener('click', handleUserInteraction, { passive: true, once: true });
|
||||
|
||||
// Keyboard events - load on first key press
|
||||
window.addEventListener('keydown', handleUserInteraction, { passive: true, once: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set timeout fallback to load AdSense after specified time
|
||||
*/
|
||||
function setTimeoutFallback() {
|
||||
debugLog('Setting timeout fallback (' + CONFIG.timeout + 'ms)');
|
||||
|
||||
loadTimeout = setTimeout(function() {
|
||||
debugLog('Timeout reached, loading AdSense');
|
||||
loadAdSense();
|
||||
}, CONFIG.timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the AdSense delayed loader
|
||||
*/
|
||||
function init() {
|
||||
// Check if AdSense delay is enabled
|
||||
if (!window.apusAdsenseDelayed) {
|
||||
debugLog('AdSense delay not enabled');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Initializing AdSense delayed loader');
|
||||
|
||||
// Check if page is already interactive or complete
|
||||
if (document.readyState === 'interactive' || document.readyState === 'complete') {
|
||||
debugLog('Page already loaded, starting listeners');
|
||||
addEventListeners();
|
||||
setTimeoutFallback();
|
||||
} else {
|
||||
// Wait for DOM to be ready
|
||||
debugLog('Waiting for DOMContentLoaded');
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
debugLog('DOMContentLoaded fired');
|
||||
addEventListeners();
|
||||
setTimeoutFallback();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Start initialization
|
||||
init();
|
||||
|
||||
})();
|
||||
7
wp-content/themes/apus-theme/assets/js/bootstrap.bundle.min.js
vendored
Normal file
7
wp-content/themes/apus-theme/assets/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
342
wp-content/themes/apus-theme/assets/js/header.js
Normal file
342
wp-content/themes/apus-theme/assets/js/header.js
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* Header Navigation JavaScript
|
||||
*
|
||||
* This file handles:
|
||||
* - Mobile hamburger menu toggle
|
||||
* - Sticky header behavior
|
||||
* - Smooth scroll to anchors (optional)
|
||||
* - Accessibility features (keyboard navigation, ARIA attributes)
|
||||
* - Body scroll locking when mobile menu is open
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize on DOM ready
|
||||
*/
|
||||
function init() {
|
||||
setupMobileMenu();
|
||||
setupStickyHeader();
|
||||
setupSmoothScroll();
|
||||
setupKeyboardNavigation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile Menu Functionality
|
||||
*/
|
||||
function setupMobileMenu() {
|
||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
||||
const mobileMenuClose = document.getElementById('mobile-menu-close');
|
||||
|
||||
if (!mobileMenuToggle || !mobileMenu || !mobileMenuOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open mobile menu
|
||||
mobileMenuToggle.addEventListener('click', function() {
|
||||
openMobileMenu();
|
||||
});
|
||||
|
||||
// Close mobile menu via close button
|
||||
if (mobileMenuClose) {
|
||||
mobileMenuClose.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile menu via overlay click
|
||||
mobileMenuOverlay.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
});
|
||||
|
||||
// Close mobile menu on Escape key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && mobileMenu.classList.contains('active')) {
|
||||
closeMobileMenu();
|
||||
mobileMenuToggle.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking a menu link
|
||||
const mobileMenuLinks = mobileMenu.querySelectorAll('a');
|
||||
mobileMenuLinks.forEach(function(link) {
|
||||
link.addEventListener('click', function() {
|
||||
closeMobileMenu();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle window resize - close mobile menu if switching to desktop
|
||||
let resizeTimer;
|
||||
window.addEventListener('resize', function() {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function() {
|
||||
if (window.innerWidth >= 768 && mobileMenu.classList.contains('active')) {
|
||||
closeMobileMenu();
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Open mobile menu
|
||||
*/
|
||||
function openMobileMenu() {
|
||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
||||
|
||||
// Add active classes
|
||||
mobileMenu.classList.add('active');
|
||||
mobileMenuOverlay.classList.add('active');
|
||||
document.body.classList.add('mobile-menu-open');
|
||||
|
||||
// Update ARIA attributes
|
||||
mobileMenuToggle.setAttribute('aria-expanded', 'true');
|
||||
mobileMenu.setAttribute('aria-hidden', 'false');
|
||||
mobileMenuOverlay.setAttribute('aria-hidden', 'false');
|
||||
|
||||
// Focus trap - focus first menu item
|
||||
const firstMenuItem = mobileMenu.querySelector('a');
|
||||
if (firstMenuItem) {
|
||||
setTimeout(function() {
|
||||
firstMenuItem.focus();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close mobile menu
|
||||
*/
|
||||
function closeMobileMenu() {
|
||||
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const mobileMenuOverlay = document.getElementById('mobile-menu-overlay');
|
||||
|
||||
// Remove active classes
|
||||
mobileMenu.classList.remove('active');
|
||||
mobileMenuOverlay.classList.remove('active');
|
||||
document.body.classList.remove('mobile-menu-open');
|
||||
|
||||
// Update ARIA attributes
|
||||
mobileMenuToggle.setAttribute('aria-expanded', 'false');
|
||||
mobileMenu.setAttribute('aria-hidden', 'true');
|
||||
mobileMenuOverlay.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sticky Header Behavior
|
||||
*/
|
||||
function setupStickyHeader() {
|
||||
const header = document.getElementById('masthead');
|
||||
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastScrollTop = 0;
|
||||
let scrollThreshold = 100;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||||
|
||||
// Add/remove scrolled class based on scroll position
|
||||
if (scrollTop > scrollThreshold) {
|
||||
header.classList.add('scrolled');
|
||||
} else {
|
||||
header.classList.remove('scrolled');
|
||||
}
|
||||
|
||||
lastScrollTop = scrollTop;
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Smooth Scroll to Anchors (Optional)
|
||||
*/
|
||||
function setupSmoothScroll() {
|
||||
// Check if user prefers reduced motion
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all anchor links
|
||||
const anchorLinks = document.querySelectorAll('a[href^="#"]');
|
||||
|
||||
anchorLinks.forEach(function(link) {
|
||||
link.addEventListener('click', function(e) {
|
||||
const href = this.getAttribute('href');
|
||||
|
||||
// Skip if href is just "#"
|
||||
if (href === '#') {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = document.querySelector(href);
|
||||
|
||||
if (target) {
|
||||
e.preventDefault();
|
||||
|
||||
// Get header height for offset
|
||||
const header = document.getElementById('masthead');
|
||||
const headerHeight = header ? header.offsetHeight : 0;
|
||||
const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - headerHeight - 20;
|
||||
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
|
||||
// Update URL hash
|
||||
if (history.pushState) {
|
||||
history.pushState(null, null, href);
|
||||
}
|
||||
|
||||
// Focus target element for accessibility
|
||||
target.setAttribute('tabindex', '-1');
|
||||
target.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard Navigation for Menus
|
||||
*/
|
||||
function setupKeyboardNavigation() {
|
||||
const menuItems = document.querySelectorAll('.primary-menu > li, .mobile-primary-menu > li');
|
||||
|
||||
menuItems.forEach(function(item) {
|
||||
const link = item.querySelector('a');
|
||||
const submenu = item.querySelector('.sub-menu');
|
||||
|
||||
if (!link || !submenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open submenu on Enter/Space
|
||||
link.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
if (submenu) {
|
||||
e.preventDefault();
|
||||
toggleSubmenu(item, submenu);
|
||||
}
|
||||
}
|
||||
|
||||
// Close submenu on Escape
|
||||
if (e.key === 'Escape') {
|
||||
closeSubmenu(item, submenu);
|
||||
link.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Close submenu when focus leaves
|
||||
const submenuLinks = submenu.querySelectorAll('a');
|
||||
if (submenuLinks.length > 0) {
|
||||
const lastSubmenuLink = submenuLinks[submenuLinks.length - 1];
|
||||
|
||||
lastSubmenuLink.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Tab' && !e.shiftKey) {
|
||||
closeSubmenu(item, submenu);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle submenu visibility
|
||||
*/
|
||||
function toggleSubmenu(item, submenu) {
|
||||
const isExpanded = item.classList.contains('submenu-open');
|
||||
|
||||
if (isExpanded) {
|
||||
closeSubmenu(item, submenu);
|
||||
} else {
|
||||
openSubmenu(item, submenu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open submenu
|
||||
*/
|
||||
function openSubmenu(item, submenu) {
|
||||
item.classList.add('submenu-open');
|
||||
submenu.setAttribute('aria-hidden', 'false');
|
||||
|
||||
const firstLink = submenu.querySelector('a');
|
||||
if (firstLink) {
|
||||
firstLink.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close submenu
|
||||
*/
|
||||
function closeSubmenu(item, submenu) {
|
||||
item.classList.remove('submenu-open');
|
||||
submenu.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trap focus within mobile menu when open
|
||||
*/
|
||||
function setupFocusTrap() {
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
if (!mobileMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (!mobileMenu.classList.contains('active')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Tab') {
|
||||
const focusableElements = mobileMenu.querySelectorAll(
|
||||
'a, button, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
if (e.shiftKey) {
|
||||
// Shift + Tab
|
||||
if (document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
}
|
||||
} else {
|
||||
// Tab
|
||||
if (document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize focus trap
|
||||
*/
|
||||
setupFocusTrap();
|
||||
|
||||
/**
|
||||
* Initialize when DOM is ready
|
||||
*/
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
})();
|
||||
216
wp-content/themes/apus-theme/assets/js/toc.js
Normal file
216
wp-content/themes/apus-theme/assets/js/toc.js
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Table of Contents JavaScript
|
||||
*
|
||||
* Provides smooth scrolling and active link highlighting for the TOC.
|
||||
* Pure vanilla JavaScript - no jQuery dependency.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize TOC functionality when DOM is ready
|
||||
*/
|
||||
function initTOC() {
|
||||
const toc = document.querySelector('.apus-toc');
|
||||
if (!toc) {
|
||||
return; // No TOC on this page
|
||||
}
|
||||
|
||||
initToggleButton();
|
||||
initSmoothScroll();
|
||||
initActiveHighlight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize toggle button functionality
|
||||
*/
|
||||
function initToggleButton() {
|
||||
const toggleButton = document.querySelector('.apus-toc-toggle');
|
||||
const tocList = document.querySelector('.apus-toc-list');
|
||||
|
||||
if (!toggleButton || !tocList) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleButton.addEventListener('click', function() {
|
||||
const isExpanded = this.getAttribute('aria-expanded') === 'true';
|
||||
this.setAttribute('aria-expanded', !isExpanded);
|
||||
|
||||
// Save state to localStorage
|
||||
try {
|
||||
localStorage.setItem('apus-toc-collapsed', isExpanded ? 'true' : 'false');
|
||||
} catch (e) {
|
||||
// localStorage might not be available
|
||||
}
|
||||
});
|
||||
|
||||
// Restore saved state
|
||||
try {
|
||||
const isCollapsed = localStorage.getItem('apus-toc-collapsed') === 'true';
|
||||
if (isCollapsed) {
|
||||
toggleButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
} catch (e) {
|
||||
// localStorage might not be available
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize smooth scrolling for TOC links
|
||||
*/
|
||||
function initSmoothScroll() {
|
||||
const tocLinks = document.querySelectorAll('.apus-toc-link');
|
||||
|
||||
tocLinks.forEach(function(link) {
|
||||
link.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const targetId = this.getAttribute('href').substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (!targetElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Smooth scroll to target
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
|
||||
// Update URL without jumping
|
||||
if (history.pushState) {
|
||||
history.pushState(null, null, '#' + targetId);
|
||||
} else {
|
||||
window.location.hash = targetId;
|
||||
}
|
||||
|
||||
// Update active state
|
||||
updateActiveLink(this);
|
||||
|
||||
// Focus the target heading for accessibility
|
||||
targetElement.setAttribute('tabindex', '-1');
|
||||
targetElement.focus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize active link highlighting based on scroll position
|
||||
*/
|
||||
function initActiveHighlight() {
|
||||
const tocLinks = document.querySelectorAll('.apus-toc-link');
|
||||
const headings = Array.from(document.querySelectorAll('h2[id], h3[id]'));
|
||||
|
||||
if (headings.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ticking = false;
|
||||
|
||||
// Debounced scroll handler
|
||||
function onScroll() {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(function() {
|
||||
updateActiveOnScroll(headings, tocLinks);
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
// Initial update
|
||||
updateActiveOnScroll(headings, tocLinks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active link based on scroll position
|
||||
*
|
||||
* @param {Array} headings Array of heading elements
|
||||
* @param {NodeList} tocLinks TOC link elements
|
||||
*/
|
||||
function updateActiveOnScroll(headings, tocLinks) {
|
||||
const scrollPosition = window.scrollY + 100; // Offset for better UX
|
||||
|
||||
// Find the current heading
|
||||
let currentHeading = null;
|
||||
for (let i = headings.length - 1; i >= 0; i--) {
|
||||
if (headings[i].offsetTop <= scrollPosition) {
|
||||
currentHeading = headings[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're at the top, use the first heading
|
||||
if (!currentHeading && scrollPosition < headings[0].offsetTop) {
|
||||
currentHeading = headings[0];
|
||||
}
|
||||
|
||||
// Update active class
|
||||
tocLinks.forEach(function(link) {
|
||||
if (currentHeading && link.getAttribute('href') === '#' + currentHeading.id) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update active link when clicked
|
||||
*
|
||||
* @param {Element} clickedLink The clicked TOC link
|
||||
*/
|
||||
function updateActiveLink(clickedLink) {
|
||||
const tocLinks = document.querySelectorAll('.apus-toc-link');
|
||||
tocLinks.forEach(function(link) {
|
||||
link.classList.remove('active');
|
||||
});
|
||||
clickedLink.classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle hash navigation on page load
|
||||
*/
|
||||
function handleHashOnLoad() {
|
||||
if (!window.location.hash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetId = window.location.hash.substring(1);
|
||||
const targetElement = document.getElementById(targetId);
|
||||
const tocLink = document.querySelector('.apus-toc-link[href="#' + targetId + '"]');
|
||||
|
||||
if (targetElement && tocLink) {
|
||||
// Small delay to ensure page is fully loaded
|
||||
setTimeout(function() {
|
||||
targetElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
updateActiveLink(tocLink);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize when DOM is ready
|
||||
*/
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initTOC();
|
||||
handleHashOnLoad();
|
||||
});
|
||||
} else {
|
||||
// DOM is already ready
|
||||
initTOC();
|
||||
handleHashOnLoad();
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user