2 * Base library for MediaWiki.
4 * Exposed globally as `mediaWiki` with `mw` as shortcut.
7 * @alternateClassName mediaWiki
10 /*jshint latedef:false */
16 hasOwn = Object.prototype.hasOwnProperty,
17 slice = Array.prototype.slice,
18 trackCallbacks = $.Callbacks( 'memory' ),
23 * Create an object that can be read from or written to from methods that allow
24 * interaction both with single and multiple properties at once.
28 * var collection, query, results;
30 * // Create your address book
31 * collection = new mw.Map();
33 * // This data could be coming from an external source (eg. API/AJAX)
35 * 'John Doe': 'john@example.org',
36 * 'Jane Doe': 'jane@example.org',
37 * 'George van Halen': 'gvanhalen@example.org'
40 * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
42 * // You can detect missing keys first
43 * if ( !collection.exists( wanted ) ) {
44 * // One or more are missing (in this case: "Daniel Jackson")
45 * mw.log( 'One or more names were not found in your address book' );
48 * // Or just let it give you what it can. Optionally fill in from a default.
49 * results = collection.get( wanted, 'nobody@example.com' );
50 * mw.log( results['Jane Doe'] ); // "jane@example.org"
51 * mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
56 * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
58 * For backwards-compatibility with mw.config, this can also be `true` in which case values
59 * are copied to the Window object as global variables (T72470). Values are copied in
60 * one direction only. Changes to globals are not reflected in the map.
62 function Map( values ) {
63 if ( values === true ) {
66 // Override #set to also set the global variable
67 this.set = function ( selection, value ) {
70 if ( $.isPlainObject( selection ) ) {
71 for ( s in selection ) {
72 setGlobalMapValue( this, s, selection[ s ] );
76 if ( typeof selection === 'string' && arguments.length ) {
77 setGlobalMapValue( this, selection, value );
86 this.values = values || {};
90 * Alias property to the global object.
96 * @param {Mixed} value
98 function setGlobalMapValue( map, key, value ) {
99 map.values[ key ] = value;
104 // Deprecation notice for mw.config globals (T58550, T72470)
105 map === mw.config && 'Use mw.config instead.'
111 * Get the value of one or more keys.
113 * If called with no arguments, all values are returned.
115 * @param {string|Array} [selection] Key or array of keys to retrieve values for.
116 * @param {Mixed} [fallback=null] Value for keys that don't exist.
117 * @return {Mixed|Object| null} If selection was a string, returns the value,
118 * If selection was an array, returns an object of key/values.
119 * If no selection is passed, the 'values' container is returned. (Beware that,
120 * as is the default in JavaScript, the object is returned by reference.)
122 get: function ( selection, fallback ) {
124 // If we only do this in the `return` block, it'll fail for the
125 // call to get() from the mutli-selection block.
126 fallback = arguments.length > 1 ? fallback : null;
128 if ( $.isArray( selection ) ) {
129 selection = slice.call( selection );
131 for ( i = 0; i < selection.length; i++ ) {
132 results[ selection[ i ] ] = this.get( selection[ i ], fallback );
137 if ( typeof selection === 'string' ) {
138 if ( !hasOwn.call( this.values, selection ) ) {
141 return this.values[ selection ];
144 if ( selection === undefined ) {
148 // Invalid selection key
153 * Set one or more key/value pairs.
155 * @param {string|Object} selection Key to set value for, or object mapping keys to values
156 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
157 * @return {boolean} True on success, false on failure
159 set: function ( selection, value ) {
162 if ( $.isPlainObject( selection ) ) {
163 for ( s in selection ) {
164 this.values[ s ] = selection[ s ];
168 if ( typeof selection === 'string' && arguments.length > 1 ) {
169 this.values[ selection ] = value;
176 * Check if one or more keys exist.
178 * @param {Mixed} selection Key or array of keys to check
179 * @return {boolean} True if the key(s) exist
181 exists: function ( selection ) {
184 if ( $.isArray( selection ) ) {
185 for ( s = 0; s < selection.length; s++ ) {
186 if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.values, selection[ s ] ) ) {
192 return typeof selection === 'string' && hasOwn.call( this.values, selection );
197 * Object constructor for messages.
199 * Similar to the Message class in MediaWiki PHP.
201 * Format defaults to 'text'.
207 * 'hello': 'Hello world',
208 * 'hello-user': 'Hello, $1!',
209 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
212 * obj = new mw.Message( mw.messages, 'hello' );
213 * mw.log( obj.text() );
216 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
217 * mw.log( obj.text() );
218 * // Hello, John Doe!
220 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
221 * mw.log( obj.text() );
222 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
224 * // Using mw.message shortcut
225 * obj = mw.message( 'hello-user', 'John Doe' );
226 * mw.log( obj.text() );
227 * // Hello, John Doe!
229 * // Using mw.msg shortcut
230 * str = mw.msg( 'hello-user', 'John Doe' );
232 * // Hello, John Doe!
234 * // Different formats
235 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
237 * obj.format = 'text';
238 * str = obj.toString();
243 * // Hello, John "Wiki" <3 Doe!
245 * mw.log( obj.escaped() );
246 * // Hello, John "Wiki" <3 Doe!
251 * @param {mw.Map} map Message store
252 * @param {string} key
253 * @param {Array} [parameters]
255 function Message( map, key, parameters ) {
256 this.format = 'text';
259 this.parameters = parameters === undefined ? [] : slice.call( parameters );
263 Message.prototype = {
265 * Get parsed contents of the message.
267 * The default parser does simple $N replacements and nothing else.
268 * This may be overridden to provide a more complex message parser.
269 * The primary override is in the mediawiki.jqueryMsg module.
271 * This function will not be called for nonexistent messages.
273 * @return {string} Parsed message
275 parser: function () {
276 return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
280 * Add (does not replace) parameters for `N$` placeholder values.
282 * @param {Array} parameters
285 params: function ( parameters ) {
287 for ( i = 0; i < parameters.length; i++ ) {
288 this.parameters.push( parameters[ i ] );
294 * Convert message object to its string form based on current format.
296 * @return {string} Message as a string in the current form, or `<key>` if key
299 toString: function () {
302 if ( !this.exists() ) {
303 // Use <key> as text if key does not exist
304 if ( this.format === 'escaped' || this.format === 'parse' ) {
305 // format 'escaped' and 'parse' need to have the brackets and key html escaped
306 return mw.html.escape( '<' + this.key + '>' );
308 return '<' + this.key + '>';
311 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
312 text = this.parser();
315 if ( this.format === 'escaped' ) {
316 text = this.parser();
317 text = mw.html.escape( text );
324 * Change format to 'parse' and convert message to string
326 * If jqueryMsg is loaded, this parses the message text from wikitext
327 * (where supported) to HTML
329 * Otherwise, it is equivalent to plain.
331 * @return {string} String form of parsed message
334 this.format = 'parse';
335 return this.toString();
339 * Change format to 'plain' and convert message to string
341 * This substitutes parameters, but otherwise does not change the
344 * @return {string} String form of plain message
347 this.format = 'plain';
348 return this.toString();
352 * Change format to 'text' and convert message to string
354 * If jqueryMsg is loaded, {{-transformation is done where supported
355 * (such as {{plural:}}, {{gender:}}, {{int:}}).
357 * Otherwise, it is equivalent to plain
359 * @return {string} String form of text message
362 this.format = 'text';
363 return this.toString();
367 * Change the format to 'escaped' and convert message to string
369 * This is equivalent to using the 'text' format (see #text), then
370 * HTML-escaping the output.
372 * @return {string} String form of html escaped message
374 escaped: function () {
375 this.format = 'escaped';
376 return this.toString();
380 * Check if a message exists
385 exists: function () {
386 return this.map.exists( this.key );
396 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
398 * On browsers that implement the Navigation Timing API, this function will produce floating-point
399 * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
400 * it will fall back to using `Date`.
402 * @return {number} Current time
405 var perf = window.performance,
406 navStart = perf && perf.timing && perf.timing.navigationStart;
407 return navStart && typeof perf.now === 'function' ?
408 function () { return navStart + perf.now(); } :
409 function () { return +new Date(); };
413 * Format a string. Replace $1, $2 ... $N with positional arguments.
415 * Used by Message#parser().
418 * @param {string} formatString Format string
419 * @param {...Mixed} parameters Values for $N replacements
420 * @return {string} Formatted string
422 format: function ( formatString ) {
423 var parameters = slice.call( arguments, 1 );
424 return formatString.replace( /\$(\d+)/g, function ( str, match ) {
425 var index = parseInt( match, 10 ) - 1;
426 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
431 * Track an analytic event.
433 * This method provides a generic means for MediaWiki JavaScript code to capture state
434 * information for analysis. Each logged event specifies a string topic name that describes
435 * the kind of event that it is. Topic names consist of dot-separated path components,
436 * arranged from most general to most specific. Each path component should have a clear and
437 * well-defined purpose.
439 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
440 * events that match their subcription, including those that fired before the handler was
443 * @param {string} topic Topic name
444 * @param {Object} [data] Data describing the event, encoded as an object
446 track: function ( topic, data ) {
447 trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
448 trackCallbacks.fire( trackQueue );
452 * Register a handler for subset of analytic events, specified by topic.
454 * Handlers will be called once for each tracked event, including any events that fired before the
455 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
456 * the exact time at which the event fired, a string 'topic' property naming the event, and a
457 * 'data' property which is an object of event-specific data. The event topic and event data are
458 * also passed to the callback as the first and second arguments, respectively.
460 * @param {string} topic Handle events whose name starts with this string prefix
461 * @param {Function} callback Handler to call for each matching tracked event
462 * @param {string} callback.topic
463 * @param {Object} [callback.data]
465 trackSubscribe: function ( topic, callback ) {
467 function handler( trackQueue ) {
469 for ( ; seen < trackQueue.length; seen++ ) {
470 event = trackQueue[ seen ];
471 if ( event.topic.indexOf( topic ) === 0 ) {
472 callback.call( event, event.topic, event.data );
477 trackHandlers.push( [ handler, callback ] );
479 trackCallbacks.add( handler );
483 * Stop handling events for a particular handler
485 * @param {Function} callback
487 trackUnsubscribe: function ( callback ) {
488 trackHandlers = $.grep( trackHandlers, function ( fns ) {
489 if ( fns[ 1 ] === callback ) {
490 trackCallbacks.remove( fns[ 0 ] );
491 // Ensure the tuple is removed to avoid holding on to closures
498 // Expose Map constructor
501 // Expose Message constructor
505 * Map of configuration values.
507 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
510 * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
511 * global `window` object.
513 * @property {mw.Map} config
515 // Dummy placeholder later assigned in ResourceLoaderStartUpModule
519 * Empty object for third-party libraries, for cases where you don't
520 * want to add a new global, or the global is bad and needs containment
528 * Access container for deprecated functionality that can be moved from
529 * from their legacy location and attached to this object (e.g. a global
530 * function that is deprecated and as stop-gap can be exposed through here).
532 * This was reserved for future use but never ended up being used.
534 * @deprecated since 1.22 Let deprecated identifiers keep their original name
535 * and use mw.log#deprecate to create an access container for tracking.
541 * Store for messages.
548 * Store for templates associated with a module.
552 templates: new Map(),
555 * Get a message object.
557 * Shorcut for `new mw.Message( mw.messages, key, parameters )`.
560 * @param {string} key Key of message to get
561 * @param {...Mixed} parameters Values for $N replacements
562 * @return {mw.Message}
564 message: function ( key ) {
565 var parameters = slice.call( arguments, 1 );
566 return new Message( mw.messages, key, parameters );
570 * Get a message string using the (default) 'text' format.
572 * Shortcut for `mw.message( key, parameters... ).text()`.
575 * @param {string} key Key of message to get
576 * @param {...Mixed} parameters Values for $N replacements
580 return mw.message.apply( mw.message, arguments ).toString();
584 * Dummy placeholder for {@link mw.log}
589 // Also update the restoration of methods in mediawiki.log.js
590 // when adding or removing methods here.
591 var log = function () {},
592 console = window.console;
600 * Write a message the console's warning channel.
601 * Actions not supported by the browser console are silently ignored.
603 * @param {...string} msg Messages to output to console
605 log.warn = console && console.warn && Function.prototype.bind ?
606 Function.prototype.bind.call( console.warn, console ) :
610 * Write a message the console's error channel.
612 * Most browsers provide a stacktrace by default if the argument
613 * is a caught Error object.
616 * @param {Error|...string} msg Messages to output to console
618 log.error = console && console.error && Function.prototype.bind ?
619 Function.prototype.bind.call( console.error, console ) :
623 * Create a property in a host object that, when accessed, will produce
624 * a deprecation warning in the console with backtrace.
626 * @param {Object} obj Host object of deprecated property
627 * @param {string} key Name of property to create in `obj`
628 * @param {Mixed} val The value this property should return when accessed
629 * @param {string} [msg] Optional text to include in the deprecation message
631 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
633 } : function ( obj, key, val, msg ) {
634 msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
636 // Can throw on Object.defineProperty.
638 Object.defineProperty( obj, key, {
642 mw.track( 'mw.deprecate', key );
646 set: function ( newVal ) {
647 mw.track( 'mw.deprecate', key );
653 // Fallback to creating a copy of the value to the object.
662 * Client for ResourceLoader server end point.
664 * This client is in charge of maintaining the module registry and state
665 * machine, initiating network (batch) requests for loading modules, as
666 * well as dependency resolution and execution of source code.
668 * For more information, refer to
669 * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
674 loader: ( function () {
677 * Fired via mw.track on various resource loading errors.
679 * @event resourceloader_exception
680 * @param {Error|Mixed} e The error that was thrown. Almost always an Error
681 * object, but in theory module code could manually throw something else, and that
682 * might also end up here.
683 * @param {string} [module] Name of the module which caused the error. Omitted if the
684 * error is not module-related or the module cannot be easily identified due to
686 * @param {string} source Source of the error. Possible values:
688 * - style: stylesheet error (only affects old IE where a special style loading method
690 * - load-callback: exception thrown by user callback
691 * - module-execute: exception thrown by module code
692 * - store-eval: could not evaluate module code cached in localStorage
693 * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
694 * - store-localstorage-json: JSON conversion error in mw.loader.store.set
695 * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
699 * Fired via mw.track on resource loading error conditions.
701 * @event resourceloader_assert
702 * @param {string} source Source of the error. Possible values:
704 * - bug-T59567: failed to cache script due to an Opera function -> string conversion
705 * bug; see <https://phabricator.wikimedia.org/T59567> for details
709 * Mapping of registered modules.
711 * See #implement and #execute for exact details on support for script, style and messages.
717 * // From mw.loader.register()
718 * 'version': '########' (hash)
719 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
720 * 'group': 'somegroup', (or) null
721 * 'source': 'local', (or) 'anotherwiki'
722 * 'skip': 'return !!window.Example', (or) null
724 * // Set from execute() or mw.loader.state()
725 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
727 * // Optionally added at run-time by mw.loader.implement()
729 * 'script': closure, array of urls, or string
730 * 'style': { ... } (see #execute)
731 * 'messages': { 'key': 'value', ... }
738 * The module is known to the system but not yet requested.
739 * Meta data is registered via mw.loader#register. Calls to that method are
740 * generated server-side by the startup module.
742 * The module is requested through mw.loader (either directly or as dependency of
743 * another module). The client will be fetching module contents from the server.
744 * The contents are then stashed in the registry via mw.loader#implement.
746 * The module has been requested from the server and stashed via mw.loader#implement.
747 * If the module has no more dependencies in-fight, the module will be executed
748 * right away. Otherwise execution is deferred, controlled via #handlePending.
750 * The module is being executed.
752 * The module has been successfully executed.
754 * The module (or one of its dependencies) produced an error during execution.
756 * The module was registered client-side and requested, but the server denied knowledge
757 * of the module's existence.
763 // Mapping of sources, keyed by source-id, values are strings.
768 // 'sourceId': 'http://example.org/w/load.php'
773 // List of modules which will be loaded as when ready
776 // List of modules to be loaded
780 * List of callback jobs waiting for modules to be ready.
782 * Jobs are created by #request() and run by #handlePending().
784 * Typically when a job is created for a module, the job's dependencies contain
785 * both the module being requested and all its recursive dependencies.
790 * 'dependencies': [ module names ],
791 * 'ready': Function callback
792 * 'error': Function callback
795 * @property {Object[]} jobs
800 // Selector cache for the marker element. Use getMarker() to get/use the marker!
803 // For #addEmbeddedCSS
805 cssBufferTimer = null,
806 cssCallbacks = $.Callbacks();
808 function getMarker() {
811 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
812 if ( !$marker.length ) {
813 mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
814 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
821 * Create a new style element and add it to the DOM.
824 * @param {string} text CSS text
825 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag
826 * should be inserted before
827 * @return {HTMLElement} Reference to the created style element
829 function newStyleTag( text, nextnode ) {
830 var s = document.createElement( 'style' );
832 // Must attach to document before setting cssText (bug 33305)
834 $( nextnode ).before( s );
836 document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
838 if ( s.styleSheet ) {
840 // Old IE ignores appended text nodes, access stylesheet directly.
841 s.styleSheet.cssText = text;
843 // Standard behaviour
844 s.appendChild( document.createTextNode( text ) );
850 * Add a bit of CSS text to the current browser page.
852 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
853 * or create a new one based on whether the given `cssText` is safe for extension.
855 * @param {string} [cssText=cssBuffer] If called without cssText,
856 * the internal buffer will be inserted instead.
857 * @param {Function} [callback]
859 function addEmbeddedCSS( cssText, callback ) {
860 var $style, styleEl, newCssText;
862 function fireCallbacks() {
863 var oldCallbacks = cssCallbacks;
864 // Reset cssCallbacks variable so it's not polluted by any calls to
865 // addEmbeddedCSS() from one of the callbacks (T105973)
866 cssCallbacks = $.Callbacks();
867 oldCallbacks.fire().empty();
871 cssCallbacks.add( callback );
874 // Yield once before creating the <style> tag. This lets multiple stylesheets
875 // accumulate into one buffer, allowing us to reduce how often new stylesheets
876 // are inserted in the browser. Appending a stylesheet and waiting for the
877 // browser to repaint is fairly expensive. (T47810)
879 // Don't extend the buffer if the item needs its own stylesheet.
880 // Keywords like `@import` are only valid at the start of a stylesheet (T37562).
881 if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
882 // Linebreak for somewhat distinguishable sections
883 cssBuffer += '\n' + cssText;
884 // TODO: Using requestAnimationFrame would perform better by not injecting
885 // styles while the browser is busy painting.
886 if ( !cssBufferTimer ) {
887 cssBufferTimer = setTimeout( function () {
888 // Support: Firefox < 13
889 // Firefox 12 has non-standard behaviour of passing a number
890 // as first argument to a setTimeout callback.
891 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
898 // This is a scheduled flush for the buffer
900 cssBufferTimer = null;
905 // By default, always create a new <style>. Appending text to a <style>
906 // tag is bad as it means the contents have to be re-parsed (bug 45810).
908 // Except, of course, in IE 9 and below. In there we default to re-using and
909 // appending to a <style> tag due to the IE stylesheet limit (bug 31676).
910 if ( 'documentMode' in document && document.documentMode <= 9 ) {
912 $style = getMarker().prev();
913 // Verify that the element before the marker actually is a
914 // <style> tag and one that came from ResourceLoader
915 // (not some other style tag or even a `<meta>` or `<script>`).
916 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) ) {
917 // There's already a dynamic <style> tag present and
918 // we are able to append more to it.
919 styleEl = $style.get( 0 );
921 if ( styleEl.styleSheet ) {
924 // We can't do styleSheet.cssText += cssText, since IE9 mangles this property on
925 // write, dropping @media queries from the CSS text. If we read it and used its
926 // value, we would accidentally apply @media-specific styles to all media. (T108727)
927 if ( document.documentMode === 9 ) {
928 newCssText = $style.data( 'ResourceLoaderDynamicStyleTag' ) + cssText;
929 styleEl.styleSheet.cssText = newCssText;
930 $style.data( 'ResourceLoaderDynamicStyleTag', newCssText );
932 styleEl.styleSheet.cssText += cssText;
935 mw.track( 'resourceloader.exception', { exception: e, source: 'stylesheet' } );
938 styleEl.appendChild( document.createTextNode( cssText ) );
945 $style = $( newStyleTag( cssText, getMarker() ) );
947 if ( document.documentMode === 9 ) {
949 // Preserve original CSS text because IE9 mangles it on write
950 $style.data( 'ResourceLoaderDynamicStyleTag', cssText );
952 $style.data( 'ResourceLoaderDynamicStyleTag', true );
960 * @param {Array} modules List of module names
961 * @return {string} Hash of concatenated version hashes.
963 function getCombinedVersion( modules ) {
964 var hashes = $.map( modules, function ( module ) {
965 return registry[ module ].version;
967 // Trim for consistency with server-side ResourceLoader::makeHash. It also helps
968 // save precious space in the limited query string. Otherwise modules are more
969 // likely to require multiple HTTP requests.
970 return sha1( hashes.join( '' ) ).slice( 0, 12 );
974 * Determine whether all dependencies are in state 'ready', which means we may
975 * execute the module or job now.
978 * @param {Array} modules Names of modules to be checked
979 * @return {boolean} True if all modules are in state 'ready', false otherwise
981 function allReady( modules ) {
983 for ( i = 0; i < modules.length; i++ ) {
984 if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
992 * Determine whether all dependencies are in state 'ready', which means we may
993 * execute the module or job now.
996 * @param {Array} modules Names of modules to be checked
997 * @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise
999 function anyFailed( modules ) {
1001 for ( i = 0; i < modules.length; i++ ) {
1002 state = mw.loader.getState( modules[ i ] );
1003 if ( state === 'error' || state === 'missing' ) {
1011 * A module has entered state 'ready', 'error', or 'missing'. Automatically update
1012 * pending jobs and modules that depend upon this module. If the given module failed,
1013 * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute
1014 * all jobs/modules now having their dependencies satisfied.
1016 * Jobs that depend on a failed module, will have their error callback ran (if any).
1019 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
1021 function handlePending( module ) {
1022 var j, job, hasErrors, m, stateChange;
1024 if ( registry[ module ].state === 'error' || registry[ module ].state === 'missing' ) {
1025 // If the current module failed, mark all dependent modules also as failed.
1026 // Iterate until steady-state to propagate the error state upwards in the
1029 stateChange = false;
1030 for ( m in registry ) {
1031 if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) {
1032 if ( anyFailed( registry[ m ].dependencies ) ) {
1033 registry[ m ].state = 'error';
1038 } while ( stateChange );
1041 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1042 for ( j = 0; j < jobs.length; j++ ) {
1043 hasErrors = anyFailed( jobs[ j ].dependencies );
1044 if ( hasErrors || allReady( jobs[ j ].dependencies ) ) {
1045 // All dependencies satisfied, or some have errors
1047 jobs.splice( j, 1 );
1051 if ( $.isFunction( job.error ) ) {
1052 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
1055 if ( $.isFunction( job.ready ) ) {
1060 // A user-defined callback raised an exception.
1061 // Swallow it to protect our state machine!
1062 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
1067 if ( registry[ module ].state === 'ready' ) {
1068 // The current module became 'ready'. Set it in the module store, and recursively execute all
1069 // dependent modules that are loaded and now have all dependencies satisfied.
1070 mw.loader.store.set( module, registry[ module ] );
1071 for ( m in registry ) {
1072 if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) {
1080 * Resolve dependencies and detect circular references.
1083 * @param {string} module Name of the top-level module whose dependencies shall be
1084 * resolved and sorted.
1085 * @param {Array} resolved Returns a topological sort of the given module and its
1086 * dependencies, such that later modules depend on earlier modules. The array
1087 * contains the module names. If the array contains already some module names,
1088 * this function appends its result to the pre-existing array.
1089 * @param {Object} [unresolved] Hash used to track the current dependency
1090 * chain; used to report loops in the dependency graph.
1091 * @throws {Error} If any unregistered module or a dependency loop is encountered
1093 function sortDependencies( module, resolved, unresolved ) {
1096 if ( !hasOwn.call( registry, module ) ) {
1097 throw new Error( 'Unknown dependency: ' + module );
1100 if ( registry[ module ].skip !== null ) {
1101 /*jshint evil:true */
1102 skip = new Function( registry[ module ].skip );
1103 registry[ module ].skip = null;
1105 registry[ module ].skipped = true;
1106 registry[ module ].dependencies = [];
1107 registry[ module ].state = 'ready';
1108 handlePending( module );
1113 // Resolves dynamic loader function and replaces it with its own results
1114 if ( $.isFunction( registry[ module ].dependencies ) ) {
1115 registry[ module ].dependencies = registry[ module ].dependencies();
1116 // Ensures the module's dependencies are always in an array
1117 if ( typeof registry[ module ].dependencies !== 'object' ) {
1118 registry[ module ].dependencies = [ registry[ module ].dependencies ];
1121 if ( $.inArray( module, resolved ) !== -1 ) {
1122 // Module already resolved; nothing to do
1125 // Create unresolved if not passed in
1126 if ( !unresolved ) {
1129 // Tracks down dependencies
1130 deps = registry[ module ].dependencies;
1131 for ( i = 0; i < deps.length; i++ ) {
1132 if ( $.inArray( deps[ i ], resolved ) === -1 ) {
1133 if ( unresolved[ deps[ i ] ] ) {
1134 throw new Error( mw.format(
1135 'Circular reference detected: $1 -> $2',
1141 // Add to unresolved
1142 unresolved[ module ] = true;
1143 sortDependencies( deps[ i ], resolved, unresolved );
1146 resolved.push( module );
1150 * Get names of module that a module depends on, in their proper dependency order.
1153 * @param {string[]} modules Array of string module names
1154 * @return {Array} List of dependencies, including 'module'.
1156 function resolve( modules ) {
1158 $.each( modules, function ( idx, module ) {
1159 sortDependencies( module, resolved );
1165 * Load and execute a script.
1168 * @param {string} src URL to script, will be used as the src attribute in the script tag
1169 * @return {jQuery.Promise}
1171 function addScript( src ) {
1175 // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1176 // XHR for a same domain request instead of <script>, which changes the request
1177 // headers (potentially missing a cache hit), and reduces caching in general
1178 // since browsers cache XHR much less (if at all). And XHR means we retreive
1179 // text, so we'd need to $.globalEval, which then messes up line numbers.
1186 * Utility function for execute()
1190 function addLink( media, url ) {
1191 var el = document.createElement( 'link' );
1193 // Insert in document *before* setting href
1194 getMarker().before( el );
1195 el.rel = 'stylesheet';
1196 if ( media && media !== 'all' ) {
1199 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1200 // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
1205 * Executes a loaded module, making it ready to use
1208 * @param {string} module Module name to execute
1210 function execute( module ) {
1211 var key, value, media, i, urls, cssHandle, checkCssHandles, runScript,
1212 cssHandlesRegistered = false;
1214 if ( !hasOwn.call( registry, module ) ) {
1215 throw new Error( 'Module has not been registered yet: ' + module );
1217 if ( registry[ module ].state !== 'loaded' ) {
1218 throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module );
1221 registry[ module ].state = 'executing';
1223 runScript = function () {
1224 var script, markModuleReady, nestedAddScript, legacyWait,
1225 // Expand to include dependencies since we have to exclude both legacy modules
1226 // and their dependencies from the legacyWait (to prevent a circular dependency).
1227 legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) );
1229 script = registry[ module ].script;
1230 markModuleReady = function () {
1231 registry[ module ].state = 'ready';
1232 handlePending( module );
1234 nestedAddScript = function ( arr, callback, i ) {
1235 // Recursively call addScript() in its own callback
1236 // for each element of arr.
1237 if ( i >= arr.length ) {
1238 // We're at the end of the array
1243 addScript( arr[ i ] ).always( function () {
1244 nestedAddScript( arr, callback, i + 1 );
1248 legacyWait = ( $.inArray( module, legacyModules ) !== -1 )
1249 ? $.Deferred().resolve()
1250 : mw.loader.using( legacyModules );
1252 legacyWait.always( function () {
1253 if ( $.isArray( script ) ) {
1254 nestedAddScript( script, markModuleReady, 0 );
1255 } else if ( $.isFunction( script ) ) {
1256 // Pass jQuery twice so that the signature of the closure which wraps
1257 // the script can bind both '$' and 'jQuery'.
1260 } else if ( typeof script === 'string' ) {
1261 // Site and user modules are a legacy scripts that run in the global scope.
1262 // This is transported as a string instead of a function to avoid needing
1263 // to use string manipulation to undo the function wrapper.
1264 if ( module === 'user' ) {
1265 // Implicit dependency on the site module. Not real dependency because
1266 // it should run after 'site' regardless of whether it succeeds or fails.
1267 mw.loader.using( 'site' ).always( function () {
1268 $.globalEval( script );
1272 $.globalEval( script );
1276 // Module without script
1281 // This needs to NOT use mw.log because these errors are common in production mode
1282 // and not in debug mode, such as when a symbol that should be global isn't exported
1283 registry[ module ].state = 'error';
1284 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
1285 handlePending( module );
1289 // Add localizations to message system
1290 if ( registry[ module ].messages ) {
1291 mw.messages.set( registry[ module ].messages );
1294 // Initialise templates
1295 if ( registry[ module ].templates ) {
1296 mw.templates.set( module, registry[ module ].templates );
1299 // Make sure we don't run the scripts until all stylesheet insertions have completed.
1302 checkCssHandles = function () {
1303 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1304 // one of the cssHandles is fired while we're still creating more handles.
1305 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1307 runScript = undefined; // Revoke
1310 cssHandle = function () {
1311 var check = checkCssHandles;
1313 return function () {
1317 check = undefined; // Revoke
1323 // Process styles (see also mw.loader.implement)
1324 // * back-compat: { <media>: css }
1325 // * back-compat: { <media>: [url, ..] }
1326 // * { "css": [css, ..] }
1327 // * { "url": { <media>: [url, ..] } }
1328 if ( registry[ module ].style ) {
1329 for ( key in registry[ module ].style ) {
1330 value = registry[ module ].style[ key ];
1333 if ( key !== 'url' && key !== 'css' ) {
1334 // Backwards compatibility, key is a media-type
1335 if ( typeof value === 'string' ) {
1336 // back-compat: { <media>: css }
1337 // Ignore 'media' because it isn't supported (nor was it used).
1338 // Strings are pre-wrapped in "@media". The media-type was just ""
1339 // (because it had to be set to something).
1340 // This is one of the reasons why this format is no longer used.
1341 addEmbeddedCSS( value, cssHandle() );
1343 // back-compat: { <media>: [url, ..] }
1349 // Array of css strings in key 'css',
1350 // or back-compat array of urls from media-type
1351 if ( $.isArray( value ) ) {
1352 for ( i = 0; i < value.length; i++ ) {
1353 if ( key === 'bc-url' ) {
1354 // back-compat: { <media>: [url, ..] }
1355 addLink( media, value[ i ] );
1356 } else if ( key === 'css' ) {
1357 // { "css": [css, ..] }
1358 addEmbeddedCSS( value[ i ], cssHandle() );
1361 // Not an array, but a regular object
1362 // Array of urls inside media-type key
1363 } else if ( typeof value === 'object' ) {
1364 // { "url": { <media>: [url, ..] } }
1365 for ( media in value ) {
1366 urls = value[ media ];
1367 for ( i = 0; i < urls.length; i++ ) {
1368 addLink( media, urls[ i ] );
1376 cssHandlesRegistered = true;
1381 * Adds a dependencies to the queue with optional callbacks to be run
1382 * when the dependencies are ready or fail
1385 * @param {string|string[]} dependencies Module name or array of string module names
1386 * @param {Function} [ready] Callback to execute when all dependencies are ready
1387 * @param {Function} [error] Callback to execute when any dependency fails
1389 function request( dependencies, ready, error ) {
1390 // Allow calling by single module name
1391 if ( typeof dependencies === 'string' ) {
1392 dependencies = [ dependencies ];
1395 // Add ready and error callbacks if they were given
1396 if ( ready !== undefined || error !== undefined ) {
1398 // Narrow down the list to modules that are worth waiting for
1399 dependencies: $.grep( dependencies, function ( module ) {
1400 var state = mw.loader.getState( module );
1401 return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing';
1408 $.each( dependencies, function ( idx, module ) {
1409 var state = mw.loader.getState( module );
1410 // Only queue modules that are still in the initial 'registered' state
1411 // (not ones already loading, ready or error).
1412 if ( state === 'registered' && $.inArray( module, queue ) === -1 ) {
1413 // Private modules must be embedded in the page. Don't bother queuing
1414 // these as the server will deny them anyway (T101806).
1415 if ( registry[ module ].group === 'private' ) {
1416 registry[ module ].state = 'error';
1417 handlePending( module );
1420 queue.push( module );
1427 function sortQuery( o ) {
1433 if ( hasOwn.call( o, key ) ) {
1438 for ( key = 0; key < a.length; key++ ) {
1439 sorted[ a[ key ] ] = o[ a[ key ] ];
1445 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1446 * to a query string of the form foo.bar,baz|bar.baz,quux
1450 function buildModulesString( moduleMap ) {
1454 for ( prefix in moduleMap ) {
1455 p = prefix === '' ? '' : prefix + '.';
1456 arr.push( p + moduleMap[ prefix ].join( ',' ) );
1458 return arr.join( '|' );
1462 * Load modules from load.php
1465 * @param {Object} moduleMap Module map, see #buildModulesString
1466 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1467 * @param {string} sourceLoadScript URL of load.php
1469 function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
1470 var request = $.extend(
1471 { modules: buildModulesString( moduleMap ) },
1474 request = sortQuery( request );
1475 addScript( sourceLoadScript + '?' + $.param( request ) );
1479 * Resolve indexed dependencies.
1481 * ResourceLoader uses an optimization to save space which replaces module names in
1482 * dependency lists with the index of that module within the array of module
1483 * registration data if it exists. The benefit is a significant reduction in the data
1484 * size of the startup module. This function changes those dependency lists back to
1485 * arrays of strings.
1487 * @param {Array} modules Modules array
1489 function resolveIndexedDependencies( modules ) {
1490 $.each( modules, function ( idx, module ) {
1491 if ( module[ 2 ] ) {
1492 module[ 2 ] = $.map( module[ 2 ], function ( dep ) {
1493 return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
1499 /* Public Members */
1502 * The module registry is exposed as an aid for debugging and inspecting page
1503 * state; it is not a public interface for modifying the registry.
1509 moduleRegistry: registry,
1512 * @inheritdoc #newStyleTag
1515 addStyleTag: newStyleTag,
1518 * Batch-request queued dependencies from the server.
1521 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1522 source, concatSource, origBatch, group, i, modules, sourceLoadScript,
1523 currReqBase, currReqBaseLength, moduleMap, l,
1524 lastDotIndex, prefix, suffix, bytesAdded;
1526 // Build a list of request parameters common to all requests.
1528 skin: mw.config.get( 'skin' ),
1529 lang: mw.config.get( 'wgUserLanguage' ),
1530 debug: mw.config.get( 'debug' )
1532 // Split module batch by source and by group.
1534 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
1536 // Appends a list of modules from the queue to the batch
1537 for ( q = 0; q < queue.length; q++ ) {
1538 // Only request modules which are registered
1539 if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
1540 // Prevent duplicate entries
1541 if ( $.inArray( queue[ q ], batch ) === -1 ) {
1542 batch.push( queue[ q ] );
1543 // Mark registered modules as loading
1544 registry[ queue[ q ] ].state = 'loading';
1549 mw.loader.store.init();
1550 if ( mw.loader.store.enabled ) {
1553 batch = $.grep( batch, function ( module ) {
1554 var source = mw.loader.store.get( module );
1556 concatSource.push( source );
1562 $.globalEval( concatSource.join( ';' ) );
1564 // Not good, the cached mw.loader.implement calls failed! This should
1565 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1566 // Depending on how corrupt the string is, it is likely that some
1567 // modules' implement() succeeded while the ones after the error will
1568 // never run and leave their modules in the 'loading' state forever.
1570 // Since this is an error not caused by an individual module but by
1571 // something that infected the implement call itself, don't take any
1572 // risks and clear everything in this cache.
1573 mw.loader.store.clear();
1574 // Re-add the ones still pending back to the batch and let the server
1575 // repopulate these modules to the cache.
1576 // This means that at most one module will be useless (the one that had
1577 // the error) instead of all of them.
1578 mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
1579 origBatch = $.grep( origBatch, function ( module ) {
1580 return registry[ module ].state === 'loading';
1582 batch = batch.concat( origBatch );
1586 // Early exit if there's nothing to load...
1587 if ( !batch.length ) {
1591 // The queue has been processed into the batch, clear up the queue.
1594 // Always order modules alphabetically to help reduce cache
1595 // misses for otherwise identical content.
1598 // Split batch by source and by group.
1599 for ( b = 0; b < batch.length; b++ ) {
1600 bSource = registry[ batch[ b ] ].source;
1601 bGroup = registry[ batch[ b ] ].group;
1602 if ( !hasOwn.call( splits, bSource ) ) {
1603 splits[ bSource ] = {};
1605 if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
1606 splits[ bSource ][ bGroup ] = [];
1608 bSourceGroup = splits[ bSource ][ bGroup ];
1609 bSourceGroup.push( batch[ b ] );
1612 // Clear the batch - this MUST happen before we append any
1613 // script elements to the body or it's possible that a script
1614 // will be locally cached, instantly load, and work the batch
1615 // again, all before we've cleared it causing each request to
1616 // include modules which are already loaded.
1619 for ( source in splits ) {
1621 sourceLoadScript = sources[ source ];
1623 for ( group in splits[ source ] ) {
1625 // Cache access to currently selected list of
1626 // modules for this group from this source.
1627 modules = splits[ source ][ group ];
1629 currReqBase = $.extend( {
1630 version: getCombinedVersion( modules )
1632 // For user modules append a user name to the request.
1633 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1634 currReqBase.user = mw.config.get( 'wgUserName' );
1636 currReqBaseLength = $.param( currReqBase ).length;
1637 // We may need to split up the request to honor the query string length limit,
1638 // so build it piece by piece.
1639 l = currReqBaseLength + 9; // '&modules='.length == 9
1641 moduleMap = {}; // { prefix: [ suffixes ] }
1643 for ( i = 0; i < modules.length; i++ ) {
1644 // Determine how many bytes this module would add to the query string
1645 lastDotIndex = modules[ i ].lastIndexOf( '.' );
1647 // If lastDotIndex is -1, substr() returns an empty string
1648 prefix = modules[ i ].substr( 0, lastDotIndex );
1649 suffix = modules[ i ].slice( lastDotIndex + 1 );
1651 bytesAdded = hasOwn.call( moduleMap, prefix )
1652 ? suffix.length + 3 // '%2C'.length == 3
1653 : modules[ i ].length + 3; // '%7C'.length == 3
1655 // If the request would become too long, create a new one,
1656 // but don't create empty requests
1657 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1658 // This request would become too long, create a new one
1659 // and fire off the old one
1660 doRequest( moduleMap, currReqBase, sourceLoadScript );
1662 l = currReqBaseLength + 9;
1663 mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
1665 if ( !hasOwn.call( moduleMap, prefix ) ) {
1666 moduleMap[ prefix ] = [];
1668 moduleMap[ prefix ].push( suffix );
1671 // If there's anything left in moduleMap, request that too
1672 if ( !$.isEmptyObject( moduleMap ) ) {
1673 doRequest( moduleMap, currReqBase, sourceLoadScript );
1680 * Register a source.
1682 * The #work() method will use this information to split up requests by source.
1684 * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1686 * @param {string|Object} id Source ID, or object mapping ids to load urls
1687 * @param {string} loadUrl Url to a load.php end point
1688 * @throws {Error} If source id is already registered
1690 addSource: function ( id, loadUrl ) {
1692 // Allow multiple additions
1693 if ( typeof id === 'object' ) {
1694 for ( source in id ) {
1695 mw.loader.addSource( source, id[ source ] );
1700 if ( hasOwn.call( sources, id ) ) {
1701 throw new Error( 'source already registered: ' + id );
1704 sources[ id ] = loadUrl;
1708 * Register a module, letting the system know about it and its properties.
1710 * The startup modules contain calls to this method.
1712 * When using multiple module registration by passing an array, dependencies that
1713 * are specified as references to modules within the array will be resolved before
1714 * the modules are registered.
1716 * @param {string|Array} module Module name or array of arrays, each containing
1717 * a list of arguments compatible with this method
1718 * @param {string|number} version Module version hash (falls backs to empty string)
1719 * Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
1720 * @param {string|Array|Function} dependencies One string or array of strings of module
1721 * names on which this module depends, or a function that returns that array.
1722 * @param {string} [group=null] Group which the module is in
1723 * @param {string} [source='local'] Name of the source
1724 * @param {string} [skip=null] Script body of the skip function
1726 register: function ( module, version, dependencies, group, source, skip ) {
1728 // Allow multiple registration
1729 if ( typeof module === 'object' ) {
1730 resolveIndexedDependencies( module );
1731 for ( i = 0; i < module.length; i++ ) {
1732 // module is an array of module names
1733 if ( typeof module[ i ] === 'string' ) {
1734 mw.loader.register( module[ i ] );
1735 // module is an array of arrays
1736 } else if ( typeof module[ i ] === 'object' ) {
1737 mw.loader.register.apply( mw.loader, module[ i ] );
1743 if ( typeof module !== 'string' ) {
1744 throw new Error( 'module must be a string, not a ' + typeof module );
1746 if ( hasOwn.call( registry, module ) ) {
1747 throw new Error( 'module already registered: ' + module );
1749 // List the module as registered
1750 registry[ module ] = {
1751 version: version !== undefined ? String( version ) : '',
1753 group: typeof group === 'string' ? group : null,
1754 source: typeof source === 'string' ? source : 'local',
1755 state: 'registered',
1756 skip: typeof skip === 'string' ? skip : null
1758 if ( typeof dependencies === 'string' ) {
1759 // Allow dependencies to be given as a single module name
1760 registry[ module ].dependencies = [ dependencies ];
1761 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1762 // Allow dependencies to be given as an array of module names
1763 // or a function which returns an array
1764 registry[ module ].dependencies = dependencies;
1769 * Implement a module given the components that make up the module.
1771 * When #load or #using requests one or more modules, the server
1772 * response contain calls to this function.
1774 * All arguments are required.
1776 * @param {string} module Name of module
1777 * @param {Function|Array} script Function with module code or Array of URLs to
1778 * be used as the src attribute of a new `<script>` tag.
1779 * @param {Object} [style] Should follow one of the following patterns:
1781 * { "css": [css, ..] }
1782 * { "url": { <media>: [url, ..] } }
1784 * And for backwards compatibility (needs to be supported forever due to caching):
1787 * { <media>: [url, ..] }
1789 * The reason css strings are not concatenated anymore is bug 31676. We now check
1790 * whether it's safe to extend the stylesheet.
1792 * @param {Object} [messages] List of key/value pairs to be added to mw#messages.
1793 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1795 implement: function ( module, script, style, messages, templates ) {
1797 if ( typeof module !== 'string' ) {
1798 throw new Error( 'module must be of type string, not ' + typeof module );
1800 if ( script && !$.isFunction( script ) && !$.isArray( script ) && typeof script !== 'string' ) {
1801 throw new Error( 'script must be of type function, array, or script; not ' + typeof script );
1803 if ( style && !$.isPlainObject( style ) ) {
1804 throw new Error( 'style must be of type object, not ' + typeof style );
1806 if ( messages && !$.isPlainObject( messages ) ) {
1807 throw new Error( 'messages must be of type object, not a ' + typeof messages );
1809 if ( templates && !$.isPlainObject( templates ) ) {
1810 throw new Error( 'templates must be of type object, not a ' + typeof templates );
1812 // Automatically register module
1813 if ( !hasOwn.call( registry, module ) ) {
1814 mw.loader.register( module );
1816 // Check for duplicate implementation
1817 if ( hasOwn.call( registry, module ) && registry[ module ].script !== undefined ) {
1818 throw new Error( 'module already implemented: ' + module );
1820 // Attach components
1821 registry[ module ].script = script || null;
1822 registry[ module ].style = style || null;
1823 registry[ module ].messages = messages || null;
1824 registry[ module ].templates = templates || null;
1825 // The module may already have been marked as erroneous
1826 if ( $.inArray( registry[ module ].state, [ 'error', 'missing' ] ) === -1 ) {
1827 registry[ module ].state = 'loaded';
1828 if ( allReady( registry[ module ].dependencies ) ) {
1835 * Execute a function as soon as one or more required modules are ready.
1837 * Example of inline dependency on OOjs:
1839 * mw.loader.using( 'oojs', function () {
1840 * OO.compare( [ 1 ], [ 1 ] );
1843 * @param {string|Array} dependencies Module name or array of modules names the callback
1844 * dependends on to be ready before executing
1845 * @param {Function} [ready] Callback to execute when all dependencies are ready
1846 * @param {Function} [error] Callback to execute if one or more dependencies failed
1847 * @return {jQuery.Promise}
1848 * @since 1.23 this returns a promise
1850 using: function ( dependencies, ready, error ) {
1851 var deferred = $.Deferred();
1853 // Allow calling with a single dependency as a string
1854 if ( typeof dependencies === 'string' ) {
1855 dependencies = [ dependencies ];
1856 } else if ( !$.isArray( dependencies ) ) {
1858 throw new Error( 'Dependencies must be a string or an array' );
1862 deferred.done( ready );
1865 deferred.fail( error );
1868 // Resolve entire dependency map
1869 dependencies = resolve( dependencies );
1870 if ( allReady( dependencies ) ) {
1871 // Run ready immediately
1873 } else if ( anyFailed( dependencies ) ) {
1874 // Execute error immediately if any dependencies have errors
1876 new Error( 'One or more dependencies failed to load' ),
1880 // Not all dependencies are ready: queue up a request
1881 request( dependencies, deferred.resolve, deferred.reject );
1884 return deferred.promise();
1888 * Load an external script or one or more modules.
1890 * @param {string|Array} modules Either the name of a module, array of modules,
1891 * or a URL of an external script or style
1892 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1893 * external script or style; acceptable values are "text/css" and
1894 * "text/javascript"; if no type is provided, text/javascript is assumed.
1896 load: function ( modules, type ) {
1900 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1901 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1903 // Allow calling with a url or single dependency as a string
1904 if ( typeof modules === 'string' ) {
1905 // "https://example.org/x.js", "http://example.org/x.js", "//example.org/x.js", "/x.js"
1906 if ( /^(https?:)?\/?\//.test( modules ) ) {
1907 if ( type === 'text/css' ) {
1909 // Use properties instead of attributes as IE throws security
1910 // warnings when inserting a <link> tag with a protocol-relative
1911 // URL set though attributes - when on HTTPS. See bug 41331.
1912 l = document.createElement( 'link' );
1913 l.rel = 'stylesheet';
1915 $( 'head' ).append( l );
1918 if ( type === 'text/javascript' || type === undefined ) {
1919 addScript( modules );
1923 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1925 // Called with single module
1926 modules = [ modules ];
1929 // Filter out undefined modules, otherwise resolve() will throw
1930 // an exception for trying to load an undefined module.
1931 // Undefined modules are acceptable here in load(), because load() takes
1932 // an array of unrelated modules, whereas the modules passed to
1933 // using() are related and must all be loaded.
1934 filtered = $.grep( modules, function ( module ) {
1935 var state = mw.loader.getState( module );
1936 return state !== null && state !== 'error' && state !== 'missing';
1939 if ( filtered.length === 0 ) {
1942 // Resolve entire dependency map
1943 filtered = resolve( filtered );
1944 // If all modules are ready, or if any modules have errors, nothing to be done.
1945 if ( allReady( filtered ) || anyFailed( filtered ) ) {
1948 // Since some modules are not yet ready, queue up a request.
1949 request( filtered, undefined, undefined );
1953 * Change the state of one or more modules.
1955 * @param {string|Object} module Module name or object of module name/state pairs
1956 * @param {string} state State name
1958 state: function ( module, state ) {
1961 if ( typeof module === 'object' ) {
1962 for ( m in module ) {
1963 mw.loader.state( m, module[ m ] );
1967 if ( !hasOwn.call( registry, module ) ) {
1968 mw.loader.register( module );
1970 if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1
1971 && registry[ module ].state !== state ) {
1972 // Make sure pending modules depending on this one get executed if their
1973 // dependencies are now fulfilled!
1974 registry[ module ].state = state;
1975 handlePending( module );
1977 registry[ module ].state = state;
1982 * Get the version of a module.
1984 * @param {string} module Name of module
1985 * @return {string|null} The version, or null if the module (or its version) is not
1988 getVersion: function ( module ) {
1989 if ( !hasOwn.call( registry, module ) || registry[ module ].version === undefined ) {
1992 return registry[ module ].version;
1996 * Get the state of a module.
1998 * @param {string} module Name of module
1999 * @return {string|null} The state, or null if the module (or its state) is not
2002 getState: function ( module ) {
2003 if ( !hasOwn.call( registry, module ) || registry[ module ].state === undefined ) {
2006 return registry[ module ].state;
2010 * Get the names of all registered modules.
2014 getModuleNames: function () {
2015 return $.map( registry, function ( i, key ) {
2021 * @inheritdoc mw.inspect#runReports
2024 inspect: function () {
2025 var args = slice.call( arguments );
2026 mw.loader.using( 'mediawiki.inspect', function () {
2027 mw.inspect.runReports.apply( mw.inspect, args );
2032 * On browsers that implement the localStorage API, the module store serves as a
2033 * smart complement to the browser cache. Unlike the browser cache, the module store
2034 * can slice a concatenated response from ResourceLoader into its constituent
2035 * modules and cache each of them separately, using each module's versioning scheme
2036 * to determine when the cache should be invalidated.
2039 * @class mw.loader.store
2042 // Whether the store is in use on this page.
2045 // Modules whose string representation exceeds 100 kB are ineligible
2046 // for storage due to bug T66721.
2047 MODULE_SIZE_MAX: 100000,
2049 // The contents of the store, mapping '[module name]@[version]' keys
2050 // to module implementations.
2054 stats: { hits: 0, misses: 0, expired: 0 },
2057 * Construct a JSON-serializable object representing the content of the store.
2059 * @return {Object} Module store contents.
2061 toJSON: function () {
2062 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
2066 * Get the localStorage key for the entire module store. The key references
2067 * $wgDBname to prevent clashes between wikis which share a common host.
2069 * @return {string} localStorage item key
2071 getStoreKey: function () {
2072 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
2076 * Get a key on which to vary the module cache.
2078 * @return {string} String of concatenated vary conditions.
2080 getVary: function () {
2082 mw.config.get( 'skin' ),
2083 mw.config.get( 'wgResourceLoaderStorageVersion' ),
2084 mw.config.get( 'wgUserLanguage' )
2089 * Get a key for a specific module. The key format is '[name]@[version]'.
2091 * @param {string} module Module name
2092 * @return {string|null} Module key or null if module does not exist
2094 getModuleKey: function ( module ) {
2095 return hasOwn.call( registry, module ) ?
2096 ( module + '@' + registry[ module ].version ) : null;
2100 * Initialize the store.
2102 * Retrieves store from localStorage and (if successfully retrieved) decoding
2103 * the stored JSON value to a plain object.
2105 * The try / catch block is used for JSON & localStorage feature detection.
2106 * See the in-line documentation for Modernizr's localStorage feature detection
2107 * code for a full account of why we need a try / catch:
2108 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
2113 if ( mw.loader.store.enabled !== null ) {
2118 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) {
2119 // Disabled by configuration.
2120 // Clear any previous store to free up space. (T66721)
2121 mw.loader.store.clear();
2122 mw.loader.store.enabled = false;
2125 if ( mw.config.get( 'debug' ) ) {
2126 // Disable module store in debug mode
2127 mw.loader.store.enabled = false;
2132 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2133 // If we get here, localStorage is available; mark enabled
2134 mw.loader.store.enabled = true;
2135 data = JSON.parse( raw );
2136 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2137 mw.loader.store.items = data.items;
2141 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
2144 if ( raw === undefined ) {
2145 // localStorage failed; disable store
2146 mw.loader.store.enabled = false;
2148 mw.loader.store.update();
2153 * Retrieve a module from the store and update cache hit stats.
2155 * @param {string} module Module name
2156 * @return {string|boolean} Module implementation or false if unavailable
2158 get: function ( module ) {
2161 if ( !mw.loader.store.enabled ) {
2165 key = mw.loader.store.getModuleKey( module );
2166 if ( key in mw.loader.store.items ) {
2167 mw.loader.store.stats.hits++;
2168 return mw.loader.store.items[ key ];
2170 mw.loader.store.stats.misses++;
2175 * Stringify a module and queue it for storage.
2177 * @param {string} module Module name
2178 * @param {Object} descriptor The module's descriptor as set in the registry
2180 set: function ( module, descriptor ) {
2183 if ( !mw.loader.store.enabled ) {
2187 key = mw.loader.store.getModuleKey( module );
2190 // Already stored a copy of this exact version
2191 key in mw.loader.store.items ||
2192 // Module failed to load
2193 descriptor.state !== 'ready' ||
2194 // Unversioned, private, or site-/user-specific
2195 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) ||
2196 // Partial descriptor
2197 $.inArray( undefined, [ descriptor.script, descriptor.style,
2198 descriptor.messages, descriptor.templates ] ) !== -1
2206 JSON.stringify( module ),
2207 typeof descriptor.script === 'function' ?
2208 String( descriptor.script ) :
2209 JSON.stringify( descriptor.script ),
2210 JSON.stringify( descriptor.style ),
2211 JSON.stringify( descriptor.messages ),
2212 JSON.stringify( descriptor.templates )
2214 // Attempted workaround for a possible Opera bug (bug T59567).
2215 // This regex should never match under sane conditions.
2216 if ( /^\s*\(/.test( args[ 1 ] ) ) {
2217 args[ 1 ] = 'function' + args[ 1 ];
2218 mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
2221 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
2225 src = 'mw.loader.implement(' + args.join( ',' ) + ');';
2226 if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
2229 mw.loader.store.items[ key ] = src;
2230 mw.loader.store.update();
2234 * Iterate through the module store, removing any item that does not correspond
2235 * (in name and version) to an item in the module registry.
2237 prune: function () {
2240 if ( !mw.loader.store.enabled ) {
2244 for ( key in mw.loader.store.items ) {
2245 module = key.slice( 0, key.indexOf( '@' ) );
2246 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2247 mw.loader.store.stats.expired++;
2248 delete mw.loader.store.items[ key ];
2249 } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) {
2250 // This value predates the enforcement of a size limit on cached modules.
2251 delete mw.loader.store.items[ key ];
2257 * Clear the entire module store right now.
2259 clear: function () {
2260 mw.loader.store.items = {};
2261 localStorage.removeItem( mw.loader.store.getStoreKey() );
2265 * Sync in-memory store back to localStorage.
2267 * This function debounces updates. When called with a flush already pending,
2268 * the call is coalesced into the pending update. The call to
2269 * localStorage.setItem will be naturally deferred until the page is quiescent.
2271 * Because localStorage is shared by all pages from the same origin, if multiple
2272 * pages are loaded with different module sets, the possibility exists that
2273 * modules saved by one page will be clobbered by another. But the impact would
2274 * be minor and the problem would be corrected by subsequent page views.
2278 update: ( function () {
2279 var hasPendingWrite = false;
2281 function flushWrites() {
2283 if ( !hasPendingWrite || !mw.loader.store.enabled ) {
2287 mw.loader.store.prune();
2288 key = mw.loader.store.getStoreKey();
2290 // Replacing the content of the module store might fail if the new
2291 // contents would exceed the browser's localStorage size limit. To
2292 // avoid clogging the browser with stale data, always remove the old
2293 // value before attempting to set the new one.
2294 localStorage.removeItem( key );
2295 data = JSON.stringify( mw.loader.store );
2296 localStorage.setItem( key, data );
2298 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
2301 hasPendingWrite = false;
2304 return function () {
2305 if ( !hasPendingWrite ) {
2306 hasPendingWrite = true;
2307 mw.requestIdleCallback( flushWrites );
2316 * HTML construction helper functions
2323 * output = Html.element( 'div', {}, new Html.Raw(
2324 * Html.element( 'img', { src: '<' } )
2326 * mw.log( output ); // <div><img src="<"/></div>
2331 html: ( function () {
2332 function escapeCallback( s ) {
2349 * Escape a string for HTML.
2351 * Converts special characters to HTML entities.
2353 * mw.html.escape( '< > \' & "' );
2354 * // Returns < > ' & "
2356 * @param {string} s The string to escape
2357 * @return {string} HTML
2359 escape: function ( s ) {
2360 return s.replace( /['"<>&]/g, escapeCallback );
2364 * Create an HTML element string, with safe escaping.
2366 * @param {string} name The tag name.
2367 * @param {Object} [attrs] An object with members mapping element names to values
2368 * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
2370 * - string: Text to be escaped.
2371 * - null: The element is treated as void with short closing form, e.g. `<br/>`.
2372 * - this.Raw: The raw value is directly included.
2373 * - this.Cdata: The raw value is directly included. An exception is
2374 * thrown if it contains any illegal ETAGO delimiter.
2375 * See <http://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
2376 * @return {string} HTML
2378 element: function ( name, attrs, contents ) {
2379 var v, attrName, s = '<' + name;
2382 for ( attrName in attrs ) {
2383 v = attrs[ attrName ];
2384 // Convert name=true, to name=name
2388 } else if ( v === false ) {
2391 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2394 if ( contents === undefined || contents === null ) {
2401 switch ( typeof contents ) {
2404 s += this.escape( contents );
2408 // Convert to string
2409 s += String( contents );
2412 if ( contents instanceof this.Raw ) {
2413 // Raw HTML inclusion
2414 s += contents.value;
2415 } else if ( contents instanceof this.Cdata ) {
2417 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2418 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2420 s += contents.value;
2422 throw new Error( 'mw.html.element: Invalid type of contents' );
2425 s += '</' + name + '>';
2430 * Wrapper object for raw HTML passed to mw.html.element().
2432 * @class mw.html.Raw
2434 Raw: function ( value ) {
2439 * Wrapper object for CDATA element contents passed to mw.html.element()
2441 * @class mw.html.Cdata
2443 Cdata: function ( value ) {
2449 // Skeleton user object, extended by the 'mediawiki.user' module.
2456 * @property {mw.Map}
2460 * @property {mw.Map}
2465 // OOUI widgets specific to MediaWiki
2469 * Registry and firing of events.
2471 * MediaWiki has various interface components that are extended, enhanced
2472 * or manipulated in some other way by extensions, gadgets and even
2475 * This framework helps streamlining the timing of when these other
2476 * code paths fire their plugins (instead of using document-ready,
2477 * which can and should be limited to firing only once).
2479 * Features like navigating to other wiki pages, previewing an edit
2480 * and editing itself – without a refresh – can then retrigger these
2481 * hooks accordingly to ensure everything still works as expected.
2485 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2486 * mw.hook( 'wikipage.content' ).fire( $content );
2488 * Handlers can be added and fired for arbitrary event names at any time. The same
2489 * event can be fired multiple times. The last run of an event is memorized
2490 * (similar to `$(document).ready` and `$.Deferred().done`).
2491 * This means if an event is fired, and a handler added afterwards, the added
2492 * function will be fired right away with the last given event data.
2494 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2495 * Thus allowing flexible use and optimal maintainability and authority control.
2496 * You can pass around the `add` and/or `fire` method to another piece of code
2497 * without it having to know the event name (or `mw.hook` for that matter).
2499 * var h = mw.hook( 'bar.ready' );
2500 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2502 * Note: Events are documented with an underscore instead of a dot in the event
2503 * name due to jsduck not supporting dots in that position.
2507 hook: ( function () {
2511 * Create an instance of mw.hook.
2515 * @param {string} name Name of hook.
2518 return function ( name ) {
2519 var list = hasOwn.call( lists, name ) ?
2521 lists[ name ] = $.Callbacks( 'memory' );
2525 * Register a hook handler
2527 * @param {...Function} handler Function to bind.
2533 * Unregister a hook handler
2535 * @param {...Function} handler Function to unbind.
2538 remove: list.remove,
2543 * @param {...Mixed} data
2547 return list.fireWith.call( this, null, slice.call( arguments ) );
2554 // Alias $j to jQuery for backwards compatibility
2555 // @deprecated since 1.23 Use $ or jQuery instead
2556 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2559 * Log a message to window.console, if possible.
2561 * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
2562 * also in production mode). Gets console references in each invocation instead of caching the
2563 * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
2567 * @param {string} topic Stream name passed by mw.track
2568 * @param {Object} data Data passed by mw.track
2569 * @param {Error} [data.exception]
2570 * @param {string} data.source Error source
2571 * @param {string} [data.module] Name of module which caused the error
2573 function log( topic, data ) {
2576 source = data.source,
2577 module = data.module,
2578 console = window.console;
2580 if ( console && console.log ) {
2581 msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
2583 msg += ' in module ' + module;
2585 msg += ( e ? ':' : '.' );
2588 // If we have an exception object, log it to the error channel to trigger a
2589 // proper stacktraces in browsers that support it. No fallback as we have no browsers
2590 // that don't support error(), but do support log().
2591 if ( e && console.error ) {
2592 console.error( String( e ), e );
2597 // Subscribe to error streams
2598 mw.trackSubscribe( 'resourceloader.exception', log );
2599 mw.trackSubscribe( 'resourceloader.assert', log );
2602 * Fired when all modules associated with the page have finished loading.
2604 * @event resourceloader_loadEnd
2608 var loading = $.grep( mw.loader.getModuleNames(), function ( module ) {
2609 return mw.loader.getState( module ) === 'loading';
2611 // In order to use jQuery.when (which stops early if one of the promises got rejected)
2612 // cast any loading failures into successes. We only need a callback, not the module.
2613 loading = $.map( loading, function ( module ) {
2614 return mw.loader.using( module ).then( null, function () {
2615 return $.Deferred().resolve();
2618 $.when.apply( $, loading ).then( function () {
2619 mwPerformance.mark( 'mwLoadEnd' );
2620 mw.hook( 'resourceloader.loadEnd' ).fire();
2624 // Attach to window and globally alias
2625 window.mw = window.mediaWiki = mw;