MediaWiki:Gadget-relativetime.js
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)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
1 // Don't load CommentsInLocalTime for namespaces it is disabled for.
2 if ( [-1, 0, 8].indexOf(mw.config.get("wgNamespaceNumber")) === -1 ) {
3 // [[w:en:User:Mxn/CommentsInLocalTime]]
4 // en.wikipedia.org/wiki/User:Mxn/CommentsInLocalTime.js
5
6 /**
7 * Comments in local time
8 * [[User:Mxn/CommentsInLocalTime]]
9 *
10 * Adjust timestamps in comment signatures to use easy-to-understand, relative
11 * local time instead of absolute UTC time.
12 *
13 * Inspired by [[Wikipedia:Comments in Local Time]].
14 *
15 * @author [[User:Mxn]]
16 */
17
18 /**
19 * Default settings for this gadget.
20 */
21 window.LocalComments = $.extend({
22 // USER OPTIONS ////////////////////////////////////////////////////////////
23
24 /**
25 * When false, this gadget does nothing.
26 */
27 enabled: true,
28
29 /**
30 * Formats to display inline for each timestamp, keyed by a few common
31 * cases.
32 *
33 * If a property of this object is set to a string, the timestamp is
34 * formatted according to the documentation at
35 * <http://momentjs.com/docs/#/displaying/format/>.
36 *
37 * If a property of this object is set to a function, it is called to
38 * retrieve the formatted timestamp string. See
39 * <http://momentjs.com/docs/#/displaying/> for the various things you can
40 * do with the passed-in moment object.
41 */
42 formats: {
43 /**
44 * Within a day, show a relative time that’s easy to relate to.
45 */
46 day: function (then) { return then.fromNow(); },
47
48 /**
49 * Within a week, show a relative date and specific time, still helpful
50 * if the user doesn’t remember today’s date. Don’t show just a relative
51 * time, because a discussion may need more context than “Last Friday”
52 * on every comment.
53 */
54 week: function (then) { return then.calendar(); },
55
56 /**
57 * The calendar() method uses an ambiguous “MM/DD/YYYY” format for
58 * faraway dates; spell things out for this international audience.
59 */
60 other: "LLL",
61 },
62
63 /**
64 * Formats to display in each timestamp’s tooltip, one per line.
65 *
66 * If an element of this array is a string, the timestamp is formatted
67 * according to the documentation at
68 * <http://momentjs.com/docs/#/displaying/format/>.
69 *
70 * If an element of this array is a function, it is called to retrieve the
71 * formatted timestamp string. See <http://momentjs.com/docs/#/displaying/>
72 * for the various things you can do with the passed-in moment object.
73 */
74 tooltipFormats: [
75 function (then) { return then.fromNow(); },
76 "LLLL",
77 "YYYY-MM-DDTHH:mmZ",
78 ],
79
80 /**
81 * When true, this gadget refreshes timestamps periodically.
82 */
83 dynamic: true,
84 }, {
85 // SITE OPTIONS ////////////////////////////////////////////////////////////
86
87 /**
88 * Numbers of namespaces to completely ignore. See [[Wikipedia:Namespace]].
89 */
90 excludeNamespaces: [-1, 0, 8, 100, 108, 118],
91
92 /**
93 * Names of tags that often directly contain timestamps.
94 *
95 * This is merely a performance optimization. This gadget will look at text
96 * nodes in any tag other than the codeTags, but adding a tag here ensures
97 * that it gets processed the most efficient way possible.
98 */
99 proseTags: ["dd", "li", "p", "td"],
100
101 /**
102 * Names of tags that don’t contain timestamps either directly or
103 * indirectly.
104 */
105 codeTags: ["code", "input", "pre", "textarea"],
106
107 /**
108 * Expected format or formats of the timestamps in existing wikitext. If
109 * very different formats have been used over the course of the wiki’s
110 * history, specify an array of formats.
111 *
112 * This option expects parsing format strings
113 * <http://momentjs.com/docs/#/parsing/string-format/>.
114 */
115 parseFormat: "H:m, D MMM YYYY",
116
117 /**
118 * Regular expression matching all the timestamps inserted by this MediaWiki
119 * installation over the years. This regular expression should more or less
120 * agree with the parseFormat option.
121 *
122 * Until 2005:
123 * 18:16, 23 Dec 2004 (UTC)
124 * 2005–present:
125 * 08:51, 23 November 2015 (UTC)
126 */
127 parseRegExp: /\d\d:\d\d, \d\d? (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w* \d{4} \(UTC\)/,
128
129 /**
130 * UTC offset of the wiki's default local timezone. See
131 * [[mw:Manual:Timezone]].
132 */
133 utcOffset: 0,
134 }, window.LocalComments);
135
136 $(function () {
137 if (!LocalComments.enabled
138 || LocalComments.excludeNamespaces.indexOf(mw.config.get("wgNamespaceNumber")) !== -1
139 || ["view", "submit"].indexOf(mw.config.get("wgAction")) === -1
140 || mw.util.getParamValue("disable") === "loco")
141 {
142 return;
143 }
144
145 var proseTags = LocalComments.proseTags.join("\n").toUpperCase().split("\n");
146 // Exclude <time> to avoid an infinite loop when iterating over text nodes.
147 var codeTags = $.merge(LocalComments.codeTags, ["time"]).join(", ");
148
149 // Look in the content body for DOM text nodes that may contain timestamps.
150 // The wiki software has already localized other parts of the page.
151 var root = $("#wikiPreview, #mw-content-text")[0];
152 if (!root || !("createNodeIterator" in document)) return;
153 var iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT, {
154 acceptNode: function (node) {
155 // We can’t just check the node’s direct parent, because templates
156 // like [[Template:Talkback]] and [[Template:Resolved]] may place a
157 // signature inside a nondescript <span>.
158 var isInProse = proseTags.indexOf(node.parentElement.nodeName) !== -1
159 || !$(node).parents(codeTags).length;
160 var isDateNode = isInProse && LocalComments.parseRegExp.test(node.data);
161 return isDateNode ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
162 },
163 });
164
165 // Mark up each timestamp found.
166 function wrapTimestamps() {
167 var prefixNode;
168 while ((prefixNode = iter.nextNode())) {
169 var result = LocalComments.parseRegExp.exec(prefixNode.data);
170 if (!result) continue;
171
172 // Split out the timestamp into a separate text node.
173 var dateNode = prefixNode.splitText(result.index);
174 var suffixNode = dateNode.splitText(result[0].length);
175
176 // Determine the represented time.
177 var then = moment.utc(result[0], LocalComments.parseFormat);
178 if (!then.isValid()) {
179 // Many Wikipedias started out with English as the default
180 // localization, so fall back to English.
181 then = moment.utc(result[0], "H:m, D MMM YYYY", "en");
182 }
183 if (!then.isValid()) continue;
184 then.utcOffset(-LocalComments.utcOffset);
185
186 // Wrap the timestamp inside a <time> element for findability.
187 var timeElt = $("<time />");
188 // MediaWiki core styles .explain[title] the same way as
189 // abbr[title], guiding the user to the tooltip.
190 timeElt.addClass("localcomments explain");
191 timeElt.attr("datetime", then.toISOString());
192 $(dateNode).wrap(timeElt);
193 }
194 }
195
196 /**
197 * Returns a formatted string for the given moment object.
198 *
199 * @param {Moment} then The moment object to format.
200 * @param {String} fmt A format string or function.
201 * @returns {String} A formatted string.
202 */
203 function formatMoment(then, fmt) {
204 return (fmt instanceof Function) ? fmt(then) : then.format(fmt);
205 }
206
207 /**
208 * Reformats a timestamp marked up with the <time> element.
209 *
210 * @param {Number} idx Unused.
211 * @param {Element} elt The <time> element.
212 */
213 function formatTimestamp(idx, elt) {
214 var iso = $(elt).attr("datetime");
215 var then = moment(iso, moment.ISO_8601);
216 var now = moment();
217 var withinHours = Math.abs(then.diff(now, "hours", true))
218 <= moment.relativeTimeThreshold("h");
219 var formats = LocalComments.formats;
220 var text;
221 if (withinHours) {
222 text = formatMoment(then, formats.day || formats.other);
223 }
224 else {
225 var dayDiff = then.diff(moment().startOf("day"), "days", true);
226 if (dayDiff > -6 && dayDiff < 7) {
227 text = formatMoment(then, formats.week || formats.other);
228 }
229 else text = formatMoment(then, formats.other);
230 }
231 $(elt).text(text);
232
233 // Add a tooltip with multiple formats.
234 elt.title = $.map(LocalComments.tooltipFormats, function (fmt, idx) {
235 return formatMoment(then, fmt);
236 }).join("\n");
237
238 // Register for periodic updates.
239 var withinMinutes = withinHours
240 && Math.abs(then.diff(now, "minutes", true))
241 <= moment.relativeTimeThreshold("m");
242 var withinSeconds = withinMinutes
243 && Math.abs(then.diff(now, "seconds", true))
244 <= moment.relativeTimeThreshold("s");
245 var unit = withinSeconds ? "seconds" :
246 (withinMinutes ? "minutes" :
247 (withinHours ? "hours" : "days"));
248 $(elt).attr("data-localcomments-unit", unit);
249 }
250
251 /**
252 * Reformat all marked-up timestamps and start updating timestamps on an
253 * interval as necessary.
254 */
255 function formatTimestamps() {
256 wrapTimestamps();
257 $(".localcomments").each(function (idx, elt) {
258 // Update every timestamp at least this once.
259 formatTimestamp(idx, elt);
260
261 if (!LocalComments.dynamic) return;
262
263 // Update this minute’s timestamps every second.
264 if ($("[data-localcomments-unit='seconds']").length) {
265 setInterval(function () {
266 $("[data-localcomments-unit='seconds']").each(formatTimestamp);
267 }, 1000 /* ms */);
268 }
269 // Update this hour’s timestamps every minute.
270 setInterval(function () {
271 $("[data-localcomments-unit='minutes']").each(formatTimestamp);
272 }, 60 /* s */ * 1000 /* ms */);
273 // Update today’s timestamps every hour.
274 setInterval(function () {
275 $("[data-localcomments-unit='hours']").each(formatTimestamp);
276 }, 60 /* min */ * 60 /* s */ * 1000 /* ms */);
277 });
278 }
279
280 mw.loader.using("moment", function () {
281 wrapTimestamps();
282 formatTimestamps();
283 });
284 });
285 }