MediaWiki:Gadget-Streamline.js

From Linguifex
Jump to navigation Jump to search

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";
// {{documentation}}
// <nowiki>
/*jshint strict:true, undef:true, latedef:true, esversion:6 */
/* global mw, createNavToggle */ 

const STREAMLINE_SYMBOL = "⌰";

// [[MediaWiki:Gadget-WiktGadgetPrefs.js]]
const preferences = mw.wiktGadgetPrefs.get(
	"streamline",
	{
		label: {
			en: "Streamline",
		},
	},
	{
		collapseElements: {
			"type": "strenum",
			"default": "aboveDefinitions",
			"label": {
				"en": "Elements to collapse"
			},
			"choices": [
				"aboveDefinitions",
				"allExceptNotes"
			],
			"choiceLabels": {
				"en": {
					"aboveDefinitions": "Sections before definitions",
					"allExceptNotes": "Sections before and after definitions"
				}
			}
		},
		runInReconstructions: {
			"type": "boolean",
			"default": true,
			"label": {
				"en": "Apply also to reconstruction pages"
			}
		}
	}
);

function makeNavFrame(heading, editLinks, toggleCat, contents) {
	// now make a collapsible box
	const navHead = document.createElement("div");
	navHead.className = "NavHead";
	navHead.style.cursor = "pointer";
	navHead.style.fontSize = "1.1em";
	
	const navContent = document.createElement("div");
	navContent.className = "NavContent boxcontent streamline-boxcontent";
	navContent.style.textAlign = "left";

	const navFrame = document.createElement("div");
	navFrame.className = "NavFrame NavFrame-streamline";
	if (toggleCat)
		navFrame.setAttribute("data-toggle-category", toggleCat);
	navFrame.style.display = "block";
	
	navFrame.append(navHead, navContent);
	navHead.append(`${STREAMLINE_SYMBOL} ${heading}`);
	
	try {
		if (createNavToggle) createNavToggle(navFrame);
	} catch (e) { }
	
	if (editLinks) {
		navContent.append(editLinks);
		editLinks.style.float = "right";
		// try displaying the edit links next to the show button
		editLinks.style.marginTop = "-1.5em";
		editLinks.style.marginRight = "8ch";
	}
	navContent.append(...contents);
	
	return navFrame;
}

function isNavFrameOpen(frame) {
	const content = frame.querySelector(".NavContent");
	if (!content) return false;
	const computedStyle = window.getComputedStyle(content);
	if (!computedStyle) return false;
	return computedStyle.display !== "none";
}

function openNavFrame(frame) {
	if (!isNavFrameOpen(frame)) {
		const navHead = frame.querySelector(".NavHead");
		if (navHead)
			navHead.click();
	}
}

function anchorInUrl(anchor) {
	if (window.location.hash) {
		return window.location.hash.replace(/^#/, "") === anchor;
	}
	return false;
}

function hasElementClass(element, className) {
	return element.classList.contains(className);
}

function shouldEndCollapsible(element) {
	return element.matches("h1, h2, h3, h4, h5, h6, hr")				// end on heading or horizontal line
		|| hasElementClass(element, "NavFrame-streamline")	 			// no nested streamline navboxes
		|| hasElementClass(element, "mw-heading")		 				// Parsoid
		; 	// this is on its own line so that it's easier to add new rules
}

function shouldExcludeFromCollapsible(element) {
	return hasElementClass(element, "thumb")					 		// images
		|| hasElementClass(element, "sister-project")					// sister project box
		|| hasElementClass(element, "kanji-table")				 		// ja-kanjitab
		|| hasElementClass(element, "floatright")				 		// various tables
		|| hasElementClass(element, "template-anchor")					// anchor
		|| hasElementClass(element, "t-thumbs-outer")					// thumbs
		|| hasElementClass(element, "interproject-box")					// interwiki boxes
		|| element.getAttribute("typeof") === "mw:File/Thumb"			// new floating image boxes
		|| hasElementClass(element, "mw-halign-right")					// mw-halign-right, by new floating image boxes
		|| element.getAttribute("align") === "right"					// anything that (explicitly) aligns
		|| element.style.float === "right"								// anything that (explicitly) floats
		; 	// this is on its own line so that it's easier to add new rules
}

function shouldCollapseForTheSakeOf(element) {
	if (element.nodeType === 1) {
		// don't collapse for the sake of senseid or etymid alone
		if (element.matches(".senseid, .etymid"))
			return false;
		if (element.matches("p, div, span"))
			return Array.prototype.some.call(element.childNodes, shouldCollapseForTheSakeOf);
		return element.tagName !== "BR";
	} else if (element.nodeType === 3) {
		return !!element.textContent.trim(); // only if not entirely whitespace
	} else {
		return false;
	}
}

function getHeadingQuery(onlyL3) {
	return (onlyL3 ? ["h2", "h3"] : ["h2", "h3", "h4", "h5"])
			.map(heading => `#mw-content-text ${heading}`)
			.join(", ");
}

const navFramesById = {};

function collapseHeadingInNavbox(headingCandidate, heading, toggleCat,
								 headingMatcher, addEditLinks) {
	let headingId;

	const hl = headingCandidate.querySelector(".mw-headline");
	if (hl && headingMatcher(hl.textContent))
		headingId = hl.id;

	// Parsoid
	if (hasElementClass(headingCandidate.parentElement, "mw-heading") &&
				headingMatcher(headingCandidate.textContent)) {
		headingId = headingCandidate.id;
		headingCandidate = headingCandidate.parentElement;
	}

	if (!headingId) return [undefined, undefined];

	// pick all elements until next heading
	const contents = [];
	let nx = headingCandidate.nextElementSibling;
	let collapse = false;
	while (nx) {
		if (shouldEndCollapsible(nx))
			break;
		if (!shouldExcludeFromCollapsible(nx)) {
			contents.push(nx);
			collapse = collapse || shouldCollapseForTheSakeOf(nx);
		}
		nx = nx.nextElementSibling;
	}
	
	if (!collapse) return [undefined, undefined];
		
	const navFrame = makeNavFrame(heading, 
		addEditLinks ? headingCandidate.querySelector(".mw-editsection") : null,
		toggleCat, contents);
	navFrame.id = headingId;
	navFramesById[navFrame.id] = navFrame;
	return [navFrame, headingCandidate];
}

function makeSingleCollapsible(heading, toggleCat, onlyL3) {
	document.querySelectorAll(getHeadingQuery(onlyL3)).forEach((e) => {
		const [navFrame, headingElement] = collapseHeadingInNavbox(e,
				heading, toggleCat,
				(headingText) => headingText === heading, true);
		if (navFrame)
			headingElement.parentNode.replaceChild(navFrame, headingElement);
	});
}

function makeNumberedCollapsible(heading, toggleCat) {
	const re = new RegExp("^" + heading + " \\d+(?:\.\\d+)?$");
	document.querySelectorAll(getHeadingQuery(false)).forEach((e) => {
		const [navFrame, headingElement] = collapseHeadingInNavbox(e,
				heading, toggleCat,
				(headingText) => headingText.match(re), false);
		if (navFrame)
			headingElement.parentNode.insertBefore(navFrame,
					headingElement.nextElementSibling);
	});
}

function moveAnagrams() {
	// move the Anagrams box above Further reading and References
	document.querySelectorAll(".NavFrame[data-toggle-category=\"anagrams\"]").forEach((e) => {
		let p = e.previousElementSibling;
		while (p) {
			if (p.tagName === "H2") {
				break;
			}
			
			if (p.tagName === "H3") {
				const hl = p.querySelector(".mw-headline");
				if (hl && (hl.textContent === "Further reading" || hl.textContent === "References")) {
					e.parentElement.insertBefore(e, p);
				}
			}
			p = p.previousElementSibling;
		}
	});
}

function addGaps() {
	// add gaps before streamline navboxes if preceded by certain elements
	document.querySelectorAll(".NavFrame-streamline").forEach((sl) => {
		const prev = sl.previousElementSibling;
		const next = sl.nextElementSibling;
		let tn;
		if (prev) {
			tn = prev.tagName;
			if (tn === "OL" || tn === "UL") {
				sl.style.marginTop = "1em";
			}
		}
		if (next) {
			tn = next.tagName;
			if (tn === "HR") {
				sl.style.marginBottom = "1em";
			}
		}
	});
}

function defaultStreamline() {
	makeSingleCollapsible("Alternative forms", "alternative forms", true);
	makeSingleCollapsible("Alternative reconstructions", "alternative reconstructions", true);
	makeSingleCollapsible("Etymology", "etymology");
	makeSingleCollapsible("Glyph origin", "etymology");
	makeSingleCollapsible("Description", "description");
	makeSingleCollapsible("Pronunciation", "pronunciations");
	makeSingleCollapsible("Production", "productions");
	
	makeNumberedCollapsible("Etymology", "etymology");
	makeNumberedCollapsible("Pronunciation", "pronunciations");
	makeNumberedCollapsible("Production", "productions");
	
	addGaps();
}

function superStreamline() {
	makeSingleCollapsible("Alternative forms", "alternative forms", false);
	makeSingleCollapsible("Alternative reconstructions", "alternative reconstructions", false);
	makeSingleCollapsible("Inflection", "inflection");
	makeSingleCollapsible("Declension", "inflection");
	makeSingleCollapsible("Conjugation", "inflection");
	makeSingleCollapsible("Mutation", "mutation");
	makeSingleCollapsible("Synonyms", "synonyms");
	makeSingleCollapsible("Antonyms", "antonyms");
	makeSingleCollapsible("Hypernyms", "hypernyms");
	makeSingleCollapsible("Hyponyms", "hyponyms");
	makeSingleCollapsible("Meronyms", "meronyms");
	makeSingleCollapsible("Holonyms", "holonyms");
	makeSingleCollapsible("Troponyms", "troponyms");
	makeSingleCollapsible("Coordinate terms", "coordinate terms");
	makeSingleCollapsible("Derived terms", "derived terms");
	makeSingleCollapsible("Compounds", "derived terms");
	makeSingleCollapsible("Related terms", "related terms");
	makeSingleCollapsible("Collocations", "collocations");
	makeSingleCollapsible("Descendants", "descendants");
	makeSingleCollapsible("Trivia", "trivia");
	makeSingleCollapsible("See also", "related terms");
	makeSingleCollapsible("References", "references");
	makeSingleCollapsible("Further reading", "further reading");
	makeSingleCollapsible("Anagrams", "anagrams");
	moveAnagrams();
	
	addGaps();
}

function openNavFrameForElementWithId(targetId) {
	const navFrameTarget = navFramesById[targetId];
	if (navFrameTarget)
		openNavFrame(navFrameTarget);

	// if what we targeted is inside a navframe, open it too
	const targetElement = document.getElementById(targetId);
	if (targetElement && targetElement.closest) {
		const navFrameContainingTarget = targetElement.closest(".NavFrame-streamline");
		if (navFrameContainingTarget)
			openNavFrame(navFrameContainingTarget);
	}
}

const { collapseElements, runInReconstructions } = preferences;

// only run in content namespaces.
// this must also run on 4 in [[MediaWiki:Gadgets-definition]] so that [[WT:GPREFS]] works.
if (mw.config.get("wgNamespaceNumber") === 0 || (runInReconstructions && mw.config.get("wgNamespaceNumber") === 118)) {
	mw.hook("wikipage.content").add(() => {
		try {
			defaultStreamline();
			if (collapseElements !== "aboveDefinitions")
				superStreamline();

			// refocus URL target in case the page layout changed, which it probably did
			// also open a navframe if targeted
			window.requestAnimationFrame(() => {
				if (window.location.hash && window.location.hash.match(/^#/)) {
					const targetId = window.location.hash.replace(/^#/, "");
					const targetElement = document.getElementById(targetId);
					if (targetElement)
						targetElement.scrollIntoView();
					openNavFrameForElementWithId(targetId);
				}
			});

			// open a navframe if an internal link targets a section
			window.addEventListener("hashchange", () => {
				if (window.location.hash) {
					const targetId = window.location.hash.replace(/^#/, "");
					openNavFrameForElementWithId(targetId);
				}
			});
		} catch (e) {
			console.error(e);
		}
	});
}

// </nowiki>