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