MediaWiki:Jotunnheim.js: Difference between revisions
Jump to navigation
Jump to search
Add responsive Jotunnheim menu and table of contents controls |
Add responsive Jotunnheim menu and table of contents controls |
||
| Line 48: | Line 48: | ||
var tableUpdateQueued = false; | var tableUpdateQueued = false; | ||
var tableObserver = window.ResizeObserver ? new ResizeObserver(scheduleTableUpdate) : null; | var tableObserver = window.ResizeObserver ? new ResizeObserver(scheduleTableUpdate) : null; | ||
function updateTableFade(wrapper) { | |||
wrapper.classList.toggle('jth-table-has-more-left', wrapper.scrollLeft > 1); | |||
wrapper.classList.toggle('jth-table-has-more-right', | |||
wrapper.scrollWidth - wrapper.clientWidth - wrapper.scrollLeft > 1); | |||
} | |||
function updateTables() { | function updateTables() { | ||
| Line 68: | Line 74: | ||
} | } | ||
tableWrappers.push(wrapper); | tableWrappers.push(wrapper); | ||
wrapper.addEventListener('scroll', function () { | |||
updateTableFade(wrapper); | |||
}, { passive: true }); | |||
if (tableObserver) { | if (tableObserver) { | ||
tableObserver.observe(wrapper); | tableObserver.observe(wrapper); | ||
| Line 85: | Line 94: | ||
wrapper.removeAttribute('aria-label'); | wrapper.removeAttribute('aria-label'); | ||
} | } | ||
updateTableFade(wrapper); | |||
}); | }); | ||
} | } | ||
Revision as of 12:03, 27 September 2026
/* Keep wide tables in the article, the desktop TOC beside it, and expose phone drawers. */
(function () {
var mobile = window.matchMedia('(max-width: 50rem)');
function makeButton(name, label, icon) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'jth-mobile-' + name;
button.setAttribute('aria-label', 'Open ' + label);
button.setAttribute('title', label);
button.setAttribute('aria-expanded', 'false');
button.innerHTML = '<svg class="jth-open-icon" viewBox="0 0 24 24" aria-hidden="true">' +
icon + '</svg>' +
'<svg class="jth-close-icon" viewBox="0 0 24 24" aria-hidden="true">' +
'<path d="M5 5l14 14M19 5L5 19"/></svg>';
return button;
}
function init() {
var shell = document.querySelector('body.skin-jotunnheim .jth-shell');
if (!shell) {
return;
}
function syncShellWidth() {
var width = document.documentElement.clientWidth + 'px';
if (shell.style.getPropertyValue('--jth-viewport-width') !== width) {
shell.style.setProperty('--jth-viewport-width', width);
}
}
syncShellWidth();
window.addEventListener('resize', syncShellWidth);
if (window.ResizeObserver) {
new ResizeObserver(syncShellWidth).observe(document.documentElement);
}
var nav = shell && shell.querySelector('.jth-nav');
var page = shell && shell.querySelector(':scope > .jth-main');
var main = page && page.querySelector('main#content');
var content = main && main.querySelector('.mw-body-content');
if (!nav || !page || !main || !content) {
return;
}
var tables = Array.from(content.querySelectorAll('table.wikitable'));
var tableWrappers = [];
var tableUpdateQueued = false;
var tableObserver = window.ResizeObserver ? new ResizeObserver(scheduleTableUpdate) : null;
function updateTableFade(wrapper) {
wrapper.classList.toggle('jth-table-has-more-left', wrapper.scrollLeft > 1);
wrapper.classList.toggle('jth-table-has-more-right',
wrapper.scrollWidth - wrapper.clientWidth - wrapper.scrollLeft > 1);
}
function updateTables() {
tableUpdateQueued = false;
var article = content.getBoundingClientRect();
tables.forEach(function (table) {
if (table.closest('.jth-wikitable-scroll')) {
return;
}
var bounds = table.getBoundingClientRect();
if (bounds.left >= article.left - 1 && bounds.right <= article.right + 1) {
return;
}
var wrapper = document.createElement('div');
wrapper.className = 'jth-wikitable-scroll';
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
if (window.getComputedStyle(table).float !== 'none') {
table.style.float = 'none';
}
tableWrappers.push(wrapper);
wrapper.addEventListener('scroll', function () {
updateTableFade(wrapper);
}, { passive: true });
if (tableObserver) {
tableObserver.observe(wrapper);
}
});
tableWrappers.forEach(function (wrapper) {
if (wrapper.scrollWidth > wrapper.clientWidth + 1) {
var caption = wrapper.querySelector('caption');
wrapper.tabIndex = 0;
wrapper.setAttribute('role', 'region');
wrapper.setAttribute('aria-label',
(caption && caption.textContent.trim()) || 'Scrollable table');
} else {
wrapper.removeAttribute('tabindex');
wrapper.removeAttribute('role');
wrapper.removeAttribute('aria-label');
}
updateTableFade(wrapper);
});
}
function scheduleTableUpdate() {
if (!tableUpdateQueued) {
tableUpdateQueued = true;
window.requestAnimationFrame(updateTables);
}
}
if (tables.length) {
if (tableObserver) {
tableObserver.observe(content);
tables.forEach(function (table) {
tableObserver.observe(table);
});
}
updateTables();
window.addEventListener('resize', scheduleTableUpdate);
}
var toc = content.querySelector('#toc');
var column = null;
if (toc) {
column = document.createElement('div');
column.className = 'jth-toc-column';
column.id = 'jth-mobile-toc';
column.tabIndex = -1;
column.setAttribute('role', 'region');
column.setAttribute('aria-label', 'Table of contents');
content.parentNode.insertBefore(column, content);
column.appendChild(toc);
}
var tocUpdateQueued = false;
function updateTocViewport() {
tocUpdateQueued = false;
if (!column) {
return;
}
if (window.innerWidth <= 1300) {
toc.style.removeProperty('--jth-toc-available-height');
toc.style.removeProperty('visibility');
toc.classList.remove('jth-toc-has-more');
return;
}
var top = Math.max(16, column.getBoundingClientRect().top);
var viewportHeight = window.innerHeight - top - 16;
var articleHeight = main.getBoundingClientRect().bottom - top - 16;
var height = Math.max(0, Math.min(viewportHeight, articleHeight));
toc.style.setProperty('--jth-toc-available-height', height + 'px');
toc.style.visibility = height < 48 ? 'hidden' : '';
toc.classList.toggle('jth-toc-has-more',
height >= 48 && toc.scrollHeight - toc.clientHeight - toc.scrollTop > 1);
}
function scheduleTocUpdate() {
if (!tocUpdateQueued) {
tocUpdateQueued = true;
window.requestAnimationFrame(updateTocViewport);
}
}
nav.id = nav.id || 'jth-mobile-sidebar';
nav.tabIndex = -1;
nav.setAttribute('aria-label', 'Site menu');
var backdrop = document.createElement('div');
backdrop.className = 'jth-mobile-backdrop';
backdrop.hidden = true;
backdrop.setAttribute('aria-hidden', 'true');
var controls = document.createElement('div');
controls.className = 'jth-mobile-controls';
var menuButton = makeButton('menu', 'menu', '<path d="M4 6h16M4 12h16M4 18h16"/>');
menuButton.setAttribute('aria-controls', nav.id);
controls.appendChild(menuButton);
var tocButton = null;
if (column) {
tocButton = makeButton('toc', 'table of contents',
'<path d="M9 6h11M9 12h11M9 18h11"/>' +
'<circle cx="5" cy="6" r="1"/><circle cx="5" cy="12" r="1"/>' +
'<circle cx="5" cy="18" r="1"/>');
tocButton.setAttribute('aria-controls', column.id);
controls.appendChild(tocButton);
}
document.body.append(backdrop, controls);
var openPanel = null;
function setPanel(next, restoreFocus) {
var previous = openPanel;
openPanel = next;
document.documentElement.classList.toggle('jth-menu-open', next === 'menu');
document.documentElement.classList.toggle('jth-toc-open', next === 'toc');
document.body.classList.toggle('jth-mobile-panel-open', !!next);
backdrop.hidden = !next;
menuButton.setAttribute('aria-expanded', String(next === 'menu'));
menuButton.setAttribute('aria-label', (next === 'menu' ? 'Close' : 'Open') + ' menu');
if (tocButton) {
tocButton.setAttribute('aria-expanded', String(next === 'toc'));
tocButton.setAttribute('aria-label',
(next === 'toc' ? 'Close' : 'Open') + ' table of contents');
}
nav.inert = mobile.matches && next !== 'menu';
if (column) {
column.inert = mobile.matches && next !== 'toc';
}
page.inert = mobile.matches && !!next;
if (next) {
(next === 'menu' ? nav : column).focus({ preventScroll: true });
} else if (restoreFocus && previous) {
(previous === 'menu' ? menuButton : tocButton).focus({ preventScroll: true });
}
}
function syncViewport() {
setPanel(null, false);
if (column) {
if (mobile.matches) {
document.body.appendChild(column);
} else {
content.parentNode.insertBefore(column, content);
}
}
nav.inert = mobile.matches;
if (column) {
column.inert = mobile.matches;
}
page.inert = false;
scheduleTocUpdate();
}
menuButton.addEventListener('click', function () {
if (mobile.matches) {
setPanel(openPanel === 'menu' ? null : 'menu', true);
}
});
if (tocButton) {
toc.addEventListener('scroll', scheduleTocUpdate, { passive: true });
window.addEventListener('scroll', scheduleTocUpdate, { passive: true });
window.addEventListener('resize', scheduleTocUpdate);
tocButton.addEventListener('click', function () {
if (mobile.matches) {
setPanel(openPanel === 'toc' ? null : 'toc', true);
}
});
column.addEventListener('click', function (event) {
if (event.target.closest('a[href^="#"]')) {
setPanel(null, false);
}
});
}
backdrop.addEventListener('click', function () {
setPanel(null, true);
});
nav.addEventListener('click', function (event) {
if (event.target.closest('a[href^="#"]')) {
setPanel(null, false);
}
});
document.addEventListener('keydown', function (event) {
if (!openPanel) {
return;
}
if (event.key === 'Escape') {
event.preventDefault();
setPanel(null, true);
} else if (event.key === 'Tab') {
var panel = openPanel === 'menu' ? nav : column;
var focusable = Array.from(panel.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), ' +
'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)).filter(function (element) {
return element.getClientRects().length > 0;
});
var toggleButton = openPanel === 'menu' ? menuButton : tocButton;
if (!focusable.length) {
event.preventDefault();
toggleButton.focus();
return;
}
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (document.activeElement === toggleButton) {
event.preventDefault();
(event.shiftKey ? last : first).focus();
} else if (!panel.contains(document.activeElement) || document.activeElement === panel) {
event.preventDefault();
(event.shiftKey ? toggleButton : first).focus();
} else if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
toggleButton.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
toggleButton.focus();
}
}
});
document.documentElement.classList.add('jth-mobile-ready');
syncViewport();
mobile.addEventListener('change', syncViewport);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
}());