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
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.
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.
20 var slice
= Array
.prototype.slice
,
21 mwLoaderTrack
= mw
.track
,
22 trackCallbacks
= $.Callbacks( 'memory' ),
27 * Object constructor for messages.
29 * Similar to the Message class in MediaWiki PHP.
31 * Format defaults to 'text'.
37 * 'hello': 'Hello world',
38 * 'hello-user': 'Hello, $1!',
39 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
42 * obj = new mw.Message( mw.messages, 'hello' );
43 * mw.log( obj.text() );
46 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
47 * mw.log( obj.text() );
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
54 * // Using mw.message shortcut
55 * obj = mw.message( 'hello-user', 'John Doe' );
56 * mw.log( obj.text() );
59 * // Using mw.msg shortcut
60 * str = mw.msg( 'hello-user', 'John Doe' );
64 * // Different formats
65 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
67 * obj.format = 'text';
68 * str = obj.toString();
73 * // Hello, John "Wiki" <3 Doe!
75 * mw.log( obj.escaped() );
76 * // Hello, John "Wiki" <3 Doe!
81 * @param {mw.Map} map Message store
83 * @param {Array} [parameters]
85 function Message( map
, key
, parameters
) {
89 this.parameters
= parameters
=== undefined ? [] : slice
.call( parameters
);
95 * Get parsed contents of the message.
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.
101 * This function will not be called for nonexistent messages.
103 * @return {string} Parsed message
105 parser: function () {
106 var text
= this.map
.get( this.key
);
108 mw
.config
.get( 'wgUserLanguage' ) === 'qqx' &&
109 ( !text
|| text
=== '(' + this.key
+ ')' )
111 text
= '(' + this.key
+ '$*)';
113 return mw
.format
.apply( null, [ text
].concat( this.parameters
) );
117 * Add (does not replace) parameters for `$N` placeholder values.
119 * @param {Array} parameters
120 * @return {mw.Message}
123 params: function ( parameters
) {
125 for ( i
= 0; i
< parameters
.length
; i
++ ) {
126 this.parameters
.push( parameters
[ i
] );
132 * Convert message object to its string form based on current format.
134 * @return {string} Message as a string in the current form, or `<key>` if key
137 toString: function () {
140 if ( !this.exists() ) {
141 // Use ⧼key⧽ as text if key does not exist
142 // Err on the side of safety, ensure that the output
143 // is always html safe in the event the message key is
144 // missing, since in that case its highly likely the
145 // message key is user-controlled.
146 // '⧼' is used instead of '<' to side-step any
147 // double-escaping issues.
148 // (Keep synchronised with Message::toString() in PHP.)
149 return '⧼' + mw
.html
.escape( this.key
) + '⧽';
152 if ( this.format
=== 'plain' || this.format
=== 'text' || this.format
=== 'parse' ) {
153 text
= this.parser();
156 if ( this.format
=== 'escaped' ) {
157 text
= this.parser();
158 text
= mw
.html
.escape( text
);
165 * Change format to 'parse' and convert message to string
167 * If jqueryMsg is loaded, this parses the message text from wikitext
168 * (where supported) to HTML
170 * Otherwise, it is equivalent to plain.
172 * @return {string} String form of parsed message
175 this.format
= 'parse';
176 return this.toString();
180 * Change format to 'plain' and convert message to string
182 * This substitutes parameters, but otherwise does not change the
185 * @return {string} String form of plain message
188 this.format
= 'plain';
189 return this.toString();
193 * Change format to 'text' and convert message to string
195 * If jqueryMsg is loaded, {{-transformation is done where supported
196 * (such as {{plural:}}, {{gender:}}, {{int:}}).
198 * Otherwise, it is equivalent to plain
200 * @return {string} String form of text message
203 this.format
= 'text';
204 return this.toString();
208 * Change the format to 'escaped' and convert message to string
210 * This is equivalent to using the 'text' format (see #text), then
211 * HTML-escaping the output.
213 * @return {string} String form of html escaped message
215 escaped: function () {
216 this.format
= 'escaped';
217 return this.toString();
221 * Check if a message exists
226 exists: function () {
227 if ( mw
.config
.get( 'wgUserLanguage' ) === 'qqx' ) {
230 return this.map
.exists( this.key
);
240 * Empty object for third-party libraries, for cases where you don't
241 * want to add a new global, or the global is bad and needs containment
248 // OOUI widgets specific to MediaWiki
252 * @inheritdoc mw.inspect#runReports
255 mw
.inspect = function () {
256 var args
= arguments
;
258 mw
.loader
.using( 'mediawiki.inspect', function () {
259 mw
.inspect
.runReports
.apply( mw
.inspect
, args
);
264 * Replace $* with a list of parameters for &uselang=qqx.
268 * @param {string} formatString Format string
269 * @param {Array} parameters Values for $N replacements
270 * @return {string} Transformed format string
272 mw
.internalDoTransformFormatForQqx = function ( formatString
, parameters
) {
273 var parametersString
;
274 if ( formatString
.indexOf( '$*' ) !== -1 ) {
275 parametersString
= '';
276 if ( parameters
.length
) {
277 parametersString
= ': ' + parameters
.map( function ( _
, i
) {
278 return '$' + ( i
+ 1 );
281 return formatString
.replace( '$*', parametersString
);
287 * Format a string. Replace $1, $2 ... $N with positional arguments.
289 * Used by Message#parser().
292 * @param {string} formatString Format string
293 * @param {...Mixed} parameters Values for $N replacements
294 * @return {string} Formatted string
296 mw
.format = function ( formatString
) {
297 var parameters
= slice
.call( arguments
, 1 );
298 formatString
= mw
.internalDoTransformFormatForQqx( formatString
, parameters
);
299 return formatString
.replace( /\$(\d+)/g, function ( str
, match
) {
300 var index
= parseInt( match
, 10 ) - 1;
301 return parameters
[ index
] !== undefined ? parameters
[ index
] : '$' + match
;
305 // Expose Message constructor
306 mw
.Message
= Message
;
309 * Get a message object.
311 * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
314 * @param {string} key Key of message to get
315 * @param {...Mixed} parameters Values for $N replacements
316 * @return {mw.Message}
318 mw
.message = function ( key
) {
319 var parameters
= slice
.call( arguments
, 1 );
320 return new Message( mw
.messages
, key
, parameters
);
324 * Get a message string using the (default) 'text' format.
326 * Shortcut for `mw.message( key, parameters... ).text()`.
329 * @param {string} key Key of message to get
330 * @param {...Mixed} parameters Values for $N replacements
333 mw
.msg = function () {
334 return mw
.message
.apply( mw
.message
, arguments
).toString();
338 * Track an analytic event.
340 * This method provides a generic means for MediaWiki JavaScript code to capture state
341 * information for analysis. Each logged event specifies a string topic name that describes
342 * the kind of event that it is. Topic names consist of dot-separated path components,
343 * arranged from most general to most specific. Each path component should have a clear and
344 * well-defined purpose.
346 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
347 * events that match their subcription, including those that fired before the handler was
350 * @param {string} topic Topic name
351 * @param {Object} [data] Data describing the event, encoded as an object
353 mw
.track = function ( topic
, data
) {
354 mwLoaderTrack( topic
, data
);
355 trackCallbacks
.fire( mw
.trackQueue
);
359 * Register a handler for subset of analytic events, specified by topic.
361 * Handlers will be called once for each tracked event, including any events that fired before the
362 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
363 * the exact time at which the event fired, a string 'topic' property naming the event, and a
364 * 'data' property which is an object of event-specific data. The event topic and event data are
365 * also passed to the callback as the first and second arguments, respectively.
367 * @param {string} topic Handle events whose name starts with this string prefix
368 * @param {Function} callback Handler to call for each matching tracked event
369 * @param {string} callback.topic
370 * @param {Object} [callback.data]
372 mw
.trackSubscribe = function ( topic
, callback
) {
374 function handler( trackQueue
) {
376 for ( ; seen
< trackQueue
.length
; seen
++ ) {
377 event
= trackQueue
[ seen
];
378 if ( event
.topic
.indexOf( topic
) === 0 ) {
379 callback
.call( event
, event
.topic
, event
.data
);
384 trackHandlers
.push( [ handler
, callback
] );
386 trackCallbacks
.add( handler
);
390 * Stop handling events for a particular handler
392 * @param {Function} callback
394 mw
.trackUnsubscribe = function ( callback
) {
395 trackHandlers
= trackHandlers
.filter( function ( fns
) {
396 if ( fns
[ 1 ] === callback
) {
397 trackCallbacks
.remove( fns
[ 0 ] );
398 // Ensure the tuple is removed to avoid holding on to closures
405 // Fire events from before track() triggered fire()
406 trackCallbacks
.fire( mw
.trackQueue
);
409 * Registry and firing of events.
411 * MediaWiki has various interface components that are extended, enhanced
412 * or manipulated in some other way by extensions, gadgets and even
415 * This framework helps streamlining the timing of when these other
416 * code paths fire their plugins (instead of using document-ready,
417 * which can and should be limited to firing only once).
419 * Features like navigating to other wiki pages, previewing an edit
420 * and editing itself – without a refresh – can then retrigger these
421 * hooks accordingly to ensure everything still works as expected.
425 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
426 * mw.hook( 'wikipage.content' ).fire( $content );
428 * Handlers can be added and fired for arbitrary event names at any time. The same
429 * event can be fired multiple times. The last run of an event is memorized
430 * (similar to `$(document).ready` and `$.Deferred().done`).
431 * This means if an event is fired, and a handler added afterwards, the added
432 * function will be fired right away with the last given event data.
434 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
435 * Thus allowing flexible use and optimal maintainability and authority control.
436 * You can pass around the `add` and/or `fire` method to another piece of code
437 * without it having to know the event name (or `mw.hook` for that matter).
439 * var h = mw.hook( 'bar.ready' );
440 * new mw.Foo( .. ).fetch( { callback: h.fire } );
442 * Note: Events are documented with an underscore instead of a dot in the event
443 * name due to jsduck not supporting dots in that position.
447 mw
.hook
= ( function () {
448 var lists
= Object
.create( null );
451 * Create an instance of mw.hook.
455 * @param {string} name Name of hook.
458 return function ( name
) {
459 var list
= lists
[ name
] || ( lists
[ name
] = $.Callbacks( 'memory' ) );
463 * Register a hook handler
465 * @param {...Function} handler Function to bind.
471 * Unregister a hook handler
473 * @param {...Function} handler Function to unbind.
481 * @param {...Mixed} data
486 return list
.fireWith
.call( this, null, slice
.call( arguments
) );
493 * HTML construction helper functions
500 * output = Html.element( 'div', {}, new Html.Raw(
501 * Html.element( 'img', { src: '<' } )
503 * mw.log( output ); // <div><img src="<"/></div>
508 mw
.html
= ( function () {
509 function escapeCallback( s
) {
526 * Escape a string for HTML.
528 * Converts special characters to HTML entities.
530 * mw.html.escape( '< > \' & "' );
531 * // Returns < > ' & "
533 * @param {string} s The string to escape
534 * @return {string} HTML
536 escape: function ( s
) {
537 return s
.replace( /['"<>&]/g, escapeCallback
);
541 * Create an HTML element string, with safe escaping.
543 * @param {string} name The tag name.
544 * @param {Object} [attrs] An object with members mapping element names to values
545 * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
547 * - string: Text to be escaped.
548 * - null: The element is treated as void with short closing form, e.g. `<br/>`.
549 * - this.Raw: The raw value is directly included.
550 * - this.Cdata: The raw value is directly included. An exception is
551 * thrown if it contains any illegal ETAGO delimiter.
552 * See <https://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
553 * @return {string} HTML
555 element: function ( name
, attrs
, contents
) {
556 var v
, attrName
, s
= '<' + name
;
559 for ( attrName
in attrs
) {
560 v
= attrs
[ attrName
];
561 // Convert name=true, to name=name
565 } else if ( v
=== false ) {
568 s
+= ' ' + attrName
+ '="' + this.escape( String( v
) ) + '"';
571 if ( contents
=== undefined || contents
=== null ) {
578 switch ( typeof contents
) {
581 s
+= this.escape( contents
);
586 s
+= String( contents
);
589 if ( contents
instanceof this.Raw
) {
590 // Raw HTML inclusion
592 } else if ( contents
instanceof this.Cdata
) {
594 if ( /<\/[a-zA-z]/.test( contents
.value
) ) {
595 throw new Error( 'Illegal end tag found in CDATA' );
599 throw new Error( 'Invalid type of contents' );
602 s
+= '</' + name
+ '>';
607 * Wrapper object for raw HTML passed to mw.html.element().
611 * @param {string} value
613 Raw: function ( value
) {
618 * Wrapper object for CDATA element contents passed to mw.html.element()
620 * @class mw.html.Cdata
622 * @param {string} value
624 Cdata: function ( value
) {
631 * Execute a function as soon as one or more required modules are ready.
633 * Example of inline dependency on OOjs:
635 * mw.loader.using( 'oojs', function () {
636 * OO.compare( [ 1 ], [ 1 ] );
639 * Example of inline dependency obtained via `require()`:
641 * mw.loader.using( [ 'mediawiki.util' ], function ( require ) {
642 * var util = require( 'mediawiki.util' );
645 * Since MediaWiki 1.23 this also returns a promise.
647 * Since MediaWiki 1.28 the promise is resolved with a `require` function.
650 * @param {string|Array} dependencies Module name or array of modules names the
651 * callback depends on to be ready before executing
652 * @param {Function} [ready] Callback to execute when all dependencies are ready
653 * @param {Function} [error] Callback to execute if one or more dependencies failed
654 * @return {jQuery.Promise} With a `require` function
656 mw
.loader
.using = function ( dependencies
, ready
, error
) {
657 var deferred
= $.Deferred();
659 // Allow calling with a single dependency as a string
660 if ( !Array
.isArray( dependencies
) ) {
661 dependencies
= [ dependencies
];
665 deferred
.done( ready
);
668 deferred
.fail( error
);
672 // Resolve entire dependency map
673 dependencies
= mw
.loader
.resolve( dependencies
);
675 return deferred
.reject( e
).promise();
680 function () { deferred
.resolve( mw
.loader
.require
); },
684 return deferred
.promise();
688 * Load a script by URL.
692 * mw.loader.getScript(
693 * 'https://example.org/x-1.0.0.js'
695 * .then( function () {
696 * // Script succeeded. You can use X now.
697 * }, function ( e ) {
698 * // Script failed. X is not avaiable
699 * mw.log.error( e.message ); // => "Failed to load script"
704 * @param {string} url Script URL
705 * @return {jQuery.Promise} Resolved when the script is loaded
707 mw
.loader
.getScript = function ( url
) {
708 return $.ajax( url
, { dataType
: 'script', cache
: true } )
709 .catch( function () {
710 throw new Error( 'Failed to load script' );
714 // Skeleton user object, extended by the 'mediawiki.user' module.
723 options
: new mw
.Map(),
730 // Alias $j to jQuery for backwards compatibility
731 // @deprecated since 1.23 Use $ or jQuery instead
732 mw
.log
.deprecate( window
, '$j', $, 'Use $ or jQuery instead.' );
734 // Process callbacks for Grade A that require modules.
736 // Replace temporary RLQ implementation from startup.js with the
737 // final implementation that also processes callbacks that can
738 // require modules. It must also support late arrivals of
739 // plain callbacks. (T208093)
741 push: function ( entry
) {
742 if ( typeof entry
=== 'function' ) {
745 mw
.loader
.using( entry
[ 0 ], entry
[ 1 ] );
749 while ( queue
[ 0 ] ) {
750 window
.RLQ
.push( queue
.shift() );