Tip of the day
Bank space is unlimited, if you are worried about the usage of something, bank it!

MediaWiki:Gadget-CollapsibleHeaders.js

From Walkscape Walkthrough
Revision as of 11:05, 17 June 2026 by Bonez565 (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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.
/**
 * CollapsibleHeaders
 * Adds expand/collapse toggles to top-level article headings.
 *
 * Section logic:
 * - Clicking a heading hides/shows everything until the next heading
 *   of the same or higher level.
 * - Headings remain open by default.
 * - Only direct article-content headings are processed, avoiding most
 *   headings inside infoboxes, navboxes, tables, and templates.
 */
(function () {
	'use strict';

	const CONTENT_SELECTOR = '#mw-content-text .mw-parser-output';
	const HEADING_SELECTOR = 'h1, h2, h3, h4, h5, h6';
	const WRAPPER_SELECTOR = '.mw-heading';

	function getHeadingLevel(heading) {
		return Number(heading.tagName.slice(1));
	}

	function getHeadingWrapper(heading) {
		return heading.closest(WRAPPER_SELECTOR) || heading;
	}

	function getBoundaryHeading(element) {
		if (element.matches(HEADING_SELECTOR)) {
			return element;
		}

		if (element.matches(WRAPPER_SELECTOR)) {
			return element.querySelector(HEADING_SELECTOR);
		}

		return null;
	}

	function getDirectArticleHeadings(content) {
		const headings = [];

		Array.from(content.children).forEach((child) => {
			if (child.matches(HEADING_SELECTOR)) {
				headings.push(child);
				return;
			}

			if (child.matches(WRAPPER_SELECTOR)) {
				const heading = child.querySelector(HEADING_SELECTOR);
				if (heading) {
					headings.push(heading);
				}
			}
		});

		return headings;
	}

	function getSectionNodes(heading) {
		const level = getHeadingLevel(heading);
		const wrapper = getHeadingWrapper(heading);
		const nodes = [];

		let node = wrapper.nextElementSibling;

		while (node) {
			const boundaryHeading = getBoundaryHeading(node);

			if (boundaryHeading && getHeadingLevel(boundaryHeading) <= level) {
				break;
			}

			nodes.push(node);
			node = node.nextElementSibling;
		}

		return nodes;
	}

	function initCollapsibleHeaders($content) {
		const content = $content && $content[0]
			? $content[0].querySelector(CONTENT_SELECTOR) || $content[0]
			: document.querySelector(CONTENT_SELECTOR);

		if (!content || content.dataset.wsCollapsibleHeadersLoaded === '1') {
			return;
		}

		const headings = getDirectArticleHeadings(content);

		if (!headings.length) {
			return;
		}

		content.dataset.wsCollapsibleHeadersLoaded = '1';

		const sections = headings.map((heading, index) => {
			const wrapper = getHeadingWrapper(heading);
			const button = document.createElement('button');

			button.type = 'button';
			button.className = 'ws-collapsible-header-toggle';
			button.setAttribute('aria-expanded', 'true');
			button.setAttribute('aria-label', 'Collapse section');
			button.title = 'Collapse section';
			button.textContent = '▾';

			heading.classList.add('ws-collapsible-header');
			wrapper.classList.add('ws-collapsible-header-wrapper');

			heading.insertBefore(button, heading.firstChild);

			return {
				id: index,
				heading,
				wrapper,
				button,
				nodes: getSectionNodes(heading),
				collapsed: false
			};
		});

		function applyState() {
			const hiddenNodes = new Set();

			sections.forEach((section) => {
				if (section.collapsed) {
					section.nodes.forEach((node) => hiddenNodes.add(node));
				}
			});

			sections.forEach((section) => {
				section.nodes.forEach((node) => {
					const hidden = hiddenNodes.has(node);
					node.hidden = hidden;
					node.classList.toggle('ws-collapsible-header-hidden', hidden);
				});

				section.wrapper.classList.toggle(
					'ws-collapsible-header-collapsed',
					section.collapsed
				);

				section.button.textContent = section.collapsed ? '▸' : '▾';
				section.button.setAttribute('aria-expanded', String(!section.collapsed));
				section.button.setAttribute(
					'aria-label',
					section.collapsed ? 'Expand section' : 'Collapse section'
				);
				section.button.title = section.collapsed ? 'Expand section' : 'Collapse section';
			});
		}

		sections.forEach((section) => {
			section.button.addEventListener('click', (event) => {
				event.preventDefault();
				event.stopPropagation();

				section.collapsed = !section.collapsed;
				applyState();
			});
		});
	}

	mw.hook('wikipage.content').add(initCollapsibleHeaders);
}());