跳转到内容

MediaWiki:Gadget-switch-tabber.js:修订间差异

来自Arcaea中文维基
Admin留言 | 贡献
同步Switch切换后的目录标题与可见性
Admin留言 | 贡献
支持SwitchTabber子页面的同名锚点定位
 
(未显示同一用户的10个中间版本)
第7行: 第7行:
var TAB_CLASS = 'oo-ui-widget oo-ui-widget-enabled oo-ui-labelElement oo-ui-optionWidget oo-ui-tabOptionWidget';
var TAB_CLASS = 'oo-ui-widget oo-ui-widget-enabled oo-ui-labelElement oo-ui-optionWidget oo-ui-tabOptionWidget';
var LABEL_CLASS = 'oo-ui-labelElement-label';
var LABEL_CLASS = 'oo-ui-labelElement-label';
var TOC_CONTAINER_SELECTOR = '#toc, .toc, .vector-toc';
var activeSwitchTab = 1;
var tocObserver;
var PLATFORM_LABELS = {
var PLATFORM_LABELS = {
mobile: {
mobile: {
第17行: 第20行:
}
}
};
};
function attributeSelector( name, value ) {
return '[' + name + '="' + String( value ).replace( /\\/g, '\\\\' ).replace( /"/g, '\\"' ) + '"]';
}


function attrSelector( value ) {
function attrSelector( value ) {
return '[data-tabber-id="' + String( value ).replace( /\\/g, '\\\\' ).replace( /"/g, '\\"' ) + '"]';
return attributeSelector( 'data-tabber-id', value );
}
 
function idSelector( value ) {
return attributeSelector( 'id', value );
}
}


第40行: 第51行:
function getTabberElements( tabberId, type ) {
function getTabberElements( tabberId, type ) {
return $( '.tabberex-' + type + attrSelector( tabberId ) );
return $( '.tabberex-' + type + attrSelector( tabberId ) );
}
function getSwitchTabIndexForElement( element ) {
var tab = element && element.closest && element.closest( '.tabberex-tab' );
var body;
var platform;
var index;
while ( tab ) {
body = tab.parentNode;
if ( body && body.matches( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ) {
platform = tab.getAttribute( 'data-switchtabber-platform' );
if ( platform === 'mobile' ) {
return 1;
}
if ( platform === 'switch' ) {
return 2;
}
index = toArray( body.children ).indexOf( tab );
return index === -1 ? null : index + 1;
}
tab = tab.parentElement && tab.parentElement.closest( '.tabberex-tab' );
}
return null;
}
}


第69行: 第104行:
}
}


function headingHasContentForTab( heading, index ) {
function toArray( collection ) {
var clone = heading.cloneNode( true );
return Array.prototype.slice.call( collection );
}
 
function removeElements( root, selector ) {
toArray( root.querySelectorAll( selector ) ).forEach( function ( element ) {
element.parentNode.removeChild( element );
} );
}
 
function getHeadingState( heading, tabIndex ) {
var headline = heading.querySelector( '.mw-headline' );
var clone = ( headline || heading ).cloneNode( true );
var containingTabIndex = getSwitchTabIndexForElement( heading );
var label;


Array.prototype.forEach.call(
toArray( clone.querySelectorAll( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ).forEach( function ( body ) {
clone.querySelectorAll( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ),
toArray( body.children ).forEach( function ( tab, index ) {
function ( body ) {
if ( index !== tabIndex - 1 ) {
Array.prototype.slice.call( body.children ).forEach( function ( tab, tabIndex ) {
tab.parentNode.removeChild( tab );
if ( tabIndex !== index - 1 ) {
}
tab.parentNode.removeChild( tab );
} );
}
} );
}
);
Array.prototype.forEach.call( clone.querySelectorAll( '.mobileonly' ), function ( label ) {
label.parentNode.removeChild( label );
} );
} );
removeElements( clone, '.mobileonly, .mw-editsection' );
label = clone.textContent.replace( /\s+/g, ' ' ).trim();


return clone.textContent.trim() !== '' ||
return {
!!clone.querySelector( 'img, svg, math, video, audio, canvas' );
label: label,
visible: label !== '' && ( containingTabIndex === null || containingTabIndex === tabIndex )
};
}
}


function updateSwitchHeadingVisibility( index ) {
function setSwitchElementVisible( element, visible ) {
Array.prototype.forEach.call(
var display;
document.querySelectorAll( 'h1, h2, h3, h4, h5, h6' ),
var priority;
function ( heading ) {
var wrapper;


if ( !heading.querySelector( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ) {
if ( !visible && element.dataset.switchtabberDisplaySet !== '1' ) {
return;
element.dataset.switchtabberDisplay = element.style.getPropertyValue( 'display' );
}
element.dataset.switchtabberDisplayPriority = element.style.getPropertyPriority( 'display' );
wrapper = heading.closest( '.mw-heading' ) || heading;
element.dataset.switchtabberDisplaySet = '1';
wrapper.hidden = !headingHasContentForTab( heading, index );
}
}
element.hidden = !visible;
);
if ( !visible ) {
element.style.setProperty( 'display', 'none', 'important' );
return;
}
if ( element.dataset.switchtabberDisplaySet !== '1' ) {
return;
}
display = element.dataset.switchtabberDisplay;
priority = element.dataset.switchtabberDisplayPriority;
if ( display ) {
element.style.setProperty( 'display', display, priority );
} else {
element.style.removeProperty( 'display' );
}
delete element.dataset.switchtabberDisplay;
delete element.dataset.switchtabberDisplayPriority;
delete element.dataset.switchtabberDisplaySet;
}
 
function getHeadingSectionNumber( heading ) {
return toArray(
document.querySelectorAll( '.mw-parser-output h2, .mw-parser-output h3, .mw-parser-output h4, .mw-parser-output h5, .mw-parser-output h6' )
).indexOf( heading ) + 1;
}
}


function getSwitchHeadingText( heading, index ) {
function getSectionEditControls( heading ) {
var clone = heading.cloneNode( true );
var controls = toArray( heading.querySelectorAll( '.mw-editsection' ) );
var sectionNumber = getHeadingSectionNumber( heading );
 
if ( sectionNumber === 0 ) {
return controls;
}
toArray( document.querySelectorAll( '.mw-editsection' ) ).forEach( function ( control ) {
var link = control.querySelector( 'a[href*="section="]' );
var match = link && /(?:[?&]section=)(\d+)/.exec( link.getAttribute( 'href' ) || '' );


Array.prototype.forEach.call(
if ( match && Number( match[ 1 ] ) === sectionNumber && controls.indexOf( control ) === -1 ) {
clone.querySelectorAll( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ),
controls.push( control );
function ( body ) {
Array.prototype.slice.call( body.children ).forEach( function ( tab, tabIndex ) {
if ( tabIndex !== index - 1 ) {
body.removeChild( tab );
}
} );
}
}
);
} );
Array.prototype.forEach.call(
return controls;
clone.querySelectorAll( '.mobileonly, .mw-editsection' ),
}
function ( element ) {
 
element.parentNode.removeChild( element );
function syncEditSectionControls( heading, label, visible ) {
}
getSectionEditControls( heading ).forEach( function ( control ) {
);
setSwitchElementVisible( control, visible );
toArray( control.querySelectorAll( 'a' ) ).forEach( function ( link ) {
var href = link.getAttribute( 'href' ) || '';
var prefix = href.indexOf( 'veaction=edit' ) !== -1 ? '编辑章节:' : '编辑章节的源代码:';


return clone.textContent.replace( /\s+/g, ' ' ).trim();
link.setAttribute( 'title', prefix + label );
} );
} );
}
}


function getSwitchHeadingFromTocLink( link ) {
function getElementByFragment( fragment ) {
var fragment = link.hash ? link.hash.slice( 1 ) : '';
var decoded;
var id;
var targets;
var activeTarget;


if ( !fragment ) {
if ( !fragment ) {
第136行: 第210行:
}
}
try {
try {
id = decodeURIComponent( fragment );
decoded = decodeURIComponent( fragment ).replace( / /g, '_' );
} catch ( e ) {
} catch ( e ) {
return null;
return null;
}
}
targets = toArray( document.querySelectorAll( idSelector( decoded ) ) ).concat(
toArray( document.querySelectorAll( attributeSelector( 'data-switchtabber-anchor', decoded ) ) )
).filter( function ( target, index, allTargets ) {
return allTargets.indexOf( target ) === index;
} );
activeTarget = targets.filter( function ( target ) {
return getSwitchTabIndexForElement( target ) === activeSwitchTab;
} )[ 0 ];
return activeTarget || targets[ 0 ] || null;
}
function getFragmentTarget( link ) {
var href = link.getAttribute( 'href' );
var hashIndex;
if ( !href || href.indexOf( '#' ) === -1 ) {
return null;
}
hashIndex = href.indexOf( '#' );
return getElementByFragment( href.slice( hashIndex + 1 ) );
}
function getStableHeadingAnchor( heading ) {
var headline = heading.querySelector( '.mw-headline' );
var anchor = headline && headline.querySelector( '[id]' );


return document.getElementById( id ) || document.getElementById( id.replace( /_/g, ' ' ) );
return anchor && anchor.id !== headline.id &&
document.querySelectorAll( idSelector( anchor.id ) ).length === 1 ? anchor.id : null;
}
}


function updateSwitchTableOfContents( index ) {
function syncTocEntry( heading, state ) {
Array.prototype.forEach.call(
var stableAnchor = getStableHeadingAnchor( heading );
document.querySelectorAll( '#toc a[href*="#"], .toc a[href*="#"], .vector-toc a[href*="#"], .mw-toc a[href*="#"]' ),
 
function ( link ) {
toArray( document.querySelectorAll( '#toc a[href*="#"], .toc a[href*="#"], .vector-toc a[href*="#"]' ) ).forEach( function ( link ) {
var heading = getSwitchHeadingFromTocLink( link );
var target = getFragmentTarget( link );
var wrapper;
var tocItem;
var item;
var labelElement;


if ( !heading || !heading.querySelector( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ) {
if ( !target || target.closest( 'h1, h2, h3, h4, h5, h6' ) !== heading ) {
return;
return;
}
}
wrapper = heading.closest( '.mw-heading' ) || heading;
tocItem = link.closest( 'li' );
item = link.closest( 'li' );
if ( tocItem ) {
if ( item ) {
setSwitchElementVisible( tocItem, state.visible );
item.hidden = wrapper.hidden;
}
}
if ( stableAnchor ) {
link.textContent = getSwitchHeadingText( heading, index );
link.setAttribute( 'href', '#' + stableAnchor );
}
if ( !state.visible ) {
return;
}
labelElement = link.querySelector( '.vector-toc-text span:not(.vector-toc-numb), .toctext, .toc-text' );
if ( labelElement && labelElement.textContent !== state.label ) {
labelElement.textContent = state.label;
}
} );
}
 
function syncSwitchHeading( heading, tabIndex ) {
var state = getHeadingState( heading, tabIndex );
var wrapper = heading.closest( '.mw-heading' ) || heading;
 
setSwitchElementVisible( wrapper, state.visible );
syncEditSectionControls( heading, state.label, state.visible );
syncTocEntry( heading, state );
}
 
function syncSwitchHeadings( tabIndex ) {
toArray( document.querySelectorAll( 'h1, h2, h3, h4, h5, h6' ) ).forEach( function ( heading ) {
if (
heading.querySelector( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ||
getSwitchTabIndexForElement( heading ) !== null
) {
syncSwitchHeading( heading, tabIndex );
}
} );
}
 
function mutationTouchesToc( mutation ) {
var target = mutation.target;
 
if ( target.nodeType === 1 && target.closest( TOC_CONTAINER_SELECTOR ) ) {
return true;
}
return toArray( mutation.addedNodes ).some( function ( node ) {
return node.nodeType === 1 && (
node.matches( TOC_CONTAINER_SELECTOR ) ||
!!node.querySelector( TOC_CONTAINER_SELECTOR )
);
} );
}
 
function observeTocChanges() {
if ( !window.MutationObserver || tocObserver || !document.body ) {
return;
}
tocObserver = new window.MutationObserver( function ( mutations ) {
if ( toArray( mutations ).some( mutationTouchesToc ) ) {
syncSwitchHeadings( activeSwitchTab );
}
}
);
} );
tocObserver.observe( document.body, {
childList: true,
subtree: true
} );
}
}


第169行: 第325行:
tabberexShowTab( tabberId, index );
tabberexShowTab( tabberId, index );
if ( tabberId === SWITCH_TABBER_ID ) {
if ( tabberId === SWITCH_TABBER_ID ) {
updateSwitchHeadingVisibility( index );
activeSwitchTab = index;
updateSwitchTableOfContents( index );
syncSwitchHeadings( index );
}
}
}
}
第244行: 第400行:
}
}


function processHead( head, defaultTabs, tabberIds, tabberIndex ) {
function collectTabDetails( container, hideTabs ) {
var tabberId = head.getAttribute( 'data-tabber-id' ) || String( tabberIndex );
var details = [];
var details = [];
var defaultIndex = 0;
var defaultIndex = 0;


if ( head.dataset.tabberexProcessed || !head.querySelector( '.tabberex-tab' ) ) {
toArray( container.querySelectorAll( ':scope > .tabberex-tab' ) ).forEach( function ( tab, index ) {
return tabberIndex;
details.push( readTabDetails( tab, index ) );
}
if ( details[ index ].isDefault ) {
 
Array.prototype.forEach.call( head.querySelectorAll( ':scope > .tabberex-tab' ), function ( tab, index ) {
var item = readTabDetails( tab, index );
details.push( item );
if ( item.isDefault ) {
defaultIndex = index;
defaultIndex = index;
}
if ( hideTabs ) {
tab.style.display = 'none';
}
}
} );
} );
return {
details: details,
defaultIndex: defaultIndex
};
}


head.dataset.tabberexProcessed = '1';
function registerTabber( defaultTabs, tabberIds, tabberId, defaultIndex ) {
buildTabHead( head, tabberId, details );
 
if ( defaultTabs[ tabberId ] === undefined ) {
if ( defaultTabs[ tabberId ] === undefined ) {
defaultTabs[ tabberId ] = defaultIndex;
defaultTabs[ tabberId ] = defaultIndex;
}
}
tabberIds.push( tabberId );
}
function processHead( head, defaultTabs, tabberIds, tabberIndex ) {
var tabberId = head.getAttribute( 'data-tabber-id' ) || String( tabberIndex );
var tabState;


tabberIds.push( tabberId );
if ( head.dataset.tabberexProcessed || !head.querySelector( '.tabberex-tab' ) ) {
return tabberIndex;
}
 
tabState = collectTabDetails( head, false );
head.dataset.tabberexProcessed = '1';
buildTabHead( head, tabberId, tabState.details );
registerTabber( defaultTabs, tabberIds, tabberId, tabState.defaultIndex );
return tabberIndex + 1;
return tabberIndex + 1;
}
}
第275行: 第444行:
var tabberId = tabber.getAttribute( 'data-tabber-id' ) || String( tabberIndex );
var tabberId = tabber.getAttribute( 'data-tabber-id' ) || String( tabberIndex );
var head = document.createElement( 'div' );
var head = document.createElement( 'div' );
var details = [];
var tabState;
var defaultIndex = 0;


if ( tabber.dataset.tabberexProcessed ) {
if ( tabber.dataset.tabberexProcessed ) {
第282行: 第450行:
}
}


Array.prototype.forEach.call( tabber.querySelectorAll( ':scope > .tabberex-tab' ), function ( tab, index ) {
tabState = collectTabDetails( tabber, true );
var item = readTabDetails( tab, index );
details.push( item );
if ( item.isDefault ) {
defaultIndex = index;
}
tab.style.display = 'none';
} );
 
tabber.dataset.tabberexProcessed = '1';
tabber.dataset.tabberexProcessed = '1';
tabber.setAttribute( 'data-tabber-id', tabberId );
tabber.setAttribute( 'data-tabber-id', tabberId );
tabber.classList.remove( 'tabberex' );
tabber.classList.remove( 'tabberex' );
tabber.classList.add( 'tabberex-body' );
tabber.classList.add( 'tabberex-body' );
buildTabHead( head, tabberId, details );
buildTabHead( head, tabberId, tabState.details );
tabber.parentNode.insertBefore( head, tabber );
tabber.parentNode.insertBefore( head, tabber );
 
registerTabber( defaultTabs, tabberIds, tabberId, tabState.defaultIndex );
if ( defaultTabs[ tabberId ] === undefined ) {
defaultTabs[ tabberId ] = defaultIndex;
}
 
tabberIds.push( tabberId );
return tabberIndex + 1;
return tabberIndex + 1;
}
}
第395行: 第550行:
tabberexSwitchTab( SWITCH_TABBER_ID, index );
tabberexSwitchTab( SWITCH_TABBER_ID, index );
setSelectedSwitchTab( index );
setSelectedSwitchTab( index );
}
function activateSwitchTabForHash() {
var fragment = window.location.hash.slice( 1 );
var target = getElementByFragment( fragment );
var tabIndex = getSwitchTabIndexForElement( target );
if ( !target ) {
return;
}
if ( tabIndex !== null && tabIndex !== activeSwitchTab ) {
switchtabberSwitchTab( tabIndex );
}
window.setTimeout( function () {
target.scrollIntoView();
}, 0 );
}
}


第465行: 第636行:
applySwitchTabberIconMasks( $( document ) );
applySwitchTabberIconMasks( $( document ) );
initialTab = getSelectedSwitchTab();
initialTab = getSelectedSwitchTab();
observeTocChanges();
tabberexSwitchTab( SWITCH_TABBER_ID, initialTab );
tabberexSwitchTab( SWITCH_TABBER_ID, initialTab );
activateSwitchTabForHash();
}
}


第474行: 第647行:
window.switchtabber_switchTab = switchtabberSwitchTab;
window.switchtabber_switchTab = switchtabberSwitchTab;


window.addEventListener( 'hashchange', activateSwitchTabForHash );
mw.hook( 'wikipage.content' ).add( processTabbers );
mw.hook( 'wikipage.content' ).add( processTabbers );
mw.hook( 'wikipage.content' ).add( addSwitchTabber );
mw.hook( 'wikipage.content' ).add( addSwitchTabber );
}( this, jQuery, mediaWiki ) );
}( this, jQuery, mediaWiki ) );

2026年8月10日 (一) 01:34的最新版本

( function ( window, $, mw ) {
	'use strict';

	var SWITCH_TABBER_ID = 'SwitchTabber';
	var COOKIE_NAME = 'SwitchTab';
	var TABBER_HEAD_CLASS = 'tabberex-head oo-ui-widget oo-ui-widget-enabled oo-ui-selectWidget oo-ui-selectWidget-unpressed oo-ui-selectWidget-depressed oo-ui-tabSelectWidget';
	var TAB_CLASS = 'oo-ui-widget oo-ui-widget-enabled oo-ui-labelElement oo-ui-optionWidget oo-ui-tabOptionWidget';
	var LABEL_CLASS = 'oo-ui-labelElement-label';
	var TOC_CONTAINER_SELECTOR = '#toc, .toc, .vector-toc';
	var activeSwitchTab = 1;
	var tocObserver;
	var PLATFORM_LABELS = {
		mobile: {
			hans: '移动版',
			hant: '行動版'
		},
		switch: {
			hans: 'Switch',
			hant: 'Switch'
		}
	};

	function attributeSelector( name, value ) {
		return '[' + name + '="' + String( value ).replace( /\\/g, '\\\\' ).replace( /"/g, '\\"' ) + '"]';
	}

	function attrSelector( value ) {
		return attributeSelector( 'data-tabber-id', value );
	}

	function idSelector( value ) {
		return attributeSelector( 'id', value );
	}

	function isTraditionalVariant() {
		var variant = String(
			mw.config.get( 'wgUserVariant' ) ||
			mw.config.get( 'wgUserLanguage' ) ||
			mw.config.get( 'wgContentLanguage' ) ||
			''
		).toLowerCase();

		return /(?:hant|tw|hk|mo)/.test( variant );
	}

	function getPlatformLabel( key ) {
		var labels = PLATFORM_LABELS[ key ];
		return isTraditionalVariant() ? labels.hant : labels.hans;
	}

	function getTabberElements( tabberId, type ) {
		return $( '.tabberex-' + type + attrSelector( tabberId ) );
	}

	function getSwitchTabIndexForElement( element ) {
		var tab = element && element.closest && element.closest( '.tabberex-tab' );
		var body;
		var platform;
		var index;

		while ( tab ) {
			body = tab.parentNode;
			if ( body && body.matches( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ) {
				platform = tab.getAttribute( 'data-switchtabber-platform' );
				if ( platform === 'mobile' ) {
					return 1;
				}
				if ( platform === 'switch' ) {
					return 2;
				}
				index = toArray( body.children ).indexOf( tab );
				return index === -1 ? null : index + 1;
			}
			tab = tab.parentElement && tab.parentElement.closest( '.tabberex-tab' );
		}
		return null;
	}

	function tabberexShowTab( tabberId, index ) {
		getTabberElements( tabberId, 'head' ).children( ':nth-child(' + index + ')' )
			.addClass( 'oo-ui-optionWidget-selected' )
			.css( 'cursor', 'default' );

		getTabberElements( tabberId, 'body' ).children( ':nth-child(' + index + ')' ).each( function () {
			var displayValue = this.getAttribute( 'data-tab-display' ) || 'block';
			this.style.display = displayValue;
		} );
	}

	function tabberexHideTab( tabberId, index ) {
		getTabberElements( tabberId, 'head' ).children( ':nth-child(' + index + ')' )
			.removeClass( 'oo-ui-optionWidget-selected' )
			.css( 'cursor', 'pointer' );

		getTabberElements( tabberId, 'body' ).children( ':nth-child(' + index + ')' ).css( 'display', 'none' );
	}

	function tabberexHideAllTabs( tabberId ) {
		getTabberElements( tabberId, 'head' ).children()
			.removeClass( 'oo-ui-optionWidget-selected' )
			.css( 'cursor', 'pointer' );

		getTabberElements( tabberId, 'body' ).children().css( 'display', 'none' );
	}

	function toArray( collection ) {
		return Array.prototype.slice.call( collection );
	}

	function removeElements( root, selector ) {
		toArray( root.querySelectorAll( selector ) ).forEach( function ( element ) {
			element.parentNode.removeChild( element );
		} );
	}

	function getHeadingState( heading, tabIndex ) {
		var headline = heading.querySelector( '.mw-headline' );
		var clone = ( headline || heading ).cloneNode( true );
		var containingTabIndex = getSwitchTabIndexForElement( heading );
		var label;

		toArray( clone.querySelectorAll( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ).forEach( function ( body ) {
			toArray( body.children ).forEach( function ( tab, index ) {
				if ( index !== tabIndex - 1 ) {
					tab.parentNode.removeChild( tab );
				}
			} );
		} );
		removeElements( clone, '.mobileonly, .mw-editsection' );
		label = clone.textContent.replace( /\s+/g, ' ' ).trim();

		return {
			label: label,
			visible: label !== '' && ( containingTabIndex === null || containingTabIndex === tabIndex )
		};
	}

	function setSwitchElementVisible( element, visible ) {
		var display;
		var priority;

		if ( !visible && element.dataset.switchtabberDisplaySet !== '1' ) {
			element.dataset.switchtabberDisplay = element.style.getPropertyValue( 'display' );
			element.dataset.switchtabberDisplayPriority = element.style.getPropertyPriority( 'display' );
			element.dataset.switchtabberDisplaySet = '1';
		}
		element.hidden = !visible;
		if ( !visible ) {
			element.style.setProperty( 'display', 'none', 'important' );
			return;
		}
		if ( element.dataset.switchtabberDisplaySet !== '1' ) {
			return;
		}
		display = element.dataset.switchtabberDisplay;
		priority = element.dataset.switchtabberDisplayPriority;
		if ( display ) {
			element.style.setProperty( 'display', display, priority );
		} else {
			element.style.removeProperty( 'display' );
		}
		delete element.dataset.switchtabberDisplay;
		delete element.dataset.switchtabberDisplayPriority;
		delete element.dataset.switchtabberDisplaySet;
	}

	function getHeadingSectionNumber( heading ) {
		return toArray(
			document.querySelectorAll( '.mw-parser-output h2, .mw-parser-output h3, .mw-parser-output h4, .mw-parser-output h5, .mw-parser-output h6' )
		).indexOf( heading ) + 1;
	}

	function getSectionEditControls( heading ) {
		var controls = toArray( heading.querySelectorAll( '.mw-editsection' ) );
		var sectionNumber = getHeadingSectionNumber( heading );

		if ( sectionNumber === 0 ) {
			return controls;
		}
		toArray( document.querySelectorAll( '.mw-editsection' ) ).forEach( function ( control ) {
			var link = control.querySelector( 'a[href*="section="]' );
			var match = link && /(?:[?&]section=)(\d+)/.exec( link.getAttribute( 'href' ) || '' );

			if ( match && Number( match[ 1 ] ) === sectionNumber && controls.indexOf( control ) === -1 ) {
				controls.push( control );
			}
		} );
		return controls;
	}

	function syncEditSectionControls( heading, label, visible ) {
		getSectionEditControls( heading ).forEach( function ( control ) {
			setSwitchElementVisible( control, visible );
			toArray( control.querySelectorAll( 'a' ) ).forEach( function ( link ) {
				var href = link.getAttribute( 'href' ) || '';
				var prefix = href.indexOf( 'veaction=edit' ) !== -1 ? '编辑章节:' : '编辑章节的源代码:';

				link.setAttribute( 'title', prefix + label );
			} );
		} );
	}

	function getElementByFragment( fragment ) {
		var decoded;
		var targets;
		var activeTarget;

		if ( !fragment ) {
			return null;
		}
		try {
			decoded = decodeURIComponent( fragment ).replace( / /g, '_' );
		} catch ( e ) {
			return null;
		}
		targets = toArray( document.querySelectorAll( idSelector( decoded ) ) ).concat(
			toArray( document.querySelectorAll( attributeSelector( 'data-switchtabber-anchor', decoded ) ) )
		).filter( function ( target, index, allTargets ) {
			return allTargets.indexOf( target ) === index;
		} );
		activeTarget = targets.filter( function ( target ) {
			return getSwitchTabIndexForElement( target ) === activeSwitchTab;
		} )[ 0 ];
		return activeTarget || targets[ 0 ] || null;
	}

	function getFragmentTarget( link ) {
		var href = link.getAttribute( 'href' );
		var hashIndex;

		if ( !href || href.indexOf( '#' ) === -1 ) {
			return null;
		}
		hashIndex = href.indexOf( '#' );
		return getElementByFragment( href.slice( hashIndex + 1 ) );
	}

	function getStableHeadingAnchor( heading ) {
		var headline = heading.querySelector( '.mw-headline' );
		var anchor = headline && headline.querySelector( '[id]' );

		return anchor && anchor.id !== headline.id &&
			document.querySelectorAll( idSelector( anchor.id ) ).length === 1 ? anchor.id : null;
	}

	function syncTocEntry( heading, state ) {
		var stableAnchor = getStableHeadingAnchor( heading );

		toArray( document.querySelectorAll( '#toc a[href*="#"], .toc a[href*="#"], .vector-toc a[href*="#"]' ) ).forEach( function ( link ) {
			var target = getFragmentTarget( link );
			var tocItem;
			var labelElement;

			if ( !target || target.closest( 'h1, h2, h3, h4, h5, h6' ) !== heading ) {
				return;
			}
			tocItem = link.closest( 'li' );
			if ( tocItem ) {
				setSwitchElementVisible( tocItem, state.visible );
			}
			if ( stableAnchor ) {
				link.setAttribute( 'href', '#' + stableAnchor );
			}
			if ( !state.visible ) {
				return;
			}
			labelElement = link.querySelector( '.vector-toc-text span:not(.vector-toc-numb), .toctext, .toc-text' );
			if ( labelElement && labelElement.textContent !== state.label ) {
				labelElement.textContent = state.label;
			}
		} );
	}

	function syncSwitchHeading( heading, tabIndex ) {
		var state = getHeadingState( heading, tabIndex );
		var wrapper = heading.closest( '.mw-heading' ) || heading;

		setSwitchElementVisible( wrapper, state.visible );
		syncEditSectionControls( heading, state.label, state.visible );
		syncTocEntry( heading, state );
	}

	function syncSwitchHeadings( tabIndex ) {
		toArray( document.querySelectorAll( 'h1, h2, h3, h4, h5, h6' ) ).forEach( function ( heading ) {
			if (
				heading.querySelector( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ||
				getSwitchTabIndexForElement( heading ) !== null
			) {
				syncSwitchHeading( heading, tabIndex );
			}
		} );
	}

	function mutationTouchesToc( mutation ) {
		var target = mutation.target;

		if ( target.nodeType === 1 && target.closest( TOC_CONTAINER_SELECTOR ) ) {
			return true;
		}
		return toArray( mutation.addedNodes ).some( function ( node ) {
			return node.nodeType === 1 && (
				node.matches( TOC_CONTAINER_SELECTOR ) ||
				!!node.querySelector( TOC_CONTAINER_SELECTOR )
			);
		} );
	}

	function observeTocChanges() {
		if ( !window.MutationObserver || tocObserver || !document.body ) {
			return;
		}
		tocObserver = new window.MutationObserver( function ( mutations ) {
			if ( toArray( mutations ).some( mutationTouchesToc ) ) {
				syncSwitchHeadings( activeSwitchTab );
			}
		} );
		tocObserver.observe( document.body, {
			childList: true,
			subtree: true
		} );
	}

	function tabberexSwitchTab( tabberId, index ) {
		tabberexHideAllTabs( tabberId );
		tabberexShowTab( tabberId, index );
		if ( tabberId === SWITCH_TABBER_ID ) {
			activeSwitchTab = index;
			syncSwitchHeadings( index );
		}
	}

	function getExHeader( element ) {
		var attributes = {};
		var names = element.getAttributeNames();
		var i;

		for ( i = 0; i < names.length; i++ ) {
			attributes[ names[ i ] ] = element.getAttribute( names[ i ] );
		}
		return {
			html: element.innerHTML,
			attributes: attributes
		};
	}

	function readTabDetails( tabElement, index ) {
		var exHeaderElement = tabElement.querySelector( 'p > span.tabberex-tab-header, span.tabberex-tab-header' );
		var details = {
			header: tabElement.getAttribute( 'data-tab-header' ) || String( index + 1 ),
			exHeader: exHeaderElement ? getExHeader( exHeaderElement ) : null,
			isDefault: tabElement.classList.contains( 'tabberex-tab-default' ),
			location: tabElement.getAttribute( 'data-tab-location' )
		};

		if ( exHeaderElement ) {
			exHeaderElement.parentNode.removeChild( exHeaderElement );
		}
		return details;
	}

	function makeTabButton( tabberId, details, index ) {
		var tab = document.createElement( 'div' );
		var label = document.createElement( 'span' );
		var attr;

		tab.className = TAB_CLASS;
		tab.style.cursor = 'pointer';
		label.className = LABEL_CLASS;

		if ( details.exHeader ) {
			for ( attr in details.exHeader.attributes ) {
				label.setAttribute( attr, details.exHeader.attributes[ attr ] );
			}
			label.innerHTML = details.exHeader.html;
		} else {
			label.textContent = details.header;
		}

		tab.appendChild( label );
		tab.addEventListener( 'click', function () {
			if ( details.location ) {
				window.location.href = details.location;
				return;
			}
			tabberexSwitchTab( tabberId, index + 1 );
		} );
		return tab;
	}

	function buildTabHead( host, tabberId, tabDetails ) {
		var i;

		host.innerHTML = '';
		host.className = TABBER_HEAD_CLASS;
		host.setAttribute( 'data-tabber-id', tabberId );

		for ( i = 0; i < tabDetails.length; i++ ) {
			host.appendChild( makeTabButton( tabberId, tabDetails[ i ], i ) );
		}
	}

	function collectTabDetails( container, hideTabs ) {
		var details = [];
		var defaultIndex = 0;

		toArray( container.querySelectorAll( ':scope > .tabberex-tab' ) ).forEach( function ( tab, index ) {
			details.push( readTabDetails( tab, index ) );
			if ( details[ index ].isDefault ) {
				defaultIndex = index;
			}
			if ( hideTabs ) {
				tab.style.display = 'none';
			}
		} );
		return {
			details: details,
			defaultIndex: defaultIndex
		};
	}

	function registerTabber( defaultTabs, tabberIds, tabberId, defaultIndex ) {
		if ( defaultTabs[ tabberId ] === undefined ) {
			defaultTabs[ tabberId ] = defaultIndex;
		}
		tabberIds.push( tabberId );
	}

	function processHead( head, defaultTabs, tabberIds, tabberIndex ) {
		var tabberId = head.getAttribute( 'data-tabber-id' ) || String( tabberIndex );
		var tabState;

		if ( head.dataset.tabberexProcessed || !head.querySelector( '.tabberex-tab' ) ) {
			return tabberIndex;
		}

		tabState = collectTabDetails( head, false );
		head.dataset.tabberexProcessed = '1';
		buildTabHead( head, tabberId, tabState.details );
		registerTabber( defaultTabs, tabberIds, tabberId, tabState.defaultIndex );
		return tabberIndex + 1;
	}

	function processStandaloneTabber( tabber, defaultTabs, tabberIds, tabberIndex ) {
		var tabberId = tabber.getAttribute( 'data-tabber-id' ) || String( tabberIndex );
		var head = document.createElement( 'div' );
		var tabState;

		if ( tabber.dataset.tabberexProcessed ) {
			return tabberIndex;
		}

		tabState = collectTabDetails( tabber, true );
		tabber.dataset.tabberexProcessed = '1';
		tabber.setAttribute( 'data-tabber-id', tabberId );
		tabber.classList.remove( 'tabberex' );
		tabber.classList.add( 'tabberex-body' );
		buildTabHead( head, tabberId, tabState.details );
		tabber.parentNode.insertBefore( head, tabber );
		registerTabber( defaultTabs, tabberIds, tabberId, tabState.defaultIndex );
		return tabberIndex + 1;
	}

	function processTabbers() {
		var defaultTabs = {};
		var tabberIds = [];
		var tabberIndex = 0;

		Array.prototype.slice.call( document.querySelectorAll( '.tabberex-head' ) ).reverse().forEach( function ( head ) {
			tabberIndex = processHead( head, defaultTabs, tabberIds, tabberIndex );
		} );

		Array.prototype.slice.call( document.querySelectorAll( '.tabberex-body' ) ).reverse().forEach( function ( body ) {
			$( body ).children().css( 'display', 'none' );
		} );

		Array.prototype.slice.call( document.querySelectorAll( '.tabberex' ) ).reverse().forEach( function ( tabber ) {
			tabberIndex = processStandaloneTabber( tabber, defaultTabs, tabberIds, tabberIndex );
		} );

		tabberIds.forEach( function ( tabberId ) {
			tabberexShowTab( tabberId, defaultTabs[ tabberId ] + 1 );
		} );
	}

	function getFileUrl( filename ) {
		return mw.util.getUrl( 'Special:FilePath/' + filename );
	}

	function setIconMask( icon, filename ) {
		var url = getFileUrl( filename );

		icon.style.webkitMaskImage = 'url("' + url + '")';
		icon.style.maskImage = 'url("' + url + '")';
	}

	function applySwitchTabberIconMasks( $content ) {
		var root = $content && $content.length ? $content[ 0 ] : document;

		$( root ).find( '.tabberex-head' + attrSelector( SWITCH_TABBER_ID ) + ' .switchtabber-icon--mobile' ).each( function () {
			setIconMask( this, 'SwitchTabber_Mobile.png' );
		} );
		$( root ).find( '.tabberex-head' + attrSelector( SWITCH_TABBER_ID ) + ' .switchtabber-icon--switch' ).each( function () {
			setIconMask( this, 'SwitchTabber_Switch.png' );
		} );
	}

	function makeSwitchTab( index, labelText, filename, extraIconClass ) {
		var tab = document.createElement( 'div' );
		var label = document.createElement( 'span' );
		var icon = document.createElement( 'span' );

		tab.className = TAB_CLASS;
		tab.style.cursor = 'pointer';
		label.className = LABEL_CLASS;

		icon.className = 'switchtabber-icon' + ( extraIconClass ? ' ' + extraIconClass : '' );
		icon.setAttribute( 'aria-hidden', 'true' );
		setIconMask( icon, filename );

		label.appendChild( icon );
		label.appendChild( document.createTextNode( labelText ) );
		tab.appendChild( label );
		tab.addEventListener( 'click', function () {
			switchtabberSwitchTab( index );
		} );
		return tab;
	}

	function createSwitchTabber( modifierClass ) {
		var switchTabber = document.createElement( 'div' );

		switchTabber.className = TABBER_HEAD_CLASS + ' switchtabber-head' + ( modifierClass ? ' ' + modifierClass : '' );
		switchTabber.setAttribute( 'data-tabber-id', SWITCH_TABBER_ID );
		switchTabber.dataset.switchtabberGenerated = '1';
		switchTabber.appendChild( makeSwitchTab( 1, getPlatformLabel( 'mobile' ), 'SwitchTabber_Mobile.png', 'switchtabber-icon--mobile' ) );
		switchTabber.appendChild( makeSwitchTab( 2, getPlatformLabel( 'switch' ), 'SwitchTabber_Switch.png', 'switchtabber-icon--switch' ) );
		return switchTabber;
	}

	function getSelectedSwitchTab() {
		var match = document.cookie.match( /(?:^|;\s*)SwitchTab=(Mobile|Switch)(?:;|$)/ );
		return match && match[ 1 ] === 'Switch' ? 2 : 1;
	}

	function setSelectedSwitchTab( index ) {
		document.cookie = COOKIE_NAME + '=' + ( index === 1 ? 'Mobile' : 'Switch' ) + '; path=/; SameSite=Lax';
	}

	function switchtabberSwitchTab( index ) {
		tabberexSwitchTab( SWITCH_TABBER_ID, index );
		setSelectedSwitchTab( index );
	}

	function activateSwitchTabForHash() {
		var fragment = window.location.hash.slice( 1 );
		var target = getElementByFragment( fragment );
		var tabIndex = getSwitchTabIndexForElement( target );

		if ( !target ) {
			return;
		}
		if ( tabIndex !== null && tabIndex !== activeSwitchTab ) {
			switchtabberSwitchTab( tabIndex );
		}
		window.setTimeout( function () {
			target.scrollIntoView();
		}, 0 );
	}

	function hasSwitchTabberContent() {
		return !!document.querySelector(
			'.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) + ', .tabberex' + attrSelector( SWITCH_TABBER_ID ) + ', .tabberex-head' + attrSelector( SWITCH_TABBER_ID ) + ':not([data-switchtabber-generated])'
		);
	}

	function addLocalPageHeaderTabber() {
		var heading = document.getElementById( 'firstHeading' ) || document.querySelector( '.mw-first-heading' );
		var wrapper;

		if ( !heading || document.querySelector( '.switchtabber-local-heading > .switchtabber-head' ) ) {
			return;
		}

		wrapper = document.createElement( 'div' );
		wrapper.className = 'switchtabber-local-heading';
		heading.parentNode.insertBefore( wrapper, heading );
		wrapper.appendChild( heading );
		wrapper.appendChild( createSwitchTabber( 'switchtabber-head--page' ) );
	}

	function addFandomPageHeaderTabber() {
		if ( document.querySelector( '.page-header__title-wrapper > .switchtabber-head' ) ) {
			return;
		}

		if ( $( '.page-header__title-wrapper' ).length ) {
			$( '.page-header__title-wrapper' )
				.css( 'display', 'flex' )
				.css( 'align-content', 'center' )
				.css( 'justify-content', 'space-between' )
				.append( createSwitchTabber( 'switchtabber-head--page' ) );
		} else {
			addLocalPageHeaderTabber();
		}
	}

	function addFandomStickyHeaderTabber() {
		if ( !$( '.fandom-sticky-header > :first-child' ).length || document.querySelector( '.fandom-sticky-header .switchtabber-head' ) ) {
			return;
		}

		$( createSwitchTabber( 'switchtabber-head--sticky' ) ).insertAfter( '.fandom-sticky-header > :first-child' );
	}

	function addEditorPreviewTabber() {
		if ( !$( '.oo-ui-processDialog-actions-safe' ).length || document.querySelector( '.oo-ui-processDialog-actions-safe .switchtabber-head' ) ) {
			return;
		}

		$( '.oo-ui-processDialog-actions-safe' )
			.css( 'flex-direction', 'row-reverse' )
			.append( createSwitchTabber( 'switchtabber-head--editor' ) );
	}

	function addSwitchTabber() {
		var initialTab;

		if ( !hasSwitchTabberContent() ) {
			return;
		}

		addFandomPageHeaderTabber();
		addFandomStickyHeaderTabber();
		addEditorPreviewTabber();

		applySwitchTabberIconMasks( $( document ) );
		initialTab = getSelectedSwitchTab();
		observeTocChanges();
		tabberexSwitchTab( SWITCH_TABBER_ID, initialTab );
		activateSwitchTabForHash();
	}

	window.tabberex_showTab = tabberexShowTab;
	window.tabberex_hideTab = tabberexHideTab;
	window.tabberex_hideAllTabs = tabberexHideAllTabs;
	window.tabberex_switchTab = tabberexSwitchTab;
	window.switchtabber_switchTab = switchtabberSwitchTab;

	window.addEventListener( 'hashchange', activateSwitchTabForHash );
	mw.hook( 'wikipage.content' ).add( processTabbers );
	mw.hook( 'wikipage.content' ).add( addSwitchTabber );
}( this, jQuery, mediaWiki ) );