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 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	 * .	 * 	 * If a property of this object is set to a function, it is called to	 * retrieve the formatted timestamp string. See	 *  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: function (then) { var pref = mw.user.options.values.date; return then.format(window.LocalComments.formatOptions[pref] || "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 * . * 	 * If an element of this array is a function, it is called to retrieve the * formatted timestamp string. See  * 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 Namespace. */	excludeNamespaces: [-1, 0, 8, 100, 108, 118], /**	 * 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"], /**	 * An object mapping the date format user options provided by this MediaWiki * installation to corresponding Moment.js format strings. The user can * choose a preferred date format in * Special:Preferences. See * mw:Manual:Date formatting. These formats determine the default * timestamp display format. * 	 * These formats come from * . * When customizing these formats for a different wiki’s content language, * consult the language’s corresponding message file’s `$dateFormats` * variable. Use only the messages with the “both” suffix, and remove that * suffix from each key. The MediaWiki date format syntax is described in	 *  * and mw:Help:Extension:ParserFunctions. The Moment.js syntax is * described in . * 	 * @todo Automatically convert MediaWiki date format syntax to Moment.js *		 date format syntax. */	formatOptions: { mdy: "HH:mm, MMMM D, YYYY", // H:i, F j, Y		dmy: "HH:mm, D MMMM YYYY", // H:i, j F Y		ymd: "HH:mm, YYYY MMMM D", // H:i, Y F j		"ISO 8601": "YYYY-MM-DDTHH:mm:ss", // xnY-xnm-xnd"T"xnH:xni:xns },	/**	 * 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 * . */	parseFormat: "H:m, D MMM 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, \d\d? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w* \d{4} \(UTC\)/, /**	 * UTC offset of the wiki's default local timezone. See * mw:Manual:Timezone. */	utcOffset: 0, }, 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 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. 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; },	});	/**	 * Marks 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.utc(result[0], LocalComments.parseFormat);			if (!then.isValid) {				// Many Wikipedias started out with English as the default				// localization, so fall back to English.				then = moment.utc(result[0], "H:m, D MMM YYYY", "en");			}			// Wrap the timestamp inside a element for findability.			// This loop must wrap the text in a element no matter what,			// even if the time is invalid, to avoid an infinite loop as the			// same node keeps coming up as a candidate that the node iterator // thinks is valid. // User talk:Mxn/CommentsInLocalTime.js var timeElt = $(" "); if (then.isValid) { then.utcOffset(-LocalComments.utcOffset); // 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 element. * 	 * @param {Number} idx Unused. * @param {Element} elt The 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 {		wrapTimestamps;		formatTimestamps;	}); });