MediaWiki:Gadget-RelativeTime.js

From Nookipedia, the Animal Crossing wiki

Note: After saving, 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)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
// Adapted from OSRS Wiki (https://oldschool.runescape.wiki/w/MediaWiki:Gadget-relativetime.js)
// Original script by Wikipedia User Minh Nguyen (https://en.wikipedia.org/wiki/User:Mxn)

// Don't load CommentsInLocalTime for namespaces it is disabled for.
if ( [-1, 0, 8].indexOf(mw.config.get("wgNamespaceNumber")) === -1 ) {
	// [[w:en:User:Mxn/CommentsInLocalTime]]
	// en.wikipedia.org/wiki/User:Mxn/CommentsInLocalTime.js
	
	/**
	 * Comments in local time
	 * [[User:Mxn/CommentsInLocalTime]]
	 * 
	 * Adjust timestamps in comment signatures to use easy-to-understand, relative
	 * local time instead of absolute UTC time.
	 * 
	 * Inspired by [[Wikipedia:Comments in Local Time]].
	 * 
	 * @author [[User:Mxn]]
	 */
	
	/**
	 * Default settings for this gadget.
	 */
	window.LocalComments = $.extend({
		// USER OPTIONS ////////////////////////////////////////////////////////////
		
		/**
		 * When false, this gadget does nothing.
		 */
		enabled: true,
		
		/**
		 * Formats to display inline for each timestamp, keyed by a few common
		 * cases.
		 * 
		 * If a property of this object is set to a string, the timestamp is
		 * formatted according to the documentation at
		 * <http://momentjs.com/docs/#/displaying/format/>.
		 * 
		 * If a property of this object is set to a function, it is called to
		 * retrieve the formatted timestamp string. See
		 * <http://momentjs.com/docs/#/displaying/> for the various things you can
		 * do with the passed-in moment object.
		 */
		formats: {
			/**
			 * Within a day, show a relative time that’s easy to relate to.
			 */
			day: function (then) { return then.fromNow(); },
			
			/**
			 * Within a week, show a relative date and specific time, still helpful
			 * if the user doesn’t remember today’s date. Don’t show just a relative
			 * time, because a discussion may need more context than “Last Friday”
			 * on every comment.
			 */
			week: function (then) { return then.calendar(); },
			
			/**
			 * The calendar() method uses an ambiguous “MM/DD/YYYY” format for
			 * faraway dates; spell things out for this international audience.
			 */
			other: "LLL",
		},
		
		/**
		 * Formats to display in each timestamp’s tooltip, one per line.
		 * 
		 * If an element of this array is a string, the timestamp is formatted
		 * according to the documentation at
		 * <http://momentjs.com/docs/#/displaying/format/>.
		 * 
		 * If an element of this array is a function, it is called to retrieve the
		 * formatted timestamp string. See <http://momentjs.com/docs/#/displaying/>
		 * for the various things you can do with the passed-in moment object.
		 */
		tooltipFormats: [
			function (then) { return then.fromNow(); },
			"LLLL",
			"YYYY-MM-DDTHH:mmZ",
		],
		
		/**
		 * When true, this gadget refreshes timestamps periodically.
		 */
		dynamic: true,
	}, {
		// SITE OPTIONS ////////////////////////////////////////////////////////////
		
		/**
		 * Numbers of namespaces to completely ignore. See [[Wikipedia:Namespace]].
		 */
		excludeNamespaces: [0, 6, 8, 10, 12, 14, 702, 708],
		
		/**
		 * Names of tags that often directly contain timestamps.
		 * 
		 * This is merely a performance optimization. This gadget will look at text
		 * nodes in any tag other than the codeTags, but adding a tag here ensures
		 * that it gets processed the most efficient way possible.
		 */
		proseTags: ["dd", "li", "p", "td"],
		
		/**
		 * Names of tags that don’t contain timestamps either directly or
		 * indirectly.
		 */
		codeTags: ["code", "input", "pre", "textarea"],
		
		/**
		 * Expected format or formats of the timestamps in existing wikitext. If
		 * very different formats have been used over the course of the wiki’s
		 * history, specify an array of formats.
		 * 
		 * This option expects parsing format strings
		 * <http://momentjs.com/docs/#/parsing/string-format/>.
		 */
		parseFormat: "H:m, MMMM DD, YYYY",
		
		/**
		 * Regular expression matching all the timestamps inserted by this MediaWiki
		 * installation over the years. This regular expression should more or less
		 * agree with the parseFormat option.
		 * 
		 * Until 2005:
		 * 	18:16, 23 Dec 2004 (UTC)
		 * 2005–present:
		 * 	08:51, 23 November 2015 (UTC)
		 */
		parseRegExp: /\d\d:\d\d, (?:January|February|March|April|May|June|July|August|September|October|November|December)\w* \d\d?, \d{4} \(E(?:S|D)T\)/,
		
		/**
		 * UTC offset of the wiki's default local timezone. See
		 * [[mw:Manual:Timezone]].
		 */
		utcOffset: -4,
	}, window.LocalComments);
	
	$(function () {
		if (!LocalComments.enabled
			|| LocalComments.excludeNamespaces.indexOf(mw.config.get("wgNamespaceNumber")) !== -1
			|| ["view", "submit"].indexOf(mw.config.get("wgAction")) === -1
			|| mw.util.getParamValue("disable") === "loco")
		{
			return;
		}
		
		var proseTags = LocalComments.proseTags.join("\n").toUpperCase().split("\n");
		// Exclude <time> to avoid an infinite loop when iterating over text nodes.
		var codeTags = $.merge(LocalComments.codeTags, ["time"]).join(", ");
		
		// Look in the content body for DOM text nodes that may contain timestamps.
		// The wiki software has already localized other parts of the page.
		var root = $("#wikiPreview, #mw-content-text")[0];
		if (!root || !("createNodeIterator" in document)) return;
		var iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, {
			acceptNode: function (node) {
				// We can’t just check the node’s direct parent, because templates
				// like [[Template:Talkback]] and [[Template:Resolved]] may place a
				// signature inside a nondescript <span>.
				var isInProse = proseTags.indexOf(node.parentElement.nodeName) !== -1
					|| !$(node).parents(codeTags).length;
				var isDateNode = isInProse && LocalComments.parseRegExp.test(node.data);
				return isDateNode ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
			},
		});
		
		// Mark up each timestamp found.
		function wrapTimestamps() {
			var prefixNode;
			while ((prefixNode = iter.nextNode())) {
				var result = LocalComments.parseRegExp.exec(prefixNode.data);
				if (!result) continue;
				
				// Split out the timestamp into a separate text node.
				var dateNode = prefixNode.splitText(result.index);
				var suffixNode = dateNode.splitText(result[0].length);
				
				// Determine the represented time.
				var then = moment.tz(result[0], LocalComments.parseFormat, "America/New_York");
				if (!then.isValid()) {
					// Many Wikipedias started out with English as the default
					// localization, so fall back to English.
					then = moment.tz(result[0], "H:m, D MMM YYYY", "en", "America/New_York");
				}
				if (!then.isValid()) continue;
				then.utcOffset(-LocalComments.utcOffset);
				
				// Wrap the timestamp inside a <time> element for findability.
				var timeElt = $("<time />");
				// MediaWiki core styles .explain[title] the same way as
				// abbr[title], guiding the user to the tooltip.
				timeElt.addClass("localcomments explain");
				timeElt.attr("datetime", then.toISOString());
				$(dateNode).wrap(timeElt);
			}
		}
		
		/**
		 * Returns a formatted string for the given moment object.
		 * 
		 * @param {Moment} then The moment object to format.
		 * @param {String} fmt A format string or function.
		 * @returns {String} A formatted string.
		 */
		function formatMoment(then, fmt) {
			return (fmt instanceof Function) ? fmt(then) : then.format(fmt);
		}
		
		/**
		 * Reformats a timestamp marked up with the <time> element.
		 * 
		 * @param {Number} idx Unused.
		 * @param {Element} elt The <time> element.
		 */
		function formatTimestamp(idx, elt) {
			var iso = $(elt).attr("datetime");
			var then = moment(iso, moment.ISO_8601);
			var now = moment();
			var withinHours = Math.abs(then.diff(now, "hours", true))
				<= moment.relativeTimeThreshold("h");
			var formats = LocalComments.formats;
			var text;
			if (withinHours) {
				text = formatMoment(then, formats.day || formats.other);
			}
			else {
				var dayDiff = then.diff(moment().startOf("day"), "days", true);
				if (dayDiff > -6 && dayDiff < 7) {
					text = formatMoment(then, formats.week || formats.other);
				}
				else text = formatMoment(then, formats.other);
			}
			$(elt).text(text);
			
			// Add a tooltip with multiple formats.
			elt.title = $.map(LocalComments.tooltipFormats, function (fmt, idx) {
				return formatMoment(then, fmt);
			}).join("\n");
			
			// Register for periodic updates.
			var withinMinutes = withinHours
				&& Math.abs(then.diff(now, "minutes", true))
					<= moment.relativeTimeThreshold("m");
			var withinSeconds = withinMinutes
				&& Math.abs(then.diff(now, "seconds", true))
					<= moment.relativeTimeThreshold("s");
			var unit = withinSeconds ? "seconds" :
				(withinMinutes ? "minutes" :
					(withinHours ? "hours" : "days"));
			$(elt).attr("data-localcomments-unit", unit);
		}
		
		/**
		 * Reformat all marked-up timestamps and start updating timestamps on an
		 * interval as necessary.
		 */
		function formatTimestamps() {
			wrapTimestamps();
			$(".localcomments").each(function (idx, elt) {
				// Update every timestamp at least this once.
				formatTimestamp(idx, elt);
				
				if (!LocalComments.dynamic) return;
				
				// Update this minute’s timestamps every second.
				if ($("[data-localcomments-unit='seconds']").length) {
					setInterval(function () {
						$("[data-localcomments-unit='seconds']").each(formatTimestamp);
					}, 1000 /* ms */);
				}
				// Update this hour’s timestamps every minute.
				setInterval(function () {
					$("[data-localcomments-unit='minutes']").each(formatTimestamp);
				}, 60 /* s */ * 1000 /* ms */);
				// Update today’s timestamps every hour.
				setInterval(function () {
					$("[data-localcomments-unit='hours']").each(formatTimestamp);
				}, 60 /* min */ * 60 /* s */ * 1000 /* ms */);
			});
		}
		
		mw.loader.using("moment", function () { });
		mw.loader.using("moment-timezone", function () {
			wrapTimestamps();
			formatTimestamps();
		});
	});
}