2021-06-21 17:49:47 +00:00
|
|
|
/**
|
2021-06-21 21:14:33 +00:00
|
|
|
* Initialize Tabber
|
|
|
|
*
|
|
|
|
* @param {HTMLElement} tabber
|
2021-10-23 04:47:49 +00:00
|
|
|
* @param {number} count
|
2021-06-21 17:49:47 +00:00
|
|
|
*/
|
2021-10-23 04:47:49 +00:00
|
|
|
function initTabber( tabber, count ) {
|
2022-04-18 23:10:58 +00:00
|
|
|
var tabPanels = tabber.querySelectorAll( ':scope > .tabber__section > .tabber__panel' );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2022-04-18 23:10:58 +00:00
|
|
|
var container = document.createElement( 'header' ),
|
2021-06-21 23:32:15 +00:00
|
|
|
tabList = document.createElement( 'nav' ),
|
2021-06-21 21:14:33 +00:00
|
|
|
prevButton = document.createElement( 'div' ),
|
|
|
|
nextButton = document.createElement( 'div' );
|
|
|
|
|
2022-04-18 23:10:58 +00:00
|
|
|
var buildTabs = function() {
|
|
|
|
var fragment = new DocumentFragment();
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2022-04-18 23:34:43 +00:00
|
|
|
Array.prototype.forEach.call( tabPanels, function( tabPanel ) {
|
2022-04-18 23:10:58 +00:00
|
|
|
var hash = mw.util.escapeIdForAttribute( tabPanel.title ) + '-' + count,
|
2021-06-21 21:14:33 +00:00
|
|
|
tab = document.createElement( 'a' );
|
|
|
|
|
|
|
|
tabPanel.setAttribute( 'id', hash );
|
|
|
|
tabPanel.setAttribute( 'role', 'tabpanel' );
|
|
|
|
tabPanel.setAttribute( 'aria-labelledby', 'tab-' + hash );
|
2021-06-21 23:38:59 +00:00
|
|
|
tabPanel.setAttribute( 'aria-hidden', true );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
tab.innerText = tabPanel.title;
|
2021-06-21 23:46:17 +00:00
|
|
|
tab.classList.add( 'tabber__tab' );
|
2021-06-21 21:14:33 +00:00
|
|
|
tab.setAttribute( 'title', tabPanel.title );
|
|
|
|
tab.setAttribute( 'role', 'tab' );
|
|
|
|
tab.setAttribute( 'href', '#' + hash );
|
|
|
|
tab.setAttribute( 'id', 'tab-' + hash );
|
2021-06-21 23:38:59 +00:00
|
|
|
tab.setAttribute( 'aria-select', false );
|
2021-06-21 21:14:33 +00:00
|
|
|
tab.setAttribute( 'aria-controls', hash );
|
|
|
|
|
2021-06-21 23:28:47 +00:00
|
|
|
fragment.append( tab );
|
2021-06-21 21:14:33 +00:00
|
|
|
} );
|
|
|
|
|
2021-06-21 23:32:15 +00:00
|
|
|
tabList.append( fragment );
|
2021-06-21 23:28:47 +00:00
|
|
|
|
|
|
|
container.classList.add( 'tabber__header' );
|
2021-06-21 23:46:17 +00:00
|
|
|
tabList.classList.add( 'tabber__tabs' );
|
2021-06-21 23:32:15 +00:00
|
|
|
tabList.setAttribute( 'role', 'tablist' );
|
2021-06-21 23:28:47 +00:00
|
|
|
prevButton.classList.add( 'tabber__header__prev' );
|
|
|
|
nextButton.classList.add( 'tabber__header__next' );
|
|
|
|
|
2021-06-21 23:32:15 +00:00
|
|
|
container.append( prevButton, tabList, nextButton );
|
2021-06-21 21:14:33 +00:00
|
|
|
};
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2022-04-18 23:59:38 +00:00
|
|
|
var updateSectionHeight = function( section, activePanel ) {
|
|
|
|
var height = activePanel.offsetHeight;
|
|
|
|
if ( height === 0 ) {
|
|
|
|
// Sometimes the tab is hidden by one of its parent elements
|
|
|
|
// and you can only get the actual height by cloning the element
|
|
|
|
var clone = activePanel.cloneNode( true );
|
|
|
|
// Hide the cloned element
|
|
|
|
clone.style.cssText = 'position:absolute;visibility:hidden;';
|
|
|
|
// Add cloned element to body
|
|
|
|
document.body.appendChild( clone );
|
|
|
|
// Measure the height of the clone
|
|
|
|
height = clone.clientHeight;
|
|
|
|
// Remove the cloned element
|
|
|
|
clone.parentNode.removeChild( clone );
|
|
|
|
}
|
|
|
|
section.style.height = String( height ) + 'px';
|
|
|
|
// Scroll to tab
|
|
|
|
section.scrollLeft = activePanel.offsetLeft;
|
|
|
|
};
|
|
|
|
|
|
|
|
var onElementResize = function( entries, observer) {
|
|
|
|
if ( entries && entries.length > 0 ) {
|
|
|
|
var targetPanel = entries[0].target;
|
|
|
|
var section = targetPanel.parentElement;
|
|
|
|
updateSectionHeight( section, targetPanel );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
var resizeObserver = null;
|
|
|
|
if ( window.ResizeObserver ) {
|
|
|
|
resizeObserver = new ResizeObserver( mw.util.debounce( 250, onElementResize ) );
|
|
|
|
}
|
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
buildTabs();
|
|
|
|
tabber.prepend( container );
|
|
|
|
|
|
|
|
// Initalize previous and next buttons
|
2022-04-18 23:10:58 +00:00
|
|
|
var initButtons = function() {
|
|
|
|
var PREVCLASS = 'tabber__header--prev-visible',
|
2021-06-21 21:14:33 +00:00
|
|
|
NEXTCLASS = 'tabber__header--next-visible';
|
|
|
|
|
|
|
|
/* eslint-disable mediawiki/class-doc */
|
2022-04-18 23:10:58 +00:00
|
|
|
var scrollTabs = function( offset ) {
|
|
|
|
var scrollLeft = tabList.scrollLeft + offset;
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
// Scroll to the start
|
|
|
|
if ( scrollLeft <= 0 ) {
|
2021-06-21 23:32:15 +00:00
|
|
|
tabList.scrollLeft = 0;
|
2022-01-31 06:35:36 +00:00
|
|
|
} else {
|
|
|
|
tabList.scrollLeft = scrollLeft;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-04-18 23:10:58 +00:00
|
|
|
var updateButtons = function() {
|
|
|
|
var scrollLeft = tabList.scrollLeft;
|
2022-01-31 06:35:36 +00:00
|
|
|
|
|
|
|
// Scroll to the start
|
|
|
|
if ( scrollLeft <= 0 ) {
|
2021-06-21 21:14:33 +00:00
|
|
|
container.classList.remove( PREVCLASS );
|
|
|
|
container.classList.add( NEXTCLASS );
|
|
|
|
} else {
|
|
|
|
// Scroll to the end
|
2021-06-21 23:32:15 +00:00
|
|
|
if ( scrollLeft + tabList.offsetWidth >= tabList.scrollWidth ) {
|
2021-06-21 21:14:33 +00:00
|
|
|
container.classList.remove( NEXTCLASS );
|
|
|
|
container.classList.add( PREVCLASS );
|
2021-06-21 17:49:47 +00:00
|
|
|
} else {
|
2021-06-21 21:14:33 +00:00
|
|
|
container.classList.add( NEXTCLASS );
|
|
|
|
container.classList.add( PREVCLASS );
|
2021-06-21 20:56:00 +00:00
|
|
|
}
|
|
|
|
}
|
2021-06-21 21:14:33 +00:00
|
|
|
};
|
|
|
|
|
2022-04-18 23:10:58 +00:00
|
|
|
var setupButtons = function() {
|
|
|
|
var isScrollable = ( tabList.scrollWidth > container.offsetWidth );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
if ( isScrollable ) {
|
2022-04-18 23:10:58 +00:00
|
|
|
var scrollOffset = container.offsetWidth / 2;
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
// Just to add the right classes
|
2022-01-31 06:35:36 +00:00
|
|
|
updateButtons();
|
2022-04-18 23:10:58 +00:00
|
|
|
prevButton.addEventListener( 'click', function() {
|
2021-06-21 21:14:33 +00:00
|
|
|
scrollTabs( -scrollOffset );
|
|
|
|
}, false );
|
|
|
|
|
2022-04-18 23:10:58 +00:00
|
|
|
nextButton.addEventListener( 'click', function() {
|
2021-06-21 21:14:33 +00:00
|
|
|
scrollTabs( scrollOffset );
|
|
|
|
}, false );
|
|
|
|
} else {
|
|
|
|
container.classList.remove( NEXTCLASS );
|
|
|
|
container.classList.remove( PREVCLASS );
|
|
|
|
}
|
|
|
|
};
|
|
|
|
/* eslint-enable mediawiki/class-doc */
|
2021-06-21 20:56:00 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
setupButtons();
|
2021-06-21 20:56:00 +00:00
|
|
|
|
2022-01-31 06:35:36 +00:00
|
|
|
// Listen for scroll event on header
|
|
|
|
// Also triggered by side-scrolling using other means other than the buttons
|
2022-04-18 23:10:58 +00:00
|
|
|
tabList.addEventListener( 'scroll', function() {
|
2022-01-31 06:35:36 +00:00
|
|
|
updateButtons();
|
|
|
|
} );
|
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
// Listen for window resize
|
2022-04-18 23:59:38 +00:00
|
|
|
window.addEventListener( 'resize', mw.util.debounce( 250, setupButtons ) );
|
2021-06-21 21:14:33 +00:00
|
|
|
};
|
2021-06-21 20:56:00 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
/**
|
|
|
|
* Show panel based on target hash
|
|
|
|
*
|
|
|
|
* @param {string} targetHash
|
|
|
|
*/
|
|
|
|
function showPanel( targetHash ) {
|
2022-04-18 23:10:58 +00:00
|
|
|
var ACTIVETABCLASS = 'tabber__tab--active',
|
2021-06-21 21:14:33 +00:00
|
|
|
ACTIVEPANELCLASS = 'tabber__panel--active',
|
|
|
|
targetPanel = document.getElementById( targetHash ),
|
|
|
|
targetTab = document.getElementById( 'tab-' + targetHash ),
|
|
|
|
section = targetPanel.parentElement,
|
2021-06-23 14:45:14 +00:00
|
|
|
activePanel = section.querySelector( ':scope > .' + ACTIVEPANELCLASS );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
/* eslint-disable mediawiki/class-doc */
|
|
|
|
if ( activePanel ) {
|
2021-06-23 14:45:14 +00:00
|
|
|
// Just to be safe since there can be multiple active classes
|
|
|
|
// even if there shouldn't be
|
2022-04-18 23:10:58 +00:00
|
|
|
var activeTabs = tabList.querySelectorAll( '.' + ACTIVETABCLASS );
|
2021-06-23 14:45:14 +00:00
|
|
|
|
|
|
|
if ( activeTabs.length > 0 ) {
|
2022-04-18 23:34:43 +00:00
|
|
|
Array.prototype.forEach.call( activeTabs, function( activeTab ) {
|
2021-06-23 14:45:14 +00:00
|
|
|
activeTab.classList.remove( ACTIVETABCLASS );
|
|
|
|
activeTab.setAttribute( 'aria-selected', false );
|
|
|
|
} );
|
2021-06-21 17:49:47 +00:00
|
|
|
}
|
|
|
|
|
2022-04-18 23:59:38 +00:00
|
|
|
if ( resizeObserver ) {
|
|
|
|
resizeObserver.unobserve( activePanel );
|
|
|
|
}
|
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
activePanel.classList.remove( ACTIVEPANELCLASS );
|
2021-06-21 23:38:59 +00:00
|
|
|
activePanel.setAttribute( 'aria-hidden', true );
|
2021-06-21 21:14:33 +00:00
|
|
|
}
|
|
|
|
|
2021-06-21 23:46:17 +00:00
|
|
|
// Add active class to the tab
|
2021-06-21 21:14:33 +00:00
|
|
|
targetTab.classList.add( ACTIVETABCLASS );
|
2021-06-21 23:38:59 +00:00
|
|
|
targetTab.setAttribute( 'aria-selected', true );
|
2021-06-21 21:14:33 +00:00
|
|
|
targetPanel.classList.add( ACTIVEPANELCLASS );
|
2021-06-21 23:38:59 +00:00
|
|
|
targetPanel.setAttribute( 'aria-hidden', false );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2022-04-18 23:59:38 +00:00
|
|
|
updateSectionHeight( section, targetPanel );
|
|
|
|
|
|
|
|
// If we're inside another tab, trigger its logic to recalc its height
|
|
|
|
parentSection = section;
|
|
|
|
// ResizeObserver should take care of the recursivity already
|
|
|
|
while ( !resizeObserver ) {
|
|
|
|
parentPanel = parentSection.closest( '.' + ACTIVEPANELCLASS );
|
|
|
|
if ( !parentPanel ) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
parentSection = parentPanel.parentElement;
|
|
|
|
updateSectionHeight( parentSection, parentPanel );
|
|
|
|
}
|
|
|
|
if ( resizeObserver ) {
|
|
|
|
resizeObserver.observe( targetPanel );
|
|
|
|
}
|
2021-06-21 21:14:33 +00:00
|
|
|
/* eslint-enable mediawiki/class-doc */
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve target hash and trigger show panel
|
|
|
|
* If no targetHash is invalid, use the first panel
|
|
|
|
*
|
|
|
|
* @param {HTMLElement} tabber
|
|
|
|
*/
|
|
|
|
function switchTab() {
|
2022-04-18 23:10:58 +00:00
|
|
|
var targetHash = new mw.Uri( location.href ).fragment;
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
// Switch to the first tab if no targetHash or no tab is detected
|
2022-04-18 23:50:20 +00:00
|
|
|
// TODO: Remove the polyfill with CSS.escape when we are dropping IE support
|
|
|
|
if ( !targetHash || !tabList.querySelector( '#tab-' + targetHash.replace( /[^a-zA-Z0-9-_]/g, '\\$&' ) ) ) {
|
2021-06-21 23:32:15 +00:00
|
|
|
targetHash = tabList.firstElementChild.getAttribute( 'id' ).substring( 4 );
|
2021-06-21 21:14:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
showPanel( targetHash );
|
|
|
|
}
|
|
|
|
|
|
|
|
switchTab();
|
|
|
|
|
|
|
|
// Only run if client is not a touch device
|
|
|
|
if ( matchMedia( '(hover: hover)' ).matches ) {
|
|
|
|
initButtons();
|
|
|
|
}
|
|
|
|
|
|
|
|
// window.addEventListener( 'hashchange', switchTab, false );
|
|
|
|
|
|
|
|
// Respond to clicks on the nav tabs
|
2022-04-18 23:10:58 +00:00
|
|
|
Array.prototype.forEach.call( tabList.children, function( tab ) {
|
|
|
|
tab.addEventListener( 'click', function( event ) {
|
|
|
|
var targetHash = tab.getAttribute( 'href' ).substring( 1 );
|
2021-06-21 21:14:33 +00:00
|
|
|
event.preventDefault();
|
|
|
|
// Add hash to the end of the URL
|
2022-04-19 00:01:02 +00:00
|
|
|
history.replaceState( null, null, '#' + targetHash );
|
2021-06-21 21:14:33 +00:00
|
|
|
showPanel( targetHash );
|
|
|
|
} );
|
|
|
|
} );
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
tabber.classList.add( 'tabber--live' );
|
|
|
|
}
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
function main() {
|
2022-04-18 23:10:58 +00:00
|
|
|
var tabbers = document.querySelectorAll( '.tabber' );
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
if ( tabbers ) {
|
2022-04-18 23:10:58 +00:00
|
|
|
var count = 0;
|
2021-06-21 21:14:33 +00:00
|
|
|
mw.loader.load( 'ext.tabberNeue.icons' );
|
2022-04-18 23:34:43 +00:00
|
|
|
Array.prototype.forEach.call( tabbers, function( tabber ) {
|
2021-10-23 04:47:49 +00:00
|
|
|
initTabber( tabber, count );
|
|
|
|
count++;
|
2021-06-21 17:49:47 +00:00
|
|
|
} );
|
2021-06-21 21:14:33 +00:00
|
|
|
}
|
|
|
|
}
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
main();
|