MediaWiki:Gadget-HubAutoHide.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* HubAutoHide.js
* Gadget: Hub navigation auto-hide and expansion
*
* Description:
* - Auto-hides sticky bottom hub navigation when scrolling down on mobile
* - Reveals it when scrolling up (mobile only, ≤768px width)
* - Handles hub expansion/collapse with submenu support
* - Mobile: Bottom drawer with dimmed overlay
* - Desktop: Dropdown menus
*
* Load priority: Early (mw.loader.using('mediawiki.util', ...))
* Dependencies: None (uses vanilla JS)
*
* Author: Mark W. Datysgeld
* Version: 2.1 (timeout tracking fix for submenu vanishing issue)
*/
(function() {
'use strict';
// Only run on client-side with JavaScript enabled
if (!document.body) return;
// Configuration
var MOBILE_MAX_WIDTH = 768;
var SCROLL_THRESHOLD = 10; // Minimum scroll distance to trigger hide/show
var MIN_SCROLL_TOP = 100; // Don't hide if user is near top of page
var ANIMATION_DURATION = 400; // Match CSS transition duration
// State
var lastScrollTop = 0;
var ticking = false;
var hubElement = null;
var activeHub = null; // Track which hub is expanded
var collapseTimeouts = {}; // Track pending collapse timeouts by hubId
/**
* Check if current viewport is mobile size
*/
function isMobileViewport() {
return window.innerWidth <= MOBILE_MAX_WIDTH;
}
/**
* Update hub visibility based on scroll direction
*/
function updateHubVisibility() {
if (!hubElement || !isMobileViewport()) {
ticking = false;
return;
}
// Don't hide if a hub is expanded
if (activeHub) {
ticking = false;
return;
}
var currentScrollTop = window.pageYOffset || document.documentElement.scrollTop;
// Scrolling down and past threshold
if (currentScrollTop > lastScrollTop && currentScrollTop > MIN_SCROLL_TOP) {
if (Math.abs(currentScrollTop - lastScrollTop) > SCROLL_THRESHOLD) {
hubElement.classList.add('is-hidden');
}
}
// Scrolling up
else if (currentScrollTop < lastScrollTop) {
if (Math.abs(currentScrollTop - lastScrollTop) > SCROLL_THRESHOLD) {
hubElement.classList.remove('is-hidden');
}
}
lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop;
ticking = false;
}
/**
* Request animation frame for smooth updates
*/
function requestTick() {
if (!ticking) {
window.requestAnimationFrame(updateHubVisibility);
ticking = true;
}
}
/**
* Handle scroll events
*/
function onScroll() {
requestTick();
}
/**
* Hub Expansion Functions
*/
function toggleHub(hubItem) {
var hubId = hubItem.dataset.hub;
var isExpanded = hubItem.dataset.state === 'expanded';
if (window.console && console.log) {
console.log('[HubNavigation] Toggle called for:', hubId, 'Current state:', isExpanded ? 'expanded' : 'collapsed');
}
if (isExpanded) {
collapse(hubItem);
activeHub = null;
hubElement.dataset.activeHub = '';
} else {
collapseAll();
expand(hubItem);
activeHub = hubId;
hubElement.dataset.activeHub = hubId;
}
}
function expand(item) {
var hubId = item.dataset.hub;
// Clear any pending collapse timeout for this hub
if (collapseTimeouts[hubId]) {
clearTimeout(collapseTimeouts[hubId]);
delete collapseTimeouts[hubId];
if (window.console && console.log) {
console.log('[HubNavigation] Cleared pending collapse timeout for:', hubId);
}
}
item.dataset.state = 'expanded';
var submenu = item.querySelector('.hub-item__submenu');
if (submenu) {
submenu.hidden = false;
}
// Show overlay
var overlay = hubElement.querySelector('.hub-drawer-overlay');
if (overlay) {
overlay.hidden = false;
}
if (window.console && console.log) {
console.log('[HubNavigation] Expanded:', hubId);
}
}
function collapse(item) {
var hubId = item.dataset.hub;
item.dataset.state = 'collapsed';
var submenu = item.querySelector('.hub-item__submenu');
if (submenu) {
// Clear any existing timeout for this hub
if (collapseTimeouts[hubId]) {
clearTimeout(collapseTimeouts[hubId]);
}
// Wait for animation before hiding
collapseTimeouts[hubId] = setTimeout(function() {
submenu.hidden = true;
delete collapseTimeouts[hubId];
if (window.console && console.log) {
console.log('[HubNavigation] Collapsed:', hubId);
}
}, ANIMATION_DURATION);
}
}
function collapseAll() {
hubElement.querySelectorAll('.hub-item').forEach(function(item) {
collapse(item);
});
// Hide overlay
var overlay = hubElement.querySelector('.hub-drawer-overlay');
if (overlay) {
setTimeout(function() {
overlay.hidden = true;
}, 300);
}
}
function handleTriggerClick(e) {
// Prevent default navigation for the trigger
e.preventDefault();
var hubItem = e.currentTarget.closest('.hub-item');
if (hubItem) {
toggleHub(hubItem);
}
}
function handleOverlayClick() {
// Clicking overlay collapses all
collapseAll();
activeHub = null;
hubElement.dataset.activeHub = '';
}
/**
* Handle resize events - cleanup if switching to desktop
*/
function onResize() {
if (!isMobileViewport() && hubElement) {
// Remove hidden class when switching to desktop
hubElement.classList.remove('is-hidden');
}
}
/**
* Initialize the gadget
*/
function init() {
// Find hub element - support both v1 (#hub) and v2 (.hub-navigation)
hubElement = document.getElementById('hub') || document.querySelector('.hub-navigation');
if (!hubElement) {
// Hub not present on this page
return;
}
// Initialize expansion functionality (works on both mobile and desktop)
initExpansion();
// Only attach scroll listeners if on mobile viewport
if (isMobileViewport()) {
// Use passive event listener for better scroll performance
window.addEventListener('scroll', onScroll, { passive: true });
}
// Always listen for resize to handle viewport changes
window.addEventListener('resize', onResize, { passive: true });
// Log initialization for debugging
if (window.console && console.log) {
console.log('[HubNavigation] Initialized on ' + (isMobileViewport() ? 'mobile' : 'desktop') + ' viewport');
}
}
/**
* Initialize expansion functionality
*/
function initExpansion() {
// Attach click handlers to hub triggers
hubElement.querySelectorAll('.hub-item__trigger').forEach(function(trigger) {
trigger.addEventListener('click', handleTriggerClick);
});
// Prevent navigation on label links (they're inside triggers)
hubElement.querySelectorAll('.hub-label a').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (window.console && console.log) {
console.log('[HubNavigation] Label link click prevented');
}
});
});
// Prevent navigation on submenu links (for now - can be enabled later)
hubElement.querySelectorAll('.hub-submenu__item a').forEach(function(link) {
link.addEventListener('click', function(e) {
if (window.console && console.log) {
console.log('[HubNavigation] Submenu link clicked:', this.href);
}
// Allow navigation - comment out to prevent:
// e.preventDefault();
});
});
// Attach overlay click handler
var overlay = hubElement.querySelector('.hub-drawer-overlay');
if (overlay) {
overlay.addEventListener('click', handleOverlayClick);
}
if (window.console && console.log) {
console.log('[HubNavigation] Expansion handlers attached');
}
}
/**
* Check for reduced motion preference
*/
function prefersReducedMotion() {
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Initialize when mediawiki.util is ready (earliest safe point)
mw.loader.using('mediawiki.util', function() {
// Don't run if user prefers reduced motion
if (prefersReducedMotion()) {
if (window.console && console.log) {
console.log('[HubAutoHide] Disabled - user prefers reduced motion');
}
return;
}
// Wait for DOM to be ready
$(function() {
init();
});
});
})();