MediaWiki:Gadget-switch-tabber.js

Admin留言 | 贡献2026年7月15日 (三) 20:28的版本 (同步Switch切换后的目录标题与可见性)

注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5
( 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 PLATFORM_LABELS = {
		mobile: {
			hans: '移动版',
			hant: '行動版'
		},
		switch: {
			hans: 'Switch',
			hant: 'Switch'
		}
	};

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

	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 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 headingHasContentForTab( heading, index ) {
		var clone = heading.cloneNode( true );

		Array.prototype.forEach.call(
			clone.querySelectorAll( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ),
			function ( body ) {
				Array.prototype.slice.call( body.children ).forEach( function ( tab, tabIndex ) {
					if ( tabIndex !== index - 1 ) {
						tab.parentNode.removeChild( tab );
					}
				} );
			}
		);
		Array.prototype.forEach.call( clone.querySelectorAll( '.mobileonly' ), function ( label ) {
			label.parentNode.removeChild( label );
		} );

		return clone.textContent.trim() !== '' ||
			!!clone.querySelector( 'img, svg, math, video, audio, canvas' );
	}

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

				if ( !heading.querySelector( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ) {
					return;
				}
				wrapper = heading.closest( '.mw-heading' ) || heading;
				wrapper.hidden = !headingHasContentForTab( heading, index );
			}
		);
	}

	function getSwitchHeadingText( heading, index ) {
		var clone = heading.cloneNode( true );

		Array.prototype.forEach.call(
			clone.querySelectorAll( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ),
			function ( body ) {
				Array.prototype.slice.call( body.children ).forEach( function ( tab, tabIndex ) {
					if ( tabIndex !== index - 1 ) {
						body.removeChild( tab );
					}
				} );
			}
		);
		Array.prototype.forEach.call(
			clone.querySelectorAll( '.mobileonly, .mw-editsection' ),
			function ( element ) {
				element.parentNode.removeChild( element );
			}
		);

		return clone.textContent.replace( /\s+/g, ' ' ).trim();
	}

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

		if ( !fragment ) {
			return null;
		}
		try {
			id = decodeURIComponent( fragment );
		} catch ( e ) {
			return null;
		}

		return document.getElementById( id ) || document.getElementById( id.replace( /_/g, ' ' ) );
	}

	function updateSwitchTableOfContents( index ) {
		Array.prototype.forEach.call(
			document.querySelectorAll( '#toc a[href*="#"], .toc a[href*="#"], .vector-toc a[href*="#"], .mw-toc a[href*="#"]' ),
			function ( link ) {
				var heading = getSwitchHeadingFromTocLink( link );
				var wrapper;
				var item;

				if ( !heading || !heading.querySelector( '.tabberex-body' + attrSelector( SWITCH_TABBER_ID ) ) ) {
					return;
				}
				wrapper = heading.closest( '.mw-heading' ) || heading;
				item = link.closest( 'li' );
				if ( item ) {
					item.hidden = wrapper.hidden;
				}
				link.textContent = getSwitchHeadingText( heading, index );
			}
		);
	}

	function tabberexSwitchTab( tabberId, index ) {
		tabberexHideAllTabs( tabberId );
		tabberexShowTab( tabberId, index );
		if ( tabberId === SWITCH_TABBER_ID ) {
			updateSwitchHeadingVisibility( index );
			updateSwitchTableOfContents( 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 processHead( head, defaultTabs, tabberIds, tabberIndex ) {
		var tabberId = head.getAttribute( 'data-tabber-id' ) || String( tabberIndex );
		var details = [];
		var defaultIndex = 0;

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

		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;
			}
		} );

		head.dataset.tabberexProcessed = '1';
		buildTabHead( head, tabberId, details );

		if ( defaultTabs[ tabberId ] === undefined ) {
			defaultTabs[ tabberId ] = defaultIndex;
		}

		tabberIds.push( tabberId );
		return tabberIndex + 1;
	}

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

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

		Array.prototype.forEach.call( tabber.querySelectorAll( ':scope > .tabberex-tab' ), function ( tab, index ) {
			var item = readTabDetails( tab, index );
			details.push( item );
			if ( item.isDefault ) {
				defaultIndex = index;
			}
			tab.style.display = 'none';
		} );

		tabber.dataset.tabberexProcessed = '1';
		tabber.setAttribute( 'data-tabber-id', tabberId );
		tabber.classList.remove( 'tabberex' );
		tabber.classList.add( 'tabberex-body' );
		buildTabHead( head, tabberId, details );
		tabber.parentNode.insertBefore( head, tabber );

		if ( defaultTabs[ tabberId ] === undefined ) {
			defaultTabs[ tabberId ] = defaultIndex;
		}

		tabberIds.push( tabberId );
		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 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();
		tabberexSwitchTab( SWITCH_TABBER_ID, initialTab );
	}

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

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