Merge "Special:AllMessages: Improve zebra striping on hover"
[lhc/web/wiklou.git] / resources / src / mediawiki.base / mediawiki.base.js
1 /*!
2 * This file is currently loaded as part of the 'mediawiki' module and therefore
3 * concatenated to mediawiki.js and executed at the same time. This file exists
4 * to help prepare for splitting up the 'mediawiki' module.
5 * This effort is tracked at https://phabricator.wikimedia.org/T192623
6 *
7 * In short:
8 *
9 * - mediawiki.js will be reduced to the minimum needed to define mw.loader and
10 * mw.config, and then moved to its own private "mediawiki.loader" module that
11 * can be embedded within the StartupModule response.
12 *
13 * - mediawiki.base.js and other files in this directory will remain part of the
14 * "mediawiki" module, and will remain a default/implicit dependency for all
15 * regular modules, just like jquery and wikibits already are.
16 */
17 ( function () {
18 'use strict';
19
20 var slice = Array.prototype.slice,
21 mwLoaderTrack = mw.track,
22 trackCallbacks = $.Callbacks( 'memory' ),
23 trackHandlers = [],
24 queue;
25
26 /**
27 * Object constructor for messages.
28 *
29 * Similar to the Message class in MediaWiki PHP.
30 *
31 * Format defaults to 'text'.
32 *
33 * @example
34 *
35 * var obj, str;
36 * mw.messages.set( {
37 * 'hello': 'Hello world',
38 * 'hello-user': 'Hello, $1!',
39 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
40 * } );
41 *
42 * obj = new mw.Message( mw.messages, 'hello' );
43 * mw.log( obj.text() );
44 * // Hello world
45 *
46 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
47 * mw.log( obj.text() );
48 * // Hello, John Doe!
49 *
50 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
51 * mw.log( obj.text() );
52 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
53 *
54 * // Using mw.message shortcut
55 * obj = mw.message( 'hello-user', 'John Doe' );
56 * mw.log( obj.text() );
57 * // Hello, John Doe!
58 *
59 * // Using mw.msg shortcut
60 * str = mw.msg( 'hello-user', 'John Doe' );
61 * mw.log( str );
62 * // Hello, John Doe!
63 *
64 * // Different formats
65 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
66 *
67 * obj.format = 'text';
68 * str = obj.toString();
69 * // Same as:
70 * str = obj.text();
71 *
72 * mw.log( str );
73 * // Hello, John "Wiki" <3 Doe!
74 *
75 * mw.log( obj.escaped() );
76 * // Hello, John &quot;Wiki&quot; &lt;3 Doe!
77 *
78 * @class mw.Message
79 *
80 * @constructor
81 * @param {mw.Map} map Message store
82 * @param {string} key
83 * @param {Array} [parameters]
84 */
85 function Message( map, key, parameters ) {
86 this.format = 'text';
87 this.map = map;
88 this.key = key;
89 this.parameters = parameters === undefined ? [] : slice.call( parameters );
90 return this;
91 }
92
93 Message.prototype = {
94 /**
95 * Get parsed contents of the message.
96 *
97 * The default parser does simple $N replacements and nothing else.
98 * This may be overridden to provide a more complex message parser.
99 * The primary override is in the mediawiki.jqueryMsg module.
100 *
101 * This function will not be called for nonexistent messages.
102 *
103 * @return {string} Parsed message
104 */
105 parser: function () {
106 var text;
107 if ( mw.config.get( 'wgUserLanguage' ) === 'qqx' ) {
108 text = '(' + this.key + '$*)';
109 } else {
110 text = this.map.get( this.key );
111 }
112 return mw.format.apply( null, [ text ].concat( this.parameters ) );
113 },
114
115 /**
116 * Add (does not replace) parameters for `$N` placeholder values.
117 *
118 * @param {Array} parameters
119 * @return {mw.Message}
120 * @chainable
121 */
122 params: function ( parameters ) {
123 var i;
124 for ( i = 0; i < parameters.length; i++ ) {
125 this.parameters.push( parameters[ i ] );
126 }
127 return this;
128 },
129
130 /**
131 * Convert message object to its string form based on current format.
132 *
133 * @return {string} Message as a string in the current form, or `<key>` if key
134 * does not exist.
135 */
136 toString: function () {
137 var text;
138
139 if ( !this.exists() ) {
140 // Use ⧼key⧽ as text if key does not exist
141 // Err on the side of safety, ensure that the output
142 // is always html safe in the event the message key is
143 // missing, since in that case its highly likely the
144 // message key is user-controlled.
145 // '⧼' is used instead of '<' to side-step any
146 // double-escaping issues.
147 // (Keep synchronised with Message::toString() in PHP.)
148 return '⧼' + mw.html.escape( this.key ) + '⧽';
149 }
150
151 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
152 text = this.parser();
153 }
154
155 if ( this.format === 'escaped' ) {
156 text = this.parser();
157 text = mw.html.escape( text );
158 }
159
160 return text;
161 },
162
163 /**
164 * Change format to 'parse' and convert message to string
165 *
166 * If jqueryMsg is loaded, this parses the message text from wikitext
167 * (where supported) to HTML
168 *
169 * Otherwise, it is equivalent to plain.
170 *
171 * @return {string} String form of parsed message
172 */
173 parse: function () {
174 this.format = 'parse';
175 return this.toString();
176 },
177
178 /**
179 * Change format to 'plain' and convert message to string
180 *
181 * This substitutes parameters, but otherwise does not change the
182 * message text.
183 *
184 * @return {string} String form of plain message
185 */
186 plain: function () {
187 this.format = 'plain';
188 return this.toString();
189 },
190
191 /**
192 * Change format to 'text' and convert message to string
193 *
194 * If jqueryMsg is loaded, {{-transformation is done where supported
195 * (such as {{plural:}}, {{gender:}}, {{int:}}).
196 *
197 * Otherwise, it is equivalent to plain
198 *
199 * @return {string} String form of text message
200 */
201 text: function () {
202 this.format = 'text';
203 return this.toString();
204 },
205
206 /**
207 * Change the format to 'escaped' and convert message to string
208 *
209 * This is equivalent to using the 'text' format (see #text), then
210 * HTML-escaping the output.
211 *
212 * @return {string} String form of html escaped message
213 */
214 escaped: function () {
215 this.format = 'escaped';
216 return this.toString();
217 },
218
219 /**
220 * Check if a message exists
221 *
222 * @see mw.Map#exists
223 * @return {boolean}
224 */
225 exists: function () {
226 if ( mw.config.get( 'wgUserLanguage' ) === 'qqx' ) {
227 return true;
228 }
229 return this.map.exists( this.key );
230 }
231 };
232
233 /**
234 * @class mw
235 * @singleton
236 */
237
238 /**
239 * @inheritdoc mw.inspect#runReports
240 * @method
241 */
242 mw.inspect = function () {
243 var args = arguments;
244 // Lazy-load
245 mw.loader.using( 'mediawiki.inspect', function () {
246 mw.inspect.runReports.apply( mw.inspect, args );
247 } );
248 };
249
250 /**
251 * Replace $* with a list of parameters for &uselang=qqx.
252 *
253 * @private
254 * @since 1.33
255 * @param {string} formatString Format string
256 * @param {Array} parameters Values for $N replacements
257 * @return {string} Transformed format string
258 */
259 mw.internalDoTransformFormatForQqx = function ( formatString, parameters ) {
260 var parametersString;
261 if ( formatString.indexOf( '$*' ) !== -1 ) {
262 parametersString = '';
263 if ( parameters.length ) {
264 parametersString = ': ' + parameters.map( function ( _, i ) {
265 return '$' + ( i + 1 );
266 } ).join( ', ' );
267 }
268 return formatString.replace( '$*', parametersString );
269 }
270 return formatString;
271 };
272
273 /**
274 * Format a string. Replace $1, $2 ... $N with positional arguments.
275 *
276 * Used by Message#parser().
277 *
278 * @since 1.25
279 * @param {string} formatString Format string
280 * @param {...Mixed} parameters Values for $N replacements
281 * @return {string} Formatted string
282 */
283 mw.format = function ( formatString ) {
284 var parameters = slice.call( arguments, 1 );
285 formatString = mw.internalDoTransformFormatForQqx( formatString, parameters );
286 return formatString.replace( /\$(\d+)/g, function ( str, match ) {
287 var index = parseInt( match, 10 ) - 1;
288 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
289 } );
290 };
291
292 // Expose Message constructor
293 mw.Message = Message;
294
295 /**
296 * Get a message object.
297 *
298 * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
299 *
300 * @see mw.Message
301 * @param {string} key Key of message to get
302 * @param {...Mixed} parameters Values for $N replacements
303 * @return {mw.Message}
304 */
305 mw.message = function ( key ) {
306 var parameters = slice.call( arguments, 1 );
307 return new Message( mw.messages, key, parameters );
308 };
309
310 /**
311 * Get a message string using the (default) 'text' format.
312 *
313 * Shortcut for `mw.message( key, parameters... ).text()`.
314 *
315 * @see mw.Message
316 * @param {string} key Key of message to get
317 * @param {...Mixed} parameters Values for $N replacements
318 * @return {string}
319 */
320 mw.msg = function () {
321 return mw.message.apply( mw.message, arguments ).toString();
322 };
323
324 /**
325 * Track an analytic event.
326 *
327 * This method provides a generic means for MediaWiki JavaScript code to capture state
328 * information for analysis. Each logged event specifies a string topic name that describes
329 * the kind of event that it is. Topic names consist of dot-separated path components,
330 * arranged from most general to most specific. Each path component should have a clear and
331 * well-defined purpose.
332 *
333 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
334 * events that match their subcription, including those that fired before the handler was
335 * bound.
336 *
337 * @param {string} topic Topic name
338 * @param {Object} [data] Data describing the event, encoded as an object
339 */
340 mw.track = function ( topic, data ) {
341 mwLoaderTrack( topic, data );
342 trackCallbacks.fire( mw.trackQueue );
343 };
344
345 /**
346 * Register a handler for subset of analytic events, specified by topic.
347 *
348 * Handlers will be called once for each tracked event, including any events that fired before the
349 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
350 * the exact time at which the event fired, a string 'topic' property naming the event, and a
351 * 'data' property which is an object of event-specific data. The event topic and event data are
352 * also passed to the callback as the first and second arguments, respectively.
353 *
354 * @param {string} topic Handle events whose name starts with this string prefix
355 * @param {Function} callback Handler to call for each matching tracked event
356 * @param {string} callback.topic
357 * @param {Object} [callback.data]
358 */
359 mw.trackSubscribe = function ( topic, callback ) {
360 var seen = 0;
361 function handler( trackQueue ) {
362 var event;
363 for ( ; seen < trackQueue.length; seen++ ) {
364 event = trackQueue[ seen ];
365 if ( event.topic.indexOf( topic ) === 0 ) {
366 callback.call( event, event.topic, event.data );
367 }
368 }
369 }
370
371 trackHandlers.push( [ handler, callback ] );
372
373 trackCallbacks.add( handler );
374 };
375
376 /**
377 * Stop handling events for a particular handler
378 *
379 * @param {Function} callback
380 */
381 mw.trackUnsubscribe = function ( callback ) {
382 trackHandlers = trackHandlers.filter( function ( fns ) {
383 if ( fns[ 1 ] === callback ) {
384 trackCallbacks.remove( fns[ 0 ] );
385 // Ensure the tuple is removed to avoid holding on to closures
386 return false;
387 }
388 return true;
389 } );
390 };
391
392 // Fire events from before track() triggered fire()
393 trackCallbacks.fire( mw.trackQueue );
394
395 /**
396 * Registry and firing of events.
397 *
398 * MediaWiki has various interface components that are extended, enhanced
399 * or manipulated in some other way by extensions, gadgets and even
400 * in core itself.
401 *
402 * This framework helps streamlining the timing of when these other
403 * code paths fire their plugins (instead of using document-ready,
404 * which can and should be limited to firing only once).
405 *
406 * Features like navigating to other wiki pages, previewing an edit
407 * and editing itself – without a refresh – can then retrigger these
408 * hooks accordingly to ensure everything still works as expected.
409 *
410 * Example usage:
411 *
412 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
413 * mw.hook( 'wikipage.content' ).fire( $content );
414 *
415 * Handlers can be added and fired for arbitrary event names at any time. The same
416 * event can be fired multiple times. The last run of an event is memorized
417 * (similar to `$(document).ready` and `$.Deferred().done`).
418 * This means if an event is fired, and a handler added afterwards, the added
419 * function will be fired right away with the last given event data.
420 *
421 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
422 * Thus allowing flexible use and optimal maintainability and authority control.
423 * You can pass around the `add` and/or `fire` method to another piece of code
424 * without it having to know the event name (or `mw.hook` for that matter).
425 *
426 * var h = mw.hook( 'bar.ready' );
427 * new mw.Foo( .. ).fetch( { callback: h.fire } );
428 *
429 * Note: Events are documented with an underscore instead of a dot in the event
430 * name due to jsduck not supporting dots in that position.
431 *
432 * @class mw.hook
433 */
434 mw.hook = ( function () {
435 var lists = Object.create( null );
436
437 /**
438 * Create an instance of mw.hook.
439 *
440 * @method hook
441 * @member mw
442 * @param {string} name Name of hook.
443 * @return {mw.hook}
444 */
445 return function ( name ) {
446 var list = lists[ name ] || ( lists[ name ] = $.Callbacks( 'memory' ) );
447
448 return {
449 /**
450 * Register a hook handler
451 *
452 * @param {...Function} handler Function to bind.
453 * @chainable
454 */
455 add: list.add,
456
457 /**
458 * Unregister a hook handler
459 *
460 * @param {...Function} handler Function to unbind.
461 * @chainable
462 */
463 remove: list.remove,
464
465 /**
466 * Run a hook.
467 *
468 * @param {...Mixed} data
469 * @return {mw.hook}
470 * @chainable
471 */
472 fire: function () {
473 return list.fireWith.call( this, null, slice.call( arguments ) );
474 }
475 };
476 };
477 }() );
478
479 /**
480 * HTML construction helper functions
481 *
482 * @example
483 *
484 * var Html, output;
485 *
486 * Html = mw.html;
487 * output = Html.element( 'div', {}, new Html.Raw(
488 * Html.element( 'img', { src: '<' } )
489 * ) );
490 * mw.log( output ); // <div><img src="&lt;"/></div>
491 *
492 * @class mw.html
493 * @singleton
494 */
495 mw.html = ( function () {
496 function escapeCallback( s ) {
497 switch ( s ) {
498 case '\'':
499 return '&#039;';
500 case '"':
501 return '&quot;';
502 case '<':
503 return '&lt;';
504 case '>':
505 return '&gt;';
506 case '&':
507 return '&amp;';
508 }
509 }
510
511 return {
512 /**
513 * Escape a string for HTML.
514 *
515 * Converts special characters to HTML entities.
516 *
517 * mw.html.escape( '< > \' & "' );
518 * // Returns &lt; &gt; &#039; &amp; &quot;
519 *
520 * @param {string} s The string to escape
521 * @return {string} HTML
522 */
523 escape: function ( s ) {
524 return s.replace( /['"<>&]/g, escapeCallback );
525 },
526
527 /**
528 * Create an HTML element string, with safe escaping.
529 *
530 * @param {string} name The tag name.
531 * @param {Object} [attrs] An object with members mapping element names to values
532 * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
533 *
534 * - string: Text to be escaped.
535 * - null: The element is treated as void with short closing form, e.g. `<br/>`.
536 * - this.Raw: The raw value is directly included.
537 * - this.Cdata: The raw value is directly included. An exception is
538 * thrown if it contains any illegal ETAGO delimiter.
539 * See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
540 * @return {string} HTML
541 */
542 element: function ( name, attrs, contents ) {
543 var v, attrName, s = '<' + name;
544
545 if ( attrs ) {
546 for ( attrName in attrs ) {
547 v = attrs[ attrName ];
548 // Convert name=true, to name=name
549 if ( v === true ) {
550 v = attrName;
551 // Skip name=false
552 } else if ( v === false ) {
553 continue;
554 }
555 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
556 }
557 }
558 if ( contents === undefined || contents === null ) {
559 // Self close tag
560 s += '/>';
561 return s;
562 }
563 // Regular open tag
564 s += '>';
565 switch ( typeof contents ) {
566 case 'string':
567 // Escaped
568 s += this.escape( contents );
569 break;
570 case 'number':
571 case 'boolean':
572 // Convert to string
573 s += String( contents );
574 break;
575 default:
576 if ( contents instanceof this.Raw ) {
577 // Raw HTML inclusion
578 s += contents.value;
579 } else if ( contents instanceof this.Cdata ) {
580 // CDATA
581 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
582 throw new Error( 'Illegal end tag found in CDATA' );
583 }
584 s += contents.value;
585 } else {
586 throw new Error( 'Invalid type of contents' );
587 }
588 }
589 s += '</' + name + '>';
590 return s;
591 },
592
593 /**
594 * Wrapper object for raw HTML passed to mw.html.element().
595 *
596 * @class mw.html.Raw
597 * @constructor
598 * @param {string} value
599 */
600 Raw: function ( value ) {
601 this.value = value;
602 },
603
604 /**
605 * Wrapper object for CDATA element contents passed to mw.html.element()
606 *
607 * @class mw.html.Cdata
608 * @constructor
609 * @param {string} value
610 */
611 Cdata: function ( value ) {
612 this.value = value;
613 }
614 };
615 }() );
616
617 /**
618 * Execute a function as soon as one or more required modules are ready.
619 *
620 * Example of inline dependency on OOjs:
621 *
622 * mw.loader.using( 'oojs', function () {
623 * OO.compare( [ 1 ], [ 1 ] );
624 * } );
625 *
626 * Example of inline dependency obtained via `require()`:
627 *
628 * mw.loader.using( [ 'mediawiki.util' ], function ( require ) {
629 * var util = require( 'mediawiki.util' );
630 * } );
631 *
632 * Since MediaWiki 1.23 this also returns a promise.
633 *
634 * Since MediaWiki 1.28 the promise is resolved with a `require` function.
635 *
636 * @member mw.loader
637 * @param {string|Array} dependencies Module name or array of modules names the
638 * callback depends on to be ready before executing
639 * @param {Function} [ready] Callback to execute when all dependencies are ready
640 * @param {Function} [error] Callback to execute if one or more dependencies failed
641 * @return {jQuery.Promise} With a `require` function
642 */
643 mw.loader.using = function ( dependencies, ready, error ) {
644 var deferred = $.Deferred();
645
646 // Allow calling with a single dependency as a string
647 if ( typeof dependencies === 'string' ) {
648 dependencies = [ dependencies ];
649 }
650
651 if ( ready ) {
652 deferred.done( ready );
653 }
654 if ( error ) {
655 deferred.fail( error );
656 }
657
658 try {
659 // Resolve entire dependency map
660 dependencies = mw.loader.resolve( dependencies );
661 } catch ( e ) {
662 return deferred.reject( e ).promise();
663 }
664
665 mw.loader.enqueue( dependencies, function () {
666 deferred.resolve( mw.loader.require );
667 }, deferred.reject );
668
669 return deferred.promise();
670 };
671
672 // Alias $j to jQuery for backwards compatibility
673 // @deprecated since 1.23 Use $ or jQuery instead
674 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
675
676 // Process callbacks for Grade A that require modules.
677 queue = window.RLQ;
678 // Replace temporary RLQ implementation from startup.js with the
679 // final implementation that also processes callbacks that can
680 // require modules. It must also support late arrivals of
681 // plain callbacks. (T208093)
682 window.RLQ = {
683 push: function ( entry ) {
684 if ( typeof entry === 'function' ) {
685 entry();
686 } else {
687 mw.loader.using( entry[ 0 ], entry[ 1 ] );
688 }
689 }
690 };
691 while ( queue[ 0 ] ) {
692 window.RLQ.push( queue.shift() );
693 }
694 }() );