MediaWiki:Gadget-ShowIDs.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.
// <nowiki>
/* jshint maxerr:1048576, strict:true, undef:true, latedef:true, esversion:6 */
/* global $, mw */

/**
 * Display sense and etym IDs.
 * 
 * Note: this gadget also loads ShowIDs-pagestyles, which offers a
 * CSS-only fallback to this gadget that does not require JavaScript.
 * The CSS is nevertheless required for this JavaScript gadget
 * to display correctly.
 * CSS: [[MediaWiki:Gadget-ShowIDs-pagestyles.css]]
 *
 * Author(s): Surjection, Fenakhay
 * Last updated: 2025-09-22
 */

$(document).ready(() => {
	'use strict';

	const ID_CLASSES = [
		{ name: 'senseid', showTooltip: true },
		{ name: 'etymid', showTooltip: true },
		{ name: 'etymonid', showTooltip: false }
	];

	const WIKIDATA_PATTERN = /^Q\d+$/;

	/**
	 * Generate CSS selectors for all ID classes
	 */
	function generateSelectors(suffix = '') {
		return ID_CLASSES.map(cls => `.${cls.name}${suffix}`).join(', ');
	}

	/**
	 * Returns the text content of element, excluding children (unlike .text()).
	 */
	function getTextOnlyRoot($elem) {
		return $elem.clone().children().remove().end().text();
	}

	/**
	 * Move element (sole child span) into the next visible, non-floated paragraph.
	 */
	function moveIntoNextParagraph($elem) {
		const $parentP = $elem.parent('p');

		// Only proceed if parent is a p with single child and no text content
		if (!$parentP.length ||
			$parentP.children().length !== 1 ||
			getTextOnlyRoot($parentP).trim()) {
			return;
		}

		let $next = $parentP.next();
		while ($next.length) {
			// Skip floated or hidden elements
			if ($next.css('float') !== 'none' || !$next.is(':visible')) {
				$next = $next.next();
				continue;
			}

			// Move content to next paragraph if found
			if ($next.is('p')) {
				$next.prepend('\n');
				$parentP.children().prependTo($next);
				$parentP.remove();
			}
			break;
		}
	}

	/**
	 * Create display element for ID (link for Wikidata, span for others).
	 */
	function createDisplayElement(idStr, tooltip) {
		const isWikidata = WIKIDATA_PATTERN.test(idStr);

		return isWikidata
			? $('<a/>', {
				href: `https://www.wikidata.org/wiki/${idStr}`,
				text: idStr,
				title: tooltip || undefined,
				target: '_blank',
				rel: 'noopener noreferrer'
			})
			: $('<span/>', {
				text: idStr,
				title: tooltip || undefined
			});
	}

	/**
	 * Generate tooltip text for template.
	 */
	function generateTooltip(classConfig, lang, idStr) {
		return (classConfig.showTooltip && lang && idStr)
			? `{{${classConfig.name}|${lang}|${idStr}}}`
			: '';
	}

	// Process each ID element
	$(generateSelectors()).each(function () {
		const $elem = $(this);

		// Find the matching class configuration
		const classConfig = ID_CLASSES.find(cls => $elem.hasClass(cls.name));
		if (!classConfig) return;

		const displayedClass = `${classConfig.name}-displayed`;

		// Skip if already processed
		if ($elem.hasClass(displayedClass)) return;

		// Extract data
		const lang = $elem.attr('data-lang');
		const rawId = $elem.data('id');
		const idStr = String(rawId);
		const tooltip = generateTooltip(classConfig, lang, idStr);
		const hasText = !!$elem.text().trim();

		// Create display element
		const $content = createDisplayElement(idStr, tooltip);

		if ($elem.is('li') || hasText) {
			// Wrap in new span to preserve existing content
			const $wrapper = $('<span/>', { class: displayedClass }).append($content);

			// Add space if needed
			if (!hasText || !$elem.text().match(/^\s/)) {
				$elem.prepend(' ');
			}
			$elem.prepend($wrapper);
		} else {
			// Replace element content entirely
			$elem.addClass(displayedClass).empty().append($content);
			moveIntoNextParagraph($elem);
		}
	});

	// Prevent CSS-only pseudo-elements from duplicating the display
	mw.util.addCSS(
		`${generateSelectors('::before')} { content: none !important; display: none !important; }`
	);
});

// Ensure CSS fallback is loaded
mw.loader.load('ext.gadget.ShowIDs-pagestyles');
// </nowiki>