2 * Base library for MediaWiki.
4 * Exposed as globally as `mediaWiki` with `mw` as shortcut.
7 * @alternateClassName mediaWiki
16 hasOwn = Object.prototype.hasOwnProperty,
17 slice = Array.prototype.slice,
18 trackCallbacks = $.Callbacks( 'memory' ),
22 * Log a message to window.console, if possible. Useful to force logging of some
23 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
24 * Gets console references in each invocation, so that delayed debugging tools work
25 * fine. No need for optimization here, which would only result in losing logs.
29 * @param {string} msg text for the log entry.
32 function log( msg, e ) {
33 var console = window.console;
34 if ( console && console.log ) {
36 // If we have an exception object, log it through .error() to trigger
37 // proper stacktraces in browsers that support it. There are no (known)
38 // browsers that don't support .error(), that do support .log() and
39 // have useful exception handling through .log().
40 if ( e && console.error ) {
41 console.error( String( e ), e );
46 /* Object constructors */
49 * Creates an object that can be read from or written to from prototype functions
50 * that allow both single and multiple variables at once.
54 * var addies, wanted, results;
56 * // Create your address book
57 * addies = new mw.Map();
59 * // This data could be coming from an external source (eg. API/AJAX)
61 * 'John Doe' : '10 Wall Street, New York, USA',
62 * 'Jane Jackson' : '21 Oxford St, London, UK',
63 * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
66 * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
68 * // You can detect missing keys first
69 * if ( !addies.exists( wanted ) ) {
70 * // One or more are missing (in this case: "George Johnson")
71 * mw.log( 'One or more names were not found in your address book' );
74 * // Or just let it give you what it can
75 * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
76 * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
77 * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
82 * @param {Object|boolean} [values] Value-bearing object to map, or boolean
83 * true to map over the global object. Defaults to an empty object.
85 function Map( values ) {
86 this.values = values === true ? window : ( values || {} );
92 * Get the value of one or multiple a keys.
94 * If called with no arguments, all values will be returned.
96 * @param {string|Array} selection String key or array of keys to get values for.
97 * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
98 * @return mixed If selection was a string returns the value or null,
99 * If selection was an array, returns an object of key/values (value is null if not found),
100 * If selection was not passed or invalid, will return the 'values' object member (be careful as
101 * objects are always passed by reference in JavaScript!).
102 * @return {string|Object|null} Values as a string or object, null if invalid/inexistant.
104 get: function ( selection, fallback ) {
106 // If we only do this in the `return` block, it'll fail for the
107 // call to get() from the mutli-selection block.
108 fallback = arguments.length > 1 ? fallback : null;
110 if ( $.isArray( selection ) ) {
111 selection = slice.call( selection );
113 for ( i = 0; i < selection.length; i++ ) {
114 results[selection[i]] = this.get( selection[i], fallback );
119 if ( typeof selection === 'string' ) {
120 if ( !hasOwn.call( this.values, selection ) ) {
123 return this.values[selection];
126 if ( selection === undefined ) {
130 // invalid selection key
135 * Sets one or multiple key/value pairs.
137 * @param {string|Object} selection String key to set value for, or object mapping keys to values.
138 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
139 * @return {Boolean} This returns true on success, false on failure.
141 set: function ( selection, value ) {
144 if ( $.isPlainObject( selection ) ) {
145 for ( s in selection ) {
146 this.values[s] = selection[s];
150 if ( typeof selection === 'string' && arguments.length > 1 ) {
151 this.values[selection] = value;
158 * Checks if one or multiple keys exist.
160 * @param {Mixed} selection String key or array of keys to check
161 * @return {boolean} Existence of key(s)
163 exists: function ( selection ) {
166 if ( $.isArray( selection ) ) {
167 for ( s = 0; s < selection.length; s++ ) {
168 if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
174 return typeof selection === 'string' && hasOwn.call( this.values, selection );
179 * Object constructor for messages.
181 * Similar to the Message class in MediaWiki PHP.
183 * Format defaults to 'text'.
189 * 'hello': 'Hello world',
190 * 'hello-user': 'Hello, $1!',
191 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
194 * obj = new mw.Message( mw.messages, 'hello' );
195 * mw.log( obj.text() );
198 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
199 * mw.log( obj.text() );
200 * // Hello, John Doe!
202 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
203 * mw.log( obj.text() );
204 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
206 * // Using mw.message shortcut
207 * obj = mw.message( 'hello-user', 'John Doe' );
208 * mw.log( obj.text() );
209 * // Hello, John Doe!
211 * // Using mw.msg shortcut
212 * str = mw.msg( 'hello-user', 'John Doe' );
214 * // Hello, John Doe!
216 * // Different formats
217 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
219 * obj.format = 'text';
220 * str = obj.toString();
225 * // Hello, John "Wiki" <3 Doe!
227 * mw.log( obj.escaped() );
228 * // Hello, John "Wiki" <3 Doe!
233 * @param {mw.Map} map Message storage
234 * @param {string} key
235 * @param {Array} [parameters]
237 function Message( map, key, parameters ) {
238 this.format = 'text';
241 this.parameters = parameters === undefined ? [] : slice.call( parameters );
245 Message.prototype = {
247 * Simple message parser, does $N replacement and nothing else.
249 * This may be overridden to provide a more complex message parser.
251 * The primary override is in mediawiki.jqueryMsg.
253 * This function will not be called for nonexistent messages.
255 parser: function () {
256 var parameters = this.parameters;
257 return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
258 var index = parseInt( match, 10 ) - 1;
259 return parameters[index] !== undefined ? parameters[index] : '$' + match;
264 * Appends (does not replace) parameters for replacement to the .parameters property.
266 * @param {Array} parameters
269 params: function ( parameters ) {
271 for ( i = 0; i < parameters.length; i += 1 ) {
272 this.parameters.push( parameters[i] );
278 * Converts message object to its string form based on the state of format.
280 * @return {string} Message as a string in the current form or `<key>` if key does not exist.
282 toString: function () {
285 if ( !this.exists() ) {
286 // Use <key> as text if key does not exist
287 if ( this.format === 'escaped' || this.format === 'parse' ) {
288 // format 'escaped' and 'parse' need to have the brackets and key html escaped
289 return mw.html.escape( '<' + this.key + '>' );
291 return '<' + this.key + '>';
294 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
295 text = this.parser();
298 if ( this.format === 'escaped' ) {
299 text = this.parser();
300 text = mw.html.escape( text );
307 * Changes format to 'parse' and converts message to string
309 * If jqueryMsg is loaded, this parses the message text from wikitext
310 * (where supported) to HTML
312 * Otherwise, it is equivalent to plain.
314 * @return {string} String form of parsed message
317 this.format = 'parse';
318 return this.toString();
322 * Changes format to 'plain' and converts message to string
324 * This substitutes parameters, but otherwise does not change the
327 * @return {string} String form of plain message
330 this.format = 'plain';
331 return this.toString();
335 * Changes format to 'text' and converts message to string
337 * If jqueryMsg is loaded, {{-transformation is done where supported
338 * (such as {{plural:}}, {{gender:}}, {{int:}}).
340 * Otherwise, it is equivalent to plain.
343 this.format = 'text';
344 return this.toString();
348 * Changes the format to 'escaped' and converts message to string
350 * This is equivalent to using the 'text' format (see text method), then
351 * HTML-escaping the output.
353 * @return {string} String form of html escaped message
355 escaped: function () {
356 this.format = 'escaped';
357 return this.toString();
361 * Checks if message exists
366 exists: function () {
367 return this.map.exists( this.key );
378 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
380 * On browsers that implement the Navigation Timing API, this function will produce floating-point
381 * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
382 * it will fall back to using `Date`.
384 * @return {number} Current time
387 var perf = window.performance,
388 navStart = perf && perf.timing && perf.timing.navigationStart;
389 return navStart && typeof perf.now === 'function' ?
390 function () { return navStart + perf.now(); } :
391 function () { return +new Date(); };
395 * Track an analytic event.
397 * This method provides a generic means for MediaWiki JavaScript code to capture state
398 * information for analysis. Each logged event specifies a string topic name that describes
399 * the kind of event that it is. Topic names consist of dot-separated path components,
400 * arranged from most general to most specific. Each path component should have a clear and
401 * well-defined purpose.
403 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
404 * events that match their subcription, including those that fired before the handler was
407 * @param {string} topic Topic name
408 * @param {Object} [data] Data describing the event, encoded as an object
410 track: function ( topic, data ) {
411 trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
412 trackCallbacks.fire( trackQueue );
416 * Register a handler for subset of analytic events, specified by topic
418 * Handlers will be called once for each tracked event, including any events that fired before the
419 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
420 * the exact time at which the event fired, a string 'topic' property naming the event, and a
421 * 'data' property which is an object of event-specific data. The event topic and event data are
422 * also passed to the callback as the first and second arguments, respectively.
424 * @param {string} topic Handle events whose name starts with this string prefix
425 * @param {Function} callback Handler to call for each matching tracked event
427 trackSubscribe: function ( topic, callback ) {
430 trackCallbacks.add( function ( trackQueue ) {
432 for ( ; seen < trackQueue.length; seen++ ) {
433 event = trackQueue[ seen ];
434 if ( event.topic.indexOf( topic ) === 0 ) {
435 callback.call( event, event.topic, event.data );
441 // Make the Map constructor publicly available.
444 // Make the Message constructor publicly available.
448 * Map of configuration values
450 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
453 * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
454 * global `window` object.
456 * @property {mw.Map} config
458 // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule to an instance of `mw.Map`.
462 * Empty object that plugins can be installed in.
468 * Access container for deprecated functionality that can be moved from
469 * from their legacy location and attached to this object (e.g. a global
470 * function that is deprecated and as stop-gap can be exposed through here).
472 * This was reserved for future use but never ended up being used.
474 * @deprecated since 1.22 Let deprecated identifiers keep their original name
475 * and use mw.log#deprecate to create an access container for tracking.
481 * Localization system
489 * Get a message object.
491 * Shorcut for `new mw.Message( mw.messages, key, parameters )`.
494 * @param {string} key Key of message to get
495 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
496 * @return {mw.Message}
498 message: function ( key ) {
499 // Variadic arguments
500 var parameters = slice.call( arguments, 1 );
501 return new Message( mw.messages, key, parameters );
505 * Get a message string using the (default) 'text' format.
507 * Shortcut for `mw.message( key, parameters... ).text()`.
510 * @param {string} key Key of message to get
511 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
515 return mw.message.apply( mw.message, arguments ).toString();
519 * Dummy placeholder for {@link mw.log}
523 // Also update the restoration of methods in mediawiki.log.js
524 // when adding or removing methods here.
525 var log = function () {};
533 * Write a message the console's warning channel.
534 * Also logs a stacktrace for easier debugging.
535 * Each action is silently ignored if the browser doesn't support it.
537 * @param {string...} msg Messages to output to console
539 log.warn = function () {
540 var console = window.console;
541 if ( console && console.warn && console.warn.apply ) {
542 console.warn.apply( console, arguments );
543 if ( console.trace ) {
550 * Create a property in a host object that, when accessed, will produce
551 * a deprecation warning in the console with backtrace.
553 * @param {Object} obj Host object of deprecated property
554 * @param {string} key Name of property to create in `obj`
555 * @param {Mixed} val The value this property should return when accessed
556 * @param {string} [msg] Optional text to include in the deprecation message.
558 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
560 } : function ( obj, key, val, msg ) {
561 msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
563 Object.defineProperty( obj, key, {
567 mw.track( 'mw.deprecate', key );
571 set: function ( newVal ) {
572 mw.track( 'mw.deprecate', key );
578 // IE8 can throw on Object.defineProperty
587 * Client-side module loader which integrates with the MediaWiki ResourceLoader
591 loader: ( function () {
593 /* Private Members */
596 * Mapping of registered modules
598 * The jquery module is pre-registered, because it must have already
599 * been provided for this object to have been built, and in debug mode
600 * jquery would have been provided through a unique loader request,
601 * making it impossible to hold back registration of jquery until after
604 * For exact details on support for script, style and messages, look at
605 * mw.loader.implement.
610 * 'version': ############## (unix timestamp),
611 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
612 * 'group': 'somegroup', (or) null,
613 * 'source': 'local', 'someforeignwiki', (or) null
614 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
617 * 'messages': { 'key': 'value' },
626 // Mapping of sources, keyed by source-id, values are objects.
630 // 'loadScript': 'http://foo.bar/w/load.php'
635 // List of modules which will be loaded as when ready
637 // List of modules to be loaded
639 // List of callback functions waiting for modules to be ready to be called
641 // Selector cache for the marker element. Use getMarker() to get/use the marker!
643 // Buffer for addEmbeddedCSS.
645 // Callbacks for addEmbeddedCSS.
646 cssCallbacks = $.Callbacks();
648 /* Private methods */
650 function getMarker() {
656 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
657 if ( $marker.length ) {
660 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
661 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
667 * Create a new style tag and add it to the DOM.
670 * @param {string} text CSS text
671 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
672 * inserted before. Otherwise it will be appended to `<head>`.
673 * @return {HTMLElement} Reference to the created `<style>` element.
675 function newStyleTag( text, nextnode ) {
676 var s = document.createElement( 'style' );
677 // Insert into document before setting cssText (bug 33305)
679 // Must be inserted with native insertBefore, not $.fn.before.
680 // When using jQuery to insert it, like $nextnode.before( s ),
681 // then IE6 will throw "Access is denied" when trying to append
682 // to .cssText later. Some kind of weird security measure.
683 // http://stackoverflow.com/q/12586482/319266
684 // Works: jsfiddle.net/zJzMy/1
685 // Fails: jsfiddle.net/uJTQz
686 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
687 if ( nextnode.jquery ) {
688 nextnode = nextnode.get( 0 );
690 nextnode.parentNode.insertBefore( s, nextnode );
692 document.getElementsByTagName( 'head' )[0].appendChild( s );
694 if ( s.styleSheet ) {
696 s.styleSheet.cssText = text;
699 // (Safari sometimes borks on non-string values,
700 // play safe by casting to a string, just in case.)
701 s.appendChild( document.createTextNode( String( text ) ) );
707 * Checks whether it is safe to add this css to a stylesheet.
710 * @param {string} cssText
711 * @return {boolean} False if a new one must be created.
713 function canExpandStylesheetWith( cssText ) {
714 // Makes sure that cssText containing `@import`
715 // rules will end up in a new stylesheet (as those only work when
716 // placed at the start of a stylesheet; bug 35562).
717 return cssText.indexOf( '@import' ) === -1;
721 * Add a bit of CSS text to the current browser page.
723 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
724 * or create a new one based on whether the given `cssText` is safe for extension.
726 * @param {string} [cssText=cssBuffer] If called without cssText,
727 * the internal buffer will be inserted instead.
728 * @param {Function} [callback]
730 function addEmbeddedCSS( cssText, callback ) {
734 cssCallbacks.add( callback );
737 // Yield once before inserting the <style> tag. There are likely
738 // more calls coming up which we can combine this way.
739 // Appending a stylesheet and waiting for the browser to repaint
740 // is fairly expensive, this reduces it (bug 45810)
742 // Be careful not to extend the buffer with css that needs a new stylesheet
743 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
744 // Linebreak for somewhat distinguishable sections
745 // (the rl-cachekey comment separating each)
746 cssBuffer += '\n' + cssText;
747 // TODO: Use requestAnimationFrame in the future which will
748 // perform even better by not injecting styles while the browser
750 setTimeout( function () {
751 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
752 // (below version 13) has the non-standard behaviour of passing a
753 // numerical "lateness" value as first argument to this callback
754 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
760 // This is a delayed call and we got a buffer still
761 } else if ( cssBuffer ) {
765 // This is a delayed call, but buffer is already cleared by
766 // another delayed call.
770 // By default, always create a new <style>. Appending text to a <style>
771 // tag is bad as it means the contents have to be re-parsed (bug 45810).
773 // Except, of course, in IE 9 and below. In there we default to re-using and
774 // appending to a <style> tag due to the IE stylesheet limit (bug 31676).
775 if ( 'documentMode' in document && document.documentMode <= 9 ) {
777 $style = getMarker().prev();
778 // Verify that the the element before Marker actually is a
779 // <style> tag and one that came from ResourceLoader
780 // (not some other style tag or even a `<meta>` or `<script>`).
781 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
782 // There's already a dynamic <style> tag present and
783 // canExpandStylesheetWith() gave a green light to append more to it.
784 styleEl = $style.get( 0 );
785 if ( styleEl.styleSheet ) {
787 styleEl.styleSheet.cssText += cssText; // IE
789 log( 'Stylesheet error', e );
792 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
794 cssCallbacks.fire().empty();
799 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
801 cssCallbacks.fire().empty();
805 * Generates an ISO8601 "basic" string from a UNIX timestamp
808 function formatVersionNumber( timestamp ) {
810 function pad( a, b, c ) {
811 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
813 d.setTime( timestamp * 1000 );
815 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
816 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
821 * Resolves dependencies and detects circular references.
824 * @param {string} module Name of the top-level module whose dependencies shall be
825 * resolved and sorted.
826 * @param {Array} resolved Returns a topological sort of the given module and its
827 * dependencies, such that later modules depend on earlier modules. The array
828 * contains the module names. If the array contains already some module names,
829 * this function appends its result to the pre-existing array.
830 * @param {Object} [unresolved] Hash used to track the current dependency
831 * chain; used to report loops in the dependency graph.
832 * @throws {Error} If any unregistered module or a dependency loop is encountered
834 function sortDependencies( module, resolved, unresolved ) {
835 var n, deps, len, skip;
837 if ( registry[module] === undefined ) {
838 throw new Error( 'Unknown dependency: ' + module );
841 if ( registry[module].skip !== null ) {
842 /*jshint evil:true */
843 skip = new Function( registry[module].skip );
844 registry[module].skip = null;
846 registry[module].dependencies = [];
847 registry[module].state = 'ready';
848 handlePending( module );
853 // Resolves dynamic loader function and replaces it with its own results
854 if ( $.isFunction( registry[module].dependencies ) ) {
855 registry[module].dependencies = registry[module].dependencies();
856 // Ensures the module's dependencies are always in an array
857 if ( typeof registry[module].dependencies !== 'object' ) {
858 registry[module].dependencies = [registry[module].dependencies];
861 if ( $.inArray( module, resolved ) !== -1 ) {
862 // Module already resolved; nothing to do.
865 // unresolved is optional, supply it if not passed in
869 // Tracks down dependencies
870 deps = registry[module].dependencies;
872 for ( n = 0; n < len; n += 1 ) {
873 if ( $.inArray( deps[n], resolved ) === -1 ) {
874 if ( unresolved[deps[n]] ) {
876 'Circular reference detected: ' + module +
882 unresolved[module] = true;
883 sortDependencies( deps[n], resolved, unresolved );
884 delete unresolved[module];
887 resolved[resolved.length] = module;
891 * Gets a list of module names that a module depends on in their proper dependency
895 * @param {string} module Module name or array of string module names
896 * @return {Array} list of dependencies, including 'module'.
897 * @throws {Error} If circular reference is detected
899 function resolve( module ) {
902 // Allow calling with an array of module names
903 if ( $.isArray( module ) ) {
905 for ( m = 0; m < module.length; m += 1 ) {
906 sortDependencies( module[m], resolved );
911 if ( typeof module === 'string' ) {
913 sortDependencies( module, resolved );
917 throw new Error( 'Invalid module argument: ' + module );
921 * Narrows a list of module names down to those matching a specific
922 * state (see comment on top of this scope for a list of valid states).
923 * One can also filter for 'unregistered', which will return the
924 * modules names that don't have a registry entry.
927 * @param {string|string[]} states Module states to filter by
928 * @param {Array} [modules] List of module names to filter (optional, by default the entire
930 * @return {Array} List of filtered module names
932 function filter( states, modules ) {
933 var list, module, s, m;
935 // Allow states to be given as a string
936 if ( typeof states === 'string' ) {
939 // If called without a list of modules, build and use a list of all modules
941 if ( modules === undefined ) {
943 for ( module in registry ) {
944 modules[modules.length] = module;
947 // Build a list of modules which are in one of the specified states
948 for ( s = 0; s < states.length; s += 1 ) {
949 for ( m = 0; m < modules.length; m += 1 ) {
950 if ( registry[modules[m]] === undefined ) {
951 // Module does not exist
952 if ( states[s] === 'unregistered' ) {
954 list[list.length] = modules[m];
957 // Module exists, check state
958 if ( registry[modules[m]].state === states[s] ) {
960 list[list.length] = modules[m];
969 * Determine whether all dependencies are in state 'ready', which means we may
970 * execute the module or job now.
973 * @param {Array} dependencies Dependencies (module names) to be checked.
974 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
976 function allReady( dependencies ) {
977 return filter( 'ready', dependencies ).length === dependencies.length;
981 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
982 * and modules that depend upon this module. if the given module failed, propagate the 'error'
983 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
984 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
987 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
989 function handlePending( module ) {
990 var j, job, hasErrors, m, stateChange;
993 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
994 // If the current module failed, mark all dependent modules also as failed.
995 // Iterate until steady-state to propagate the error state upwards in the
999 for ( m in registry ) {
1000 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
1001 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
1002 registry[m].state = 'error';
1007 } while ( stateChange );
1010 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1011 for ( j = 0; j < jobs.length; j += 1 ) {
1012 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
1013 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
1014 // All dependencies satisfied, or some have errors
1016 jobs.splice( j, 1 );
1020 if ( $.isFunction( job.error ) ) {
1021 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
1024 if ( $.isFunction( job.ready ) ) {
1029 // A user-defined callback raised an exception.
1030 // Swallow it to protect our state machine!
1031 log( 'Exception thrown by user callback', e );
1036 if ( registry[module].state === 'ready' ) {
1037 // The current module became 'ready'. Set it in the module store, and recursively execute all
1038 // dependent modules that are loaded and now have all dependencies satisfied.
1039 mw.loader.store.set( module, registry[module] );
1040 for ( m in registry ) {
1041 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
1049 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
1050 * depending on whether document-ready has occurred yet and whether we are in async mode.
1053 * @param {string} src URL to script, will be used as the src attribute in the script tag
1054 * @param {Function} [callback] Callback which will be run when the script is done
1055 * @param {boolean} [async=false] Whether to load modules asynchronously.
1056 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1058 function addScript( src, callback, async ) {
1059 // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
1060 if ( $.isReady || async ) {
1064 // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1065 // XHR for a same domain request instead of <script>, which changes the request
1066 // headers (potentially missing a cache hit), and reduces caching in general
1067 // since browsers cache XHR much less (if at all). And XHR means we retreive
1068 // text, so we'd need to $.globalEval, which then messes up line numbers.
1072 } ).always( function () {
1078 /*jshint evil:true */
1079 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
1081 // Document.write is synchronous, so this is called when it's done.
1082 // FIXME: That's a lie. doc.write isn't actually synchronous.
1089 * Executes a loaded module, making it ready to use
1092 * @param {string} module Module name to execute
1094 function execute( module ) {
1095 var key, value, media, i, urls, cssHandle, checkCssHandles,
1096 cssHandlesRegistered = false;
1098 if ( registry[module] === undefined ) {
1099 throw new Error( 'Module has not been registered yet: ' + module );
1100 } else if ( registry[module].state === 'registered' ) {
1101 throw new Error( 'Module has not been requested from the server yet: ' + module );
1102 } else if ( registry[module].state === 'loading' ) {
1103 throw new Error( 'Module has not completed loading yet: ' + module );
1104 } else if ( registry[module].state === 'ready' ) {
1105 throw new Error( 'Module has already been executed: ' + module );
1109 * Define loop-function here for efficiency
1110 * and to avoid re-using badly scoped variables.
1113 function addLink( media, url ) {
1114 var el = document.createElement( 'link' );
1115 // For IE: Insert in document *before* setting href
1116 getMarker().before( el );
1117 el.rel = 'stylesheet';
1118 if ( media && media !== 'all' ) {
1121 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1122 // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
1126 function runScript() {
1127 var script, markModuleReady, nestedAddScript;
1129 script = registry[module].script;
1130 markModuleReady = function () {
1131 registry[module].state = 'ready';
1132 handlePending( module );
1134 nestedAddScript = function ( arr, callback, async, i ) {
1135 // Recursively call addScript() in its own callback
1136 // for each element of arr.
1137 if ( i >= arr.length ) {
1138 // We're at the end of the array
1143 addScript( arr[i], function () {
1144 nestedAddScript( arr, callback, async, i + 1 );
1148 if ( $.isArray( script ) ) {
1149 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1150 } else if ( $.isFunction( script ) ) {
1151 registry[module].state = 'ready';
1152 // Pass jQuery twice so that the signature of the closure which wraps
1153 // the script can bind both '$' and 'jQuery'.
1155 handlePending( module );
1158 // This needs to NOT use mw.log because these errors are common in production mode
1159 // and not in debug mode, such as when a symbol that should be global isn't exported
1160 log( 'Exception thrown by ' + module, e );
1161 registry[module].state = 'error';
1162 handlePending( module );
1166 // This used to be inside runScript, but since that is now fired asychronously
1167 // (after CSS is loaded) we need to set it here right away. It is crucial that
1168 // when execute() is called this is set synchronously, otherwise modules will get
1169 // executed multiple times as the registry will state that it isn't loading yet.
1170 registry[module].state = 'loading';
1172 // Add localizations to message system
1173 if ( $.isPlainObject( registry[module].messages ) ) {
1174 mw.messages.set( registry[module].messages );
1177 if ( $.isReady || registry[module].async ) {
1178 // Make sure we don't run the scripts until all (potentially asynchronous)
1179 // stylesheet insertions have completed.
1182 checkCssHandles = function () {
1183 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1184 // one of the cssHandles is fired while we're still creating more handles.
1185 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1187 runScript = undefined; // Revoke
1190 cssHandle = function () {
1191 var check = checkCssHandles;
1193 return function () {
1197 check = undefined; // Revoke
1203 // We are in blocking mode, and so we can't afford to wait for CSS
1204 cssHandle = function () {};
1206 checkCssHandles = runScript;
1209 // Process styles (see also mw.loader.implement)
1210 // * back-compat: { <media>: css }
1211 // * back-compat: { <media>: [url, ..] }
1212 // * { "css": [css, ..] }
1213 // * { "url": { <media>: [url, ..] } }
1214 if ( $.isPlainObject( registry[module].style ) ) {
1215 for ( key in registry[module].style ) {
1216 value = registry[module].style[key];
1219 if ( key !== 'url' && key !== 'css' ) {
1220 // Backwards compatibility, key is a media-type
1221 if ( typeof value === 'string' ) {
1222 // back-compat: { <media>: css }
1223 // Ignore 'media' because it isn't supported (nor was it used).
1224 // Strings are pre-wrapped in "@media". The media-type was just ""
1225 // (because it had to be set to something).
1226 // This is one of the reasons why this format is no longer used.
1227 addEmbeddedCSS( value, cssHandle() );
1229 // back-compat: { <media>: [url, ..] }
1235 // Array of css strings in key 'css',
1236 // or back-compat array of urls from media-type
1237 if ( $.isArray( value ) ) {
1238 for ( i = 0; i < value.length; i += 1 ) {
1239 if ( key === 'bc-url' ) {
1240 // back-compat: { <media>: [url, ..] }
1241 addLink( media, value[i] );
1242 } else if ( key === 'css' ) {
1243 // { "css": [css, ..] }
1244 addEmbeddedCSS( value[i], cssHandle() );
1247 // Not an array, but a regular object
1248 // Array of urls inside media-type key
1249 } else if ( typeof value === 'object' ) {
1250 // { "url": { <media>: [url, ..] } }
1251 for ( media in value ) {
1252 urls = value[media];
1253 for ( i = 0; i < urls.length; i += 1 ) {
1254 addLink( media, urls[i] );
1262 cssHandlesRegistered = true;
1267 * Adds a dependencies to the queue with optional callbacks to be run
1268 * when the dependencies are ready or fail
1271 * @param {string|string[]} dependencies Module name or array of string module names
1272 * @param {Function} [ready] Callback to execute when all dependencies are ready
1273 * @param {Function} [error] Callback to execute when any dependency fails
1274 * @param {boolean} [async=false] Whether to load modules asynchronously.
1275 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1277 function request( dependencies, ready, error, async ) {
1280 // Allow calling by single module name
1281 if ( typeof dependencies === 'string' ) {
1282 dependencies = [dependencies];
1285 // Add ready and error callbacks if they were given
1286 if ( ready !== undefined || error !== undefined ) {
1287 jobs[jobs.length] = {
1288 'dependencies': filter(
1289 ['registered', 'loading', 'loaded'],
1297 // Queue up any dependencies that are registered
1298 dependencies = filter( ['registered'], dependencies );
1299 for ( n = 0; n < dependencies.length; n += 1 ) {
1300 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1301 queue[queue.length] = dependencies[n];
1303 // Mark this module as async in the registry
1304 registry[dependencies[n]].async = true;
1313 function sortQuery( o ) {
1314 var sorted = {}, key, a = [];
1316 if ( hasOwn.call( o, key ) ) {
1321 for ( key = 0; key < a.length; key += 1 ) {
1322 sorted[a[key]] = o[a[key]];
1328 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1329 * to a query string of the form foo.bar,baz|bar.baz,quux
1332 function buildModulesString( moduleMap ) {
1333 var arr = [], p, prefix;
1334 for ( prefix in moduleMap ) {
1335 p = prefix === '' ? '' : prefix + '.';
1336 arr.push( p + moduleMap[prefix].join( ',' ) );
1338 return arr.join( '|' );
1342 * Asynchronously append a script tag to the end of the body
1343 * that invokes load.php
1345 * @param {Object} moduleMap Module map, see #buildModulesString
1346 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1347 * @param {string} sourceLoadScript URL of load.php
1348 * @param {boolean} async Whether to load modules asynchronously.
1349 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1351 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1352 var request = $.extend(
1353 { modules: buildModulesString( moduleMap ) },
1356 request = sortQuery( request );
1357 // Append &* to avoid triggering the IE6 extension check
1358 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1361 /* Public Members */
1364 * The module registry is exposed as an aid for debugging and inspecting page
1365 * state; it is not a public interface for modifying the registry.
1371 moduleRegistry: registry,
1374 * @inheritdoc #newStyleTag
1377 addStyleTag: newStyleTag,
1380 * Batch-request queued dependencies from the server.
1383 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1384 source, concatSource, origBatch, group, g, i, modules, maxVersion, sourceLoadScript,
1385 currReqBase, currReqBaseLength, moduleMap, l,
1386 lastDotIndex, prefix, suffix, bytesAdded, async;
1388 // Build a list of request parameters common to all requests.
1390 skin: mw.config.get( 'skin' ),
1391 lang: mw.config.get( 'wgUserLanguage' ),
1392 debug: mw.config.get( 'debug' )
1394 // Split module batch by source and by group.
1396 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1398 // Appends a list of modules from the queue to the batch
1399 for ( q = 0; q < queue.length; q += 1 ) {
1400 // Only request modules which are registered
1401 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1402 // Prevent duplicate entries
1403 if ( $.inArray( queue[q], batch ) === -1 ) {
1404 batch[batch.length] = queue[q];
1405 // Mark registered modules as loading
1406 registry[queue[q]].state = 'loading';
1411 mw.loader.store.init();
1412 if ( mw.loader.store.enabled ) {
1415 batch = $.grep( batch, function ( module ) {
1416 var source = mw.loader.store.get( module );
1418 concatSource.push( source );
1424 $.globalEval( concatSource.join( ';' ) );
1426 // Not good, the cached mw.loader.implement calls failed! This should
1427 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1428 // Depending on how corrupt the string is, it is likely that some
1429 // modules' implement() succeeded while the ones after the error will
1430 // never run and leave their modules in the 'loading' state forever.
1432 // Since this is an error not caused by an individual module but by
1433 // something that infected the implement call itself, don't take any
1434 // risks and clear everything in this cache.
1435 mw.loader.store.clear();
1436 // Re-add the ones still pending back to the batch and let the server
1437 // repopulate these modules to the cache.
1438 // This means that at most one module will be useless (the one that had
1439 // the error) instead of all of them.
1440 log( 'Error while evaluating data from mw.loader.store', err );
1441 origBatch = $.grep( origBatch, function ( module ) {
1442 return registry[module].state === 'loading';
1444 batch = batch.concat( origBatch );
1448 // Early exit if there's nothing to load...
1449 if ( !batch.length ) {
1453 // The queue has been processed into the batch, clear up the queue.
1456 // Always order modules alphabetically to help reduce cache
1457 // misses for otherwise identical content.
1460 // Split batch by source and by group.
1461 for ( b = 0; b < batch.length; b += 1 ) {
1462 bSource = registry[batch[b]].source;
1463 bGroup = registry[batch[b]].group;
1464 if ( splits[bSource] === undefined ) {
1465 splits[bSource] = {};
1467 if ( splits[bSource][bGroup] === undefined ) {
1468 splits[bSource][bGroup] = [];
1470 bSourceGroup = splits[bSource][bGroup];
1471 bSourceGroup[bSourceGroup.length] = batch[b];
1474 // Clear the batch - this MUST happen before we append any
1475 // script elements to the body or it's possible that a script
1476 // will be locally cached, instantly load, and work the batch
1477 // again, all before we've cleared it causing each request to
1478 // include modules which are already loaded.
1481 for ( source in splits ) {
1483 sourceLoadScript = sources[source].loadScript;
1485 for ( group in splits[source] ) {
1487 // Cache access to currently selected list of
1488 // modules for this group from this source.
1489 modules = splits[source][group];
1491 // Calculate the highest timestamp
1493 for ( g = 0; g < modules.length; g += 1 ) {
1494 if ( registry[modules[g]].version > maxVersion ) {
1495 maxVersion = registry[modules[g]].version;
1499 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1500 // For user modules append a user name to the request.
1501 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1502 currReqBase.user = mw.config.get( 'wgUserName' );
1504 currReqBaseLength = $.param( currReqBase ).length;
1506 // We may need to split up the request to honor the query string length limit,
1507 // so build it piece by piece.
1508 l = currReqBaseLength + 9; // '&modules='.length == 9
1510 moduleMap = {}; // { prefix: [ suffixes ] }
1512 for ( i = 0; i < modules.length; i += 1 ) {
1513 // Determine how many bytes this module would add to the query string
1514 lastDotIndex = modules[i].lastIndexOf( '.' );
1515 // Note that these substr() calls work even if lastDotIndex == -1
1516 prefix = modules[i].substr( 0, lastDotIndex );
1517 suffix = modules[i].substr( lastDotIndex + 1 );
1518 bytesAdded = moduleMap[prefix] !== undefined
1519 ? suffix.length + 3 // '%2C'.length == 3
1520 : modules[i].length + 3; // '%7C'.length == 3
1522 // If the request would become too long, create a new one,
1523 // but don't create empty requests
1524 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1525 // This request would become too long, create a new one
1526 // and fire off the old one
1527 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1530 l = currReqBaseLength + 9;
1532 if ( moduleMap[prefix] === undefined ) {
1533 moduleMap[prefix] = [];
1535 moduleMap[prefix].push( suffix );
1536 if ( !registry[modules[i]].async ) {
1537 // If this module is blocking, make the entire request blocking
1538 // This is slightly suboptimal, but in practice mixing of blocking
1539 // and async modules will only occur in debug mode.
1544 // If there's anything left in moduleMap, request that too
1545 if ( !$.isEmptyObject( moduleMap ) ) {
1546 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1553 * Register a source.
1555 * The #work method will use this information to split up requests by source.
1557 * mw.loader.addSource( 'mediawikiwiki', { loadScript: '//www.mediawiki.org/w/load.php' } );
1559 * @param {string} id Short string representing a source wiki, used internally for
1560 * registered modules to indicate where they should be loaded from (usually lowercase a-z).
1561 * @param {Object} props
1562 * @param {string} props.loadScript Url to the load.php entry point of the source wiki.
1565 addSource: function ( id, props ) {
1567 // Allow multiple additions
1568 if ( typeof id === 'object' ) {
1569 for ( source in id ) {
1570 mw.loader.addSource( source, id[source] );
1575 if ( sources[id] !== undefined ) {
1576 throw new Error( 'source already registered: ' + id );
1579 sources[id] = props;
1585 * Register a module, letting the system know about it and its
1586 * properties. Startup modules contain calls to this function.
1588 * @param {string} module Module name
1589 * @param {number} version Module version number as a timestamp (falls backs to 0)
1590 * @param {string|Array|Function} dependencies One string or array of strings of module
1591 * names on which this module depends, or a function that returns that array.
1592 * @param {string} [group=null] Group which the module is in
1593 * @param {string} [source='local'] Name of the source
1594 * @param {string} [skip=null] Script body of the skip function
1596 register: function ( module, version, dependencies, group, source, skip ) {
1598 // Allow multiple registration
1599 if ( typeof module === 'object' ) {
1600 for ( m = 0; m < module.length; m += 1 ) {
1601 // module is an array of module names
1602 if ( typeof module[m] === 'string' ) {
1603 mw.loader.register( module[m] );
1604 // module is an array of arrays
1605 } else if ( typeof module[m] === 'object' ) {
1606 mw.loader.register.apply( mw.loader, module[m] );
1612 if ( typeof module !== 'string' ) {
1613 throw new Error( 'module must be a string, not a ' + typeof module );
1615 if ( registry[module] !== undefined ) {
1616 throw new Error( 'module already registered: ' + module );
1618 // List the module as registered
1619 registry[module] = {
1620 version: version !== undefined ? parseInt( version, 10 ) : 0,
1622 group: typeof group === 'string' ? group : null,
1623 source: typeof source === 'string' ? source : 'local',
1624 state: 'registered',
1625 skip: typeof skip === 'string' ? skip : null
1627 if ( typeof dependencies === 'string' ) {
1628 // Allow dependencies to be given as a single module name
1629 registry[module].dependencies = [ dependencies ];
1630 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1631 // Allow dependencies to be given as an array of module names
1632 // or a function which returns an array
1633 registry[module].dependencies = dependencies;
1638 * Implement a module given the components that make up the module.
1640 * When #load or #using requests one or more modules, the server
1641 * response contain calls to this function.
1643 * All arguments are required.
1645 * @param {string} module Name of module
1646 * @param {Function|Array} script Function with module code or Array of URLs to
1647 * be used as the src attribute of a new `<script>` tag.
1648 * @param {Object} style Should follow one of the following patterns:
1650 * { "css": [css, ..] }
1651 * { "url": { <media>: [url, ..] } }
1653 * And for backwards compatibility (needs to be supported forever due to caching):
1656 * { <media>: [url, ..] }
1658 * The reason css strings are not concatenated anymore is bug 31676. We now check
1659 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1661 * @param {Object} msgs List of key/value pairs to be added to mw#messages.
1663 implement: function ( module, script, style, msgs ) {
1665 if ( typeof module !== 'string' ) {
1666 throw new Error( 'module must be a string, not a ' + typeof module );
1668 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1669 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1671 if ( !$.isPlainObject( style ) ) {
1672 throw new Error( 'style must be an object, not a ' + typeof style );
1674 if ( !$.isPlainObject( msgs ) ) {
1675 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1677 // Automatically register module
1678 if ( registry[module] === undefined ) {
1679 mw.loader.register( module );
1681 // Check for duplicate implementation
1682 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1683 throw new Error( 'module already implemented: ' + module );
1685 // Attach components
1686 registry[module].script = script;
1687 registry[module].style = style;
1688 registry[module].messages = msgs;
1689 // The module may already have been marked as erroneous
1690 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1691 registry[module].state = 'loaded';
1692 if ( allReady( registry[module].dependencies ) ) {
1699 * Execute a function as soon as one or more required modules are ready.
1701 * If the required modules are already loaded, the function will be
1702 * executed immediately and the modules will not be reloaded.
1704 * Example of inline dependency on OOjs:
1706 * mw.loader.using( 'oojs', function () {
1707 * OO.compare( [ 1 ], [ 1 ] );
1710 * @param {string|Array} dependencies Module name or array of modules names the callback
1711 * dependends on to be ready before executing
1712 * @param {Function} [ready] Callback to execute when all dependencies are ready
1713 * @param {Function} [error] Callback to execute if one or more dependencies failed
1714 * @return {jQuery.Promise}
1716 using: function ( dependencies, ready, error ) {
1717 var deferred = $.Deferred();
1719 // Allow calling with a single dependency as a string
1720 if ( typeof dependencies === 'string' ) {
1721 dependencies = [ dependencies ];
1722 } else if ( !$.isArray( dependencies ) ) {
1724 throw new Error( 'Dependencies must be a string or an array' );
1728 deferred.done( ready );
1731 deferred.fail( error );
1734 // Resolve entire dependency map
1735 dependencies = resolve( dependencies );
1736 if ( allReady( dependencies ) ) {
1737 // Run ready immediately
1739 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1740 // Execute error immediately if any dependencies have errors
1742 new Error( 'One or more dependencies failed to load' ),
1746 // Not all dependencies are ready: queue up a request
1747 request( dependencies, deferred.resolve, deferred.reject );
1750 return deferred.promise();
1754 * Load an external script or one or more modules.
1756 * @param {string|Array} modules Either the name of a module, array of modules,
1757 * or a URL of an external script or style
1758 * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
1759 * external script or style; acceptable values are "text/css" and
1760 * "text/javascript"; if no type is provided, text/javascript is assumed.
1761 * @param {boolean} [async] Whether to load modules asynchronously.
1762 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1763 * Defaults to `true` if loading a URL, `false` otherwise.
1765 load: function ( modules, type, async ) {
1766 var filtered, m, module, l;
1769 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1770 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1772 // Allow calling with an external url or single dependency as a string
1773 if ( typeof modules === 'string' ) {
1774 // Support adding arbitrary external scripts
1775 if ( /^(https?:)?\/\//.test( modules ) ) {
1776 if ( async === undefined ) {
1777 // Assume async for bug 34542
1780 if ( type === 'text/css' ) {
1781 // IE7-8 throws security warnings when inserting a <link> tag
1782 // with a protocol-relative URL set though attributes (instead of
1783 // properties) - when on HTTPS. See also bug 41331.
1784 l = document.createElement( 'link' );
1785 l.rel = 'stylesheet';
1787 $( 'head' ).append( l );
1790 if ( type === 'text/javascript' || type === undefined ) {
1791 addScript( modules, null, async );
1795 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1797 // Called with single module
1798 modules = [ modules ];
1801 // Filter out undefined modules, otherwise resolve() will throw
1802 // an exception for trying to load an undefined module.
1803 // Undefined modules are acceptable here in load(), because load() takes
1804 // an array of unrelated modules, whereas the modules passed to
1805 // using() are related and must all be loaded.
1806 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1807 module = registry[modules[m]];
1808 if ( module !== undefined ) {
1809 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1810 filtered[filtered.length] = modules[m];
1815 if ( filtered.length === 0 ) {
1818 // Resolve entire dependency map
1819 filtered = resolve( filtered );
1820 // If all modules are ready, nothing to be done
1821 if ( allReady( filtered ) ) {
1824 // If any modules have errors: also quit.
1825 if ( filter( ['error', 'missing'], filtered ).length ) {
1828 // Since some modules are not yet ready, queue up a request.
1829 request( filtered, undefined, undefined, async );
1833 * Change the state of one or more modules.
1835 * @param {string|Object} module Module name or object of module name/state pairs
1836 * @param {string} state State name
1838 state: function ( module, state ) {
1841 if ( typeof module === 'object' ) {
1842 for ( m in module ) {
1843 mw.loader.state( m, module[m] );
1847 if ( registry[module] === undefined ) {
1848 mw.loader.register( module );
1850 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1851 && registry[module].state !== state ) {
1852 // Make sure pending modules depending on this one get executed if their
1853 // dependencies are now fulfilled!
1854 registry[module].state = state;
1855 handlePending( module );
1857 registry[module].state = state;
1862 * Get the version of a module.
1864 * @param {string} module Name of module to get version for
1865 * @return {string|null} The version, or null if the module (or its version) is not
1868 getVersion: function ( module ) {
1869 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1870 return formatVersionNumber( registry[module].version );
1876 * Get the state of a module.
1878 * @param {string} module Name of module to get state for
1880 getState: function ( module ) {
1881 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1882 return registry[module].state;
1888 * Get the names of all registered modules.
1892 getModuleNames: function () {
1893 return $.map( registry, function ( i, key ) {
1899 * @inheritdoc mw.inspect#runReports
1902 inspect: function () {
1903 var args = slice.call( arguments );
1904 mw.loader.using( 'mediawiki.inspect', function () {
1905 mw.inspect.runReports.apply( mw.inspect, args );
1910 * On browsers that implement the localStorage API, the module store serves as a
1911 * smart complement to the browser cache. Unlike the browser cache, the module store
1912 * can slice a concatenated response from ResourceLoader into its constituent
1913 * modules and cache each of them separately, using each module's versioning scheme
1914 * to determine when the cache should be invalidated.
1917 * @class mw.loader.store
1920 // Whether the store is in use on this page.
1923 // The contents of the store, mapping '[module name]@[version]' keys
1924 // to module implementations.
1928 stats: { hits: 0, misses: 0, expired: 0 },
1931 * Construct a JSON-serializable object representing the content of the store.
1932 * @return {Object} Module store contents.
1934 toJSON: function () {
1935 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
1939 * Get the localStorage key for the entire module store. The key references
1940 * $wgDBname to prevent clashes between wikis which share a common host.
1942 * @return {string} localStorage item key
1944 getStoreKey: function () {
1945 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
1949 * Get a string key on which to vary the module cache.
1950 * @return {string} String of concatenated vary conditions.
1952 getVary: function () {
1954 mw.config.get( 'skin' ),
1955 mw.config.get( 'wgResourceLoaderStorageVersion' ),
1956 mw.config.get( 'wgUserLanguage' )
1961 * Get a string key for a specific module. The key format is '[name]@[version]'.
1963 * @param {string} module Module name
1964 * @return {string|null} Module key or null if module does not exist
1966 getModuleKey: function ( module ) {
1967 return typeof registry[module] === 'object' ?
1968 ( module + '@' + registry[module].version ) : null;
1972 * Initialize the store.
1974 * Retrieves store from localStorage and (if successfully retrieved) decoding
1975 * the stored JSON value to a plain object.
1977 * The try / catch block is used for JSON & localStorage feature detection.
1978 * See the in-line documentation for Modernizr's localStorage feature detection
1979 * code for a full account of why we need a try / catch:
1980 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
1985 if ( mw.loader.store.enabled !== null ) {
1990 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) {
1991 // Disabled by configuration, or because debug mode is set
1992 mw.loader.store.enabled = false;
1997 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
1998 // If we get here, localStorage is available; mark enabled
1999 mw.loader.store.enabled = true;
2000 data = JSON.parse( raw );
2001 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2002 mw.loader.store.items = data.items;
2006 log( 'Storage error', e );
2009 if ( raw === undefined ) {
2010 // localStorage failed; disable store
2011 mw.loader.store.enabled = false;
2013 mw.loader.store.update();
2018 * Retrieve a module from the store and update cache hit stats.
2020 * @param {string} module Module name
2021 * @return {string|boolean} Module implementation or false if unavailable
2023 get: function ( module ) {
2026 if ( !mw.loader.store.enabled ) {
2030 key = mw.loader.store.getModuleKey( module );
2031 if ( key in mw.loader.store.items ) {
2032 mw.loader.store.stats.hits++;
2033 return mw.loader.store.items[key];
2035 mw.loader.store.stats.misses++;
2040 * Stringify a module and queue it for storage.
2042 * @param {string} module Module name
2043 * @param {Object} descriptor The module's descriptor as set in the registry
2045 set: function ( module, descriptor ) {
2048 if ( !mw.loader.store.enabled ) {
2052 key = mw.loader.store.getModuleKey( module );
2055 // Already stored a copy of this exact version
2056 key in mw.loader.store.items ||
2057 // Module failed to load
2058 descriptor.state !== 'ready' ||
2059 // Unversioned, private, or site-/user-specific
2060 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
2061 // Partial descriptor
2062 $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1
2070 JSON.stringify( module ),
2071 typeof descriptor.script === 'function' ?
2072 String( descriptor.script ) :
2073 JSON.stringify( descriptor.script ),
2074 JSON.stringify( descriptor.style ),
2075 JSON.stringify( descriptor.messages )
2077 // Attempted workaround for a possible Opera bug (bug 57567).
2078 // This regex should never match under sane conditions.
2079 if ( /^\s*\(/.test( args[1] ) ) {
2080 args[1] = 'function' + args[1];
2081 log( 'Detected malformed function stringification (bug 57567)' );
2084 log( 'Storage error', e );
2088 mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');';
2089 mw.loader.store.update();
2093 * Iterate through the module store, removing any item that does not correspond
2094 * (in name and version) to an item in the module registry.
2096 prune: function () {
2099 if ( !mw.loader.store.enabled ) {
2103 for ( key in mw.loader.store.items ) {
2104 module = key.substring( 0, key.indexOf( '@' ) );
2105 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2106 mw.loader.store.stats.expired++;
2107 delete mw.loader.store.items[key];
2113 * Clear the entire module store right now.
2115 clear: function () {
2116 mw.loader.store.items = {};
2117 localStorage.removeItem( mw.loader.store.getStoreKey() );
2121 * Sync modules to localStorage.
2123 * This function debounces localStorage updates. When called multiple times in
2124 * quick succession, the calls are coalesced into a single update operation.
2125 * This allows us to call #update without having to consider the module load
2126 * queue; the call to localStorage.setItem will be naturally deferred until the
2127 * page is quiescent.
2129 * Because localStorage is shared by all pages with the same origin, if multiple
2130 * pages are loaded with different module sets, the possibility exists that
2131 * modules saved by one page will be clobbered by another. But the impact would
2132 * be minor and the problem would be corrected by subsequent page views.
2136 update: ( function () {
2141 key = mw.loader.store.getStoreKey();
2143 if ( !mw.loader.store.enabled ) {
2146 mw.loader.store.prune();
2148 // Replacing the content of the module store might fail if the new
2149 // contents would exceed the browser's localStorage size limit. To
2150 // avoid clogging the browser with stale data, always remove the old
2151 // value before attempting to set the new one.
2152 localStorage.removeItem( key );
2153 data = JSON.stringify( mw.loader.store );
2154 localStorage.setItem( key, data );
2156 log( 'Storage error', e );
2160 return function () {
2161 clearTimeout( timer );
2162 timer = setTimeout( flush, 2000 );
2170 * HTML construction helper functions
2177 * output = Html.element( 'div', {}, new Html.Raw(
2178 * Html.element( 'img', { src: '<' } )
2180 * mw.log( output ); // <div><img src="<"/></div>
2185 html: ( function () {
2186 function escapeCallback( s ) {
2203 * Escape a string for HTML.
2205 * Converts special characters to HTML entities.
2207 * mw.html.escape( '< > \' & "' );
2208 * // Returns < > ' & "
2210 * @param {string} s The string to escape
2211 * @return {string} HTML
2213 escape: function ( s ) {
2214 return s.replace( /['"<>&]/g, escapeCallback );
2218 * Create an HTML element string, with safe escaping.
2220 * @param {string} name The tag name.
2221 * @param {Object} attrs An object with members mapping element names to values
2222 * @param {Mixed} contents The contents of the element. May be either:
2224 * - string: The string is escaped.
2225 * - null or undefined: The short closing form is used, e.g. `<br/>`.
2226 * - this.Raw: The value attribute is included without escaping.
2227 * - this.Cdata: The value attribute is included, and an exception is
2228 * thrown if it contains an illegal ETAGO delimiter.
2229 * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
2230 * @return {string} HTML
2232 element: function ( name, attrs, contents ) {
2233 var v, attrName, s = '<' + name;
2235 for ( attrName in attrs ) {
2236 v = attrs[attrName];
2237 // Convert name=true, to name=name
2241 } else if ( v === false ) {
2244 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2246 if ( contents === undefined || contents === null ) {
2253 switch ( typeof contents ) {
2256 s += this.escape( contents );
2260 // Convert to string
2261 s += String( contents );
2264 if ( contents instanceof this.Raw ) {
2265 // Raw HTML inclusion
2266 s += contents.value;
2267 } else if ( contents instanceof this.Cdata ) {
2269 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2270 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2272 s += contents.value;
2274 throw new Error( 'mw.html.element: Invalid type of contents' );
2277 s += '</' + name + '>';
2282 * Wrapper object for raw HTML passed to mw.html.element().
2283 * @class mw.html.Raw
2285 Raw: function ( value ) {
2290 * Wrapper object for CDATA element contents passed to mw.html.element()
2291 * @class mw.html.Cdata
2293 Cdata: function ( value ) {
2299 // Skeleton user object. mediawiki.user.js extends this
2306 * Registry and firing of events.
2308 * MediaWiki has various interface components that are extended, enhanced
2309 * or manipulated in some other way by extensions, gadgets and even
2312 * This framework helps streamlining the timing of when these other
2313 * code paths fire their plugins (instead of using document-ready,
2314 * which can and should be limited to firing only once).
2316 * Features like navigating to other wiki pages, previewing an edit
2317 * and editing itself – without a refresh – can then retrigger these
2318 * hooks accordingly to ensure everything still works as expected.
2322 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2323 * mw.hook( 'wikipage.content' ).fire( $content );
2325 * Handlers can be added and fired for arbitrary event names at any time. The same
2326 * event can be fired multiple times. The last run of an event is memorized
2327 * (similar to `$(document).ready` and `$.Deferred().done`).
2328 * This means if an event is fired, and a handler added afterwards, the added
2329 * function will be fired right away with the last given event data.
2331 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2332 * Thus allowing flexible use and optimal maintainability and authority control.
2333 * You can pass around the `add` and/or `fire` method to another piece of code
2334 * without it having to know the event name (or `mw.hook` for that matter).
2336 * var h = mw.hook( 'bar.ready' );
2337 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2339 * Note: Events are documented with an underscore instead of a dot in the event
2340 * name due to jsduck not supporting dots in that position.
2344 hook: ( function () {
2348 * Create an instance of mw.hook.
2352 * @param {string} name Name of hook.
2355 return function ( name ) {
2356 var list = hasOwn.call( lists, name ) ?
2358 lists[name] = $.Callbacks( 'memory' );
2362 * Register a hook handler
2363 * @param {Function...} handler Function to bind.
2369 * Unregister a hook handler
2370 * @param {Function...} handler Function to unbind.
2373 remove: list.remove,
2377 * @param {Mixed...} data
2381 return list.fireWith.call( this, null, slice.call( arguments ) );
2388 // Alias $j to jQuery for backwards compatibility
2389 // @deprecated since 1.23 Use $ or jQuery instead
2390 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2392 // Attach to window and globally alias
2393 window.mw = window.mediaWiki = mw;
2395 // Auto-register from pre-loaded startup scripts
2396 if ( $.isFunction( window.startUp ) ) {
2398 window.startUp = undefined;