User:Mxn/CommentsInLocalTime/sandbox.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.

*

* 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 .

*/

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

* and .

*/

// 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:

*

*

*

*

* 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: /(?\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

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 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

// This loop must wrap the text in a

// 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 = $("

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

*

* @param {Number} idx Unused.

* @param {Element} elt The

*/

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.

// TODO: Replace with formatDate(then, {type: "relative"}) once gerrit:1149440 is deployed.

var component = relativeDateComponent(then);

var text;

if (component.unit === "weeks") {

text = formatDate(then, {

type: "absolute",

options: LocalComments.outputFormats.absolute,

});

} else {

text = formatDate(then, {

type: "relative",

options: LocalComments.outputFormats.relative,

});

}

if (text) {

$(elt).text(text);

}

// Register for periodic updates.

$(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;

});