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 ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
const
|
|
|
|
ACTIVETAB_SELECTOR = '[aria-selected="true"]',
|
2022-10-23 21:11:18 +00:00
|
|
|
ACTIVEPANEL_SELECTOR = '[aria-hidden="false"]';
|
2022-10-21 22:01:38 +00:00
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
const
|
|
|
|
config = require( './config.json' ),
|
2023-07-06 02:09:39 +00:00
|
|
|
header = tabber.querySelector( ':scope > .tabber__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' ),
|
2022-10-21 22:01:38 +00:00
|
|
|
nextButton = document.createElement( 'div' ),
|
|
|
|
indicator = document.createElement( 'div' );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
const buildTabs = function () {
|
|
|
|
const
|
|
|
|
tabPanels = tabber.querySelectorAll( ':scope > .tabber__section > .tabber__panel' ),
|
2022-10-23 21:33:39 +00:00
|
|
|
fragment = new DocumentFragment(),
|
|
|
|
hashList = [];
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2022-04-23 00:00:32 +00:00
|
|
|
Array.prototype.forEach.call( tabPanels, function ( tabPanel ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
const
|
|
|
|
title = tabPanel.getAttribute( 'data-title' ),
|
2021-06-21 21:14:33 +00:00
|
|
|
tab = document.createElement( 'a' );
|
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
let hash = mw.util.escapeIdForAttribute( title ) + '-' + count;
|
|
|
|
|
2022-06-05 19:16:48 +00:00
|
|
|
// add to list of already used hash
|
|
|
|
hashList.push( hash );
|
|
|
|
|
|
|
|
// check if the hash is already used before
|
2023-07-05 21:43:41 +00:00
|
|
|
let hashCount = 0;
|
2022-10-21 22:01:38 +00:00
|
|
|
hashList.forEach(
|
|
|
|
function ( h ) {
|
|
|
|
hashCount += ( h === hash ) ? 1 : 0;
|
|
|
|
}
|
|
|
|
);
|
2022-06-05 19:16:48 +00:00
|
|
|
|
|
|
|
// append counter if the same hash already used
|
2022-10-21 22:01:38 +00:00
|
|
|
hash += ( hashCount === 1 ) ? '' : ( '-' + hashCount );
|
2022-06-05 19:16:48 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
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
|
|
|
|
2022-11-02 23:55:46 +00:00
|
|
|
tab.innerText = title;
|
2021-06-21 23:46:17 +00:00
|
|
|
tab.classList.add( 'tabber__tab' );
|
2021-06-21 21:14:33 +00:00
|
|
|
tab.setAttribute( 'role', 'tab' );
|
|
|
|
tab.setAttribute( 'href', '#' + hash );
|
|
|
|
tab.setAttribute( 'id', 'tab-' + hash );
|
2022-04-30 21:14:44 +00:00
|
|
|
tab.setAttribute( 'aria-selected', 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
|
|
|
|
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' );
|
2022-10-21 22:01:38 +00:00
|
|
|
indicator.classList.add( 'tabber__indicator' );
|
2021-06-21 23:28:47 +00:00
|
|
|
|
2022-10-21 22:01:38 +00:00
|
|
|
header.append( prevButton, tabList, nextButton, indicator );
|
2021-06-21 21:14:33 +00:00
|
|
|
};
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2022-10-22 03:07:56 +00:00
|
|
|
// There is probably a smarter way to do this
|
2023-07-05 21:43:41 +00:00
|
|
|
const getActualSize = function ( element, type ) {
|
|
|
|
let value;
|
2022-10-22 03:07:56 +00:00
|
|
|
|
|
|
|
switch ( type ) {
|
|
|
|
case 'width':
|
|
|
|
value = element.offsetWidth;
|
|
|
|
break;
|
|
|
|
case 'height':
|
|
|
|
value = element.offsetHeight;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( value === 0 ) {
|
2022-04-18 23:59:38 +00:00
|
|
|
// Sometimes the tab is hidden by one of its parent elements
|
2022-10-22 03:07:56 +00:00
|
|
|
// and you can only get the actual size by cloning the element
|
2023-07-05 21:43:41 +00:00
|
|
|
const clone = element.cloneNode( true );
|
2022-04-18 23:59:38 +00:00
|
|
|
// Hide the cloned element
|
|
|
|
clone.style.cssText = 'position:absolute;visibility:hidden;';
|
|
|
|
// Add cloned element to body
|
|
|
|
document.body.appendChild( clone );
|
2022-10-22 03:07:56 +00:00
|
|
|
// Measure the size of the clone
|
|
|
|
switch ( type ) {
|
|
|
|
case 'width':
|
2022-10-22 03:10:38 +00:00
|
|
|
value = clone.offsetWidth;
|
2022-10-22 03:07:56 +00:00
|
|
|
break;
|
|
|
|
case 'height':
|
2022-10-22 03:10:38 +00:00
|
|
|
value = clone.offsetHeight;
|
2022-10-22 03:07:56 +00:00
|
|
|
break;
|
|
|
|
}
|
2022-04-18 23:59:38 +00:00
|
|
|
// Remove the cloned element
|
|
|
|
clone.parentNode.removeChild( clone );
|
|
|
|
}
|
2022-10-22 03:07:56 +00:00
|
|
|
|
|
|
|
return value;
|
|
|
|
};
|
|
|
|
|
2023-07-28 06:47:59 +00:00
|
|
|
const updateSectionHeight = function ( section, panel ) {
|
|
|
|
/* Exit early if it is not the active panel */
|
2023-07-28 07:27:42 +00:00
|
|
|
if ( panel.getAttribute( 'aria-hidden' ) !== 'false' ) {
|
2023-07-28 06:47:59 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-10-22 03:07:56 +00:00
|
|
|
|
2023-07-28 06:47:59 +00:00
|
|
|
const height = getActualSize( panel, 'height' );
|
2022-10-22 03:07:56 +00:00
|
|
|
section.style.height = height + 'px';
|
2022-04-18 23:59:38 +00:00
|
|
|
// Scroll to tab
|
2023-07-28 06:47:59 +00:00
|
|
|
section.scrollLeft = panel.offsetLeft;
|
2022-04-18 23:59:38 +00:00
|
|
|
};
|
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
const onElementResize = function ( entries ) {
|
2022-04-18 23:59:38 +00:00
|
|
|
if ( entries && entries.length > 0 ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
const targetPanel = entries[ 0 ].target;
|
2023-07-28 06:47:59 +00:00
|
|
|
const section = targetPanel.parentNode;
|
|
|
|
updateSectionHeight( section, targetPanel );
|
2022-04-18 23:59:38 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
const updateIndicator = function ( showTransition ) {
|
|
|
|
const
|
|
|
|
activeTab = tabList.querySelector( ACTIVETAB_SELECTOR ),
|
|
|
|
width = getActualSize( activeTab, 'width' );
|
2022-10-23 22:06:52 +00:00
|
|
|
|
2022-10-22 03:07:56 +00:00
|
|
|
indicator.style.width = width + 'px';
|
|
|
|
indicator.style.transform = 'translateX(' + ( activeTab.offsetLeft - tabList.scrollLeft ) + 'px)';
|
2022-10-23 22:06:52 +00:00
|
|
|
indicator.style.transition = '';
|
|
|
|
|
|
|
|
// Do not animate when user prefers reduced motion
|
|
|
|
if ( window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( showTransition ) {
|
|
|
|
indicator.style.transition = 'transform 250ms ease, width 250ms ease';
|
|
|
|
}
|
2022-10-21 22:01:38 +00:00
|
|
|
};
|
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
let resizeObserver = null;
|
2022-04-18 23:59:38 +00:00
|
|
|
if ( window.ResizeObserver ) {
|
|
|
|
resizeObserver = new ResizeObserver( mw.util.debounce( 250, onElementResize ) );
|
|
|
|
}
|
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
buildTabs();
|
2022-05-06 21:45:05 +00:00
|
|
|
tabber.prepend( header );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
// Initalize previous and next buttons
|
2023-07-28 21:46:58 +00:00
|
|
|
const initHeaderButtons = function () {
|
|
|
|
const updateTabsNavigation = function () {
|
|
|
|
/* eslint-disable mediawiki/class-doc */
|
|
|
|
const
|
|
|
|
PREVCLASS = 'tabber__header--prev-visible',
|
|
|
|
NEXTCLASS = 'tabber__header--next-visible';
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2023-07-28 21:46:58 +00:00
|
|
|
const isScrollable = ( tabList.scrollWidth > header.offsetWidth );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2023-07-28 21:46:58 +00:00
|
|
|
if ( !isScrollable ) {
|
|
|
|
header.classList.remove( NEXTCLASS );
|
|
|
|
header.classList.remove( PREVCLASS );
|
|
|
|
return;
|
2022-01-31 06:35:36 +00:00
|
|
|
}
|
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
const scrollLeft = tabList.scrollLeft;
|
2022-01-31 06:35:36 +00:00
|
|
|
|
|
|
|
// Scroll to the start
|
|
|
|
if ( scrollLeft <= 0 ) {
|
2022-05-06 21:45:05 +00:00
|
|
|
header.classList.remove( PREVCLASS );
|
|
|
|
header.classList.add( NEXTCLASS );
|
2021-06-21 21:14:33 +00:00
|
|
|
} else {
|
|
|
|
// Scroll to the end
|
2021-06-21 23:32:15 +00:00
|
|
|
if ( scrollLeft + tabList.offsetWidth >= tabList.scrollWidth ) {
|
2022-05-06 21:45:05 +00:00
|
|
|
header.classList.remove( NEXTCLASS );
|
|
|
|
header.classList.add( PREVCLASS );
|
2021-06-21 17:49:47 +00:00
|
|
|
} else {
|
2022-05-06 21:45:05 +00:00
|
|
|
header.classList.add( NEXTCLASS );
|
|
|
|
header.classList.add( PREVCLASS );
|
2021-06-21 20:56:00 +00:00
|
|
|
}
|
|
|
|
}
|
2023-07-28 21:46:58 +00:00
|
|
|
/* eslint-enable mediawiki/class-doc */
|
2021-06-21 21:14:33 +00:00
|
|
|
};
|
|
|
|
|
2023-07-28 21:46:58 +00:00
|
|
|
updateTabsNavigation();
|
|
|
|
|
|
|
|
// Set up click event listener
|
|
|
|
header.addEventListener( 'click', function ( event ) {
|
|
|
|
// Tab button
|
|
|
|
if ( event.target.classList.contains( 'tabber__tab' ) ) {
|
|
|
|
const targetHash = event.target.getAttribute( 'href' ).slice( 1 );
|
|
|
|
event.preventDefault();
|
|
|
|
if ( !config || config.updateLocationOnTabChange ) {
|
|
|
|
// Add hash to the end of the URL
|
|
|
|
history.replaceState( null, null, '#' + targetHash );
|
|
|
|
}
|
|
|
|
showPanel( targetHash, true );
|
|
|
|
// Handle tab navigation buttons when device uses a pointer device
|
|
|
|
} else if ( matchMedia( '(hover: hover)' ).matches ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
const scrollOffset = header.offsetWidth / 2;
|
2023-07-28 21:46:58 +00:00
|
|
|
const scrollTabs = function ( offset ) {
|
|
|
|
const scrollLeft = tabList.scrollLeft + offset;
|
|
|
|
// Scroll to the start
|
|
|
|
if ( scrollLeft <= 0 ) {
|
|
|
|
tabList.scrollLeft = 0;
|
|
|
|
} else {
|
|
|
|
tabList.scrollLeft = scrollLeft;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
// Prev button
|
|
|
|
if ( event.target.classList.contains( 'tabber__header__prev' ) ) {
|
|
|
|
scrollTabs( -scrollOffset );
|
|
|
|
// Next button
|
|
|
|
} else if ( event.target.classList.contains( 'tabber__header__next' ) ) {
|
|
|
|
scrollTabs( scrollOffset );
|
2022-04-19 20:34:04 +00:00
|
|
|
}
|
2021-06-21 21:14:33 +00:00
|
|
|
}
|
2023-07-28 21:46:58 +00:00
|
|
|
} );
|
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-23 00:00:32 +00:00
|
|
|
tabList.addEventListener( 'scroll', function () {
|
2022-10-23 22:06:52 +00:00
|
|
|
window.requestAnimationFrame( function () {
|
2023-07-28 21:46:58 +00:00
|
|
|
updateTabsNavigation();
|
2022-10-23 22:06:52 +00:00
|
|
|
updateIndicator( false );
|
|
|
|
} );
|
2022-01-31 06:35:36 +00:00
|
|
|
} );
|
|
|
|
|
2022-06-03 20:46:42 +00:00
|
|
|
// Add class to enable animation
|
|
|
|
// TODO: Change default to true when Safari bug is resolved
|
|
|
|
//
|
|
|
|
// Safari does not scroll when scroll-behavior: smooth and overflow: hidden
|
|
|
|
// Therefore the default is set to false now until it gets resolved
|
|
|
|
// https://bugs.webkit.org/show_bug.cgi?id=238497
|
|
|
|
if ( !config || config.enableAnimation ) {
|
|
|
|
tabber.classList.add( 'tabber--animate' );
|
|
|
|
}
|
|
|
|
|
2022-04-19 01:45:33 +00:00
|
|
|
// Listen for element resize
|
|
|
|
if ( window.ResizeObserver ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
const tabListResizeObserver = new ResizeObserver(
|
2023-07-28 21:46:58 +00:00
|
|
|
mw.util.debounce( 250, updateTabsNavigation )
|
2023-07-05 21:43:41 +00:00
|
|
|
);
|
2022-04-19 01:45:33 +00:00
|
|
|
tabListResizeObserver.observe( tabList );
|
|
|
|
}
|
2021-06-21 21:14:33 +00:00
|
|
|
};
|
2021-06-21 20:56:00 +00:00
|
|
|
|
2022-04-19 01:45:33 +00:00
|
|
|
// NOTE: Are there better ways to scope them?
|
2023-07-05 21:43:41 +00:00
|
|
|
const xhr = new XMLHttpRequest();
|
|
|
|
let currentRequest = null, nextRequest = null;
|
2022-04-19 01:45:33 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads page contents into tab
|
|
|
|
*
|
2022-04-23 00:00:32 +00:00
|
|
|
* @param {HTMLElement} targetPanel
|
|
|
|
* @param {string} url
|
2022-04-19 01:45:33 +00:00
|
|
|
*/
|
|
|
|
function loadPage( targetPanel, url ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
const requestData = {
|
2022-04-19 01:45:33 +00:00
|
|
|
url: url,
|
|
|
|
targetPanel: targetPanel
|
|
|
|
};
|
|
|
|
if ( currentRequest ) {
|
2022-04-23 00:00:32 +00:00
|
|
|
if ( currentRequest.url !== requestData.url ) {
|
2022-04-19 01:45:33 +00:00
|
|
|
nextRequest = requestData;
|
|
|
|
}
|
|
|
|
// busy
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
xhr.open( 'GET', url );
|
|
|
|
currentRequest = requestData;
|
|
|
|
xhr.send( null );
|
|
|
|
}
|
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
/**
|
|
|
|
* Show panel based on target hash
|
|
|
|
*
|
|
|
|
* @param {string} targetHash
|
2022-04-23 00:00:32 +00:00
|
|
|
* @param {boolean} allowRemoteLoad
|
2022-09-28 13:53:14 +00:00
|
|
|
* @param {boolean} scrollIntoView
|
2021-06-21 21:14:33 +00:00
|
|
|
*/
|
2022-09-28 13:53:14 +00:00
|
|
|
function showPanel( targetHash, allowRemoteLoad, scrollIntoView ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
const
|
|
|
|
targetPanel = document.getElementById( targetHash ),
|
2021-06-21 21:14:33 +00:00
|
|
|
targetTab = document.getElementById( 'tab-' + targetHash ),
|
2022-04-23 00:00:32 +00:00
|
|
|
section = targetPanel.parentNode,
|
2023-07-05 21:43:41 +00:00
|
|
|
activePanel = section.querySelector( ':scope > ' + ACTIVEPANEL_SELECTOR );
|
|
|
|
|
|
|
|
let parentPanel, parentSection;
|
2022-04-19 01:45:33 +00:00
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
const loadTransclusion = function () {
|
|
|
|
const
|
|
|
|
loading = document.createElement( 'div' ),
|
2022-10-21 22:01:38 +00:00
|
|
|
loadingIndicator = document.createElement( 'div' );
|
2022-04-19 02:46:40 +00:00
|
|
|
|
2022-04-19 03:09:13 +00:00
|
|
|
targetPanel.setAttribute( 'aria-live', 'polite' );
|
|
|
|
targetPanel.setAttribute( 'aria-busy', 'true' );
|
2022-04-19 02:46:40 +00:00
|
|
|
loading.setAttribute( 'class', 'tabber__transclusion--loading' );
|
2022-10-21 22:01:38 +00:00
|
|
|
loadingIndicator.setAttribute( 'class', 'tabber__loading-indicator' );
|
|
|
|
loading.appendChild( loadingIndicator );
|
2022-04-19 01:45:33 +00:00
|
|
|
targetPanel.textContent = '';
|
|
|
|
targetPanel.appendChild( loading );
|
|
|
|
loadPage( targetPanel, targetPanel.dataset.tabberLoadUrl );
|
2022-04-23 00:00:32 +00:00
|
|
|
};
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
if ( activePanel ) {
|
2022-10-23 21:07:19 +00:00
|
|
|
// Just to be safe since there can be multiple active tabs
|
2021-06-23 14:45:14 +00:00
|
|
|
// even if there shouldn't be
|
2023-07-05 21:43:41 +00:00
|
|
|
const activeTabs = tabList.querySelectorAll( ACTIVETAB_SELECTOR );
|
2021-06-23 14:45:14 +00:00
|
|
|
|
|
|
|
if ( activeTabs.length > 0 ) {
|
2022-04-23 00:00:32 +00:00
|
|
|
Array.prototype.forEach.call( activeTabs, function ( activeTab ) {
|
2021-06-23 14:45:14 +00:00
|
|
|
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 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 23:38:59 +00:00
|
|
|
targetTab.setAttribute( 'aria-selected', true );
|
|
|
|
targetPanel.setAttribute( 'aria-hidden', false );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2022-10-23 22:06:52 +00:00
|
|
|
updateIndicator( true );
|
2022-10-21 22:01:38 +00:00
|
|
|
|
2022-04-19 03:09:13 +00:00
|
|
|
// Lazyload transclusion if needed
|
2022-04-23 00:00:32 +00:00
|
|
|
if ( allowRemoteLoad &&
|
|
|
|
targetPanel.dataset.tabberPendingLoad &&
|
|
|
|
targetPanel.dataset.tabberLoadUrl
|
|
|
|
) {
|
2022-04-19 03:09:13 +00:00
|
|
|
loadTransclusion();
|
|
|
|
}
|
|
|
|
|
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
|
2022-04-23 00:00:32 +00:00
|
|
|
/* eslint-disable-next-line no-unmodified-loop-condition */
|
2022-04-18 23:59:38 +00:00
|
|
|
while ( !resizeObserver ) {
|
2022-10-23 21:11:18 +00:00
|
|
|
parentPanel = parentSection.closest( ACTIVEPANEL_SELECTOR );
|
2022-04-18 23:59:38 +00:00
|
|
|
if ( !parentPanel ) {
|
|
|
|
break;
|
|
|
|
}
|
2022-04-23 00:00:32 +00:00
|
|
|
parentSection = parentPanel.parentNode;
|
2022-04-18 23:59:38 +00:00
|
|
|
updateSectionHeight( parentSection, parentPanel );
|
|
|
|
}
|
|
|
|
if ( resizeObserver ) {
|
|
|
|
resizeObserver.observe( targetPanel );
|
|
|
|
}
|
2021-06-21 21:14:33 +00:00
|
|
|
/* eslint-enable mediawiki/class-doc */
|
2022-09-28 13:53:14 +00:00
|
|
|
|
|
|
|
// If requested, scroll the tabber into view (browser fails to do that
|
|
|
|
// on its own as it tries to look up the anchor before we add it to the
|
|
|
|
// DOM)
|
|
|
|
if ( scrollIntoView ) {
|
|
|
|
targetTab.scrollIntoView();
|
|
|
|
}
|
2021-06-21 21:14:33 +00:00
|
|
|
}
|
|
|
|
|
2022-04-19 01:45:33 +00:00
|
|
|
/**
|
|
|
|
* Event handler for XMLHttpRequest where ends loading
|
|
|
|
*/
|
|
|
|
function onLoadEndPage() {
|
2023-07-05 21:43:41 +00:00
|
|
|
const targetPanel = currentRequest.targetPanel;
|
2022-04-23 00:00:32 +00:00
|
|
|
if ( xhr.status !== 200 ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
const err = document.createElement( 'div' ),
|
2022-04-19 14:24:49 +00:00
|
|
|
errMsg = mw.message( 'error' ).text() + ': HTTP ' + xhr.status;
|
|
|
|
|
|
|
|
err.setAttribute( 'class', 'tabber__transclusion--error error' );
|
|
|
|
err.appendChild( document.createTextNode( errMsg ) );
|
2022-04-19 01:45:33 +00:00
|
|
|
targetPanel.textContent = '';
|
|
|
|
targetPanel.appendChild( err );
|
|
|
|
} else {
|
2023-07-05 21:43:41 +00:00
|
|
|
const result = JSON.parse( xhr.responseText );
|
2022-04-19 01:45:33 +00:00
|
|
|
targetPanel.innerHTML = result.parse.text;
|
|
|
|
// wikipage.content hook requires a jQuery object
|
2022-04-23 00:00:32 +00:00
|
|
|
/* eslint-disable-next-line no-undef */
|
2022-04-19 01:45:33 +00:00
|
|
|
mw.hook( 'wikipage.content' ).fire( $( targetPanel ) );
|
|
|
|
delete targetPanel.dataset.tabberPendingLoad;
|
|
|
|
delete targetPanel.dataset.tabberLoadUrl;
|
2022-04-19 03:09:13 +00:00
|
|
|
targetPanel.setAttribute( 'aria-busy', 'false' );
|
2022-04-19 01:45:33 +00:00
|
|
|
}
|
|
|
|
|
2023-07-05 21:43:41 +00:00
|
|
|
const targetHash = targetPanel.getAttribute( 'id' ),
|
2022-04-23 00:00:32 +00:00
|
|
|
section = targetPanel.parentNode,
|
2022-10-23 21:11:18 +00:00
|
|
|
activePanel = section.querySelector( ':scope > ' + ACTIVEPANEL_SELECTOR );
|
2022-04-19 01:45:33 +00:00
|
|
|
|
|
|
|
if ( nextRequest ) {
|
|
|
|
currentRequest = nextRequest;
|
|
|
|
nextRequest = null;
|
|
|
|
xhr.open( 'GET', currentRequest.url );
|
|
|
|
xhr.send( null );
|
|
|
|
} else {
|
|
|
|
currentRequest = null;
|
|
|
|
}
|
|
|
|
if ( activePanel ) {
|
|
|
|
// Refresh height
|
|
|
|
showPanel( targetHash, false );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
xhr.timeout = 20000;
|
|
|
|
xhr.addEventListener( 'loadend', onLoadEndPage );
|
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
/**
|
|
|
|
* Retrieve target hash and trigger show panel
|
|
|
|
* If no targetHash is invalid, use the first panel
|
|
|
|
*
|
2022-09-29 06:13:56 +00:00
|
|
|
* @param {boolean} scrollIntoView
|
2021-06-21 21:14:33 +00:00
|
|
|
*/
|
2022-09-29 06:13:56 +00:00
|
|
|
function switchTab( scrollIntoView ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
let targetHash = new mw.Uri( location.href ).fragment;
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2022-09-29 06:13:56 +00:00
|
|
|
// Switch to the first tab if no targetHash or no tab is detected and do not scroll to it
|
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, '\\$&' ) ) ) {
|
2022-10-21 22:01:38 +00:00
|
|
|
targetHash = tabList.firstElementChild.getAttribute( 'id' ).slice( 4 );
|
2022-09-29 06:13:56 +00:00
|
|
|
scrollIntoView = false;
|
2021-06-21 21:14:33 +00:00
|
|
|
}
|
|
|
|
|
2022-09-29 06:13:56 +00:00
|
|
|
showPanel( targetHash, false, scrollIntoView );
|
2021-06-21 21:14:33 +00:00
|
|
|
}
|
|
|
|
|
2022-09-28 13:53:14 +00:00
|
|
|
switchTab( true );
|
2021-06-21 21:14:33 +00:00
|
|
|
|
2023-07-28 21:46:58 +00:00
|
|
|
initHeaderButtons();
|
2021-06-21 21:14:33 +00:00
|
|
|
|
|
|
|
// window.addEventListener( 'hashchange', switchTab, false );
|
|
|
|
|
|
|
|
tabber.classList.add( 'tabber--live' );
|
|
|
|
}
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
function main() {
|
2023-07-06 02:09:39 +00:00
|
|
|
const
|
|
|
|
tabbers = document.querySelectorAll( '.tabber:not( .tabber--live )' ),
|
2022-05-06 21:45:05 +00:00
|
|
|
style = document.getElementById( 'tabber-style' );
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2021-06-21 21:14:33 +00:00
|
|
|
if ( tabbers ) {
|
2023-07-05 21:43:41 +00:00
|
|
|
let count = 0;
|
2021-06-21 21:14:33 +00:00
|
|
|
mw.loader.load( 'ext.tabberNeue.icons' );
|
2022-04-23 00:00:32 +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
|
|
|
} );
|
2022-05-06 21:45:05 +00:00
|
|
|
// Remove critical render styles after done
|
|
|
|
// IE compatiblity
|
|
|
|
style.parentNode.removeChild( style );
|
2021-06-21 21:14:33 +00:00
|
|
|
}
|
|
|
|
}
|
2021-06-21 17:49:47 +00:00
|
|
|
|
2022-05-31 07:21:17 +00:00
|
|
|
if ( document.readyState === 'interactive' || document.readyState === 'complete' ) {
|
|
|
|
main();
|
|
|
|
} else {
|
|
|
|
document.addEventListener( 'DOMContentLoaded', function () {
|
|
|
|
main();
|
|
|
|
} );
|
2022-06-03 20:46:42 +00:00
|
|
|
}
|
2023-07-06 02:09:39 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Add hooks for Tabber when Visual Editor is used.
|
|
|
|
*/
|
|
|
|
mw.loader.using( 'ext.visualEditor.desktopArticleTarget.init', function () {
|
|
|
|
// After saving edits
|
|
|
|
mw.hook( 'postEdit.afterRemoval' ).add( () => {
|
|
|
|
main();
|
|
|
|
} );
|
2023-07-06 02:10:01 +00:00
|
|
|
} );
|