2 * Base library for MediaWiki.
4 * Exposed globally as `mediaWiki` with `mw` as shortcut.
7 * @alternateClassName mediaWiki
14 hasOwn = Object.prototype.hasOwnProperty,
15 slice = Array.prototype.slice,
16 trackCallbacks = $.Callbacks( 'memory' ),
20 * Log a message to window.console, if possible.
22 * Useful to force logging of some errors that are otherwise hard to detect (i.e., this logs
23 * also in production mode). Gets console references in each invocation instead of caching the
24 * reference, so that debugging tools loaded later are supported (e.g. Firebug Lite in IE).
28 * @param {string} msg Text for the log entry.
31 function log( msg, e ) {
32 var console = window.console;
33 if ( console && console.log ) {
35 // If we have an exception object, log it to the error channel to trigger a
36 // proper stacktraces in browsers that support it. No fallback as we have no browsers
37 // that don't support error(), but do support log().
38 if ( e && console.error ) {
39 console.error( String( e ), e );
45 * Create an object that can be read from or written to from methods that allow
46 * interaction both with single and multiple properties at once.
50 * var collection, query, results;
52 * // Create your address book
53 * collection = new mw.Map();
55 * // This data could be coming from an external source (eg. API/AJAX)
57 * 'John Doe': 'john@example.org',
58 * 'Jane Doe': 'jane@example.org',
59 * 'George van Halen': 'gvanhalen@example.org'
62 * wanted = ['John Doe', 'Jane Doe', 'Daniel Jackson'];
64 * // You can detect missing keys first
65 * if ( !collection.exists( wanted ) ) {
66 * // One or more are missing (in this case: "Daniel Jackson")
67 * mw.log( 'One or more names were not found in your address book' );
70 * // Or just let it give you what it can. Optionally fill in from a default.
71 * results = collection.get( wanted, 'nobody@example.com' );
72 * mw.log( results['Jane Doe'] ); // "jane@example.org"
73 * mw.log( results['Daniel Jackson'] ); // "nobody@example.com"
78 * @param {Object|boolean} [values] The value-baring object to be mapped. Defaults to an
80 * For backwards-compatibility with mw.config, this can also be `true` in which case values
81 * are copied to the Window object as global variables (T72470). Values are copied in
82 * one direction only. Changes to globals are not reflected in the map.
84 function Map( values ) {
85 if ( values === true ) {
88 // Override #set to also set the global variable
89 this.set = function ( selection, value ) {
92 if ( $.isPlainObject( selection ) ) {
93 for ( s in selection ) {
94 setGlobalMapValue( this, s, selection[s] );
98 if ( typeof selection === 'string' && arguments.length ) {
99 setGlobalMapValue( this, selection, value );
108 this.values = values || {};
112 * Alias property to the global object.
116 * @param {mw.Map} map
117 * @param {string} key
118 * @param {Mixed} value
120 function setGlobalMapValue( map, key, value ) {
121 map.values[key] = value;
126 // Deprecation notice for mw.config globals (T58550, T72470)
127 map === mw.config && 'Use mw.config instead.'
133 * Get the value of one or more keys.
135 * If called with no arguments, all values are returned.
137 * @param {string|Array} [selection] Key or array of keys to retrieve values for.
138 * @param {Mixed} [fallback=null] Value for keys that don't exist.
139 * @return {Mixed|Object| null} If selection was a string, returns the value,
140 * If selection was an array, returns an object of key/values.
141 * If no selection is passed, the 'values' container is returned. (Beware that,
142 * as is the default in JavaScript, the object is returned by reference.)
144 get: function ( selection, fallback ) {
146 // If we only do this in the `return` block, it'll fail for the
147 // call to get() from the mutli-selection block.
148 fallback = arguments.length > 1 ? fallback : null;
150 if ( $.isArray( selection ) ) {
151 selection = slice.call( selection );
153 for ( i = 0; i < selection.length; i++ ) {
154 results[selection[i]] = this.get( selection[i], fallback );
159 if ( typeof selection === 'string' ) {
160 if ( !hasOwn.call( this.values, selection ) ) {
163 return this.values[selection];
166 if ( selection === undefined ) {
170 // Invalid selection key
175 * Set one or more key/value pairs.
177 * @param {string|Object} selection Key to set value for, or object mapping keys to values
178 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
179 * @return {boolean} True on success, false on failure
181 set: function ( selection, value ) {
184 if ( $.isPlainObject( selection ) ) {
185 for ( s in selection ) {
186 this.values[s] = selection[s];
190 if ( typeof selection === 'string' && arguments.length > 1 ) {
191 this.values[selection] = value;
198 * Check if one or more keys exist.
200 * @param {Mixed} selection Key or array of keys to check
201 * @return {boolean} True if the key(s) exist
203 exists: function ( selection ) {
206 if ( $.isArray( selection ) ) {
207 for ( s = 0; s < selection.length; s++ ) {
208 if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
214 return typeof selection === 'string' && hasOwn.call( this.values, selection );
219 * Object constructor for messages.
221 * Similar to the Message class in MediaWiki PHP.
223 * Format defaults to 'text'.
229 * 'hello': 'Hello world',
230 * 'hello-user': 'Hello, $1!',
231 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
234 * obj = new mw.Message( mw.messages, 'hello' );
235 * mw.log( obj.text() );
238 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
239 * mw.log( obj.text() );
240 * // Hello, John Doe!
242 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
243 * mw.log( obj.text() );
244 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
246 * // Using mw.message shortcut
247 * obj = mw.message( 'hello-user', 'John Doe' );
248 * mw.log( obj.text() );
249 * // Hello, John Doe!
251 * // Using mw.msg shortcut
252 * str = mw.msg( 'hello-user', 'John Doe' );
254 * // Hello, John Doe!
256 * // Different formats
257 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
259 * obj.format = 'text';
260 * str = obj.toString();
265 * // Hello, John "Wiki" <3 Doe!
267 * mw.log( obj.escaped() );
268 * // Hello, John "Wiki" <3 Doe!
273 * @param {mw.Map} map Message store
274 * @param {string} key
275 * @param {Array} [parameters]
277 function Message( map, key, parameters ) {
278 this.format = 'text';
281 this.parameters = parameters === undefined ? [] : slice.call( parameters );
285 Message.prototype = {
287 * Get parsed contents of the message.
289 * The default parser does simple $N replacements and nothing else.
290 * This may be overridden to provide a more complex message parser.
291 * The primary override is in the mediawiki.jqueryMsg module.
293 * This function will not be called for nonexistent messages.
295 * @return {string} Parsed message
297 parser: function () {
298 return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) );
302 * Add (does not replace) parameters for `N$` placeholder values.
304 * @param {Array} parameters
307 params: function ( parameters ) {
309 for ( i = 0; i < parameters.length; i += 1 ) {
310 this.parameters.push( parameters[i] );
316 * Convert message object to its string form based on current format.
318 * @return {string} Message as a string in the current form, or `<key>` if key
321 toString: function () {
324 if ( !this.exists() ) {
325 // Use <key> as text if key does not exist
326 if ( this.format === 'escaped' || this.format === 'parse' ) {
327 // format 'escaped' and 'parse' need to have the brackets and key html escaped
328 return mw.html.escape( '<' + this.key + '>' );
330 return '<' + this.key + '>';
333 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
334 text = this.parser();
337 if ( this.format === 'escaped' ) {
338 text = this.parser();
339 text = mw.html.escape( text );
346 * Change format to 'parse' and convert message to string
348 * If jqueryMsg is loaded, this parses the message text from wikitext
349 * (where supported) to HTML
351 * Otherwise, it is equivalent to plain.
353 * @return {string} String form of parsed message
356 this.format = 'parse';
357 return this.toString();
361 * Change format to 'plain' and convert message to string
363 * This substitutes parameters, but otherwise does not change the
366 * @return {string} String form of plain message
369 this.format = 'plain';
370 return this.toString();
374 * Change format to 'text' and convert message to string
376 * If jqueryMsg is loaded, {{-transformation is done where supported
377 * (such as {{plural:}}, {{gender:}}, {{int:}}).
379 * Otherwise, it is equivalent to plain
381 * @return {string} String form of text message
384 this.format = 'text';
385 return this.toString();
389 * Change the format to 'escaped' and convert message to string
391 * This is equivalent to using the 'text' format (see #text), then
392 * HTML-escaping the output.
394 * @return {string} String form of html escaped message
396 escaped: function () {
397 this.format = 'escaped';
398 return this.toString();
402 * Check if a message exists
407 exists: function () {
408 return this.map.exists( this.key );
418 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
420 * On browsers that implement the Navigation Timing API, this function will produce floating-point
421 * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
422 * it will fall back to using `Date`.
424 * @return {number} Current time
427 var perf = window.performance,
428 navStart = perf && perf.timing && perf.timing.navigationStart;
429 return navStart && typeof perf.now === 'function' ?
430 function () { return navStart + perf.now(); } :
431 function () { return +new Date(); };
435 * Format a string. Replace $1, $2 ... $N with positional arguments.
437 * Used by Message#parser().
440 * @param {string} fmt Format string
441 * @param {Mixed...} parameters Values for $N replacements
442 * @return {string} Formatted string
444 format: function ( formatString ) {
445 var parameters = slice.call( arguments, 1 );
446 return formatString.replace( /\$(\d+)/g, function ( str, match ) {
447 var index = parseInt( match, 10 ) - 1;
448 return parameters[index] !== undefined ? parameters[index] : '$' + match;
453 * Track an analytic event.
455 * This method provides a generic means for MediaWiki JavaScript code to capture state
456 * information for analysis. Each logged event specifies a string topic name that describes
457 * the kind of event that it is. Topic names consist of dot-separated path components,
458 * arranged from most general to most specific. Each path component should have a clear and
459 * well-defined purpose.
461 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
462 * events that match their subcription, including those that fired before the handler was
465 * @param {string} topic Topic name
466 * @param {Object} [data] Data describing the event, encoded as an object
468 track: function ( topic, data ) {
469 trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
470 trackCallbacks.fire( trackQueue );
474 * Register a handler for subset of analytic events, specified by topic.
476 * Handlers will be called once for each tracked event, including any events that fired before the
477 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
478 * the exact time at which the event fired, a string 'topic' property naming the event, and a
479 * 'data' property which is an object of event-specific data. The event topic and event data are
480 * also passed to the callback as the first and second arguments, respectively.
482 * @param {string} topic Handle events whose name starts with this string prefix
483 * @param {Function} callback Handler to call for each matching tracked event
484 * @param {string} callback.topic
485 * @param {Object} [callback.data]
487 trackSubscribe: function ( topic, callback ) {
490 trackCallbacks.add( function ( trackQueue ) {
492 for ( ; seen < trackQueue.length; seen++ ) {
493 event = trackQueue[ seen ];
494 if ( event.topic.indexOf( topic ) === 0 ) {
495 callback.call( event, event.topic, event.data );
501 // Expose Map constructor
504 // Expose Message constructor
508 * Map of configuration values.
510 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
513 * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
514 * global `window` object.
516 * @property {mw.Map} config
518 // Dummy placeholder later assigned in ResourceLoaderStartUpModule
522 * Empty object for third-party libraries, for cases where you don't
523 * want to add a new global, or the global is bad and needs containment
531 * Access container for deprecated functionality that can be moved from
532 * from their legacy location and attached to this object (e.g. a global
533 * function that is deprecated and as stop-gap can be exposed through here).
535 * This was reserved for future use but never ended up being used.
537 * @deprecated since 1.22 Let deprecated identifiers keep their original name
538 * and use mw.log#deprecate to create an access container for tracking.
544 * Store for messages.
551 * Store for templates associated with a module.
555 templates: new Map(),
558 * Get a message object.
560 * Shorcut for `new mw.Message( mw.messages, key, parameters )`.
563 * @param {string} key Key of message to get
564 * @param {Mixed...} parameters Values for $N replacements
565 * @return {mw.Message}
567 message: function ( key ) {
568 var parameters = slice.call( arguments, 1 );
569 return new Message( mw.messages, key, parameters );
573 * Get a message string using the (default) 'text' format.
575 * Shortcut for `mw.message( key, parameters... ).text()`.
578 * @param {string} key Key of message to get
579 * @param {Mixed...} parameters Values for $N replacements
583 return mw.message.apply( mw.message, arguments ).toString();
587 * Dummy placeholder for {@link mw.log}
591 // Also update the restoration of methods in mediawiki.log.js
592 // when adding or removing methods here.
593 var log = function () {};
601 * Write a message the console's warning channel.
602 * Also logs a stacktrace for easier debugging.
603 * Actions not supported by the browser console are silently ignored.
605 * @param {string...} msg Messages to output to console
607 log.warn = function () {
608 var console = window.console;
609 if ( console && console.warn && console.warn.apply ) {
610 console.warn.apply( console, arguments );
611 if ( console.trace ) {
618 * Create a property in a host object that, when accessed, will produce
619 * a deprecation warning in the console with backtrace.
621 * @param {Object} obj Host object of deprecated property
622 * @param {string} key Name of property to create in `obj`
623 * @param {Mixed} val The value this property should return when accessed
624 * @param {string} [msg] Optional text to include in the deprecation message
626 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
628 } : function ( obj, key, val, msg ) {
629 msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
631 // Can throw on Object.defineProperty.
633 Object.defineProperty( obj, key, {
637 mw.track( 'mw.deprecate', key );
641 set: function ( newVal ) {
642 mw.track( 'mw.deprecate', key );
648 // Fallback to creating a copy of the value to the object.
657 * Client for ResourceLoader server end point.
659 * This client is in charge of maintaining the module registry and state
660 * machine, initiating network (batch) requests for loading modules, as
661 * well as dependency resolution and execution of source code.
663 * For more information, refer to
664 * <https://www.mediawiki.org/wiki/ResourceLoader/Features>
669 loader: ( function () {
672 * Mapping of registered modules.
674 * See #implement for exact details on support for script, style and messages.
680 * // From startup mdoule
681 * 'version': ############## (unix timestamp)
682 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
683 * 'group': 'somegroup', (or) null
684 * 'source': 'local', (or) 'anotherwiki'
685 * 'skip': 'return !!window.Example', (or) null
686 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error', or 'missing'
688 * // Added during implementation
692 * 'messages': { 'key': 'value' }
700 // Mapping of sources, keyed by source-id, values are strings.
705 // 'sourceId': 'http://example.org/w/load.php'
710 // List of modules which will be loaded as when ready
713 // List of modules to be loaded
716 // List of callback functions waiting for modules to be ready to be called
719 // Selector cache for the marker element. Use getMarker() to get/use the marker!
722 // Buffer for #addEmbeddedCSS
725 // Callbacks for #addEmbeddedCSS
726 cssCallbacks = $.Callbacks();
728 function getMarker() {
731 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
732 if ( !$marker.length ) {
733 mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
734 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
741 * Create a new style element and add it to the DOM.
744 * @param {string} text CSS text
745 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag
746 * should be inserted before
747 * @return {HTMLElement} Reference to the created style element
749 function newStyleTag( text, nextnode ) {
750 var s = document.createElement( 'style' );
752 // Must attach to document before setting cssText (bug 33305)
754 $( nextnode ).before( s );
756 document.getElementsByTagName( 'head' )[0].appendChild( s );
758 if ( s.styleSheet ) {
760 // Old IE ignores appended text nodes, access stylesheet directly.
761 s.styleSheet.cssText = text;
763 // Standard behaviour
764 s.appendChild( document.createTextNode( text ) );
770 * Check whether given styles are safe to to a stylesheet.
773 * @param {string} cssText
774 * @return {boolean} False if a new one must be created.
776 function canExpandStylesheetWith( cssText ) {
777 // Makes sure that cssText containing `@import`
778 // rules will end up in a new stylesheet (as those only work when
779 // placed at the start of a stylesheet; bug 35562).
780 return cssText.indexOf( '@import' ) === -1;
784 * Add a bit of CSS text to the current browser page.
786 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
787 * or create a new one based on whether the given `cssText` is safe for extension.
789 * @param {string} [cssText=cssBuffer] If called without cssText,
790 * the internal buffer will be inserted instead.
791 * @param {Function} [callback]
793 function addEmbeddedCSS( cssText, callback ) {
797 cssCallbacks.add( callback );
800 // Yield once before inserting the <style> tag. There are likely
801 // more calls coming up which we can combine this way.
802 // Appending a stylesheet and waiting for the browser to repaint
803 // is fairly expensive, this reduces that (bug 45810)
805 // Be careful not to extend the buffer with css that needs a new stylesheet
806 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
807 // Linebreak for somewhat distinguishable sections
808 // (the rl-cachekey comment separating each)
809 cssBuffer += '\n' + cssText;
810 // TODO: Use requestAnimationFrame in the future which will
811 // perform even better by not injecting styles while the browser
813 setTimeout( function () {
814 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
815 // (below version 13) has the non-standard behaviour of passing a
816 // numerical "lateness" value as first argument to this callback
817 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
823 // This is a delayed call and we got a buffer still
824 } else if ( cssBuffer ) {
829 // This is a delayed call, but buffer was already cleared by
830 // another delayed call.
834 // By default, always create a new <style>. Appending text to a <style>
835 // tag is bad as it means the contents have to be re-parsed (bug 45810).
837 // Except, of course, in IE 9 and below. In there we default to re-using and
838 // appending to a <style> tag due to the IE stylesheet limit (bug 31676).
839 if ( 'documentMode' in document && document.documentMode <= 9 ) {
841 $style = getMarker().prev();
842 // Verify that the element before the marker actually is a
843 // <style> tag and one that came from ResourceLoader
844 // (not some other style tag or even a `<meta>` or `<script>`).
845 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
846 // There's already a dynamic <style> tag present and
847 // canExpandStylesheetWith() gave a green light to append more to it.
848 styleEl = $style.get( 0 );
850 if ( styleEl.styleSheet ) {
852 styleEl.styleSheet.cssText += cssText;
854 log( 'Stylesheet error', e );
857 styleEl.appendChild( document.createTextNode( cssText ) );
859 cssCallbacks.fire().empty();
864 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
866 cssCallbacks.fire().empty();
870 * Zero-pad three numbers.
878 function pad( a, b, c ) {
880 a < 10 ? '0' + a : a,
881 b < 10 ? '0' + b : b,
887 * Convert UNIX timestamp to ISO8601 format.
890 * @param {number} timestamp UNIX timestamp
892 function formatVersionNumber( timestamp ) {
894 d.setTime( timestamp * 1000 );
896 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ),
898 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ),
904 * Resolve dependencies and detect circular references.
907 * @param {string} module Name of the top-level module whose dependencies shall be
908 * resolved and sorted.
909 * @param {Array} resolved Returns a topological sort of the given module and its
910 * dependencies, such that later modules depend on earlier modules. The array
911 * contains the module names. If the array contains already some module names,
912 * this function appends its result to the pre-existing array.
913 * @param {Object} [unresolved] Hash used to track the current dependency
914 * chain; used to report loops in the dependency graph.
915 * @throws {Error} If any unregistered module or a dependency loop is encountered
917 function sortDependencies( module, resolved, unresolved ) {
918 var n, deps, len, skip;
920 if ( !hasOwn.call( registry, module ) ) {
921 throw new Error( 'Unknown dependency: ' + module );
924 if ( registry[module].skip !== null ) {
925 /*jshint evil:true */
926 skip = new Function( registry[module].skip );
927 registry[module].skip = null;
929 registry[module].skipped = true;
930 registry[module].dependencies = [];
931 registry[module].state = 'ready';
932 handlePending( module );
937 // Resolves dynamic loader function and replaces it with its own results
938 if ( $.isFunction( registry[module].dependencies ) ) {
939 registry[module].dependencies = registry[module].dependencies();
940 // Ensures the module's dependencies are always in an array
941 if ( typeof registry[module].dependencies !== 'object' ) {
942 registry[module].dependencies = [registry[module].dependencies];
945 if ( $.inArray( module, resolved ) !== -1 ) {
946 // Module already resolved; nothing to do
949 // Create unresolved if not passed in
953 // Tracks down dependencies
954 deps = registry[module].dependencies;
956 for ( n = 0; n < len; n += 1 ) {
957 if ( $.inArray( deps[n], resolved ) === -1 ) {
958 if ( unresolved[deps[n]] ) {
960 'Circular reference detected: ' + module +
966 unresolved[module] = true;
967 sortDependencies( deps[n], resolved, unresolved );
968 delete unresolved[module];
971 resolved[resolved.length] = module;
975 * Get a list of module names that a module depends on in their proper dependency
979 * @param {string} module Module name or array of string module names
980 * @return {Array} List of dependencies, including 'module'.
981 * @throws {Error} If circular reference is detected
983 function resolve( module ) {
986 // Allow calling with an array of module names
987 if ( $.isArray( module ) ) {
989 for ( m = 0; m < module.length; m += 1 ) {
990 sortDependencies( module[m], resolved );
995 if ( typeof module === 'string' ) {
997 sortDependencies( module, resolved );
1001 throw new Error( 'Invalid module argument: ' + module );
1005 * Narrow down a list of module names to those matching a specific
1006 * state (see #registry for a list of valid states).
1008 * One can also filter for 'unregistered', which will return the
1009 * modules names that don't have a registry entry.
1012 * @param {string|string[]} states Module states to filter by
1013 * @param {Array} [modules] List of module names to filter (optional, by default the
1014 * entire registry is used)
1015 * @return {Array} List of filtered module names
1017 function filter( states, modules ) {
1018 var list, module, s, m;
1020 // Allow states to be given as a string
1021 if ( typeof states === 'string' ) {
1024 // If called without a list of modules, build and use a list of all modules
1026 if ( modules === undefined ) {
1028 for ( module in registry ) {
1029 modules[modules.length] = module;
1032 // Build a list of modules which are in one of the specified states
1033 for ( s = 0; s < states.length; s += 1 ) {
1034 for ( m = 0; m < modules.length; m += 1 ) {
1035 if ( !hasOwn.call( registry, modules[m] ) ) {
1036 // Module does not exist
1037 if ( states[s] === 'unregistered' ) {
1039 list[list.length] = modules[m];
1042 // Module exists, check state
1043 if ( registry[modules[m]].state === states[s] ) {
1044 // OK, correct state
1045 list[list.length] = modules[m];
1054 * Determine whether all dependencies are in state 'ready', which means we may
1055 * execute the module or job now.
1058 * @param {Array} dependencies Dependencies (module names) to be checked.
1059 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
1061 function allReady( dependencies ) {
1062 return filter( 'ready', dependencies ).length === dependencies.length;
1066 * A module has entered state 'ready', 'error', or 'missing'. Automatically update
1067 * pending jobs and modules that depend upon this module. If the given module failed,
1068 * propagate the 'error' state up the dependency tree. Otherwise, go ahead an execute
1069 * all jobs/modules now having their dependencies satisfied.
1071 * Jobs that depend on a failed module, will have their error callback ran (if any).
1074 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
1076 function handlePending( module ) {
1077 var j, job, hasErrors, m, stateChange;
1079 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
1080 // If the current module failed, mark all dependent modules also as failed.
1081 // Iterate until steady-state to propagate the error state upwards in the
1084 stateChange = false;
1085 for ( m in registry ) {
1086 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
1087 if ( filter( ['error', 'missing'], registry[m].dependencies ).length ) {
1088 registry[m].state = 'error';
1093 } while ( stateChange );
1096 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1097 for ( j = 0; j < jobs.length; j += 1 ) {
1098 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
1099 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
1100 // All dependencies satisfied, or some have errors
1102 jobs.splice( j, 1 );
1106 if ( $.isFunction( job.error ) ) {
1107 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
1110 if ( $.isFunction( job.ready ) ) {
1115 // A user-defined callback raised an exception.
1116 // Swallow it to protect our state machine!
1117 log( 'Exception thrown by user callback', e );
1122 if ( registry[module].state === 'ready' ) {
1123 // The current module became 'ready'. Set it in the module store, and recursively execute all
1124 // dependent modules that are loaded and now have all dependencies satisfied.
1125 mw.loader.store.set( module, registry[module] );
1126 for ( m in registry ) {
1127 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
1135 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
1136 * depending on whether document-ready has occurred yet and whether we are in async mode.
1139 * @param {string} src URL to script, will be used as the src attribute in the script tag
1140 * @param {Function} [callback] Callback which will be run when the script is done
1141 * @param {boolean} [async=false] Whether to load modules asynchronously.
1142 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1144 function addScript( src, callback, async ) {
1145 // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
1146 if ( $.isReady || async ) {
1150 // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1151 // XHR for a same domain request instead of <script>, which changes the request
1152 // headers (potentially missing a cache hit), and reduces caching in general
1153 // since browsers cache XHR much less (if at all). And XHR means we retreive
1154 // text, so we'd need to $.globalEval, which then messes up line numbers.
1158 } ).always( callback );
1160 /*jshint evil:true */
1161 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
1163 // Document.write is synchronous, so this is called when it's done.
1164 // FIXME: That's a lie. doc.write isn't actually synchronous.
1171 * Executes a loaded module, making it ready to use
1174 * @param {string} module Module name to execute
1176 function execute( module ) {
1177 var key, value, media, i, urls, cssHandle, checkCssHandles,
1178 cssHandlesRegistered = false;
1180 if ( !hasOwn.call( registry, module ) ) {
1181 throw new Error( 'Module has not been registered yet: ' + module );
1182 } else if ( registry[module].state === 'registered' ) {
1183 throw new Error( 'Module has not been requested from the server yet: ' + module );
1184 } else if ( registry[module].state === 'loading' ) {
1185 throw new Error( 'Module has not completed loading yet: ' + module );
1186 } else if ( registry[module].state === 'ready' ) {
1187 throw new Error( 'Module has already been executed: ' + module );
1191 * Define loop-function here for efficiency
1192 * and to avoid re-using badly scoped variables.
1195 function addLink( media, url ) {
1196 var el = document.createElement( 'link' );
1198 // Insert in document *before* setting href
1199 getMarker().before( el );
1200 el.rel = 'stylesheet';
1201 if ( media && media !== 'all' ) {
1204 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1205 // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
1209 function runScript() {
1210 var script, markModuleReady, nestedAddScript;
1212 script = registry[module].script;
1213 markModuleReady = function () {
1214 registry[module].state = 'ready';
1215 handlePending( module );
1217 nestedAddScript = function ( arr, callback, async, i ) {
1218 // Recursively call addScript() in its own callback
1219 // for each element of arr.
1220 if ( i >= arr.length ) {
1221 // We're at the end of the array
1226 addScript( arr[i], function () {
1227 nestedAddScript( arr, callback, async, i + 1 );
1231 if ( $.isArray( script ) ) {
1232 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1233 } else if ( $.isFunction( script ) ) {
1234 registry[module].state = 'ready';
1235 // Pass jQuery twice so that the signature of the closure which wraps
1236 // the script can bind both '$' and 'jQuery'.
1238 handlePending( module );
1241 // This needs to NOT use mw.log because these errors are common in production mode
1242 // and not in debug mode, such as when a symbol that should be global isn't exported
1243 log( 'Exception thrown by ' + module, e );
1244 registry[module].state = 'error';
1245 handlePending( module );
1249 // This used to be inside runScript, but since that is now fired asychronously
1250 // (after CSS is loaded) we need to set it here right away. It is crucial that
1251 // when execute() is called this is set synchronously, otherwise modules will get
1252 // executed multiple times as the registry will state that it isn't loading yet.
1253 registry[module].state = 'loading';
1255 // Add localizations to message system
1256 if ( $.isPlainObject( registry[module].messages ) ) {
1257 mw.messages.set( registry[module].messages );
1260 // Initialise templates
1261 if ( registry[module].templates ) {
1262 mw.templates.set( module, registry[module].templates );
1265 if ( $.isReady || registry[module].async ) {
1266 // Make sure we don't run the scripts until all (potentially asynchronous)
1267 // stylesheet insertions have completed.
1270 checkCssHandles = function () {
1271 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1272 // one of the cssHandles is fired while we're still creating more handles.
1273 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1275 runScript = undefined; // Revoke
1278 cssHandle = function () {
1279 var check = checkCssHandles;
1281 return function () {
1285 check = undefined; // Revoke
1291 // We are in blocking mode, and so we can't afford to wait for CSS
1292 cssHandle = function () {};
1294 checkCssHandles = runScript;
1297 // Process styles (see also mw.loader.implement)
1298 // * back-compat: { <media>: css }
1299 // * back-compat: { <media>: [url, ..] }
1300 // * { "css": [css, ..] }
1301 // * { "url": { <media>: [url, ..] } }
1302 if ( $.isPlainObject( registry[module].style ) ) {
1303 for ( key in registry[module].style ) {
1304 value = registry[module].style[key];
1307 if ( key !== 'url' && key !== 'css' ) {
1308 // Backwards compatibility, key is a media-type
1309 if ( typeof value === 'string' ) {
1310 // back-compat: { <media>: css }
1311 // Ignore 'media' because it isn't supported (nor was it used).
1312 // Strings are pre-wrapped in "@media". The media-type was just ""
1313 // (because it had to be set to something).
1314 // This is one of the reasons why this format is no longer used.
1315 addEmbeddedCSS( value, cssHandle() );
1317 // back-compat: { <media>: [url, ..] }
1323 // Array of css strings in key 'css',
1324 // or back-compat array of urls from media-type
1325 if ( $.isArray( value ) ) {
1326 for ( i = 0; i < value.length; i += 1 ) {
1327 if ( key === 'bc-url' ) {
1328 // back-compat: { <media>: [url, ..] }
1329 addLink( media, value[i] );
1330 } else if ( key === 'css' ) {
1331 // { "css": [css, ..] }
1332 addEmbeddedCSS( value[i], cssHandle() );
1335 // Not an array, but a regular object
1336 // Array of urls inside media-type key
1337 } else if ( typeof value === 'object' ) {
1338 // { "url": { <media>: [url, ..] } }
1339 for ( media in value ) {
1340 urls = value[media];
1341 for ( i = 0; i < urls.length; i += 1 ) {
1342 addLink( media, urls[i] );
1350 cssHandlesRegistered = true;
1355 * Adds a dependencies to the queue with optional callbacks to be run
1356 * when the dependencies are ready or fail
1359 * @param {string|string[]} dependencies Module name or array of string module names
1360 * @param {Function} [ready] Callback to execute when all dependencies are ready
1361 * @param {Function} [error] Callback to execute when any dependency fails
1362 * @param {boolean} [async=false] Whether to load modules asynchronously.
1363 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1365 function request( dependencies, ready, error, async ) {
1368 // Allow calling by single module name
1369 if ( typeof dependencies === 'string' ) {
1370 dependencies = [dependencies];
1373 // Add ready and error callbacks if they were given
1374 if ( ready !== undefined || error !== undefined ) {
1375 jobs[jobs.length] = {
1376 'dependencies': filter(
1377 ['registered', 'loading', 'loaded'],
1385 // Queue up any dependencies that are registered
1386 dependencies = filter( ['registered'], dependencies );
1387 for ( n = 0; n < dependencies.length; n += 1 ) {
1388 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1389 queue[queue.length] = dependencies[n];
1391 // Mark this module as async in the registry
1392 registry[dependencies[n]].async = true;
1401 function sortQuery( o ) {
1407 if ( hasOwn.call( o, key ) ) {
1412 for ( key = 0; key < a.length; key += 1 ) {
1413 sorted[a[key]] = o[a[key]];
1419 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1420 * to a query string of the form foo.bar,baz|bar.baz,quux
1423 function buildModulesString( moduleMap ) {
1427 for ( prefix in moduleMap ) {
1428 p = prefix === '' ? '' : prefix + '.';
1429 arr.push( p + moduleMap[prefix].join( ',' ) );
1431 return arr.join( '|' );
1435 * Asynchronously append a script tag to the end of the body
1436 * that invokes load.php
1438 * @param {Object} moduleMap Module map, see #buildModulesString
1439 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1440 * @param {string} sourceLoadScript URL of load.php
1441 * @param {boolean} async Whether to load modules asynchronously.
1442 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1444 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1445 var request = $.extend(
1446 { modules: buildModulesString( moduleMap ) },
1449 request = sortQuery( request );
1451 // Append &* to satisfy load.php's WebRequest::checkUrlExtension test. This script
1452 // isn't actually used in IE6, but MediaWiki enforces it in general.
1453 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1457 * Resolve indexed dependencies.
1459 * ResourceLoader uses an optimization to save space which replaces module names in
1460 * dependency lists with the index of that module within the array of module
1461 * registration data if it exists. The benefit is a significant reduction in the data
1462 * size of the startup module. This function changes those dependency lists back to
1463 * arrays of strings.
1465 * @param {Array} modules Modules array
1467 function resolveIndexedDependencies( modules ) {
1468 var i, iLen, j, jLen, module, dependency;
1470 // Expand indexed dependency names
1471 for ( i = 0, iLen = modules.length; i < iLen; i++ ) {
1472 module = modules[i];
1474 for ( j = 0, jLen = module[2].length; j < jLen; j++ ) {
1475 dependency = module[2][j];
1476 if ( typeof dependency === 'number' ) {
1477 module[2][j] = modules[dependency][0];
1484 /* Public Members */
1487 * The module registry is exposed as an aid for debugging and inspecting page
1488 * state; it is not a public interface for modifying the registry.
1494 moduleRegistry: registry,
1497 * @inheritdoc #newStyleTag
1500 addStyleTag: newStyleTag,
1503 * Batch-request queued dependencies from the server.
1506 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1507 source, concatSource, origBatch, group, g, i, modules, maxVersion, sourceLoadScript,
1508 currReqBase, currReqBaseLength, moduleMap, l,
1509 lastDotIndex, prefix, suffix, bytesAdded, async;
1511 // Build a list of request parameters common to all requests.
1513 skin: mw.config.get( 'skin' ),
1514 lang: mw.config.get( 'wgUserLanguage' ),
1515 debug: mw.config.get( 'debug' )
1517 // Split module batch by source and by group.
1519 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1521 // Appends a list of modules from the queue to the batch
1522 for ( q = 0; q < queue.length; q += 1 ) {
1523 // Only request modules which are registered
1524 if ( hasOwn.call( registry, queue[q] ) && registry[queue[q]].state === 'registered' ) {
1525 // Prevent duplicate entries
1526 if ( $.inArray( queue[q], batch ) === -1 ) {
1527 batch[batch.length] = queue[q];
1528 // Mark registered modules as loading
1529 registry[queue[q]].state = 'loading';
1534 mw.loader.store.init();
1535 if ( mw.loader.store.enabled ) {
1538 batch = $.grep( batch, function ( module ) {
1539 var source = mw.loader.store.get( module );
1541 concatSource.push( source );
1547 $.globalEval( concatSource.join( ';' ) );
1549 // Not good, the cached mw.loader.implement calls failed! This should
1550 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1551 // Depending on how corrupt the string is, it is likely that some
1552 // modules' implement() succeeded while the ones after the error will
1553 // never run and leave their modules in the 'loading' state forever.
1555 // Since this is an error not caused by an individual module but by
1556 // something that infected the implement call itself, don't take any
1557 // risks and clear everything in this cache.
1558 mw.loader.store.clear();
1559 // Re-add the ones still pending back to the batch and let the server
1560 // repopulate these modules to the cache.
1561 // This means that at most one module will be useless (the one that had
1562 // the error) instead of all of them.
1563 log( 'Error while evaluating data from mw.loader.store', err );
1564 origBatch = $.grep( origBatch, function ( module ) {
1565 return registry[module].state === 'loading';
1567 batch = batch.concat( origBatch );
1571 // Early exit if there's nothing to load...
1572 if ( !batch.length ) {
1576 // The queue has been processed into the batch, clear up the queue.
1579 // Always order modules alphabetically to help reduce cache
1580 // misses for otherwise identical content.
1583 // Split batch by source and by group.
1584 for ( b = 0; b < batch.length; b += 1 ) {
1585 bSource = registry[batch[b]].source;
1586 bGroup = registry[batch[b]].group;
1587 if ( !hasOwn.call( splits, bSource ) ) {
1588 splits[bSource] = {};
1590 if ( !hasOwn.call( splits[bSource], bGroup ) ) {
1591 splits[bSource][bGroup] = [];
1593 bSourceGroup = splits[bSource][bGroup];
1594 bSourceGroup[bSourceGroup.length] = batch[b];
1597 // Clear the batch - this MUST happen before we append any
1598 // script elements to the body or it's possible that a script
1599 // will be locally cached, instantly load, and work the batch
1600 // again, all before we've cleared it causing each request to
1601 // include modules which are already loaded.
1604 for ( source in splits ) {
1606 sourceLoadScript = sources[source];
1608 for ( group in splits[source] ) {
1610 // Cache access to currently selected list of
1611 // modules for this group from this source.
1612 modules = splits[source][group];
1614 // Calculate the highest timestamp
1616 for ( g = 0; g < modules.length; g += 1 ) {
1617 if ( registry[modules[g]].version > maxVersion ) {
1618 maxVersion = registry[modules[g]].version;
1622 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1623 // For user modules append a user name to the request.
1624 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1625 currReqBase.user = mw.config.get( 'wgUserName' );
1627 currReqBaseLength = $.param( currReqBase ).length;
1629 // We may need to split up the request to honor the query string length limit,
1630 // so build it piece by piece.
1631 l = currReqBaseLength + 9; // '&modules='.length == 9
1633 moduleMap = {}; // { prefix: [ suffixes ] }
1635 for ( i = 0; i < modules.length; i += 1 ) {
1636 // Determine how many bytes this module would add to the query string
1637 lastDotIndex = modules[i].lastIndexOf( '.' );
1639 // If lastDotIndex is -1, substr() returns an empty string
1640 prefix = modules[i].substr( 0, lastDotIndex );
1641 suffix = modules[i].slice( lastDotIndex + 1 );
1643 bytesAdded = hasOwn.call( moduleMap, prefix )
1644 ? suffix.length + 3 // '%2C'.length == 3
1645 : modules[i].length + 3; // '%7C'.length == 3
1647 // If the request would become too long, create a new one,
1648 // but don't create empty requests
1649 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1650 // This request would become too long, create a new one
1651 // and fire off the old one
1652 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1655 l = currReqBaseLength + 9;
1657 if ( !hasOwn.call( moduleMap, prefix ) ) {
1658 moduleMap[prefix] = [];
1660 moduleMap[prefix].push( suffix );
1661 if ( !registry[modules[i]].async ) {
1662 // If this module is blocking, make the entire request blocking
1663 // This is slightly suboptimal, but in practice mixing of blocking
1664 // and async modules will only occur in debug mode.
1669 // If there's anything left in moduleMap, request that too
1670 if ( !$.isEmptyObject( moduleMap ) ) {
1671 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1678 * Register a source.
1680 * The #work method will use this information to split up requests by source.
1682 * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1684 * @param {string} id Short string representing a source wiki, used internally for
1685 * registered modules to indicate where they should be loaded from (usually lowercase a-z).
1686 * @param {Object|string} loadUrl load.php url, may be an object for backwards-compatibility
1689 addSource: function ( id, loadUrl ) {
1691 // Allow multiple additions
1692 if ( typeof id === 'object' ) {
1693 for ( source in id ) {
1694 mw.loader.addSource( source, id[source] );
1699 if ( hasOwn.call( sources, id ) ) {
1700 throw new Error( 'source already registered: ' + id );
1703 if ( typeof loadUrl === 'object' ) {
1704 loadUrl = loadUrl.loadScript;
1707 sources[id] = loadUrl;
1713 * Register a module, letting the system know about it and its
1714 * properties. Startup modules contain calls to this function.
1716 * When using multiple module registration by passing an array, dependencies that
1717 * are specified as references to modules within the array will be resolved before
1718 * the modules are registered.
1720 * @param {string|Array} module Module name or array of arrays, each containing
1721 * a list of arguments compatible with this method
1722 * @param {number} version Module version number as a timestamp (falls backs to 0)
1723 * @param {string|Array|Function} dependencies One string or array of strings of module
1724 * names on which this module depends, or a function that returns that array.
1725 * @param {string} [group=null] Group which the module is in
1726 * @param {string} [source='local'] Name of the source
1727 * @param {string} [skip=null] Script body of the skip function
1729 register: function ( module, version, dependencies, group, source, skip ) {
1731 // Allow multiple registration
1732 if ( typeof module === 'object' ) {
1733 resolveIndexedDependencies( module );
1734 for ( i = 0, len = module.length; i < len; i++ ) {
1735 // module is an array of module names
1736 if ( typeof module[i] === 'string' ) {
1737 mw.loader.register( module[i] );
1738 // module is an array of arrays
1739 } else if ( typeof module[i] === 'object' ) {
1740 mw.loader.register.apply( mw.loader, module[i] );
1746 if ( typeof module !== 'string' ) {
1747 throw new Error( 'module must be a string, not a ' + typeof module );
1749 if ( hasOwn.call( registry, module ) ) {
1750 throw new Error( 'module already registered: ' + module );
1752 // List the module as registered
1753 registry[module] = {
1754 version: version !== undefined ? parseInt( version, 10 ) : 0,
1756 group: typeof group === 'string' ? group : null,
1757 source: typeof source === 'string' ? source : 'local',
1758 state: 'registered',
1759 skip: typeof skip === 'string' ? skip : null
1761 if ( typeof dependencies === 'string' ) {
1762 // Allow dependencies to be given as a single module name
1763 registry[module].dependencies = [ dependencies ];
1764 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1765 // Allow dependencies to be given as an array of module names
1766 // or a function which returns an array
1767 registry[module].dependencies = dependencies;
1772 * Implement a module given the components that make up the module.
1774 * When #load or #using requests one or more modules, the server
1775 * response contain calls to this function.
1777 * All arguments are required.
1779 * @param {string} module Name of module
1780 * @param {Function|Array} script Function with module code or Array of URLs to
1781 * be used as the src attribute of a new `<script>` tag.
1782 * @param {Object} [style] Should follow one of the following patterns:
1784 * { "css": [css, ..] }
1785 * { "url": { <media>: [url, ..] } }
1787 * And for backwards compatibility (needs to be supported forever due to caching):
1790 * { <media>: [url, ..] }
1792 * The reason css strings are not concatenated anymore is bug 31676. We now check
1793 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1795 * @param {Object} [msgs] List of key/value pairs to be added to mw#messages.
1796 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1798 implement: function ( module, script, style, msgs, templates ) {
1800 if ( typeof module !== 'string' ) {
1801 throw new Error( 'module must be of type string, not ' + typeof module );
1803 if ( script && !$.isFunction( script ) && !$.isArray( script ) ) {
1804 throw new Error( 'script must be of type function or array, not ' + typeof script );
1806 if ( style && !$.isPlainObject( style ) ) {
1807 throw new Error( 'style must be of type object, not ' + typeof style );
1809 if ( msgs && !$.isPlainObject( msgs ) ) {
1810 throw new Error( 'msgs must be of type object, not a ' + typeof msgs );
1812 if ( templates && !$.isPlainObject( templates ) ) {
1813 throw new Error( 'templates must be of type object, not a ' + typeof templates );
1815 // Automatically register module
1816 if ( !hasOwn.call( registry, module ) ) {
1817 mw.loader.register( module );
1819 // Check for duplicate implementation
1820 if ( hasOwn.call( registry, module ) && registry[module].script !== undefined ) {
1821 throw new Error( 'module already implemented: ' + module );
1823 // Attach components
1824 registry[module].script = script || [];
1825 registry[module].style = style || {};
1826 registry[module].messages = msgs || {};
1827 registry[module].templates = templates || {};
1828 // The module may already have been marked as erroneous
1829 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1830 registry[module].state = 'loaded';
1831 if ( allReady( registry[module].dependencies ) ) {
1838 * Execute a function as soon as one or more required modules are ready.
1840 * Example of inline dependency on OOjs:
1842 * mw.loader.using( 'oojs', function () {
1843 * OO.compare( [ 1 ], [ 1 ] );
1846 * @param {string|Array} dependencies Module name or array of modules names the callback
1847 * dependends on to be ready before executing
1848 * @param {Function} [ready] Callback to execute when all dependencies are ready
1849 * @param {Function} [error] Callback to execute if one or more dependencies failed
1850 * @return {jQuery.Promise}
1851 * @since 1.23 this returns a promise
1853 using: function ( dependencies, ready, error ) {
1854 var deferred = $.Deferred();
1856 // Allow calling with a single dependency as a string
1857 if ( typeof dependencies === 'string' ) {
1858 dependencies = [ dependencies ];
1859 } else if ( !$.isArray( dependencies ) ) {
1861 throw new Error( 'Dependencies must be a string or an array' );
1865 deferred.done( ready );
1868 deferred.fail( error );
1871 // Resolve entire dependency map
1872 dependencies = resolve( dependencies );
1873 if ( allReady( dependencies ) ) {
1874 // Run ready immediately
1876 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1877 // Execute error immediately if any dependencies have errors
1879 new Error( 'One or more dependencies failed to load' ),
1883 // Not all dependencies are ready: queue up a request
1884 request( dependencies, deferred.resolve, deferred.reject );
1887 return deferred.promise();
1891 * Load an external script or one or more modules.
1893 * @param {string|Array} modules Either the name of a module, array of modules,
1894 * or a URL of an external script or style
1895 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1896 * external script or style; acceptable values are "text/css" and
1897 * "text/javascript"; if no type is provided, text/javascript is assumed.
1898 * @param {boolean} [async] Whether to load modules asynchronously.
1899 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1900 * Defaults to `true` if loading a URL, `false` otherwise.
1902 load: function ( modules, type, async ) {
1903 var filtered, m, module, l;
1906 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1907 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1909 // Allow calling with an external url or single dependency as a string
1910 if ( typeof modules === 'string' ) {
1911 if ( /^(https?:)?\/\//.test( modules ) ) {
1912 if ( async === undefined ) {
1913 // Assume async for bug 34542
1916 if ( type === 'text/css' ) {
1918 // Use properties instead of attributes as IE throws security
1919 // warnings when inserting a <link> tag with a protocol-relative
1920 // URL set though attributes - when on HTTPS. See bug 41331.
1921 l = document.createElement( 'link' );
1922 l.rel = 'stylesheet';
1924 $( 'head' ).append( l );
1927 if ( type === 'text/javascript' || type === undefined ) {
1928 addScript( modules, null, async );
1932 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1934 // Called with single module
1935 modules = [ modules ];
1938 // Filter out undefined modules, otherwise resolve() will throw
1939 // an exception for trying to load an undefined module.
1940 // Undefined modules are acceptable here in load(), because load() takes
1941 // an array of unrelated modules, whereas the modules passed to
1942 // using() are related and must all be loaded.
1943 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1944 if ( hasOwn.call( registry, modules[m] ) ) {
1945 module = registry[modules[m]];
1946 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1947 filtered[filtered.length] = modules[m];
1952 if ( filtered.length === 0 ) {
1955 // Resolve entire dependency map
1956 filtered = resolve( filtered );
1957 // If all modules are ready, nothing to be done
1958 if ( allReady( filtered ) ) {
1961 // If any modules have errors: also quit.
1962 if ( filter( ['error', 'missing'], filtered ).length ) {
1965 // Since some modules are not yet ready, queue up a request.
1966 request( filtered, undefined, undefined, async );
1970 * Change the state of one or more modules.
1972 * @param {string|Object} module Module name or object of module name/state pairs
1973 * @param {string} state State name
1975 state: function ( module, state ) {
1978 if ( typeof module === 'object' ) {
1979 for ( m in module ) {
1980 mw.loader.state( m, module[m] );
1984 if ( !hasOwn.call( registry, module ) ) {
1985 mw.loader.register( module );
1987 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1988 && registry[module].state !== state ) {
1989 // Make sure pending modules depending on this one get executed if their
1990 // dependencies are now fulfilled!
1991 registry[module].state = state;
1992 handlePending( module );
1994 registry[module].state = state;
1999 * Get the version of a module.
2001 * @param {string} module Name of module
2002 * @return {string|null} The version, or null if the module (or its version) is not
2005 getVersion: function ( module ) {
2006 if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) {
2009 return formatVersionNumber( registry[module].version );
2013 * Get the state of a module.
2015 * @param {string} module Name of module
2016 * @return {string|null} The state, or null if the module (or its state) is not
2019 getState: function ( module ) {
2020 if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) {
2023 return registry[module].state;
2027 * Get the names of all registered modules.
2031 getModuleNames: function () {
2032 return $.map( registry, function ( i, key ) {
2038 * @inheritdoc mw.inspect#runReports
2041 inspect: function () {
2042 var args = slice.call( arguments );
2043 mw.loader.using( 'mediawiki.inspect', function () {
2044 mw.inspect.runReports.apply( mw.inspect, args );
2049 * On browsers that implement the localStorage API, the module store serves as a
2050 * smart complement to the browser cache. Unlike the browser cache, the module store
2051 * can slice a concatenated response from ResourceLoader into its constituent
2052 * modules and cache each of them separately, using each module's versioning scheme
2053 * to determine when the cache should be invalidated.
2056 * @class mw.loader.store
2059 // Whether the store is in use on this page.
2062 // The contents of the store, mapping '[module name]@[version]' keys
2063 // to module implementations.
2067 stats: { hits: 0, misses: 0, expired: 0 },
2070 * Construct a JSON-serializable object representing the content of the store.
2071 * @return {Object} Module store contents.
2073 toJSON: function () {
2074 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
2078 * Get the localStorage key for the entire module store. The key references
2079 * $wgDBname to prevent clashes between wikis which share a common host.
2081 * @return {string} localStorage item key
2083 getStoreKey: function () {
2084 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
2088 * Get a key on which to vary the module cache.
2089 * @return {string} String of concatenated vary conditions.
2091 getVary: function () {
2093 mw.config.get( 'skin' ),
2094 mw.config.get( 'wgResourceLoaderStorageVersion' ),
2095 mw.config.get( 'wgUserLanguage' )
2100 * Get a key for a specific module. The key format is '[name]@[version]'.
2102 * @param {string} module Module name
2103 * @return {string|null} Module key or null if module does not exist
2105 getModuleKey: function ( module ) {
2106 return hasOwn.call( registry, module ) ?
2107 ( module + '@' + registry[module].version ) : null;
2111 * Initialize the store.
2113 * Retrieves store from localStorage and (if successfully retrieved) decoding
2114 * the stored JSON value to a plain object.
2116 * The try / catch block is used for JSON & localStorage feature detection.
2117 * See the in-line documentation for Modernizr's localStorage feature detection
2118 * code for a full account of why we need a try / catch:
2119 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
2124 if ( mw.loader.store.enabled !== null ) {
2129 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) {
2130 // Disabled by configuration.
2131 // Clear any previous store to free up space. (T66721)
2132 mw.loader.store.clear();
2133 mw.loader.store.enabled = false;
2136 if ( mw.config.get( 'debug' ) ) {
2137 // Disable module store in debug mode
2138 mw.loader.store.enabled = false;
2143 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2144 // If we get here, localStorage is available; mark enabled
2145 mw.loader.store.enabled = true;
2146 data = JSON.parse( raw );
2147 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2148 mw.loader.store.items = data.items;
2152 log( 'Storage error', e );
2155 if ( raw === undefined ) {
2156 // localStorage failed; disable store
2157 mw.loader.store.enabled = false;
2159 mw.loader.store.update();
2164 * Retrieve a module from the store and update cache hit stats.
2166 * @param {string} module Module name
2167 * @return {string|boolean} Module implementation or false if unavailable
2169 get: function ( module ) {
2172 if ( !mw.loader.store.enabled ) {
2176 key = mw.loader.store.getModuleKey( module );
2177 if ( key in mw.loader.store.items ) {
2178 mw.loader.store.stats.hits++;
2179 return mw.loader.store.items[key];
2181 mw.loader.store.stats.misses++;
2186 * Stringify a module and queue it for storage.
2188 * @param {string} module Module name
2189 * @param {Object} descriptor The module's descriptor as set in the registry
2191 set: function ( module, descriptor ) {
2194 if ( !mw.loader.store.enabled ) {
2198 key = mw.loader.store.getModuleKey( module );
2201 // Already stored a copy of this exact version
2202 key in mw.loader.store.items ||
2203 // Module failed to load
2204 descriptor.state !== 'ready' ||
2205 // Unversioned, private, or site-/user-specific
2206 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
2207 // Partial descriptor
2208 $.inArray( undefined, [ descriptor.script, descriptor.style,
2209 descriptor.messages, descriptor.templates ] ) !== -1
2217 JSON.stringify( module ),
2218 typeof descriptor.script === 'function' ?
2219 String( descriptor.script ) :
2220 JSON.stringify( descriptor.script ),
2221 JSON.stringify( descriptor.style ),
2222 JSON.stringify( descriptor.messages ),
2223 JSON.stringify( descriptor.templates )
2225 // Attempted workaround for a possible Opera bug (bug 57567).
2226 // This regex should never match under sane conditions.
2227 if ( /^\s*\(/.test( args[1] ) ) {
2228 args[1] = 'function' + args[1];
2229 log( 'Detected malformed function stringification (bug 57567)' );
2232 log( 'Storage error', e );
2236 mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');';
2237 mw.loader.store.update();
2241 * Iterate through the module store, removing any item that does not correspond
2242 * (in name and version) to an item in the module registry.
2244 prune: function () {
2247 if ( !mw.loader.store.enabled ) {
2251 for ( key in mw.loader.store.items ) {
2252 module = key.slice( 0, key.indexOf( '@' ) );
2253 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2254 mw.loader.store.stats.expired++;
2255 delete mw.loader.store.items[key];
2261 * Clear the entire module store right now.
2263 clear: function () {
2264 mw.loader.store.items = {};
2265 localStorage.removeItem( mw.loader.store.getStoreKey() );
2269 * Sync modules to localStorage.
2271 * This function debounces localStorage updates. When called multiple times in
2272 * quick succession, the calls are coalesced into a single update operation.
2273 * This allows us to call #update without having to consider the module load
2274 * queue; the call to localStorage.setItem will be naturally deferred until the
2275 * page is quiescent.
2277 * Because localStorage is shared by all pages with the same origin, if multiple
2278 * pages are loaded with different module sets, the possibility exists that
2279 * modules saved by one page will be clobbered by another. But the impact would
2280 * be minor and the problem would be corrected by subsequent page views.
2284 update: ( function () {
2289 key = mw.loader.store.getStoreKey();
2291 if ( !mw.loader.store.enabled ) {
2294 mw.loader.store.prune();
2296 // Replacing the content of the module store might fail if the new
2297 // contents would exceed the browser's localStorage size limit. To
2298 // avoid clogging the browser with stale data, always remove the old
2299 // value before attempting to set the new one.
2300 localStorage.removeItem( key );
2301 data = JSON.stringify( mw.loader.store );
2302 localStorage.setItem( key, data );
2304 log( 'Storage error', e );
2308 return function () {
2309 clearTimeout( timer );
2310 timer = setTimeout( flush, 2000 );
2318 * HTML construction helper functions
2325 * output = Html.element( 'div', {}, new Html.Raw(
2326 * Html.element( 'img', { src: '<' } )
2328 * mw.log( output ); // <div><img src="<"/></div>
2333 html: ( function () {
2334 function escapeCallback( s ) {
2351 * Escape a string for HTML.
2353 * Converts special characters to HTML entities.
2355 * mw.html.escape( '< > \' & "' );
2356 * // Returns < > ' & "
2358 * @param {string} s The string to escape
2359 * @return {string} HTML
2361 escape: function ( s ) {
2362 return s.replace( /['"<>&]/g, escapeCallback );
2366 * Create an HTML element string, with safe escaping.
2368 * @param {string} name The tag name.
2369 * @param {Object} attrs An object with members mapping element names to values
2370 * @param {Mixed} contents The contents of the element. May be either:
2372 * - string: The string is escaped.
2373 * - null or undefined: The short closing form is used, e.g. `<br/>`.
2374 * - this.Raw: The value attribute is included without escaping.
2375 * - this.Cdata: The value attribute is included, and an exception is
2376 * thrown if it contains an illegal ETAGO delimiter.
2377 * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
2378 * @return {string} HTML
2380 element: function ( name, attrs, contents ) {
2381 var v, attrName, s = '<' + name;
2383 for ( attrName in attrs ) {
2384 v = attrs[attrName];
2385 // Convert name=true, to name=name
2389 } else if ( v === false ) {
2392 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().
2431 * @class mw.html.Raw
2433 Raw: function ( value ) {
2438 * Wrapper object for CDATA element contents passed to mw.html.element()
2439 * @class mw.html.Cdata
2441 Cdata: function ( value ) {
2447 // Skeleton user object. mediawiki.user.js extends this
2454 * Registry and firing of events.
2456 * MediaWiki has various interface components that are extended, enhanced
2457 * or manipulated in some other way by extensions, gadgets and even
2460 * This framework helps streamlining the timing of when these other
2461 * code paths fire their plugins (instead of using document-ready,
2462 * which can and should be limited to firing only once).
2464 * Features like navigating to other wiki pages, previewing an edit
2465 * and editing itself – without a refresh – can then retrigger these
2466 * hooks accordingly to ensure everything still works as expected.
2470 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2471 * mw.hook( 'wikipage.content' ).fire( $content );
2473 * Handlers can be added and fired for arbitrary event names at any time. The same
2474 * event can be fired multiple times. The last run of an event is memorized
2475 * (similar to `$(document).ready` and `$.Deferred().done`).
2476 * This means if an event is fired, and a handler added afterwards, the added
2477 * function will be fired right away with the last given event data.
2479 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2480 * Thus allowing flexible use and optimal maintainability and authority control.
2481 * You can pass around the `add` and/or `fire` method to another piece of code
2482 * without it having to know the event name (or `mw.hook` for that matter).
2484 * var h = mw.hook( 'bar.ready' );
2485 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2487 * Note: Events are documented with an underscore instead of a dot in the event
2488 * name due to jsduck not supporting dots in that position.
2492 hook: ( function () {
2496 * Create an instance of mw.hook.
2500 * @param {string} name Name of hook.
2503 return function ( name ) {
2504 var list = hasOwn.call( lists, name ) ?
2506 lists[name] = $.Callbacks( 'memory' );
2510 * Register a hook handler
2511 * @param {Function...} handler Function to bind.
2517 * Unregister a hook handler
2518 * @param {Function...} handler Function to unbind.
2521 remove: list.remove,
2525 * @param {Mixed...} data
2529 return list.fireWith.call( this, null, slice.call( arguments ) );
2536 // Alias $j to jQuery for backwards compatibility
2537 // @deprecated since 1.23 Use $ or jQuery instead
2538 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2540 // Attach to window and globally alias
2541 window.mw = window.mediaWiki = mw;