MediaWiki:Common.css: Difference between revisions
Jump to navigation
Jump to search
mNo edit summary Tag: Manual revert |
mNo edit summary Tag: Reverted |
||
| Line 1: | Line 1: | ||
/* | /* MediaWiki:Common.js | ||
Lightweight fallback mobile menu | |||
Activates only when Minerva/native mobile menu is not present | |||
*/ | |||
mw.hook('wikipage.content').add(function () { | |||
// Only run on narrow screens | |||
if (window.innerWidth > 800) return; | |||
// If native Minerva menu exists, do nothing | |||
if (document.querySelector('.minerva-menu-button, .minerva-hamburger, .mw-mobile-menu')) return; | |||
// Avoid injecting twice | |||
if (document.querySelector('.fallback-mobile-hamburger')) return; | |||
. | // Create hamburger button | ||
var btn = document.createElement('button'); | |||
btn.className = 'fallback-mobile-hamburger'; | |||
btn.type = 'button'; | |||
btn.setAttribute('aria-label', 'Open menu'); | |||
btn.setAttribute('aria-expanded', 'false'); | |||
btn.innerHTML = '☰'; | |||
// Insert into header area (best-effort) | |||
var header = document.querySelector('.minerva-header') || document.querySelector('#mw-head') || document.body; | |||
try { | |||
header.insertBefore(btn, header.firstChild); | |||
} catch (e) { | |||
document.body.appendChild(btn); | |||
} | |||
// Create overlay and panel | |||
var overlay = document.createElement('div'); | |||
overlay.className = 'fallback-mobile-overlay'; | |||
overlay.setAttribute('role', 'dialog'); | |||
overlay.setAttribute('aria-modal', 'true'); | |||
overlay.style.display = 'none'; | |||
var panel = document.createElement('nav'); | |||
panel.className = 'fallback-mobile-panel'; | |||
panel.setAttribute('aria-label', 'Mobile menu'); | |||
. | panel.setAttribute('role', 'menu'); | ||
overlay.appendChild(panel); | |||
/ | // Close control | ||
var close = document.createElement('div'); | |||
close.className = 'fallback-mobile-close'; | |||
close.innerHTML = '<a href="#" aria-label="Close menu">✕</a>'; | |||
panel.appendChild(close); | |||
// Menu links — mirror typical Mobile-menu entries | |||
var links = [ | |||
{ href: mw.util.getUrl('Main_Page'), label: 'Home' }, | |||
{ href: mw.util.getUrl('Special:Search'), label: 'Search' }, | |||
{ href: mw.util.getUrl('Recentchanges'), label: 'Recent changes' }, | |||
{ href: mw.util.getUrl('Special:Random'), label: 'Random page' }, | |||
{ href: mw.util.getUrl('Special:AllPages'), label: 'All pages' }, | |||
{ href: mw.util.getUrl('Special:Categories'), label: 'Categories' }, | |||
{ href: mw.util.getUrl('Special:Upload'), label: 'Upload file' } | |||
]; | |||
links.forEach(function (lnk) { | |||
var a = document.createElement('a'); | |||
a.href = lnk.href; | |||
a.textContent = lnk.label; | |||
a.setAttribute('role', 'menuitem'); | |||
a.tabIndex = 0; | |||
panel.appendChild(a); | |||
}); | |||
document.body.appendChild(overlay); | |||
/ | // Focus management helpers | ||
function trapFocus(container) { | |||
var focusable = container.querySelectorAll('a, button, [tabindex]:not([tabindex="-1"])'); | |||
if (!focusable.length) return null; | |||
var first = focusable[0]; | |||
var last = focusable[focusable.length - 1]; | |||
function handleTab(e) { | |||
if (e.key !== 'Tab') return; | |||
if (e.shiftKey && document.activeElement === first) { | |||
e.preventDefault(); | |||
last.focus(); | |||
} else if (!e.shiftKey && document.activeElement === last) { | |||
e.preventDefault(); | |||
first.focus(); | |||
} | |||
} | } | ||
} | document.addEventListener('keydown', handleTab); | ||
return function remove() { document.removeEventListener('keydown', handleTab); }; | |||
} | |||
var removeTrap = null; | |||
. | function openMenu() { | ||
overlay.style.display = 'flex'; | |||
document.body.style.overflow = 'hidden'; | |||
} | btn.setAttribute('aria-expanded', 'true'); | ||
// focus first link | |||
var firstLink = panel.querySelector('a'); | |||
if (firstLink) firstLink.focus(); | |||
removeTrap = trapFocus(panel); | |||
} | |||
function closeMenu() { | |||
. | overlay.style.display = 'none'; | ||
. | document.body.style.overflow = ''; | ||
. | btn.setAttribute('aria-expanded', 'false'); | ||
. | if (removeTrap) { removeTrap(); removeTrap = null; } | ||
. | btn.focus(); | ||
} | |||
. | |||
. | // Event listeners | ||
. | btn.addEventListener('click', function (e) { | ||
e.preventDefault(); | |||
openMenu(); | |||
}); | |||
overlay.addEventListener('click', function (e) { | |||
if (e.target === overlay) closeMenu(); | |||
}); | |||
close.addEventListener('click', function (e) { | |||
e.preventDefault(); | |||
closeMenu(); | |||
}); | |||
/ | // Close on Escape | ||
document.addEventListener('keydown', function (e) { | |||
if (e.key === 'Escape' && overlay.style.display === 'flex') { | |||
closeMenu(); | |||
} | |||
}); | |||
/ | // If viewport is resized to desktop, remove fallback elements | ||
window.addEventListener('resize', function () { | |||
if (window.innerWidth > 800) { | |||
if (overlay.parentNode) overlay.parentNode.removeChild(overlay); | |||
if (btn.parentNode) btn.parentNode.removeChild(btn); | |||
document.body.style.overflow = ''; | |||
} | |||
}); | |||
}); | |||
. | |||
. | |||
. | |||
. | |||
. | |||
. | |||
} | |||
Revision as of 23:21, 31 March 2026
/* MediaWiki:Common.js
Lightweight fallback mobile menu
Activates only when Minerva/native mobile menu is not present
*/
mw.hook('wikipage.content').add(function () {
// Only run on narrow screens
if (window.innerWidth > 800) return;
// If native Minerva menu exists, do nothing
if (document.querySelector('.minerva-menu-button, .minerva-hamburger, .mw-mobile-menu')) return;
// Avoid injecting twice
if (document.querySelector('.fallback-mobile-hamburger')) return;
// Create hamburger button
var btn = document.createElement('button');
btn.className = 'fallback-mobile-hamburger';
btn.type = 'button';
btn.setAttribute('aria-label', 'Open menu');
btn.setAttribute('aria-expanded', 'false');
btn.innerHTML = '☰';
// Insert into header area (best-effort)
var header = document.querySelector('.minerva-header') || document.querySelector('#mw-head') || document.body;
try {
header.insertBefore(btn, header.firstChild);
} catch (e) {
document.body.appendChild(btn);
}
// Create overlay and panel
var overlay = document.createElement('div');
overlay.className = 'fallback-mobile-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.style.display = 'none';
var panel = document.createElement('nav');
panel.className = 'fallback-mobile-panel';
panel.setAttribute('aria-label', 'Mobile menu');
panel.setAttribute('role', 'menu');
overlay.appendChild(panel);
// Close control
var close = document.createElement('div');
close.className = 'fallback-mobile-close';
close.innerHTML = '<a href="#" aria-label="Close menu">✕</a>';
panel.appendChild(close);
// Menu links — mirror typical Mobile-menu entries
var links = [
{ href: mw.util.getUrl('Main_Page'), label: 'Home' },
{ href: mw.util.getUrl('Special:Search'), label: 'Search' },
{ href: mw.util.getUrl('Recentchanges'), label: 'Recent changes' },
{ href: mw.util.getUrl('Special:Random'), label: 'Random page' },
{ href: mw.util.getUrl('Special:AllPages'), label: 'All pages' },
{ href: mw.util.getUrl('Special:Categories'), label: 'Categories' },
{ href: mw.util.getUrl('Special:Upload'), label: 'Upload file' }
];
links.forEach(function (lnk) {
var a = document.createElement('a');
a.href = lnk.href;
a.textContent = lnk.label;
a.setAttribute('role', 'menuitem');
a.tabIndex = 0;
panel.appendChild(a);
});
document.body.appendChild(overlay);
// Focus management helpers
function trapFocus(container) {
var focusable = container.querySelectorAll('a, button, [tabindex]:not([tabindex="-1"])');
if (!focusable.length) return null;
var first = focusable[0];
var last = focusable[focusable.length - 1];
function handleTab(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener('keydown', handleTab);
return function remove() { document.removeEventListener('keydown', handleTab); };
}
var removeTrap = null;
function openMenu() {
overlay.style.display = 'flex';
document.body.style.overflow = 'hidden';
btn.setAttribute('aria-expanded', 'true');
// focus first link
var firstLink = panel.querySelector('a');
if (firstLink) firstLink.focus();
removeTrap = trapFocus(panel);
}
function closeMenu() {
overlay.style.display = 'none';
document.body.style.overflow = '';
btn.setAttribute('aria-expanded', 'false');
if (removeTrap) { removeTrap(); removeTrap = null; }
btn.focus();
}
// Event listeners
btn.addEventListener('click', function (e) {
e.preventDefault();
openMenu();
});
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeMenu();
});
close.addEventListener('click', function (e) {
e.preventDefault();
closeMenu();
});
// Close on Escape
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && overlay.style.display === 'flex') {
closeMenu();
}
});
// If viewport is resized to desktop, remove fallback elements
window.addEventListener('resize', function () {
if (window.innerWidth > 800) {
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
if (btn.parentNode) btn.parentNode.removeChild(btn);
document.body.style.overflow = '';
}
});
});