MediaWiki:Gadget-relativetime.js

From Old School Near-Reality Wiki
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 }