User:Nikki/MiscFeatures.js

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.
/**
 * This script includes an assortment of small features.
 * 
 * To use it, add the following line to your common.js:
 * mw.loader.load("//www.wikidata.org/w/index.php?title=User:Nikki/MiscFeatures.js&action=raw&ctype=text/javascript");
 * 
 * @license CC0-1.0
*/
/* jshint esnext: false, esversion: 8 */

(function () {
	"use strict";

	/* Add tab indexes to links which act like buttons */
	function add_tab_indexes() {
		$(".wikibase-toolbar-button a:not([tabindex])").each(function () {
			this.tabIndex = 0;

			$(this).on("click keydown", function (event) {
				if (event.type === "click" || event.key == "Enter") {
					setTimeout(add_tab_indexes, 500);
				}
			});
		});
	}

	/* Display hex colours as a little box before the code */
	function display_colours() {
		let elements = [];
		
		/* select usage as main value */
		elements.concat(document.querySelectorAll("#P465 .wikibase-snakview-value"));

		/* select usage as qualifier */
		for (const el of document.querySelectorAll("a[title='Property:P465']")) {
			const el2 = el.parentNode.parentNode.parentNode.querySelector(".wikibase-snakview-value");
			elements.push(el2);
		}

		for (const el of elements) {
			const hex = el.textContent;
			mw.util.addCSS(".misc-features-colour-preview {\
				display: inline-block;\
				height: 1.2em;\
				border: 1px solid #222;\
				aspect-ratio: 1;\
				margin-right: 3px;\
				line-height: 1.2em\
			}");

			el.insertAdjacentHTML("afterbegin", "<span class='misc-features-colour-preview' style='background-color: #" + mw.html.escape(hex) + "'>&nbsp;</span>");
		}
	}

	/* Improve the display of URLs by adding <wbr> around punctuation */
	function better_url_display() {
		for (let e of document.querySelectorAll("a.external")) {
			if (e.href !== e.innerHTML) {
				continue;
			}
			e.innerHTML = e.innerHTML.replace(/(?!<[:\/])([\/.-]+)/g, "<wbr>$1<wbr>");
		}
	}

	/* Collapse references section, change the text colour instead */
	function collapse_references() {
		for (let el of document.querySelectorAll(".wikibase-statementview-references")) {
			if (!el.querySelector(".wikibase-listview")) {
				var a = el.parentNode.querySelector("a.ui-toggler");
				a.style.color = "#c00";
				a.click();
				a.click();
			}
		}
	}

	function display_statement_count() {
		mw.util.addCSS(".statement-count { color: #777; font-size:smaller }");

		$("div.wikibase-statementgroupview.listview-item").each(function () {
			var c = $(this).find(".wikibase-statementview").length;
			if (c > 10) {
				$(this).find(".wikibase-statementgroupview-property-label").append("<br><span class='statement-count'>(" + c + " statements)</span>");
			}
		});
	}

	/* Add links for uselang=qqx, safemode=1 and debug=1 to the top menu */
	function add_debug_links() {
		mw.util.addPortletLink(
			"p-cactions",
			mw.Uri().extend({ "uselang": "qqx" }).toString(),
			"uselang=qqx",
			"ca-qqx",
			"Show untranslated message IDs"
		);
		mw.util.addPortletLink(
			"p-cactions",
			mw.Uri().extend({ "safemode": "1" }).toString(),
			"safemode=1",
			"ca-safemode",
			"Enable safe mode (no gadgets or user CSS/JS)"
		);
		mw.util.addPortletLink(
			"p-cactions",
			mw.Uri().extend({ "debug": "1" }).toString(),
			"debug=1",
			"ca-debug",
			"Enable debug mode (No minification of CSS/JS files)"
		);
	}

	/* Expand the termbox on load if it's collapsed */
	function expand_termbox() {
		$(".wikibase-entitytermsview-entitytermsforlanguagelistview-toggler.ui-toggler-toggle-collapsed").click();
	}

	function fix_dwds_ids() {
		for (let e of document.querySelectorAll("a[href^='https://wikidata-externalid-url.toolforge.org/?p=9940&']")) {
			e.href = e.href.replace(/.*&url_prefix=(.*?)&id=/, "$1").replace(/%23/, "#");
		}
	}

	/* Link the datatype on property pages to the list of properties */
	function link_property_datatype(e) {
		if (e.type === "property") {
			let t = document.querySelector(".wikibase-propertyview-datatype-value");
			t.innerHTML = "<a href='/wiki/Special:ListProperties?datatype=" + e.datatype + "&limit=500'>" + t.textContent + "</a>";
		}
	}

	/* Link sitelink language codes to the page to edit the label/desc/aliases for that language */
	function link_sitelink_codes (e) {
		mw.util.addCSS(".wikibase-sitelinkview-siteid-container a { color: inherit; }");
		for (let el of document.querySelectorAll(".wikibase-sitelinkview")) {
			const lang = el.querySelector("a[hreflang]").hreflang;
			let a = document.createElement("a");
			a.href = `/wiki/Special:SetLabelDescriptionAliases/${e.id}/${lang}`;
			a.appendChild(el.querySelector(".wikibase-sitelinkview-siteid"));
			el.querySelector(".wikibase-sitelinkview-siteid-container").appendChild(a);
		}
	}

	// Link units in quantities to their items
	function link_units(e) {
		for (let st of Object.values(e.claims).flat()) {
			replace_unit(st.mainsnak);
			if (st.hasOwnProperty("qualifiers")) {
				for (const q of Object.values(st.qualifiers).flat()) {
					replace_unit(q);
				}
			}
		}
	}
	function replace_unit(snak) {
		if (snak.datatype === "quantity") {
			if (snak.datavalue.value.unit.match(/^http:\/\/www.wikidata\.org\/entity\/Q[0-9]+$/)) {
				const unit = snak.datavalue.value.unit.replace("http://www.wikidata.org/entity/", "/wiki/");
				let el = document.querySelector(`.wikibase-snakview-${snak.hash} .wb-unit`);
				el.innerHTML = `<a href="${unit}">${el.innerHTML}</a>`;
			}
		}
	}

	// Tag Klingon script more explicitly so that the right fonts can be used
	function klingon_script() {
		for (const el of document.querySelectorAll(":lang(mis-x-q56627865)")) {
			el.lang = "tlh-piqd";
		}
	}

	// Fix vertical scripts
	function vertical_script() {
		const map = {
			"mong": /[\u1800-\u18AA]/,
			"phag": /[\uA840-\uA87F]/,
		};

		for (let el of document.querySelectorAll(".wb-monolingualtext-value")) {
			for (let [script, re] of Object.entries(map)) {
				if (!el.textContent.match(re))
					continue;

				// Skip language codes with subtags
				// TODO: Handle subtags
				if (el.lang.length > 3)
					continue;

				el.lang = el.lang + "-" + script;
			}
		}
	}

	// Add tab on property pages linking to the constraint violations
	// Make the property tab on property talk subpages point to the main property page
	function property_tabs() {
		var ns = mw.config.get("wgNamespaceNumber");
		if (ns !== 120 && ns !== 121)
			// not a property
			return;

		var title_parts = mw.config.get("wgTitle").split("/");
		var id = title_parts[0];

		// Add a tab linking to the constraint violations page
		var href = "/wiki/Wikidata:Database_reports/Constraint_violations/" + id;
		mw.util.addPortletLink("p-namespaces", href, "Constraints", "t-constraints", "Constraint violations report for this property");

		// Make the property tab on subpages point to the main property page
		if (title_parts.length > 1) {
			var tab = document.querySelector("#ca-nstab-property a");
			tab.href = "/wiki/Property:" + id;
			// Remove the class which makes it a red link
			// (the property is more likely to exist than not)
			tab.parentNode.classList.remove("new");
		}
	}

	// Render hiero syntax in P7383 (name in hiero syntax)
	function render_hiero() {
		var api = new mw.Api();

		async function render_hiero_node(e) {
			let text = "<hiero>" + e.textContent + "</hiero>";
			let res = await api.get({
				action: "parse",
				contentmodel: "wikitext",
				disablelimitreport: 1,
				formatversion: 2,
				prop: "text",
				text: text,
				wrapoutputclass: "hiero",
			});
			let hiero = res.parse.text;
			e.innerHTML = hiero;
		}

		mw.util.addCSS(`
			.hiero {
				display: inline;
			}
			.mw-hiero-outer {
				display: inline-block;
				vertical-align: top;
			}
			.mw-hiero-table td {
				text-align: center;
			}
		`);

		for (let e of document.querySelectorAll(
			"#P7383 .wikibase-statementview-mainsnak .wikibase-snakview-value,"
			+ ".wikibase-statementgroupview[data-property-id=\"P7383\"] .wikibase-statementview-mainsnak .wikibase-snakview-value"
		)) {
			render_hiero_node(e);
		}
		for (let e of document.querySelectorAll(".wikibase-statementview-qualifiers a[href='/wiki/Property:P7383']")) {
			render_hiero_node(e.parentNode.parentNode.parentNode.querySelector(".wikibase-snakview-value"));
		}
	}

	// Replace Creative Commons license names with their abbreviations
	// Currently not including country-specific versions
	// Query for CC licenses and their abbreviations: https://w.wiki/6cX5
	function shorten_creative_commons_names() {
		const map = {
			"Q6938433": "CC0",

			"Q6905323": "CC BY",
			"Q30942811": "CC BY 1.0",
			"Q19125117": "CC BY 2.0",
			"Q18810333": "CC BY 2.5",
			"Q14947546": "CC BY 3.0",
			"Q20007257": "CC BY 4.0",

			"Q6936496": "CC BY-NC",
			"Q44283370": "CC BY-NC 1.0",
			"Q44128984": "CC BY-NC 2.0",
			"Q19113746": "CC BY-NC 2.5",
			"Q18810331": "CC BY-NC 3.0",
			"Q34179348": "CC BY-NC 4.0",

			"Q6937225": "CC BY-NC-ND",
			"Q47008926": "CC BY-NC-ND 1.0",
			"Q47008927": "CC BY-NC-ND 2.0",
			"Q19068204": "CC BY-NC-ND 2.5",
			"Q19125045": "CC BY-NC-ND 3.0",
			"Q24082749": "CC BY-NC-ND 4.0",

			"Q6998997": "CC BY-NC-SA",
			"Q47008954": "CC BY-NC-SA 1.0",
			"Q28050835": "CC BY-NC-SA 2.0",
			"Q19068212": "CC BY-NC-SA 2.5",
			"Q15643954": "CC BY-NC-SA 3.0",
			"Q42553662": "CC BY-NC-SA 4.0",

			"Q6999319": "CC BY-ND",
			"Q47008966": "CC BY-ND 1.0",
			"Q35254645": "CC BY-ND 2.0",
			"Q18810338": "CC BY-ND 2.5",
			"Q18810160": "CC BY-ND 3.0",
			"Q36795408": "CC BY-ND 4.0",

			"Q6905942": "CC BY-SA",
			"Q47001652": "CC BY-SA 1.0",
			"Q19068220": "CC BY-SA 2.0",
			"Q19113751": "CC BY-SA 2.5",
			"Q14946043": "CC BY-SA 3.0",
			"Q18199165": "CC BY-SA 4.0",

			"Q75209430": "CC SA 1.0",
		};

		const sel = Object.keys(map).map(a => `.wikibase-entityview a[href='/wiki/${a}']`).join(", ");
		for (let e of document.querySelectorAll(sel)) {
			e.textContent = map[e.href.replace(/.*\//, "")];
		}
	}

	/* Shorten property proposal statements */
	function shorten_prop_proposal_links() {
		const elements = document.querySelectorAll("#P3254 .wikibase-statementview-mainsnak .wikibase-snakview-value a");
		for (let e of elements) {
			e.textContent = e.textContent.replace(/^https:\/\/www.wikidata.org\/wiki\/Wikidata:Property_proposal\//, "");
		}
	}

	/* Replace language names with language codes in the translations box */
	function shorten_translations_box() {
		for (let e of document.querySelectorAll(".mw-pt-languages-list [lang]")) {
			e.textContent = e.getAttribute("lang");
		}
	}

	/* Show label language codes */
	function show_label_language_codes() {
		mw.util.addCSS(`
			.langcode {
				color: grey;
			}
		`);
		$(".wikibase-entitytermsforlanguageview").each(function () {
			var lang = $(this).find("[lang]:not('.wb-empty')").first().attr("lang");
			var $td = $(this).find(".wikibase-entitytermsforlanguageview-language");
			if ($td.find(".langcode").length) {
				return;
			}
			$td.append(" <span class=\"langcode\">("+lang+")</span>");
		});
	}

	/* Sort lexeme glosses alphabetically by language code,
	   add the language code after the language name
	   and put user languages at the top */
	function sort_lexeme_glosses() {
		if (mw.config.get("wgPageContentModel") !== "wikibase-lexeme") {
			// Not a lexeme
			return;
		}

		// Use slice() to copy the array, otherwise it changes the config variable
		const senselangs = mw.config.get("wgULSBabelLanguages").slice().reverse();
		mw.util.addCSS(`
			.wikibase-lexeme-sense-gloss.n-highlight-row {
				background-color: #d9f2d9;
			}
			.n-langcode {
				color: grey;
				font-size: small;
			}
		`);

		$(".wikibase-lexeme-sense-glosses-table").each(function (i, v) {
			// Sort lexeme glosses by language
			$(v).find(".wikibase-lexeme-sense-gloss-value-cell")
				.map(function (j, w) { return w.lang })
				.sort()
				.each(function (k, lang) {
					var glossrow = $(v).find(".wikibase-lexeme-sense-gloss-value-cell[lang=" + lang + "]")
						.closest(".wikibase-lexeme-sense-gloss");
					$(glossrow).find(".wikibase-lexeme-sense-gloss-language")
						.append(" <span class=\"n-langcode\">(" + lang + ")</span>");
					$(v).append(glossrow);
				});

			// Put languages from the user's Babel box at the top and highlight them
			for (var x = 0; x < senselangs.length; x++) {
				var glossrow = $(v).find(".wikibase-lexeme-sense-gloss-value-cell[lang=" + senselangs[x] + "]")
					.closest(".wikibase-lexeme-sense-gloss").addClass("n-highlight-row").prependTo(v);
			}
		});
	}

	/* Sort qualifier values */
	function sort_qualifier_values() {
		for (const e of document.querySelectorAll(".wikibase-snaklistview-listview")) {
			let qualifiers = [...e.querySelectorAll(".wikibase-snakview")];
			qualifiers.sort(function (a, b) {
				// Select the value of the link, if present (e.g. entity
				// datatypes), use the entire value if not
				const sel = ".wikibase-snakview-value a, .wikibase-snakview-value";
				const at = a.querySelector(sel).textContent;
				const bt = b.querySelector(sel).textContent;
				return at.localeCompare(bt);
			});
			for (const q of qualifiers) {
				e.appendChild(q);
			}
	
			// Move the property to the first qualifier
			const p1 = e.querySelector(".wikibase-snakview .wikibase-snakview-property-container .wikibase-snakview-property");
			const pc1 = p1.parentNode;
			const p2 = e.querySelector(".wikibase-snakview-property-container a").parentNode;
			const pc2 = p2.parentNode;
			pc1.insertAdjacentElement("afterbegin", p2);
			pc2.insertAdjacentElement("afterbegin", p1);
		}
	}

	function sort_statements_by_rank() {
		$(".wikibase-statementgroupview .wikibase-statementlistview-listview").each(function() {
			$(this).find(".wb-preferred").prependTo(this);
			$(this).find(".wb-deprecated").appendTo(this);
		});
	}

	/* Add property name to the page title on talk pages */
	function talk_page_title() {
		if (mw.config.get("wgNamespaceNumber") !== 121)
			return;

	    const p = document.querySelector(".property-navibox-header div b");
	    if (!p)
	        return;

		const pname = p.textContent;
		document.getElementById("firstHeading").insertAdjacentText("beforeend", ` (${pname})`);
		document.title = document.title.replace(/ - Wikidata$/, ` (${pname}) - Wikidata`);
	}

	/* Render title in HTML (P6833) as HTML */
	function title_as_html() {
		for (const e of document.querySelectorAll("a[title='Property:P6833']")) {
			let n = e.parentNode.parentNode.parentNode.querySelector(".wb-monolingualtext-value");
			// To avoid problems caused by bad HTML, this only replaces the
			// elements allowed for this property, i.e. <sup>, <sub> and <i>
			n.innerHTML = n.innerHTML.replace(/&lt;(su[pb]|i)&gt;([^<>]+?)&lt;\/\1&gt;/g, "<$1>$2</$1>");
		}
	}
	
	/* Show MediaWiki version in footer */
	async function display_mediawiki_version() {
		await load_translations();

		const footer = document.querySelector("#footer-info");
		const text = $.i18n("version") + document.querySelector("meta[name='generator']").content;
		const float = document.dir == "rtl" ? "left" : "right";
		footer.insertAdjacentHTML("afterbegin", "<li style='float:" + float + "'>" + text + "</li>");
	}

	async function load_translations() {
		// base URL has to end with .json, due to bad jquery.i18n assumptions
		const translations = new mw.Title("User:Nikki/translations.json").getUrl() +
			"?action=raw&ctype=application/json";
		await $.i18n().load(translations);
	}

	property_tabs();
	shorten_translations_box();

	mw.loader.using("jquery.i18n").then(display_mediawiki_version);
	mw.loader.using("jquery.i18n").then(talk_page_title);

	mw.hook("wikibase.entityPage.entityView.rendered").add(add_tab_indexes);
	mw.hook("wikibase.entityPage.entityView.rendered").add(add_debug_links);
	mw.hook("wikibase.entityPage.entityView.rendered").add(better_url_display);
	mw.hook("wikibase.entityPage.entityView.rendered").add(collapse_references);
	mw.hook("wikibase.entityPage.entityView.rendered").add(display_colours);
	mw.hook("wikibase.entityPage.entityView.rendered").add(display_statement_count);
	mw.hook("wikibase.entityPage.entityView.rendered").add(expand_termbox);
	mw.hook("wikibase.entityPage.entityView.rendered").add(fix_dwds_ids);
	mw.hook("wikibase.entityPage.entityView.rendered").add(klingon_script);
	mw.hook("wikibase.entityPage.entityView.rendered").add(render_hiero);
	mw.hook("wikibase.entityPage.entityView.rendered").add(shorten_creative_commons_names);
	mw.hook("wikibase.entityPage.entityView.rendered").add(shorten_prop_proposal_links);
	mw.hook("wikibase.entityPage.entityView.rendered").add(sort_lexeme_glosses);
	mw.hook("wikibase.entityPage.entityView.rendered").add(sort_qualifier_values);
	mw.hook("wikibase.entityPage.entityView.rendered").add(sort_statements_by_rank);
	mw.hook("wikibase.entityPage.entityView.rendered").add(title_as_html);
	mw.hook("wikibase.entityPage.entityView.rendered").add(vertical_script);

	mw.hook("wikibase.statement.saved").add(add_tab_indexes);
	mw.hook("wikibase.statement.saved").add(fix_dwds_ids);
	mw.hook("wikibase.statement.saved").add(vertical_script);

	mw.hook("wikibase.entityPage.entityView.rendered").add(function () {
		mw.hook("wikibase.entityPage.entityLoaded").add(link_property_datatype);
		mw.hook("wikibase.entityPage.entityLoaded").add(link_sitelink_codes);
		mw.hook("wikibase.entityPage.entityLoaded").add(link_units);
	});

	mw.hook("wikibase.entityPage.entityView.rendered").add(function () {
		show_label_language_codes();
		$(".wikibase-entitytermsforlanguagelistview-more a").click(function () { show_label_language_codes(); });
	});

})();