User:Mxn/CommentsInLocalTime/sandbox.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump.
This code will be executed when previewing this page.
This code will be executed when previewing this page.
Documentation for this user script can be added at User:Mxn/CommentsInLocalTime/sandbox.
/**
* 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.
*
* If a property is an object, its `type` and `options` may be:
*
* `type` | `options`
* -----------|----------
* `relative` | `Intl.RelativeTimeFormat` options
* `absolute` | `Intl.AbsoluteTimeFormat` options
* `iso8601` | —
*
* If a property is a function, it is called to retrieve the formatted
* timestamp string. The function must accept one argument, a `Date` object.
*
* If no `options` is specified, the timestamp adheres to the user’s date
* format and timezone preferences.
*/
outputFormats: {
/**
* Relative dates are helpful if the user doesn’t remember today’s date.
* The tooltip provides a more specific timestamp to distinguish
* comments in rapid succession.
*
* See <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#options>.
*/
relative: {
numeric: "auto",
},
/**
* Absolute dates are helpful for more distant dates, so that the user
* doesn’t have to do math in their head.
*
* See <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#date-time_component_options>
* and <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#style_shortcuts>.
*/
// absolute: {
// dateStyle: "long",
// timeStyle: "short",
// },
},
/**
* Formats to display in each timestamp’s tooltip, one per line.
*
* If an element of this array is an object its `type` and `options` may be:
*
* `type` | `options`
* ------------|----------
* `relative` | `Intl.RelativeTimeFormat` options
* `absolute` | `Intl.AbsoluteTimeFormat` options
* `mediawiki` | —
* `iso8601` | —
*
* See:
* <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat#options>
* <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#date-time_component_options>
* <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#style_shortcuts>
*
* If an element of this array is a function, it is called to retrieve the
* formatted timestamp string. The function must accept one argument, a
* `Date` object.
*/
tooltipComponents: [
{
type: "relative",
options: {
numeric: "auto",
},
},
{
type: "absolute",
options: {
dateStyle: "full",
timeStyle: "short",
},
},
{
type: "iso8601",
},
],
/**
* When true, this gadget refreshes timestamps periodically.
*/
dynamic: true,
}, {
// SITE OPTIONS ////////////////////////////////////////////////////////////
/**
* Numbers of namespaces to completely ignore. See [[Wikipedia: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"],
/**
* Regular expression matching all the timestamps inserted by this MediaWiki
* installation over the years. This regular expression includes the named
* capturing groups `hours`, `minutes`, `day`, `month`, `year`, and
* `timezone`.
*
* Until 2005:
* 18:16, 23 Dec 2004 (UTC)
* 2005–present:
* 08:51, 23 November 2015 (UTC)
*/
parseRegExp: /(?<hours>\d\d):(?<minutes>\d\d), (?<day>\d\d?) (?<month>(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*) (?<year>\d{4}) \((?<timezone>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 <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;
},
});
/**
* Marks up each timestamp found.
*/
function wrapTimestamps() {
var prefixNode;
while ((prefixNode = iter.nextNode())) {
var then;
var dateNode;
var result = LocalComments.parseRegExp.exec(prefixNode.data);
if (result) {
// Split out the timestamp into a separate text node.
dateNode = prefixNode.splitText(result.index);
var suffixNode = dateNode.splitText(result[0].length);
// Determine the represented time.
var components = result.groups;
var monthIndex = mw.config.get("wgMonthNames").slice(1).indexOf(components.month);
// Many Wikipedias started out with English as the default
// localization, so fall back to English.
if (monthIndex === -1) {
monthIndex = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"].indexOf(components.month);
}
if (monthIndex === -1) {
monthIndex = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"].indexOf(components.month);
}
if (monthIndex !== -1) {
var offsetHours = components.hours - LocalComments.utcOffset;
var minuteOffset = (LocalComments.utcOffset - Math.round(LocalComments.utcOffset)) % 60;
var offsetMinutes = components.minutes - minuteOffset;
then = new Date(Date.UTC(components.year, monthIndex, components.day, offsetHours, offsetMinutes));
}
}
// Wrap the timestamp inside a <time> element for findability.
// This loop must wrap the text in a <time> 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#Interface-protected edit request on 18 November 2022]]
var timeElt = $("<time />");
if (!isNaN(then)) {
// 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());
}
if (dateNode) $(dateNode).wrap(timeElt);
}
}
/**
* Returns the coarsest relative date component that fits within the time
* elapsed between the given date and the current date.
*
* @param {Date} then The date object before or after the current date.
* @returns {Object} An object indicating the date component’s value and
* unit compatible with `Intl.RelativeTimeFormat`.
*/
function relativeDateComponent(then) {
var now = new Date();
var value;
var unit;
var seconds = (then - now) / 1000; // convert ms to s
value = seconds;
unit = "seconds";
var minutes = seconds / 60;
if (Math.abs(seconds) > 45) { // moment.relativeTimeThreshold("s")
value = minutes;
unit = "minutes";
}
var hours = minutes / 60;
if (Math.abs(minutes) > 45) { // moment.relativeTimeThreshold("m")
value = hours;
unit = "hours";
}
var days = hours / 24;
if (Math.abs(hours) > 22) { // moment.relativeTimeThreshold("h")
value = days;
unit = "days";
}
var weeks = days / 7;
if (Math.abs(days) > 7) {
value = weeks;
unit = "weeks";
}
return {
value: Math.round(value),
unit: unit,
};
}
/**
* Returns a formatted string for the given date object.
*
* @param {Date} then The date object to format.
* @param {String} fmt A format string or function.
* @returns {String} A formatted string.
*/
function formatDate(then, fmt) {
if (fmt instanceof Function) {
return fmt(then);
}
var lang = mw.config.get("wgPageViewLanguage");
var formatter = mw.loader.require("mediawiki.DateFormatter");
switch (fmt.type) {
case "absolute":
if (fmt.options) {
var absolute = new Intl.DateTimeFormat(lang, fmt.options);
return absolute.format(then);
}
return formatter.formatTimeAndDate(then);
case "relative":
if (fmt.options) {
var relative = new Intl.RelativeTimeFormat(lang, fmt.options);
var component = relativeDateComponent(then);
return relative.format(component.value, component.unit);
}
return formatter.formatRelativeTimeOrDate(then);
case "iso8601":
return formatter.formatIso(then);
}
}
/**
* 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.dateTime;
if (!iso) {
return;
}
var then = new Date(Date.parse(iso));
// Add a tooltip with multiple formats.
elt.title = $.map(LocalComments.tooltipComponents, function (fmt, idx) {
return formatDate(then, fmt) || "";
}).join("\n");
// Replace the text.
var text = formatDate(then, {type: "relative"});
if (text) {
$(elt).text(text);
}
// Register for periodic updates.
var component = relativeDateComponent(then);
$(elt).attr("data-localcomments-unit", component.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 */);
});
}
wrapTimestamps();
mw.loader.using("mediawiki.DateFormatter", function () {
formatTimestamps();
});
LocalComments.wrapTimestamps = wrapTimestamps;
LocalComments.formatTimestamps = formatTimestamps;
});