Jump to content

MediaWiki:Gadget-codexTabs.js

From testwiki, a Wikimedia project for testing

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
'use strict';

const Vue = require( 'vue' );
const App = require( './codexTabs.vue' );

function makeSafeName( label, index ) {
	return String( label || 'tab-' + index )
		.toLowerCase()
		.replace( /[^a-z0-9_-]+/g, '-' )
		.replace( /^-+|-+$/g, '' ) || 'tab-' + index;
}

function getHeadingLabel( heading ) {
	const clone = heading.cloneNode( true );

	// Remove MediaWiki's [edit] links from the heading label.
	Array.prototype.forEach.call(
		clone.querySelectorAll( '.mw-editsection, .mw-editsection-like' ),
		function ( node ) {
			node.remove();
		}
	);

	const headline = clone.querySelector( '.mw-headline' );
	return ( headline ? headline.textContent : clone.textContent ).trim();
}

function isTopLevelTabHeading( node ) {
	return node &&
		node.nodeType === Node.ELEMENT_NODE &&
		node.tagName &&
		node.tagName.toLowerCase() === 'h2';
}

function collectSectionTabs( wrapper ) {
	const headings = Array.prototype.slice.call(
		wrapper.querySelectorAll( 'h2' )
	).filter( function ( heading ) {
		// Ignore the page/table-of-contents heading.
		if ( heading.id === 'mw-toc-heading' ) {
			return false;
		}

		if ( heading.closest( '#toc, .toc' ) ) {
			return false;
		}

		// Only use h2 headings that are actually inside this tab wrapper.
		return wrapper.contains( heading );
	} );

	if ( headings.length < 2 ) {
		console.warn(
			'codexTabs: expected at least 2 h2 headings inside .codex-tabs-sections, found',
			headings.length,
			headings,
			wrapper
		);
		return [];
	}

	function getTopLevelNodeInsideWrapper( node ) {
		let current = node;

		while ( current.parentNode && current.parentNode !== wrapper ) {
			current = current.parentNode;
		}

		return current;
	}

	function getHeadingLabel( heading ) {
		const clone = heading.cloneNode( true );

		Array.prototype.forEach.call(
			clone.querySelectorAll( '.mw-editsection, .mw-editsection-like' ),
			function ( node ) {
				node.remove();
			}
		);

		return clone.textContent.trim();
	}

	return headings.map( function ( heading, index ) {
		const label = getHeadingLabel( heading );
		const startNode = getTopLevelNodeInsideWrapper( heading );
		const nextHeading = headings[ index + 1 ];
		const endNode = nextHeading ?
			getTopLevelNodeInsideWrapper( nextHeading ) :
			null;

		const container = document.createElement( 'div' );
		let node = startNode.nextSibling;

		while ( node && node !== endNode ) {
			container.appendChild( node.cloneNode( true ) );
			node = node.nextSibling;
		}

		return {
			name: makeSafeName( label, index ) + '-' + index,
			label: label,
			html: container.innerHTML.trim()
		};
	} ).filter( function ( tab ) {
		return tab.label;
	} );
}
function mountOne( wrapper, index ) {
	const tabs = collectSectionTabs( wrapper );

	if ( tabs.length < 2 ) {
		// Helpful debugging message.
		// eslint-disable-next-line no-console
		console.warn(
			'codexTabs: expected at least 2 == headings inside .codex-tabs-sections, found',
			tabs.length,
			wrapper
		);
		return;
	}

	const framed = wrapper.getAttribute( 'data-framed' ) !== 'false';
	const activeLabel = wrapper.getAttribute( 'data-active-label' );
	let active = tabs[ 0 ].name;

	if ( activeLabel ) {
		tabs.forEach( function ( tab ) {
			if ( tab.label === activeLabel ) {
				active = tab.name;
			}
		} );
	}

	const mountPoint = document.createElement( 'div' );
	mountPoint.className = 'codex-tabs-gadget';
	mountPoint.id = 'codex-tabs-gadget-' + index;

	wrapper.parentNode.replaceChild( mountPoint, wrapper );

	Vue.createMwApp( App, {
		tabs: tabs,
		framed: framed,
		active: active
	} ).mount( mountPoint );
}

function initCodexSectionTabs( $content ) {
	const root = $content && $content[ 0 ] ? $content[ 0 ] : document;

	const wrappers = Array.prototype.slice.call(
		root.querySelectorAll( '.codex-tabs-sections' )
	);

	wrappers.forEach( function ( wrapper, index ) {
		if ( wrapper.classList.contains( 'codex-tabs--mounted' ) ) {
			return;
		}

		wrapper.classList.add( 'codex-tabs--mounted' );
		mountOne( wrapper, index );
	} );
}

mw.hook( 'wikipage.content' ).add( initCodexSectionTabs );

$( function () {
	initCodexSectionTabs( $( document.body ) );
} );