2 * Base library for MediaWiki.
4 * Exposed globally as `mediaWiki` with `mw` as shortcut.
7 * @alternateClassName mediaWiki
10 /*jshint latedef:false */
15 hasOwn = Object.prototype.hasOwnProperty,
16 slice = Array.prototype.slice,
17 trackCallbacks = $.Callbacks( 'memory' ),
22 * FNV132 hash function
24 * This function implements the 32-bit version of FNV-1.
25 * It is equivalent to hash( 'fnv132', ... ) in PHP, except
26 * its output is base 36 rather than hex.
27 * See <https://en.wikipedia.org/wiki/FNV_hash_function>
30 * @param {string} str String to hash
31 * @return {string} hash as an seven-character base 36 string
33 function fnv132( str ) {
34 /*jshint bitwise:false */
35 var hash = 0x811C9DC5,
38 for ( i = 0; i < str.length; i++ ) {
39 hash += ( hash << 1 ) + ( hash << 4 ) + ( hash << 7 ) + ( hash << 8 ) + ( hash << 24 );
40 hash ^= str.charCodeAt( i );
43 hash = ( hash >>> 0 ).toString( 36 );
44 while ( hash.length < 7 ) {
52 * Create an object that can be read from or written to from methods that allow
53 * interaction both with single and multiple properties at once.
57 * var collection, query, results;
59 * // Create your address book
60 * collection = new mw.Map();
62 * // This data could be coming from an external source (eg. API/AJAX)
64 * 'John Doe': 'john@example.org',
65 * 'Jane Doe': 'jane@example.org',
66 * 'George van Halen': 'gvanhalen@example.org'
69 * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
71 * // You can detect missing keys first
72 * if ( !collection.exists( wanted ) ) {
73 * // One or more are missing (in this case: "Daniel Jackson")
74 * mw.log( 'One or more names were not found in your address book' );
77 * // Or just let it give you what it can. Optionally fill in from a default.
78 * results = collection.get( wanted, 'nobody@example.com' );
79 * mw.log( results['Jane Doe'] ); // "jane@example.org"
80 * mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
85 * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
87 * For backwards-compatibility with mw.config, this can also be `true` in which case values
88 * are copied to the Window object as global variables (T72470). Values are copied in
89 * one direction only. Changes to globals are not reflected in the map.
91 function Map( values ) {
92 if ( values === true ) {
95 // Override #set to also set the global variable
96 this.set = function ( selection, value ) {
99 if ( $.isPlainObject( selection ) ) {
100 for ( s in selection ) {
101 setGlobalMapValue( this, s, selection[ s ] );
105 if ( typeof selection === 'string' && arguments.length ) {
106 setGlobalMapValue( this, selection, value );
115 this.values = values || {};
119 * Alias property to the global object.
123 * @param {mw.Map} map
124 * @param {string} key
125 * @param {Mixed} value
127 function setGlobalMapValue( map, key, value ) {
128 map.values[ key ] = value;
133 // Deprecation notice for mw.config globals (T58550, T72470)
134 map === mw.config && 'Use mw.config instead.'
140 * Get the value of one or more keys.
142 * If called with no arguments, all values are returned.
144 * @param {string|Array} [selection] Key or array of keys to retrieve values for.
145 * @param {Mixed} [fallback=null] Value for keys that don't exist.
146 * @return {Mixed|Object| null} If selection was a string, returns the value,
147 * If selection was an array, returns an object of key/values.
148 * If no selection is passed, the 'values' container is returned. (Beware that,
149 * as is the default in JavaScript, the object is returned by reference.)
151 get: function ( selection, fallback ) {
153 // If we only do this in the `return` block, it'll fail for the
154 // call to get() from the mutli-selection block.
155 fallback = arguments.length > 1 ? fallback : null;
157 if ( $.isArray( selection ) ) {
158 selection = slice.call( selection );
160 for ( i = 0; i < selection.length; i++ ) {
161 results[ selection[ i ] ] = this.get( selection[ i ], fallback );
166 if ( typeof selection === 'string' ) {
167 if ( !hasOwn.call( this.values, selection ) ) {
170 return this.values[ selection ];
173 if ( selection === undefined ) {
177 // Invalid selection key
182 * Set one or more key/value pairs.
184 * @param {string|Object} selection Key to set value for, or object mapping keys to values
185 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
186 * @return {boolean} True on success, false on failure
188 set: function ( selection, value ) {
191 if ( $.isPlainObject( selection ) ) {
192 for ( s in selection ) {
193 this.values[ s ] = selection[ s ];
197 if ( typeof selection === 'string' && arguments.length > 1 ) {
198 this.values[ selection ] = value;
205 * Check if one or more keys exist.
207 * @param {Mixed} selection Key or array of keys to check
208 * @return {boolean} True if the key(s) exist
210 exists: function ( selection ) {
213 if ( $.isArray( selection ) ) {
214 for ( s = 0; s < selection.length; s++ ) {
215 if ( typeof selection[ s ] !== 'string' || !hasOwn.call( this.values, selection[ s ] ) ) {
221 return typeof selection === 'string' && hasOwn.call( this.values, selection );
226 * Object constructor for messages.
228 * Similar to the Message class in MediaWiki PHP.
230 * Format defaults to 'text'.
236 * 'hello': 'Hello world',
237 * 'hello-user': 'Hello, $1!',
238 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
241 * obj = new mw.Message( mw.messages, 'hello' );
242 * mw.log( obj.text() );
245 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
246 * mw.log( obj.text() );
247 * // Hello, John Doe!
249 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
250 * mw.log( obj.text() );
251 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
253 * // Using mw.message shortcut
254 * obj = mw.message( 'hello-user', 'John Doe' );
255 * mw.log( obj.text() );
256 * // Hello, John Doe!
258 * // Using mw.msg shortcut
259 * str = mw.msg( 'hello-user', 'John Doe' );
261 * // Hello, John Doe!
263 * // Different formats
264 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
266 * obj.format = 'text';
267 * str = obj.toString();
272 * // Hello, John "Wiki" <3 Doe!
274 * mw.log( obj.escaped() );
275 * // Hello, John "Wiki" <3 Doe!
280 * @param {mw.Map} map Message store
281 * @param {string} key
282 * @param {Array} [parameters]
284 function Message( map, key, parameters ) {
285 this.format = 'text';
288 this.parameters = parameters === undefined ? [] : slice.call( parameters );
292 Message.prototype = {
294 * Get parsed contents of the message.
296 * The default parser does simple $N replacements and nothing else.
297 * This may be overridden to provide a more complex message parser.
298 * The primary override is in the mediawiki.jqueryMsg module.
300 * This function will not be called for nonexistent messages.
302 * @return {string} Parsed message
304 parser: function () {
305 return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
309 * Add (does not replace) parameters for `$N` placeholder values.
311 * @param {Array} parameters
314 params: function ( parameters ) {
316 for ( i = 0; i < parameters.length; i++ ) {
317 this.parameters.push( parameters[ i ] );
323 * Convert message object to its string form based on current format.
325 * @return {string} Message as a string in the current form, or `<key>` if key
328 toString: function () {
331 if ( !this.exists() ) {
332 // Use <key> as text if key does not exist
333 if ( this.format === 'escaped' || this.format === 'parse' ) {
334 // format 'escaped' and 'parse' need to have the brackets and key html escaped
335 return mw.html.escape( '<' + this.key + '>' );
337 return '<' + this.key + '>';
340 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
341 text = this.parser();
344 if ( this.format === 'escaped' ) {
345 text = this.parser();
346 text = mw.html.escape( text );
353 * Change format to 'parse' and convert message to string
355 * If jqueryMsg is loaded, this parses the message text from wikitext
356 * (where supported) to HTML
358 * Otherwise, it is equivalent to plain.
360 * @return {string} String form of parsed message
363 this.format = 'parse';
364 return this.toString();
368 * Change format to 'plain' and convert message to string
370 * This substitutes parameters, but otherwise does not change the
373 * @return {string} String form of plain message
376 this.format = 'plain';
377 return this.toString();
381 * Change format to 'text' and convert message to string
383 * If jqueryMsg is loaded, {{-transformation is done where supported
384 * (such as {{plural:}}, {{gender:}}, {{int:}}).
386 * Otherwise, it is equivalent to plain
388 * @return {string} String form of text message
391 this.format = 'text';
392 return this.toString();
396 * Change the format to 'escaped' and convert message to string
398 * This is equivalent to using the 'text' format (see #text), then
399 * HTML-escaping the output.
401 * @return {string} String form of html escaped message
403 escaped: function () {
404 this.format = 'escaped';
405 return this.toString();
409 * Check if a message exists
414 exists: function () {
415 return this.map.exists( this.key );
425 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
427 * On browsers that implement the Navigation Timing API, this function will produce floating-point
428 * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
429 * it will fall back to using `Date`.
431 * @return {number} Current time
434 var perf = window.performance,
435 navStart = perf && perf.timing && perf.timing.navigationStart;
436 return navStart && typeof perf.now === 'function' ?
437 function () { return navStart + perf.now(); } :
438 function () { return +new Date(); };
442 * Format a string. Replace $1, $2 ... $N with positional arguments.
444 * Used by Message#parser().
447 * @param {string} formatString Format string
448 * @param {...Mixed} parameters Values for $N replacements
449 * @return {string} Formatted string
451 format: function ( formatString ) {
452 var parameters = slice.call( arguments, 1 );
453 return formatString.replace( /\$(\d+)/g, function ( str, match ) {
454 var index = parseInt( match, 10 ) - 1;
455 return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match;
460 * Track an analytic event.
462 * This method provides a generic means for MediaWiki JavaScript code to capture state
463 * information for analysis. Each logged event specifies a string topic name that describes
464 * the kind of event that it is. Topic names consist of dot-separated path components,
465 * arranged from most general to most specific. Each path component should have a clear and
466 * well-defined purpose.
468 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
469 * events that match their subcription, including those that fired before the handler was
472 * @param {string} topic Topic name
473 * @param {Object} [data] Data describing the event, encoded as an object
475 track: function ( topic, data ) {
476 trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
477 trackCallbacks.fire( trackQueue );
481 * Register a handler for subset of analytic events, specified by topic.
483 * Handlers will be called once for each tracked event, including any events that fired before the
484 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
485 * the exact time at which the event fired, a string 'topic' property naming the event, and a
486 * 'data' property which is an object of event-specific data. The event topic and event data are
487 * also passed to the callback as the first and second arguments, respectively.
489 * @param {string} topic Handle events whose name starts with this string prefix
490 * @param {Function} callback Handler to call for each matching tracked event
491 * @param {string} callback.topic
492 * @param {Object} [callback.data]
494 trackSubscribe: function ( topic, callback ) {
496 function handler( trackQueue ) {
498 for ( ; seen < trackQueue.length; seen++ ) {
499 event = trackQueue[ seen ];
500 if ( event.topic.indexOf( topic ) === 0 ) {
501 callback.call( event, event.topic, event.data );
506 trackHandlers.push( [ handler, callback ] );
508 trackCallbacks.add( handler );
512 * Stop handling events for a particular handler
514 * @param {Function} callback
516 trackUnsubscribe: function ( callback ) {
517 trackHandlers = $.grep( trackHandlers, function ( fns ) {
518 if ( fns[ 1 ] === callback ) {
519 trackCallbacks.remove( fns[ 0 ] );
520 // Ensure the tuple is removed to avoid holding on to closures
527 // Expose Map constructor
530 // Expose Message constructor
534 * Map of configuration values.
536 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
539 * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
540 * global `window` object.
542 * @property {mw.Map} config
544 // Dummy placeholder later assigned in ResourceLoaderStartUpModule
548 * Empty object for third-party libraries, for cases where you don't
549 * want to add a new global, or the global is bad and needs containment
557 * Access container for deprecated functionality that can be moved from
558 * from their legacy location and attached to this object (e.g. a global
559 * function that is deprecated and as stop-gap can be exposed through here).
561 * This was reserved for future use but never ended up being used.
563 * @deprecated since 1.22 Let deprecated identifiers keep their original name
564 * and use mw.log#deprecate to create an access container for tracking.
570 * Store for messages.
577 * Store for templates associated with a module.
581 templates: new Map(),
584 * Get a message object.
586 * Shortcut for `new mw.Message( mw.messages, key, parameters )`.
589 * @param {string} key Key of message to get
590 * @param {...Mixed} parameters Values for $N replacements
591 * @return {mw.Message}
593 message: function ( key ) {
594 var parameters = slice.call( arguments, 1 );
595 return new Message( mw.messages, key, parameters );
599 * Get a message string using the (default) 'text' format.
601 * Shortcut for `mw.message( key, parameters... ).text()`.
604 * @param {string} key Key of message to get
605 * @param {...Mixed} parameters Values for $N replacements
609 return mw.message.apply( mw.message, arguments ).toString();
613 * Dummy placeholder for {@link mw.log}
618 // Also update the restoration of methods in mediawiki.log.js
619 // when adding or removing methods here.
620 var log = function () {},
621 console = window.console;
629 * Write a message to the console's warning channel.
630 * Actions not supported by the browser console are silently ignored.
632 * @param {...string} msg Messages to output to console
634 log.warn = console && console.warn && Function.prototype.bind ?
635 Function.prototype.bind.call( console.warn, console ) :
639 * Write a message to the console's error channel.
641 * Most browsers provide a stacktrace by default if the argument
642 * is a caught Error object.
645 * @param {Error|...string} msg Messages to output to console
647 log.error = console && console.error && Function.prototype.bind ?
648 Function.prototype.bind.call( console.error, console ) :
652 * Create a property in a host object that, when accessed, will produce
653 * a deprecation warning in the console with backtrace.
655 * @param {Object} obj Host object of deprecated property
656 * @param {string} key Name of property to create in `obj`
657 * @param {Mixed} val The value this property should return when accessed
658 * @param {string} [msg] Optional text to include in the deprecation message
660 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
662 } : function ( obj, key, val, msg ) {
664 msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
665 var logged, loggedIsSet, uniqueTrace;
673 uniqueTrace = function () {
674 var trace = new Error().stack;
676 if ( logged.has( trace ) ) {
682 if ( logged.hasOwnProperty( trace ) ) {
689 Object.defineProperty( obj, key, {
693 if ( uniqueTrace() ) {
694 mw.track( 'mw.deprecate', key );
699 set: function ( newVal ) {
700 if ( uniqueTrace() ) {
701 mw.track( 'mw.deprecate', key );
714 * Client for ResourceLoader server end point.
716 * This client is in charge of maintaining the module registry and state
717 * machine, initiating network (batch) requests for loading modules, as
718 * well as dependency resolution and execution of source code.
720 * For more information, refer to
721 * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
726 loader: ( function () {
729 * Fired via mw.track on various resource loading errors.
731 * @event resourceloader_exception
732 * @param {Error|Mixed} e The error that was thrown. Almost always an Error
733 * object, but in theory module code could manually throw something else, and that
734 * might also end up here.
735 * @param {string} [module] Name of the module which caused the error. Omitted if the
736 * error is not module-related or the module cannot be easily identified due to
738 * @param {string} source Source of the error. Possible values:
740 * - style: stylesheet error (only affects old IE where a special style loading method
742 * - load-callback: exception thrown by user callback
743 * - module-execute: exception thrown by module code
744 * - store-eval: could not evaluate module code cached in localStorage
745 * - store-localstorage-init: localStorage or JSON parse error in mw.loader.store.init
746 * - store-localstorage-json: JSON conversion error in mw.loader.store.set
747 * - store-localstorage-update: localStorage or JSON conversion error in mw.loader.store.update
751 * Fired via mw.track on resource loading error conditions.
753 * @event resourceloader_assert
754 * @param {string} source Source of the error. Possible values:
756 * - bug-T59567: failed to cache script due to an Opera function -> string conversion
757 * bug; see <https://phabricator.wikimedia.org/T59567> for details
761 * Mapping of registered modules.
763 * See #implement and #execute for exact details on support for script, style and messages.
769 * // From mw.loader.register()
770 * 'version': '########' (hash)
771 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
772 * 'group': 'somegroup', (or) null
773 * 'source': 'local', (or) 'anotherwiki'
774 * 'skip': 'return !!window.Example', (or) null
775 * 'module': export Object
777 * // Set from execute() or mw.loader.state()
778 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
780 * // Optionally added at run-time by mw.loader.implement()
782 * 'script': closure, array of urls, or string
783 * 'style': { ... } (see #execute)
784 * 'messages': { 'key': 'value', ... }
791 * The module is known to the system but not yet requested.
792 * Meta data is registered via mw.loader#register. Calls to that method are
793 * generated server-side by the startup module.
795 * The module is requested through mw.loader (either directly or as dependency of
796 * another module). The client will be fetching module contents from the server.
797 * The contents are then stashed in the registry via mw.loader#implement.
799 * The module has been requested from the server and stashed via mw.loader#implement.
800 * If the module has no more dependencies in-fight, the module will be executed
801 * right away. Otherwise execution is deferred, controlled via #handlePending.
803 * The module is being executed.
805 * The module has been successfully executed.
807 * The module (or one of its dependencies) produced an error during execution.
809 * The module was registered client-side and requested, but the server denied knowledge
810 * of the module's existence.
816 // Mapping of sources, keyed by source-id, values are strings.
821 // 'sourceId': 'http://example.org/w/load.php'
826 // List of modules which will be loaded as when ready
829 // Pending queueModuleScript() requests
830 handlingPendingRequests = false,
831 pendingRequests = [],
833 // List of modules to be loaded
837 * List of callback jobs waiting for modules to be ready.
839 * Jobs are created by #request() and run by #handlePending().
841 * Typically when a job is created for a module, the job's dependencies contain
842 * both the module being requested and all its recursive dependencies.
847 * 'dependencies': [ module names ],
848 * 'ready': Function callback
849 * 'error': Function callback
852 * @property {Object[]} jobs
860 // For addEmbeddedCSS()
862 cssBufferTimer = null,
863 cssCallbacks = $.Callbacks(),
864 isIE9 = document.documentMode === 9;
866 function getMarker() {
869 marker = document.querySelector( 'meta[name="ResourceLoaderDynamicStyles"]' );
871 mw.log( 'Create <meta name="ResourceLoaderDynamicStyles"> dynamically' );
872 marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' )[ 0 ];
879 * Create a new style element and add it to the DOM.
882 * @param {string} text CSS text
883 * @param {Node} [nextNode] The element where the style tag
884 * should be inserted before
885 * @return {HTMLElement} Reference to the created style element
887 function newStyleTag( text, nextNode ) {
888 var s = document.createElement( 'style' );
890 s.appendChild( document.createTextNode( text ) );
891 if ( nextNode && nextNode.parentNode ) {
892 nextNode.parentNode.insertBefore( s, nextNode );
894 document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
901 * Add a bit of CSS text to the current browser page.
903 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
904 * or create a new one based on whether the given `cssText` is safe for extension.
907 * @param {string} [cssText=cssBuffer] If called without cssText,
908 * the internal buffer will be inserted instead.
909 * @param {Function} [callback]
911 function addEmbeddedCSS( cssText, callback ) {
914 function fireCallbacks() {
915 var oldCallbacks = cssCallbacks;
916 // Reset cssCallbacks variable so it's not polluted by any calls to
917 // addEmbeddedCSS() from one of the callbacks (T105973)
918 cssCallbacks = $.Callbacks();
919 oldCallbacks.fire().empty();
923 cssCallbacks.add( callback );
926 // Yield once before creating the <style> tag. This lets multiple stylesheets
927 // accumulate into one buffer, allowing us to reduce how often new stylesheets
928 // are inserted in the browser. Appending a stylesheet and waiting for the
929 // browser to repaint is fairly expensive. (T47810)
931 // Don't extend the buffer if the item needs its own stylesheet.
932 // Keywords like `@import` are only valid at the start of a stylesheet (T37562).
933 if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
934 // Linebreak for somewhat distinguishable sections
935 cssBuffer += '\n' + cssText;
936 // TODO: Using requestAnimationFrame would perform better by not injecting
937 // styles while the browser is busy painting.
938 if ( !cssBufferTimer ) {
939 cssBufferTimer = setTimeout( function () {
940 // Support: Firefox < 13
941 // Firefox 12 has non-standard behaviour of passing a number
942 // as first argument to a setTimeout callback.
943 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
950 // This is a scheduled flush for the buffer
952 cssBufferTimer = null;
957 // By default, always create a new <style>. Appending text to a <style> tag is
958 // is a performance anti-pattern as it requires CSS to be reparsed (T47810).
961 // Try to re-use existing <style> tags due to the IE stylesheet limit (T33676).
963 $style = $( getMarker() ).prev();
964 // Verify that the element before the marker actually is a <style> tag created
965 // by mw.loader (not some other style tag, or e.g. a <meta> tag).
966 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) ) {
967 styleEl = $style[ 0 ];
968 styleEl.appendChild( document.createTextNode( cssText ) );
972 // Else: No existing tag to reuse. Continue below and create the first one.
975 $style = $( newStyleTag( cssText, getMarker() ) );
978 $style.data( 'ResourceLoaderDynamicStyleTag', true );
986 * @param {Array} modules List of module names
987 * @return {string} Hash of concatenated version hashes.
989 function getCombinedVersion( modules ) {
990 var hashes = $.map( modules, function ( module ) {
991 return registry[ module ].version;
993 return fnv132( hashes.join( '' ) );
997 * Determine whether all dependencies are in state 'ready', which means we may
998 * execute the module or job now.
1001 * @param {Array} modules Names of modules to be checked
1002 * @return {boolean} True if all modules are in state 'ready', false otherwise
1004 function allReady( modules ) {
1006 for ( i = 0; i < modules.length; i++ ) {
1007 if ( mw.loader.getState( modules[ i ] ) !== 'ready' ) {
1015 * Determine whether all dependencies are in state 'ready', which means we may
1016 * execute the module or job now.
1019 * @param {Array} modules Names of modules to be checked
1020 * @return {boolean} True if no modules are in state 'error' or 'missing', false otherwise
1022 function anyFailed( modules ) {
1024 for ( i = 0; i < modules.length; i++ ) {
1025 state = mw.loader.getState( modules[ i ] );
1026 if ( state === 'error' || state === 'missing' ) {
1034 * A module has entered state 'ready', 'error', or 'missing'. Automatically update
1035 * pending jobs and modules that depend upon this module. If the given module failed,
1036 * propagate the 'error' state up the dependency tree. Otherwise, go ahead and execute
1037 * all jobs/modules now having their dependencies satisfied.
1039 * Jobs that depend on a failed module, will have their error callback ran (if any).
1042 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
1044 function handlePending( module ) {
1045 var j, job, hasErrors, m, stateChange;
1047 if ( registry[ module ].state === 'error' || registry[ module ].state === 'missing' ) {
1048 // If the current module failed, mark all dependent modules also as failed.
1049 // Iterate until steady-state to propagate the error state upwards in the
1052 stateChange = false;
1053 for ( m in registry ) {
1054 if ( registry[ m ].state !== 'error' && registry[ m ].state !== 'missing' ) {
1055 if ( anyFailed( registry[ m ].dependencies ) ) {
1056 registry[ m ].state = 'error';
1061 } while ( stateChange );
1064 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1065 for ( j = 0; j < jobs.length; j++ ) {
1066 hasErrors = anyFailed( jobs[ j ].dependencies );
1067 if ( hasErrors || allReady( jobs[ j ].dependencies ) ) {
1068 // All dependencies satisfied, or some have errors
1070 jobs.splice( j, 1 );
1074 if ( $.isFunction( job.error ) ) {
1075 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [ module ] );
1078 if ( $.isFunction( job.ready ) ) {
1083 // A user-defined callback raised an exception.
1084 // Swallow it to protect our state machine!
1085 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'load-callback' } );
1090 if ( registry[ module ].state === 'ready' ) {
1091 // The current module became 'ready'. Set it in the module store, and recursively execute all
1092 // dependent modules that are loaded and now have all dependencies satisfied.
1093 mw.loader.store.set( module, registry[ module ] );
1094 for ( m in registry ) {
1095 if ( registry[ m ].state === 'loaded' && allReady( registry[ m ].dependencies ) ) {
1103 * Resolve dependencies and detect circular references.
1106 * @param {string} module Name of the top-level module whose dependencies shall be
1107 * resolved and sorted.
1108 * @param {Array} resolved Returns a topological sort of the given module and its
1109 * dependencies, such that later modules depend on earlier modules. The array
1110 * contains the module names. If the array contains already some module names,
1111 * this function appends its result to the pre-existing array.
1112 * @param {Object} [unresolved] Hash used to track the current dependency
1113 * chain; used to report loops in the dependency graph.
1114 * @throws {Error} If any unregistered module or a dependency loop is encountered
1116 function sortDependencies( module, resolved, unresolved ) {
1119 if ( !hasOwn.call( registry, module ) ) {
1120 throw new Error( 'Unknown dependency: ' + module );
1123 if ( registry[ module ].skip !== null ) {
1124 /*jshint evil:true */
1125 skip = new Function( registry[ module ].skip );
1126 registry[ module ].skip = null;
1128 registry[ module ].skipped = true;
1129 registry[ module ].dependencies = [];
1130 registry[ module ].state = 'ready';
1131 handlePending( module );
1136 // Resolves dynamic loader function and replaces it with its own results
1137 if ( $.isFunction( registry[ module ].dependencies ) ) {
1138 registry[ module ].dependencies = registry[ module ].dependencies();
1139 // Ensures the module's dependencies are always in an array
1140 if ( typeof registry[ module ].dependencies !== 'object' ) {
1141 registry[ module ].dependencies = [ registry[ module ].dependencies ];
1144 if ( $.inArray( module, resolved ) !== -1 ) {
1145 // Module already resolved; nothing to do
1148 // Create unresolved if not passed in
1149 if ( !unresolved ) {
1152 // Tracks down dependencies
1153 deps = registry[ module ].dependencies;
1154 for ( i = 0; i < deps.length; i++ ) {
1155 if ( $.inArray( deps[ i ], resolved ) === -1 ) {
1156 if ( unresolved[ deps[ i ] ] ) {
1157 throw new Error( mw.format(
1158 'Circular reference detected: $1 -> $2',
1164 // Add to unresolved
1165 unresolved[ module ] = true;
1166 sortDependencies( deps[ i ], resolved, unresolved );
1169 resolved.push( module );
1173 * Get names of module that a module depends on, in their proper dependency order.
1176 * @param {string[]} modules Array of string module names
1177 * @return {Array} List of dependencies, including 'module'.
1179 function resolve( modules ) {
1181 $.each( modules, function ( idx, module ) {
1182 sortDependencies( module, resolved );
1188 * Load and execute a script.
1191 * @param {string} src URL to script, will be used as the src attribute in the script tag
1192 * @return {jQuery.Promise}
1194 function addScript( src ) {
1198 // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1199 // XHR for a same domain request instead of <script>, which changes the request
1200 // headers (potentially missing a cache hit), and reduces caching in general
1201 // since browsers cache XHR much less (if at all). And XHR means we retreive
1202 // text, so we'd need to $.globalEval, which then messes up line numbers.
1209 * Queue the loading and execution of a script for a particular module.
1212 * @param {string} src URL of the script
1213 * @param {string} [moduleName] Name of currently executing module
1214 * @return {jQuery.Promise}
1216 function queueModuleScript( src, moduleName ) {
1217 var r = $.Deferred();
1219 pendingRequests.push( function () {
1220 if ( moduleName && hasOwn.call( registry, moduleName ) ) {
1221 window.require = mw.loader.require;
1222 window.module = registry[ moduleName ].module;
1224 addScript( src ).always( function () {
1225 // Clear environment
1226 delete window.require;
1227 delete window.module;
1230 // Start the next one (if any)
1231 if ( pendingRequests[ 0 ] ) {
1232 pendingRequests.shift()();
1234 handlingPendingRequests = false;
1238 if ( !handlingPendingRequests && pendingRequests[ 0 ] ) {
1239 handlingPendingRequests = true;
1240 pendingRequests.shift()();
1246 * Utility function for execute()
1250 function addLink( media, url ) {
1251 var el = document.createElement( 'link' );
1253 el.rel = 'stylesheet';
1254 if ( media && media !== 'all' ) {
1257 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1258 // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
1261 $( getMarker() ).before( el );
1265 * Executes a loaded module, making it ready to use
1268 * @param {string} module Module name to execute
1270 function execute( module ) {
1271 var key, value, media, i, urls, cssHandle, checkCssHandles, runScript,
1272 cssHandlesRegistered = false;
1274 if ( !hasOwn.call( registry, module ) ) {
1275 throw new Error( 'Module has not been registered yet: ' + module );
1277 if ( registry[ module ].state !== 'loaded' ) {
1278 throw new Error( 'Module in state "' + registry[ module ].state + '" may not be executed: ' + module );
1281 registry[ module ].state = 'executing';
1283 runScript = function () {
1284 var script, markModuleReady, nestedAddScript, legacyWait,
1285 // Expand to include dependencies since we have to exclude both legacy modules
1286 // and their dependencies from the legacyWait (to prevent a circular dependency).
1287 legacyModules = resolve( mw.config.get( 'wgResourceLoaderLegacyModules', [] ) );
1289 script = registry[ module ].script;
1290 markModuleReady = function () {
1291 registry[ module ].state = 'ready';
1292 handlePending( module );
1294 nestedAddScript = function ( arr, callback, i ) {
1295 // Recursively call queueModuleScript() in its own callback
1296 // for each element of arr.
1297 if ( i >= arr.length ) {
1298 // We're at the end of the array
1303 queueModuleScript( arr[ i ], module ).always( function () {
1304 nestedAddScript( arr, callback, i + 1 );
1308 legacyWait = ( $.inArray( module, legacyModules ) !== -1 )
1309 ? $.Deferred().resolve()
1310 : mw.loader.using( legacyModules );
1312 legacyWait.always( function () {
1313 if ( $.isArray( script ) ) {
1314 nestedAddScript( script, markModuleReady, 0 );
1315 } else if ( $.isFunction( script ) ) {
1316 // Pass jQuery twice so that the signature of the closure which wraps
1317 // the script can bind both '$' and 'jQuery'.
1318 script( $, $, mw.loader.require, registry[ module ].module );
1321 } else if ( typeof script === 'string' ) {
1322 // Site and user modules are legacy scripts that run in the global scope.
1323 // This is transported as a string instead of a function to avoid needing
1324 // to use string manipulation to undo the function wrapper.
1325 if ( module === 'user' ) {
1326 // Implicit dependency on the site module. Not real dependency because
1327 // it should run after 'site' regardless of whether it succeeds or fails.
1328 mw.loader.using( 'site' ).always( function () {
1329 $.globalEval( script );
1333 $.globalEval( script );
1337 // Module without script
1342 // This needs to NOT use mw.log because these errors are common in production mode
1343 // and not in debug mode, such as when a symbol that should be global isn't exported
1344 registry[ module ].state = 'error';
1345 mw.track( 'resourceloader.exception', { exception: e, module: module, source: 'module-execute' } );
1346 handlePending( module );
1350 // Add localizations to message system
1351 if ( registry[ module ].messages ) {
1352 mw.messages.set( registry[ module ].messages );
1355 // Initialise templates
1356 if ( registry[ module ].templates ) {
1357 mw.templates.set( module, registry[ module ].templates );
1360 // Make sure we don't run the scripts until all stylesheet insertions have completed.
1363 checkCssHandles = function () {
1364 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1365 // one of the cssHandles is fired while we're still creating more handles.
1366 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1368 runScript = undefined; // Revoke
1371 cssHandle = function () {
1372 var check = checkCssHandles;
1374 return function () {
1378 check = undefined; // Revoke
1384 // Process styles (see also mw.loader.implement)
1385 // * back-compat: { <media>: css }
1386 // * back-compat: { <media>: [url, ..] }
1387 // * { "css": [css, ..] }
1388 // * { "url": { <media>: [url, ..] } }
1389 if ( registry[ module ].style ) {
1390 for ( key in registry[ module ].style ) {
1391 value = registry[ module ].style[ key ];
1394 if ( key !== 'url' && key !== 'css' ) {
1395 // Backwards compatibility, key is a media-type
1396 if ( typeof value === 'string' ) {
1397 // back-compat: { <media>: css }
1398 // Ignore 'media' because it isn't supported (nor was it used).
1399 // Strings are pre-wrapped in "@media". The media-type was just ""
1400 // (because it had to be set to something).
1401 // This is one of the reasons why this format is no longer used.
1402 addEmbeddedCSS( value, cssHandle() );
1404 // back-compat: { <media>: [url, ..] }
1410 // Array of css strings in key 'css',
1411 // or back-compat array of urls from media-type
1412 if ( $.isArray( value ) ) {
1413 for ( i = 0; i < value.length; i++ ) {
1414 if ( key === 'bc-url' ) {
1415 // back-compat: { <media>: [url, ..] }
1416 addLink( media, value[ i ] );
1417 } else if ( key === 'css' ) {
1418 // { "css": [css, ..] }
1419 addEmbeddedCSS( value[ i ], cssHandle() );
1422 // Not an array, but a regular object
1423 // Array of urls inside media-type key
1424 } else if ( typeof value === 'object' ) {
1425 // { "url": { <media>: [url, ..] } }
1426 for ( media in value ) {
1427 urls = value[ media ];
1428 for ( i = 0; i < urls.length; i++ ) {
1429 addLink( media, urls[ i ] );
1437 cssHandlesRegistered = true;
1442 * Adds all dependencies to the queue with optional callbacks to be run
1443 * when the dependencies are ready or fail
1446 * @param {string|string[]} dependencies Module name or array of string module names
1447 * @param {Function} [ready] Callback to execute when all dependencies are ready
1448 * @param {Function} [error] Callback to execute when any dependency fails
1450 function request( dependencies, ready, error ) {
1451 // Allow calling by single module name
1452 if ( typeof dependencies === 'string' ) {
1453 dependencies = [ dependencies ];
1456 // Add ready and error callbacks if they were given
1457 if ( ready !== undefined || error !== undefined ) {
1459 // Narrow down the list to modules that are worth waiting for
1460 dependencies: $.grep( dependencies, function ( module ) {
1461 var state = mw.loader.getState( module );
1462 return state === 'registered' || state === 'loaded' || state === 'loading' || state === 'executing';
1469 $.each( dependencies, function ( idx, module ) {
1470 var state = mw.loader.getState( module );
1471 // Only queue modules that are still in the initial 'registered' state
1472 // (not ones already loading, ready or error).
1473 if ( state === 'registered' && $.inArray( module, queue ) === -1 ) {
1474 // Private modules must be embedded in the page. Don't bother queuing
1475 // these as the server will deny them anyway (T101806).
1476 if ( registry[ module ].group === 'private' ) {
1477 registry[ module ].state = 'error';
1478 handlePending( module );
1481 queue.push( module );
1488 function sortQuery( o ) {
1494 if ( hasOwn.call( o, key ) ) {
1499 for ( key = 0; key < a.length; key++ ) {
1500 sorted[ a[ key ] ] = o[ a[ key ] ];
1506 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1507 * to a query string of the form foo.bar,baz|bar.baz,quux
1511 function buildModulesString( moduleMap ) {
1515 for ( prefix in moduleMap ) {
1516 p = prefix === '' ? '' : prefix + '.';
1517 arr.push( p + moduleMap[ prefix ].join( ',' ) );
1519 return arr.join( '|' );
1523 * Load modules from load.php
1526 * @param {Object} moduleMap Module map, see #buildModulesString
1527 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1528 * @param {string} sourceLoadScript URL of load.php
1530 function doRequest( moduleMap, currReqBase, sourceLoadScript ) {
1531 var request = $.extend(
1532 { modules: buildModulesString( moduleMap ) },
1535 request = sortQuery( request );
1536 addScript( sourceLoadScript + '?' + $.param( request ) );
1540 * Resolve indexed dependencies.
1542 * ResourceLoader uses an optimization to save space which replaces module names in
1543 * dependency lists with the index of that module within the array of module
1544 * registration data if it exists. The benefit is a significant reduction in the data
1545 * size of the startup module. This function changes those dependency lists back to
1546 * arrays of strings.
1549 * @param {Array} modules Modules array
1551 function resolveIndexedDependencies( modules ) {
1552 $.each( modules, function ( idx, module ) {
1553 if ( module[ 2 ] ) {
1554 module[ 2 ] = $.map( module[ 2 ], function ( dep ) {
1555 return typeof dep === 'number' ? modules[ dep ][ 0 ] : dep;
1561 /* Public Members */
1564 * The module registry is exposed as an aid for debugging and inspecting page
1565 * state; it is not a public interface for modifying the registry.
1571 moduleRegistry: registry,
1574 * @inheritdoc #newStyleTag
1577 addStyleTag: newStyleTag,
1580 * Batch-request queued dependencies from the server.
1585 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1586 source, concatSource, origBatch, group, i, modules, sourceLoadScript,
1587 currReqBase, currReqBaseLength, moduleMap, l,
1588 lastDotIndex, prefix, suffix, bytesAdded;
1590 // Build a list of request parameters common to all requests.
1592 skin: mw.config.get( 'skin' ),
1593 lang: mw.config.get( 'wgUserLanguage' ),
1594 debug: mw.config.get( 'debug' )
1596 // Split module batch by source and by group.
1598 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', 2000 );
1600 // Appends a list of modules from the queue to the batch
1601 for ( q = 0; q < queue.length; q++ ) {
1602 // Only request modules which are registered
1603 if ( hasOwn.call( registry, queue[ q ] ) && registry[ queue[ q ] ].state === 'registered' ) {
1604 // Prevent duplicate entries
1605 if ( $.inArray( queue[ q ], batch ) === -1 ) {
1606 batch.push( queue[ q ] );
1607 // Mark registered modules as loading
1608 registry[ queue[ q ] ].state = 'loading';
1613 mw.loader.store.init();
1614 if ( mw.loader.store.enabled ) {
1617 batch = $.grep( batch, function ( module ) {
1618 var source = mw.loader.store.get( module );
1620 concatSource.push( source );
1626 $.globalEval( concatSource.join( ';' ) );
1628 // Not good, the cached mw.loader.implement calls failed! This should
1629 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1630 // Depending on how corrupt the string is, it is likely that some
1631 // modules' implement() succeeded while the ones after the error will
1632 // never run and leave their modules in the 'loading' state forever.
1634 // Since this is an error not caused by an individual module but by
1635 // something that infected the implement call itself, don't take any
1636 // risks and clear everything in this cache.
1637 mw.loader.store.clear();
1638 // Re-add the ones still pending back to the batch and let the server
1639 // repopulate these modules to the cache.
1640 // This means that at most one module will be useless (the one that had
1641 // the error) instead of all of them.
1642 mw.track( 'resourceloader.exception', { exception: err, source: 'store-eval' } );
1643 origBatch = $.grep( origBatch, function ( module ) {
1644 return registry[ module ].state === 'loading';
1646 batch = batch.concat( origBatch );
1650 // Early exit if there's nothing to load...
1651 if ( !batch.length ) {
1655 // The queue has been processed into the batch, clear up the queue.
1658 // Always order modules alphabetically to help reduce cache
1659 // misses for otherwise identical content.
1662 // Split batch by source and by group.
1663 for ( b = 0; b < batch.length; b++ ) {
1664 bSource = registry[ batch[ b ] ].source;
1665 bGroup = registry[ batch[ b ] ].group;
1666 if ( !hasOwn.call( splits, bSource ) ) {
1667 splits[ bSource ] = {};
1669 if ( !hasOwn.call( splits[ bSource ], bGroup ) ) {
1670 splits[ bSource ][ bGroup ] = [];
1672 bSourceGroup = splits[ bSource ][ bGroup ];
1673 bSourceGroup.push( batch[ b ] );
1676 // Clear the batch - this MUST happen before we append any
1677 // script elements to the body or it's possible that a script
1678 // will be locally cached, instantly load, and work the batch
1679 // again, all before we've cleared it causing each request to
1680 // include modules which are already loaded.
1683 for ( source in splits ) {
1685 sourceLoadScript = sources[ source ];
1687 for ( group in splits[ source ] ) {
1689 // Cache access to currently selected list of
1690 // modules for this group from this source.
1691 modules = splits[ source ][ group ];
1693 currReqBase = $.extend( {
1694 version: getCombinedVersion( modules )
1696 // For user modules append a user name to the request.
1697 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1698 currReqBase.user = mw.config.get( 'wgUserName' );
1700 currReqBaseLength = $.param( currReqBase ).length;
1701 // We may need to split up the request to honor the query string length limit,
1702 // so build it piece by piece.
1703 l = currReqBaseLength + 9; // '&modules='.length == 9
1705 moduleMap = {}; // { prefix: [ suffixes ] }
1707 for ( i = 0; i < modules.length; i++ ) {
1708 // Determine how many bytes this module would add to the query string
1709 lastDotIndex = modules[ i ].lastIndexOf( '.' );
1711 // If lastDotIndex is -1, substr() returns an empty string
1712 prefix = modules[ i ].substr( 0, lastDotIndex );
1713 suffix = modules[ i ].slice( lastDotIndex + 1 );
1715 bytesAdded = hasOwn.call( moduleMap, prefix )
1716 ? suffix.length + 3 // '%2C'.length == 3
1717 : modules[ i ].length + 3; // '%7C'.length == 3
1719 // If the request would become too long, create a new one,
1720 // but don't create empty requests
1721 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1722 // This request would become too long, create a new one
1723 // and fire off the old one
1724 doRequest( moduleMap, currReqBase, sourceLoadScript );
1726 l = currReqBaseLength + 9;
1727 mw.track( 'resourceloader.splitRequest', { maxQueryLength: maxQueryLength } );
1729 if ( !hasOwn.call( moduleMap, prefix ) ) {
1730 moduleMap[ prefix ] = [];
1732 moduleMap[ prefix ].push( suffix );
1735 // If there's anything left in moduleMap, request that too
1736 if ( !$.isEmptyObject( moduleMap ) ) {
1737 doRequest( moduleMap, currReqBase, sourceLoadScript );
1744 * Register a source.
1746 * The #work() method will use this information to split up requests by source.
1748 * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1750 * @param {string|Object} id Source ID, or object mapping ids to load urls
1751 * @param {string} loadUrl Url to a load.php end point
1752 * @throws {Error} If source id is already registered
1754 addSource: function ( id, loadUrl ) {
1756 // Allow multiple additions
1757 if ( typeof id === 'object' ) {
1758 for ( source in id ) {
1759 mw.loader.addSource( source, id[ source ] );
1764 if ( hasOwn.call( sources, id ) ) {
1765 throw new Error( 'source already registered: ' + id );
1768 sources[ id ] = loadUrl;
1772 * Register a module, letting the system know about it and its properties.
1774 * The startup modules contain calls to this method.
1776 * When using multiple module registration by passing an array, dependencies that
1777 * are specified as references to modules within the array will be resolved before
1778 * the modules are registered.
1780 * @param {string|Array} module Module name or array of arrays, each containing
1781 * a list of arguments compatible with this method
1782 * @param {string|number} version Module version hash (falls backs to empty string)
1783 * Can also be a number (timestamp) for compatibility with MediaWiki 1.25 and earlier.
1784 * @param {string|Array|Function} dependencies One string or array of strings of module
1785 * names on which this module depends, or a function that returns that array.
1786 * @param {string} [group=null] Group which the module is in
1787 * @param {string} [source='local'] Name of the source
1788 * @param {string} [skip=null] Script body of the skip function
1790 register: function ( module, version, dependencies, group, source, skip ) {
1792 // Allow multiple registration
1793 if ( typeof module === 'object' ) {
1794 resolveIndexedDependencies( module );
1795 for ( i = 0; i < module.length; i++ ) {
1796 // module is an array of module names
1797 if ( typeof module[ i ] === 'string' ) {
1798 mw.loader.register( module[ i ] );
1799 // module is an array of arrays
1800 } else if ( typeof module[ i ] === 'object' ) {
1801 mw.loader.register.apply( mw.loader, module[ i ] );
1806 if ( hasOwn.call( registry, module ) ) {
1807 throw new Error( 'module already registered: ' + module );
1809 // List the module as registered
1810 registry[ module ] = {
1811 // Exposed to execute() for mw.loader.implement() closures.
1812 // Import happens via require().
1816 version: version !== undefined ? String( version ) : '',
1818 group: typeof group === 'string' ? group : null,
1819 source: typeof source === 'string' ? source : 'local',
1820 state: 'registered',
1821 skip: typeof skip === 'string' ? skip : null
1823 if ( typeof dependencies === 'string' ) {
1824 // Allow dependencies to be given as a single module name
1825 registry[ module ].dependencies = [ dependencies ];
1826 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1827 // Allow dependencies to be given as an array of module names
1828 // or a function which returns an array
1829 registry[ module ].dependencies = dependencies;
1834 * Implement a module given the components that make up the module.
1836 * When #load or #using requests one or more modules, the server
1837 * response contain calls to this function.
1839 * @param {string} module Name of module
1840 * @param {Function|Array} [script] Function with module code or Array of URLs to
1841 * be used as the src attribute of a new `<script>` tag.
1842 * @param {Object} [style] Should follow one of the following patterns:
1844 * { "css": [css, ..] }
1845 * { "url": { <media>: [url, ..] } }
1847 * And for backwards compatibility (needs to be supported forever due to caching):
1850 * { <media>: [url, ..] }
1852 * The reason css strings are not concatenated anymore is bug 31676. We now check
1853 * whether it's safe to extend the stylesheet.
1856 * @param {Object} [messages] List of key/value pairs to be added to mw#messages.
1857 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1859 implement: function ( module, script, style, messages, templates ) {
1860 // Automatically register module
1861 if ( !hasOwn.call( registry, module ) ) {
1862 mw.loader.register( module );
1864 // Check for duplicate implementation
1865 if ( hasOwn.call( registry, module ) && registry[ module ].script !== undefined ) {
1866 throw new Error( 'module already implemented: ' + module );
1868 // Attach components
1869 registry[ module ].script = script || null;
1870 registry[ module ].style = style || null;
1871 registry[ module ].messages = messages || null;
1872 registry[ module ].templates = templates || null;
1873 // The module may already have been marked as erroneous
1874 if ( $.inArray( registry[ module ].state, [ 'error', 'missing' ] ) === -1 ) {
1875 registry[ module ].state = 'loaded';
1876 if ( allReady( registry[ module ].dependencies ) ) {
1883 * Execute a function as soon as one or more required modules are ready.
1885 * Example of inline dependency on OOjs:
1887 * mw.loader.using( 'oojs', function () {
1888 * OO.compare( [ 1 ], [ 1 ] );
1891 * Since MediaWiki 1.23 this also returns a promise.
1893 * Since MediaWiki 1.28 the promise is resolved with a `require` function.
1895 * @param {string|Array} dependencies Module name or array of modules names the
1896 * callback depends on to be ready before executing
1897 * @param {Function} [ready] Callback to execute when all dependencies are ready
1898 * @param {Function} [error] Callback to execute if one or more dependencies failed
1899 * @return {jQuery.Promise} With a `require` function
1901 using: function ( dependencies, ready, error ) {
1902 var deferred = $.Deferred();
1904 // Allow calling with a single dependency as a string
1905 if ( typeof dependencies === 'string' ) {
1906 dependencies = [ dependencies ];
1910 deferred.done( ready );
1913 deferred.fail( error );
1916 // Resolve entire dependency map
1917 dependencies = resolve( dependencies );
1918 if ( allReady( dependencies ) ) {
1919 // Run ready immediately
1920 deferred.resolve( mw.loader.require );
1921 } else if ( anyFailed( dependencies ) ) {
1922 // Execute error immediately if any dependencies have errors
1924 new Error( 'One or more dependencies failed to load' ),
1928 // Not all dependencies are ready: queue up a request
1929 request( dependencies, function () {
1930 deferred.resolve( mw.loader.require );
1931 }, deferred.reject );
1934 return deferred.promise();
1938 * Load an external script or one or more modules.
1940 * @param {string|Array} modules Either the name of a module, array of modules,
1941 * or a URL of an external script or style
1942 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1943 * external script or style; acceptable values are "text/css" and
1944 * "text/javascript"; if no type is provided, text/javascript is assumed.
1946 load: function ( modules, type ) {
1949 // Allow calling with a url or single dependency as a string
1950 if ( typeof modules === 'string' ) {
1951 // "https://example.org/x.js", "http://example.org/x.js", "//example.org/x.js", "/x.js"
1952 if ( /^(https?:)?\/?\//.test( modules ) ) {
1953 if ( type === 'text/css' ) {
1955 // Use properties instead of attributes as IE throws security
1956 // warnings when inserting a <link> tag with a protocol-relative
1957 // URL set though attributes - when on HTTPS. See bug 41331.
1958 l = document.createElement( 'link' );
1959 l.rel = 'stylesheet';
1961 $( 'head' ).append( l );
1964 if ( type === 'text/javascript' || type === undefined ) {
1965 addScript( modules );
1969 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1971 // Called with single module
1972 modules = [ modules ];
1975 // Filter out undefined modules, otherwise resolve() will throw
1976 // an exception for trying to load an undefined module.
1977 // Undefined modules are acceptable here in load(), because load() takes
1978 // an array of unrelated modules, whereas the modules passed to
1979 // using() are related and must all be loaded.
1980 filtered = $.grep( modules, function ( module ) {
1981 var state = mw.loader.getState( module );
1982 return state !== null && state !== 'error' && state !== 'missing';
1985 if ( filtered.length === 0 ) {
1988 // Resolve entire dependency map
1989 filtered = resolve( filtered );
1990 // If all modules are ready, or if any modules have errors, nothing to be done.
1991 if ( allReady( filtered ) || anyFailed( filtered ) ) {
1994 // Since some modules are not yet ready, queue up a request.
1995 request( filtered, undefined, undefined );
1999 * Change the state of one or more modules.
2001 * @param {string|Object} module Module name or object of module name/state pairs
2002 * @param {string} state State name
2004 state: function ( module, state ) {
2007 if ( typeof module === 'object' ) {
2008 for ( m in module ) {
2009 mw.loader.state( m, module[ m ] );
2013 if ( !hasOwn.call( registry, module ) ) {
2014 mw.loader.register( module );
2016 if ( $.inArray( state, [ 'ready', 'error', 'missing' ] ) !== -1
2017 && registry[ module ].state !== state ) {
2018 // Make sure pending modules depending on this one get executed if their
2019 // dependencies are now fulfilled!
2020 registry[ module ].state = state;
2021 handlePending( module );
2023 registry[ module ].state = state;
2028 * Get the version of a module.
2030 * @param {string} module Name of module
2031 * @return {string|null} The version, or null if the module (or its version) is not
2034 getVersion: function ( module ) {
2035 if ( !hasOwn.call( registry, module ) || registry[ module ].version === undefined ) {
2038 return registry[ module ].version;
2042 * Get the state of a module.
2044 * @param {string} module Name of module
2045 * @return {string|null} The state, or null if the module (or its state) is not
2048 getState: function ( module ) {
2049 if ( !hasOwn.call( registry, module ) || registry[ module ].state === undefined ) {
2052 return registry[ module ].state;
2056 * Get the names of all registered modules.
2060 getModuleNames: function () {
2061 return $.map( registry, function ( i, key ) {
2067 * Get the exported value of a module.
2069 * Modules may provide this via their local `module.exports`.
2074 require: function ( moduleName ) {
2075 var state = mw.loader.getState( moduleName );
2077 // Only ready modules can be required
2078 if ( state !== 'ready' ) {
2079 // Module may've forgotten to declare a dependency
2080 throw new Error( 'Module "' + moduleName + '" is not loaded.' );
2083 return registry[ moduleName ].module.exports;
2087 * @inheritdoc mw.inspect#runReports
2090 inspect: function () {
2091 var args = slice.call( arguments );
2092 mw.loader.using( 'mediawiki.inspect', function () {
2093 mw.inspect.runReports.apply( mw.inspect, args );
2098 * On browsers that implement the localStorage API, the module store serves as a
2099 * smart complement to the browser cache. Unlike the browser cache, the module store
2100 * can slice a concatenated response from ResourceLoader into its constituent
2101 * modules and cache each of them separately, using each module's versioning scheme
2102 * to determine when the cache should be invalidated.
2105 * @class mw.loader.store
2108 // Whether the store is in use on this page.
2111 MODULE_SIZE_MAX: 100 * 1000,
2113 // The contents of the store, mapping '[module name]@[version]' keys
2114 // to module implementations.
2118 stats: { hits: 0, misses: 0, expired: 0 },
2121 * Construct a JSON-serializable object representing the content of the store.
2123 * @return {Object} Module store contents.
2125 toJSON: function () {
2126 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
2130 * Get the localStorage key for the entire module store. The key references
2131 * $wgDBname to prevent clashes between wikis which share a common host.
2133 * @return {string} localStorage item key
2135 getStoreKey: function () {
2136 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
2140 * Get a key on which to vary the module cache.
2142 * @return {string} String of concatenated vary conditions.
2144 getVary: function () {
2146 mw.config.get( 'skin' ),
2147 mw.config.get( 'wgResourceLoaderStorageVersion' ),
2148 mw.config.get( 'wgUserLanguage' )
2153 * Get a key for a specific module. The key format is '[name]@[version]'.
2155 * @param {string} module Module name
2156 * @return {string|null} Module key or null if module does not exist
2158 getModuleKey: function ( module ) {
2159 return hasOwn.call( registry, module ) ?
2160 ( module + '@' + registry[ module ].version ) : null;
2164 * Initialize the store.
2166 * Retrieves store from localStorage and (if successfully retrieved) decoding
2167 * the stored JSON value to a plain object.
2169 * The try / catch block is used for JSON & localStorage feature detection.
2170 * See the in-line documentation for Modernizr's localStorage feature detection
2171 * code for a full account of why we need a try / catch:
2172 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
2177 if ( mw.loader.store.enabled !== null ) {
2183 // Disabled because localStorage quotas are tight and (in Firefox's case)
2184 // shared by multiple origins.
2185 // See T66721, and <https://bugzilla.mozilla.org/show_bug.cgi?id=1064466>.
2186 /Firefox|Opera/.test( navigator.userAgent ) ||
2188 // Disabled by configuration.
2189 !mw.config.get( 'wgResourceLoaderStorageEnabled' )
2191 // Clear any previous store to free up space. (T66721)
2192 mw.loader.store.clear();
2193 mw.loader.store.enabled = false;
2196 if ( mw.config.get( 'debug' ) ) {
2197 // Disable module store in debug mode
2198 mw.loader.store.enabled = false;
2203 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2204 // If we get here, localStorage is available; mark enabled
2205 mw.loader.store.enabled = true;
2206 data = JSON.parse( raw );
2207 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2208 mw.loader.store.items = data.items;
2212 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-init' } );
2215 if ( raw === undefined ) {
2216 // localStorage failed; disable store
2217 mw.loader.store.enabled = false;
2219 mw.loader.store.update();
2224 * Retrieve a module from the store and update cache hit stats.
2226 * @param {string} module Module name
2227 * @return {string|boolean} Module implementation or false if unavailable
2229 get: function ( module ) {
2232 if ( !mw.loader.store.enabled ) {
2236 key = mw.loader.store.getModuleKey( module );
2237 if ( key in mw.loader.store.items ) {
2238 mw.loader.store.stats.hits++;
2239 return mw.loader.store.items[ key ];
2241 mw.loader.store.stats.misses++;
2246 * Stringify a module and queue it for storage.
2248 * @param {string} module Module name
2249 * @param {Object} descriptor The module's descriptor as set in the registry
2251 set: function ( module, descriptor ) {
2254 if ( !mw.loader.store.enabled ) {
2258 key = mw.loader.store.getModuleKey( module );
2261 // Already stored a copy of this exact version
2262 key in mw.loader.store.items ||
2263 // Module failed to load
2264 descriptor.state !== 'ready' ||
2265 // Unversioned, private, or site-/user-specific
2266 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user' ] ) !== -1 ) ||
2267 // Partial descriptor
2268 $.inArray( undefined, [ descriptor.script, descriptor.style,
2269 descriptor.messages, descriptor.templates ] ) !== -1
2277 JSON.stringify( module ),
2278 typeof descriptor.script === 'function' ?
2279 String( descriptor.script ) :
2280 JSON.stringify( descriptor.script ),
2281 JSON.stringify( descriptor.style ),
2282 JSON.stringify( descriptor.messages ),
2283 JSON.stringify( descriptor.templates )
2285 // Attempted workaround for a possible Opera bug (bug T59567).
2286 // This regex should never match under sane conditions.
2287 if ( /^\s*\(/.test( args[ 1 ] ) ) {
2288 args[ 1 ] = 'function' + args[ 1 ];
2289 mw.track( 'resourceloader.assert', { source: 'bug-T59567' } );
2292 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-json' } );
2296 src = 'mw.loader.implement(' + args.join( ',' ) + ');';
2297 if ( src.length > mw.loader.store.MODULE_SIZE_MAX ) {
2300 mw.loader.store.items[ key ] = src;
2301 mw.loader.store.update();
2305 * Iterate through the module store, removing any item that does not correspond
2306 * (in name and version) to an item in the module registry.
2308 prune: function () {
2311 if ( !mw.loader.store.enabled ) {
2315 for ( key in mw.loader.store.items ) {
2316 module = key.slice( 0, key.indexOf( '@' ) );
2317 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2318 mw.loader.store.stats.expired++;
2319 delete mw.loader.store.items[ key ];
2320 } else if ( mw.loader.store.items[ key ].length > mw.loader.store.MODULE_SIZE_MAX ) {
2321 // This value predates the enforcement of a size limit on cached modules.
2322 delete mw.loader.store.items[ key ];
2328 * Clear the entire module store right now.
2330 clear: function () {
2331 mw.loader.store.items = {};
2333 localStorage.removeItem( mw.loader.store.getStoreKey() );
2334 } catch ( ignored ) {}
2338 * Sync in-memory store back to localStorage.
2340 * This function debounces updates. When called with a flush already pending,
2341 * the call is coalesced into the pending update. The call to
2342 * localStorage.setItem will be naturally deferred until the page is quiescent.
2344 * Because localStorage is shared by all pages from the same origin, if multiple
2345 * pages are loaded with different module sets, the possibility exists that
2346 * modules saved by one page will be clobbered by another. But the impact would
2347 * be minor and the problem would be corrected by subsequent page views.
2351 update: ( function () {
2352 var hasPendingWrite = false;
2354 function flushWrites() {
2356 if ( !hasPendingWrite || !mw.loader.store.enabled ) {
2360 mw.loader.store.prune();
2361 key = mw.loader.store.getStoreKey();
2363 // Replacing the content of the module store might fail if the new
2364 // contents would exceed the browser's localStorage size limit. To
2365 // avoid clogging the browser with stale data, always remove the old
2366 // value before attempting to set the new one.
2367 localStorage.removeItem( key );
2368 data = JSON.stringify( mw.loader.store );
2369 localStorage.setItem( key, data );
2371 mw.track( 'resourceloader.exception', { exception: e, source: 'store-localstorage-update' } );
2374 hasPendingWrite = false;
2377 return function () {
2378 if ( !hasPendingWrite ) {
2379 hasPendingWrite = true;
2380 mw.requestIdleCallback( flushWrites );
2389 * HTML construction helper functions
2396 * output = Html.element( 'div', {}, new Html.Raw(
2397 * Html.element( 'img', { src: '<' } )
2399 * mw.log( output ); // <div><img src="<"/></div>
2404 html: ( function () {
2405 function escapeCallback( s ) {
2422 * Escape a string for HTML.
2424 * Converts special characters to HTML entities.
2426 * mw.html.escape( '< > \' & "' );
2427 * // Returns < > ' & "
2429 * @param {string} s The string to escape
2430 * @return {string} HTML
2432 escape: function ( s ) {
2433 return s.replace( /['"<>&]/g, escapeCallback );
2437 * Create an HTML element string, with safe escaping.
2439 * @param {string} name The tag name.
2440 * @param {Object} [attrs] An object with members mapping element names to values
2441 * @param {string|mw.html.Raw|mw.html.Cdata|null} [contents=null] The contents of the element.
2443 * - string: Text to be escaped.
2444 * - null: The element is treated as void with short closing form, e.g. `<br/>`.
2445 * - this.Raw: The raw value is directly included.
2446 * - this.Cdata: The raw value is directly included. An exception is
2447 * thrown if it contains any illegal ETAGO delimiter.
2448 * See <http://www.w3.org/TR/html401/appendix/notes.html#h-B.3.2>.
2449 * @return {string} HTML
2451 element: function ( name, attrs, contents ) {
2452 var v, attrName, s = '<' + name;
2455 for ( attrName in attrs ) {
2456 v = attrs[ attrName ];
2457 // Convert name=true, to name=name
2461 } else if ( v === false ) {
2464 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2467 if ( contents === undefined || contents === null ) {
2474 switch ( typeof contents ) {
2477 s += this.escape( contents );
2481 // Convert to string
2482 s += String( contents );
2485 if ( contents instanceof this.Raw ) {
2486 // Raw HTML inclusion
2487 s += contents.value;
2488 } else if ( contents instanceof this.Cdata ) {
2490 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2491 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2493 s += contents.value;
2495 throw new Error( 'mw.html.element: Invalid type of contents' );
2498 s += '</' + name + '>';
2503 * Wrapper object for raw HTML passed to mw.html.element().
2505 * @class mw.html.Raw
2507 Raw: function ( value ) {
2512 * Wrapper object for CDATA element contents passed to mw.html.element()
2514 * @class mw.html.Cdata
2516 Cdata: function ( value ) {
2522 // Skeleton user object, extended by the 'mediawiki.user' module.
2529 * @property {mw.Map}
2533 * @property {mw.Map}
2538 // OOUI widgets specific to MediaWiki
2542 * Registry and firing of events.
2544 * MediaWiki has various interface components that are extended, enhanced
2545 * or manipulated in some other way by extensions, gadgets and even
2548 * This framework helps streamlining the timing of when these other
2549 * code paths fire their plugins (instead of using document-ready,
2550 * which can and should be limited to firing only once).
2552 * Features like navigating to other wiki pages, previewing an edit
2553 * and editing itself – without a refresh – can then retrigger these
2554 * hooks accordingly to ensure everything still works as expected.
2558 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2559 * mw.hook( 'wikipage.content' ).fire( $content );
2561 * Handlers can be added and fired for arbitrary event names at any time. The same
2562 * event can be fired multiple times. The last run of an event is memorized
2563 * (similar to `$(document).ready` and `$.Deferred().done`).
2564 * This means if an event is fired, and a handler added afterwards, the added
2565 * function will be fired right away with the last given event data.
2567 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2568 * Thus allowing flexible use and optimal maintainability and authority control.
2569 * You can pass around the `add` and/or `fire` method to another piece of code
2570 * without it having to know the event name (or `mw.hook` for that matter).
2572 * var h = mw.hook( 'bar.ready' );
2573 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2575 * Note: Events are documented with an underscore instead of a dot in the event
2576 * name due to jsduck not supporting dots in that position.
2580 hook: ( function () {
2584 * Create an instance of mw.hook.
2588 * @param {string} name Name of hook.
2591 return function ( name ) {
2592 var list = hasOwn.call( lists, name ) ?
2594 lists[ name ] = $.Callbacks( 'memory' );
2598 * Register a hook handler
2600 * @param {...Function} handler Function to bind.
2606 * Unregister a hook handler
2608 * @param {...Function} handler Function to unbind.
2611 remove: list.remove,
2616 * @param {...Mixed} data
2620 return list.fireWith.call( this, null, slice.call( arguments ) );
2627 // Alias $j to jQuery for backwards compatibility
2628 // @deprecated since 1.23 Use $ or jQuery instead
2629 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2632 * Log a message to window.console, if possible.
2634 * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
2635 * also in production mode). Gets console references in each invocation instead of caching the
2636 * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
2640 * @param {string} topic Stream name passed by mw.track
2641 * @param {Object} data Data passed by mw.track
2642 * @param {Error} [data.exception]
2643 * @param {string} data.source Error source
2644 * @param {string} [data.module] Name of module which caused the error
2646 function log( topic, data ) {
2649 source = data.source,
2650 module = data.module,
2651 console = window.console;
2653 if ( console && console.log ) {
2654 msg = ( e ? 'Exception' : 'Error' ) + ' in ' + source;
2656 msg += ' in module ' + module;
2658 msg += ( e ? ':' : '.' );
2661 // If we have an exception object, log it to the error channel to trigger
2662 // proper stacktraces in browsers that support it. No fallback as we have
2663 // no browsers that don't support error(), but do support log().
2664 if ( e && console.error ) {
2665 console.error( String( e ), e );
2670 // Subscribe to error streams
2671 mw.trackSubscribe( 'resourceloader.exception', log );
2672 mw.trackSubscribe( 'resourceloader.assert', log );
2675 * Fired when all modules associated with the page have finished loading.
2677 * @event resourceloader_loadEnd
2681 var loading = $.grep( mw.loader.getModuleNames(), function ( module ) {
2682 return mw.loader.getState( module ) === 'loading';
2684 // In order to use jQuery.when (which stops early if one of the promises got rejected)
2685 // cast any loading failures into successes. We only need a callback, not the module.
2686 loading = $.map( loading, function ( module ) {
2687 return mw.loader.using( module ).then( null, function () {
2688 return $.Deferred().resolve();
2691 $.when.apply( $, loading ).then( function () {
2692 mwPerformance.mark( 'mwLoadEnd' );
2693 mw.hook( 'resourceloader.loadEnd' ).fire();
2697 // Attach to window and globally alias
2698 window.mw = window.mediaWiki = mw;