2 * Base library for MediaWiki.
4 * Exposed as globally as `mediaWiki` with `mw` as shortcut.
7 * @alternateClassName mediaWiki
11 var mw = ( function ( $, undefined ) {
16 var 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 * @returns {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 );
442 * Dummy placeholder for {@link mw.log}
446 var log = function () {};
447 log.warn = function () {};
448 log.deprecate = function ( obj, key, val ) {
454 // Make the Map constructor publicly available.
457 // Make the Message constructor publicly available.
461 * Map of configuration values
463 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
466 * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
467 * global `window` object.
469 * @property {mw.Map} config
471 // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule to an instance of `mw.Map`.
475 * Empty object that plugins can be installed in.
481 * Access container for deprecated functionality that can be moved from
482 * from their legacy location and attached to this object (e.g. a global
483 * function that is deprecated and as stop-gap can be exposed through here).
485 * This was reserved for future use but never ended up being used.
487 * @deprecated since 1.22: Let deprecated identifiers keep their original name
488 * and use mw.log#deprecate to create an access container for tracking.
494 * Localization system
502 * Get a message object.
504 * Shorcut for `new mw.Message( mw.messages, key, parameters )`.
507 * @param {string} key Key of message to get
508 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
509 * @return {mw.Message}
511 message: function ( key ) {
512 // Variadic arguments
513 var parameters = slice.call( arguments, 1 );
514 return new Message( mw.messages, key, parameters );
518 * Get a message string using the (default) 'text' format.
520 * Shortcut for `mw.message( key, parameters... ).text()`.
523 * @param {string} key Key of message to get
524 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
528 return mw.message.apply( mw.message, arguments ).toString();
532 * Client-side module loader which integrates with the MediaWiki ResourceLoader
536 loader: ( function () {
538 /* Private Members */
541 * Mapping of registered modules
543 * The jquery module is pre-registered, because it must have already
544 * been provided for this object to have been built, and in debug mode
545 * jquery would have been provided through a unique loader request,
546 * making it impossible to hold back registration of jquery until after
549 * For exact details on support for script, style and messages, look at
550 * mw.loader.implement.
555 * 'version': ############## (unix timestamp),
556 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
557 * 'group': 'somegroup', (or) null,
558 * 'source': 'local', 'someforeignwiki', (or) null
559 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
562 * 'messages': { 'key': 'value' },
571 // Mapping of sources, keyed by source-id, values are objects.
575 // 'loadScript': 'http://foo.bar/w/load.php'
580 // List of modules which will be loaded as when ready
582 // List of modules to be loaded
584 // List of callback functions waiting for modules to be ready to be called
586 // Selector cache for the marker element. Use getMarker() to get/use the marker!
588 // Buffer for addEmbeddedCSS.
590 // Callbacks for addEmbeddedCSS.
591 cssCallbacks = $.Callbacks();
593 /* Private methods */
595 function getMarker() {
601 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
602 if ( $marker.length ) {
605 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
606 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
612 * Create a new style tag and add it to the DOM.
615 * @param {string} text CSS text
616 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
617 * inserted before. Otherwise it will be appended to `<head>`.
618 * @return {HTMLElement} Reference to the created `<style>` element.
620 function newStyleTag( text, nextnode ) {
621 var s = document.createElement( 'style' );
622 // Insert into document before setting cssText (bug 33305)
624 // Must be inserted with native insertBefore, not $.fn.before.
625 // When using jQuery to insert it, like $nextnode.before( s ),
626 // then IE6 will throw "Access is denied" when trying to append
627 // to .cssText later. Some kind of weird security measure.
628 // http://stackoverflow.com/q/12586482/319266
629 // Works: jsfiddle.net/zJzMy/1
630 // Fails: jsfiddle.net/uJTQz
631 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
632 if ( nextnode.jquery ) {
633 nextnode = nextnode.get( 0 );
635 nextnode.parentNode.insertBefore( s, nextnode );
637 document.getElementsByTagName( 'head' )[0].appendChild( s );
639 if ( s.styleSheet ) {
641 s.styleSheet.cssText = text;
644 // (Safari sometimes borks on non-string values,
645 // play safe by casting to a string, just in case.)
646 s.appendChild( document.createTextNode( String( text ) ) );
652 * Checks whether it is safe to add this css to a stylesheet.
655 * @param {string} cssText
656 * @return {boolean} False if a new one must be created.
658 function canExpandStylesheetWith( cssText ) {
659 // Makes sure that cssText containing `@import`
660 // rules will end up in a new stylesheet (as those only work when
661 // placed at the start of a stylesheet; bug 35562).
662 return cssText.indexOf( '@import' ) === -1;
666 * Add a bit of CSS text to the current browser page.
668 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
669 * or create a new one based on whether the given `cssText` is safe for extension.
671 * @param {string} [cssText=cssBuffer] If called without cssText,
672 * the internal buffer will be inserted instead.
673 * @param {Function} [callback]
675 function addEmbeddedCSS( cssText, callback ) {
679 cssCallbacks.add( callback );
682 // Yield once before inserting the <style> tag. There are likely
683 // more calls coming up which we can combine this way.
684 // Appending a stylesheet and waiting for the browser to repaint
685 // is fairly expensive, this reduces it (bug 45810)
687 // Be careful not to extend the buffer with css that needs a new stylesheet
688 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
689 // Linebreak for somewhat distinguishable sections
690 // (the rl-cachekey comment separating each)
691 cssBuffer += '\n' + cssText;
692 // TODO: Use requestAnimationFrame in the future which will
693 // perform even better by not injecting styles while the browser
695 setTimeout( function () {
696 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
697 // (below version 13) has the non-standard behaviour of passing a
698 // numerical "lateness" value as first argument to this callback
699 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
705 // This is a delayed call and we got a buffer still
706 } else if ( cssBuffer ) {
710 // This is a delayed call, but buffer is already cleared by
711 // another delayed call.
715 // By default, always create a new <style>. Appending text
716 // to a <style> tag means the contents have to be re-parsed (bug 45810).
717 // Except, of course, in IE below 9, in there we default to
718 // re-using and appending to a <style> tag due to the
719 // IE stylesheet limit (bug 31676).
720 if ( 'documentMode' in document && document.documentMode <= 9 ) {
722 $style = getMarker().prev();
723 // Verify that the the element before Marker actually is a
724 // <style> tag and one that came from ResourceLoader
725 // (not some other style tag or even a `<meta>` or `<script>`).
726 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
727 // There's already a dynamic <style> tag present and
728 // canExpandStylesheetWith() gave a green light to append more to it.
729 styleEl = $style.get( 0 );
730 if ( styleEl.styleSheet ) {
732 styleEl.styleSheet.cssText += cssText; // IE
734 log( 'addEmbeddedCSS fail', e );
737 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
739 cssCallbacks.fire().empty();
744 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
746 cssCallbacks.fire().empty();
750 * Generates an ISO8601 "basic" string from a UNIX timestamp
753 function formatVersionNumber( timestamp ) {
755 function pad( a, b, c ) {
756 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
758 d.setTime( timestamp * 1000 );
760 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
761 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
766 * Resolves dependencies and detects circular references.
769 * @param {string} module Name of the top-level module whose dependencies shall be
770 * resolved and sorted.
771 * @param {Array} resolved Returns a topological sort of the given module and its
772 * dependencies, such that later modules depend on earlier modules. The array
773 * contains the module names. If the array contains already some module names,
774 * this function appends its result to the pre-existing array.
775 * @param {Object} [unresolved] Hash used to track the current dependency
776 * chain; used to report loops in the dependency graph.
777 * @throws {Error} If any unregistered module or a dependency loop is encountered
779 function sortDependencies( module, resolved, unresolved ) {
782 if ( registry[module] === undefined ) {
783 throw new Error( 'Unknown dependency: ' + module );
785 // Resolves dynamic loader function and replaces it with its own results
786 if ( $.isFunction( registry[module].dependencies ) ) {
787 registry[module].dependencies = registry[module].dependencies();
788 // Ensures the module's dependencies are always in an array
789 if ( typeof registry[module].dependencies !== 'object' ) {
790 registry[module].dependencies = [registry[module].dependencies];
793 if ( $.inArray( module, resolved ) !== -1 ) {
794 // Module already resolved; nothing to do.
797 // unresolved is optional, supply it if not passed in
801 // Tracks down dependencies
802 deps = registry[module].dependencies;
804 for ( n = 0; n < len; n += 1 ) {
805 if ( $.inArray( deps[n], resolved ) === -1 ) {
806 if ( unresolved[deps[n]] ) {
808 'Circular reference detected: ' + module +
814 unresolved[module] = true;
815 sortDependencies( deps[n], resolved, unresolved );
816 delete unresolved[module];
819 resolved[resolved.length] = module;
823 * Gets a list of module names that a module depends on in their proper dependency
827 * @param {string} module Module name or array of string module names
828 * @return {Array} list of dependencies, including 'module'.
829 * @throws {Error} If circular reference is detected
831 function resolve( module ) {
834 // Allow calling with an array of module names
835 if ( $.isArray( module ) ) {
837 for ( m = 0; m < module.length; m += 1 ) {
838 sortDependencies( module[m], resolved );
843 if ( typeof module === 'string' ) {
845 sortDependencies( module, resolved );
849 throw new Error( 'Invalid module argument: ' + module );
853 * Narrows a list of module names down to those matching a specific
854 * state (see comment on top of this scope for a list of valid states).
855 * One can also filter for 'unregistered', which will return the
856 * modules names that don't have a registry entry.
859 * @param {string|string[]} states Module states to filter by
860 * @param {Array} [modules] List of module names to filter (optional, by default the entire
862 * @return {Array} List of filtered module names
864 function filter( states, modules ) {
865 var list, module, s, m;
867 // Allow states to be given as a string
868 if ( typeof states === 'string' ) {
871 // If called without a list of modules, build and use a list of all modules
873 if ( modules === undefined ) {
875 for ( module in registry ) {
876 modules[modules.length] = module;
879 // Build a list of modules which are in one of the specified states
880 for ( s = 0; s < states.length; s += 1 ) {
881 for ( m = 0; m < modules.length; m += 1 ) {
882 if ( registry[modules[m]] === undefined ) {
883 // Module does not exist
884 if ( states[s] === 'unregistered' ) {
886 list[list.length] = modules[m];
889 // Module exists, check state
890 if ( registry[modules[m]].state === states[s] ) {
892 list[list.length] = modules[m];
901 * Determine whether all dependencies are in state 'ready', which means we may
902 * execute the module or job now.
905 * @param {Array} dependencies Dependencies (module names) to be checked.
906 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
908 function allReady( dependencies ) {
909 return filter( 'ready', dependencies ).length === dependencies.length;
913 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
914 * and modules that depend upon this module. if the given module failed, propagate the 'error'
915 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
916 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
919 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
921 function handlePending( module ) {
922 var j, job, hasErrors, m, stateChange;
925 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
926 // If the current module failed, mark all dependent modules also as failed.
927 // Iterate until steady-state to propagate the error state upwards in the
931 for ( m in registry ) {
932 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
933 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
934 registry[m].state = 'error';
939 } while ( stateChange );
942 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
943 for ( j = 0; j < jobs.length; j += 1 ) {
944 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
945 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
946 // All dependencies satisfied, or some have errors
952 if ( $.isFunction( job.error ) ) {
953 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
956 if ( $.isFunction( job.ready ) ) {
961 // A user-defined callback raised an exception.
962 // Swallow it to protect our state machine!
963 log( 'Exception thrown by job.error', e );
968 if ( registry[module].state === 'ready' ) {
969 // The current module became 'ready'. Set it in the module store, and recursively execute all
970 // dependent modules that are loaded and now have all dependencies satisfied.
971 mw.loader.store.set( module, registry[module] );
972 for ( m in registry ) {
973 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
981 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
982 * depending on whether document-ready has occurred yet and whether we are in async mode.
985 * @param {string} src URL to script, will be used as the src attribute in the script tag
986 * @param {Function} [callback] Callback which will be run when the script is done
988 function addScript( src, callback, async ) {
989 /*jshint evil:true */
990 var script, head, done;
992 // Using isReady directly instead of storing it locally from
993 // a $.fn.ready callback (bug 31895).
994 if ( $.isReady || async ) {
995 // Can't use jQuery.getScript because that only uses <script> for cross-domain,
996 // it uses XHR and eval for same-domain scripts, which we don't want because it
997 // messes up line numbers.
998 // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
1000 // IE-safe way of getting the <head>. document.head isn't supported
1001 // in old IE, and doesn't work when in the <head>.
1003 head = document.getElementsByTagName( 'head' )[0] || document.body;
1005 script = document.createElement( 'script' );
1006 script.async = true;
1008 if ( $.isFunction( callback ) ) {
1009 script.onload = script.onreadystatechange = function () {
1014 || /loaded|complete/.test( script.readyState )
1019 // Handle memory leak in IE
1020 script.onload = script.onreadystatechange = null;
1022 // Detach the element from the document
1023 if ( script.parentNode ) {
1024 script.parentNode.removeChild( script );
1027 // Dereference the element from javascript
1035 if ( window.opera ) {
1036 // Appending to the <head> blocks rendering completely in Opera,
1037 // so append to the <body> after document ready. This means the
1038 // scripts only start loading after the document has been rendered,
1039 // but so be it. Opera users don't deserve faster web pages if their
1040 // browser makes it impossible.
1042 document.body.appendChild( script );
1045 head.appendChild( script );
1048 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
1049 if ( $.isFunction( callback ) ) {
1050 // Document.write is synchronous, so this is called when it's done
1051 // FIXME: that's a lie. doc.write isn't actually synchronous
1058 * Executes a loaded module, making it ready to use
1061 * @param {string} module Module name to execute
1063 function execute( module ) {
1064 var key, value, media, i, urls, cssHandle, checkCssHandles,
1065 cssHandlesRegistered = false;
1067 if ( registry[module] === undefined ) {
1068 throw new Error( 'Module has not been registered yet: ' + module );
1069 } else if ( registry[module].state === 'registered' ) {
1070 throw new Error( 'Module has not been requested from the server yet: ' + module );
1071 } else if ( registry[module].state === 'loading' ) {
1072 throw new Error( 'Module has not completed loading yet: ' + module );
1073 } else if ( registry[module].state === 'ready' ) {
1074 throw new Error( 'Module has already been executed: ' + module );
1078 * Define loop-function here for efficiency
1079 * and to avoid re-using badly scoped variables.
1082 function addLink( media, url ) {
1083 var el = document.createElement( 'link' );
1084 getMarker().before( el ); // IE: Insert in dom before setting href
1085 el.rel = 'stylesheet';
1086 if ( media && media !== 'all' ) {
1092 function runScript() {
1093 var script, markModuleReady, nestedAddScript;
1095 script = registry[module].script;
1096 markModuleReady = function () {
1097 registry[module].state = 'ready';
1098 handlePending( module );
1100 nestedAddScript = function ( arr, callback, async, i ) {
1101 // Recursively call addScript() in its own callback
1102 // for each element of arr.
1103 if ( i >= arr.length ) {
1104 // We're at the end of the array
1109 addScript( arr[i], function () {
1110 nestedAddScript( arr, callback, async, i + 1 );
1114 if ( $.isArray( script ) ) {
1115 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1116 } else if ( $.isFunction( script ) ) {
1117 registry[module].state = 'ready';
1119 handlePending( module );
1122 // This needs to NOT use mw.log because these errors are common in production mode
1123 // and not in debug mode, such as when a symbol that should be global isn't exported
1124 log( 'Exception thrown by ' + module, e );
1125 registry[module].state = 'error';
1126 handlePending( module );
1130 // This used to be inside runScript, but since that is now fired asychronously
1131 // (after CSS is loaded) we need to set it here right away. It is crucial that
1132 // when execute() is called this is set synchronously, otherwise modules will get
1133 // executed multiple times as the registry will state that it isn't loading yet.
1134 registry[module].state = 'loading';
1136 // Add localizations to message system
1137 if ( $.isPlainObject( registry[module].messages ) ) {
1138 mw.messages.set( registry[module].messages );
1141 if ( $.isReady || registry[module].async ) {
1142 // Make sure we don't run the scripts until all (potentially asynchronous)
1143 // stylesheet insertions have completed.
1146 checkCssHandles = function () {
1147 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1148 // one of the cssHandles is fired while we're still creating more handles.
1149 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1151 runScript = undefined; // Revoke
1154 cssHandle = function () {
1155 var check = checkCssHandles;
1157 return function () {
1161 check = undefined; // Revoke
1167 // We are in blocking mode, and so we can't afford to wait for CSS
1168 cssHandle = function () {};
1170 checkCssHandles = runScript;
1173 // Process styles (see also mw.loader.implement)
1174 // * back-compat: { <media>: css }
1175 // * back-compat: { <media>: [url, ..] }
1176 // * { "css": [css, ..] }
1177 // * { "url": { <media>: [url, ..] } }
1178 if ( $.isPlainObject( registry[module].style ) ) {
1179 for ( key in registry[module].style ) {
1180 value = registry[module].style[key];
1183 if ( key !== 'url' && key !== 'css' ) {
1184 // Backwards compatibility, key is a media-type
1185 if ( typeof value === 'string' ) {
1186 // back-compat: { <media>: css }
1187 // Ignore 'media' because it isn't supported (nor was it used).
1188 // Strings are pre-wrapped in "@media". The media-type was just ""
1189 // (because it had to be set to something).
1190 // This is one of the reasons why this format is no longer used.
1191 addEmbeddedCSS( value, cssHandle() );
1193 // back-compat: { <media>: [url, ..] }
1199 // Array of css strings in key 'css',
1200 // or back-compat array of urls from media-type
1201 if ( $.isArray( value ) ) {
1202 for ( i = 0; i < value.length; i += 1 ) {
1203 if ( key === 'bc-url' ) {
1204 // back-compat: { <media>: [url, ..] }
1205 addLink( media, value[i] );
1206 } else if ( key === 'css' ) {
1207 // { "css": [css, ..] }
1208 addEmbeddedCSS( value[i], cssHandle() );
1211 // Not an array, but a regular object
1212 // Array of urls inside media-type key
1213 } else if ( typeof value === 'object' ) {
1214 // { "url": { <media>: [url, ..] } }
1215 for ( media in value ) {
1216 urls = value[media];
1217 for ( i = 0; i < urls.length; i += 1 ) {
1218 addLink( media, urls[i] );
1226 cssHandlesRegistered = true;
1231 * Adds a dependencies to the queue with optional callbacks to be run
1232 * when the dependencies are ready or fail
1235 * @param {string|string[]} dependencies Module name or array of string module names
1236 * @param {Function} [ready] Callback to execute when all dependencies are ready
1237 * @param {Function} [error] Callback to execute when any dependency fails
1238 * @param {boolean} [async] If true, load modules asynchronously even if
1239 * document ready has not yet occurred.
1241 function request( dependencies, ready, error, async ) {
1244 // Allow calling by single module name
1245 if ( typeof dependencies === 'string' ) {
1246 dependencies = [dependencies];
1249 // Add ready and error callbacks if they were given
1250 if ( ready !== undefined || error !== undefined ) {
1251 jobs[jobs.length] = {
1252 'dependencies': filter(
1253 ['registered', 'loading', 'loaded'],
1261 // Queue up any dependencies that are registered
1262 dependencies = filter( ['registered'], dependencies );
1263 for ( n = 0; n < dependencies.length; n += 1 ) {
1264 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1265 queue[queue.length] = dependencies[n];
1267 // Mark this module as async in the registry
1268 registry[dependencies[n]].async = true;
1277 function sortQuery( o ) {
1278 var sorted = {}, key, a = [];
1280 if ( hasOwn.call( o, key ) ) {
1285 for ( key = 0; key < a.length; key += 1 ) {
1286 sorted[a[key]] = o[a[key]];
1292 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1293 * to a query string of the form foo.bar,baz|bar.baz,quux
1296 function buildModulesString( moduleMap ) {
1297 var arr = [], p, prefix;
1298 for ( prefix in moduleMap ) {
1299 p = prefix === '' ? '' : prefix + '.';
1300 arr.push( p + moduleMap[prefix].join( ',' ) );
1302 return arr.join( '|' );
1306 * Asynchronously append a script tag to the end of the body
1307 * that invokes load.php
1309 * @param {Object} moduleMap Module map, see #buildModulesString
1310 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1311 * @param {string} sourceLoadScript URL of load.php
1312 * @param {boolean} async If true, use an asynchronous request even if document ready has not yet occurred
1314 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1315 var request = $.extend(
1316 { modules: buildModulesString( moduleMap ) },
1319 request = sortQuery( request );
1320 // Asynchronously append a script tag to the end of the body
1321 // Append &* to avoid triggering the IE6 extension check
1322 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1325 /* Public Members */
1328 * The module registry is exposed as an aid for debugging and inspecting page
1329 * state; it is not a public interface for modifying the registry.
1335 moduleRegistry: registry,
1338 * @inheritdoc #newStyleTag
1341 addStyleTag: newStyleTag,
1344 * Batch-request queued dependencies from the server.
1347 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1348 source, concatSource, group, g, i, modules, maxVersion, sourceLoadScript,
1349 currReqBase, currReqBaseLength, moduleMap, l,
1350 lastDotIndex, prefix, suffix, bytesAdded, async;
1352 // Build a list of request parameters common to all requests.
1354 skin: mw.config.get( 'skin' ),
1355 lang: mw.config.get( 'wgUserLanguage' ),
1356 debug: mw.config.get( 'debug' )
1358 // Split module batch by source and by group.
1360 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1362 // Appends a list of modules from the queue to the batch
1363 for ( q = 0; q < queue.length; q += 1 ) {
1364 // Only request modules which are registered
1365 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1366 // Prevent duplicate entries
1367 if ( $.inArray( queue[q], batch ) === -1 ) {
1368 batch[batch.length] = queue[q];
1369 // Mark registered modules as loading
1370 registry[queue[q]].state = 'loading';
1375 mw.loader.store.init();
1376 if ( mw.loader.store.enabled ) {
1378 batch = $.grep( batch, function ( module ) {
1379 var source = mw.loader.store.get( module );
1381 concatSource.push( source );
1386 $.globalEval( concatSource.join( ';' ) );
1389 // Early exit if there's nothing to load...
1390 if ( !batch.length ) {
1394 // The queue has been processed into the batch, clear up the queue.
1397 // Always order modules alphabetically to help reduce cache
1398 // misses for otherwise identical content.
1401 // Split batch by source and by group.
1402 for ( b = 0; b < batch.length; b += 1 ) {
1403 bSource = registry[batch[b]].source;
1404 bGroup = registry[batch[b]].group;
1405 if ( splits[bSource] === undefined ) {
1406 splits[bSource] = {};
1408 if ( splits[bSource][bGroup] === undefined ) {
1409 splits[bSource][bGroup] = [];
1411 bSourceGroup = splits[bSource][bGroup];
1412 bSourceGroup[bSourceGroup.length] = batch[b];
1415 // Clear the batch - this MUST happen before we append any
1416 // script elements to the body or it's possible that a script
1417 // will be locally cached, instantly load, and work the batch
1418 // again, all before we've cleared it causing each request to
1419 // include modules which are already loaded.
1422 for ( source in splits ) {
1424 sourceLoadScript = sources[source].loadScript;
1426 for ( group in splits[source] ) {
1428 // Cache access to currently selected list of
1429 // modules for this group from this source.
1430 modules = splits[source][group];
1432 // Calculate the highest timestamp
1434 for ( g = 0; g < modules.length; g += 1 ) {
1435 if ( registry[modules[g]].version > maxVersion ) {
1436 maxVersion = registry[modules[g]].version;
1440 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1441 // For user modules append a user name to the request.
1442 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1443 currReqBase.user = mw.config.get( 'wgUserName' );
1445 currReqBaseLength = $.param( currReqBase ).length;
1447 // We may need to split up the request to honor the query string length limit,
1448 // so build it piece by piece.
1449 l = currReqBaseLength + 9; // '&modules='.length == 9
1451 moduleMap = {}; // { prefix: [ suffixes ] }
1453 for ( i = 0; i < modules.length; i += 1 ) {
1454 // Determine how many bytes this module would add to the query string
1455 lastDotIndex = modules[i].lastIndexOf( '.' );
1456 // Note that these substr() calls work even if lastDotIndex == -1
1457 prefix = modules[i].substr( 0, lastDotIndex );
1458 suffix = modules[i].substr( lastDotIndex + 1 );
1459 bytesAdded = moduleMap[prefix] !== undefined
1460 ? suffix.length + 3 // '%2C'.length == 3
1461 : modules[i].length + 3; // '%7C'.length == 3
1463 // If the request would become too long, create a new one,
1464 // but don't create empty requests
1465 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1466 // This request would become too long, create a new one
1467 // and fire off the old one
1468 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1471 l = currReqBaseLength + 9;
1473 if ( moduleMap[prefix] === undefined ) {
1474 moduleMap[prefix] = [];
1476 moduleMap[prefix].push( suffix );
1477 if ( !registry[modules[i]].async ) {
1478 // If this module is blocking, make the entire request blocking
1479 // This is slightly suboptimal, but in practice mixing of blocking
1480 // and async modules will only occur in debug mode.
1485 // If there's anything left in moduleMap, request that too
1486 if ( !$.isEmptyObject( moduleMap ) ) {
1487 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1494 * Register a source.
1496 * The #work method will use this information to split up requests by source.
1498 * mw.loader.addSource( 'mediawikiwiki', { loadScript: '//www.mediawiki.org/w/load.php' } );
1500 * @param {string} id Short string representing a source wiki, used internally for
1501 * registered modules to indicate where they should be loaded from (usually lowercase a-z).
1502 * @param {Object} props
1503 * @param {string} props.loadScript Url to the load.php entry point of the source wiki.
1506 addSource: function ( id, props ) {
1508 // Allow multiple additions
1509 if ( typeof id === 'object' ) {
1510 for ( source in id ) {
1511 mw.loader.addSource( source, id[source] );
1516 if ( sources[id] !== undefined ) {
1517 throw new Error( 'source already registered: ' + id );
1520 sources[id] = props;
1526 * Register a module, letting the system know about it and its
1527 * properties. Startup modules contain calls to this function.
1529 * @param {string} module Module name
1530 * @param {number} version Module version number as a timestamp (falls backs to 0)
1531 * @param {string|Array|Function} dependencies One string or array of strings of module
1532 * names on which this module depends, or a function that returns that array.
1533 * @param {string} [group=null] Group which the module is in
1534 * @param {string} [source='local'] Name of the source
1536 register: function ( module, version, dependencies, group, source ) {
1538 // Allow multiple registration
1539 if ( typeof module === 'object' ) {
1540 for ( m = 0; m < module.length; m += 1 ) {
1541 // module is an array of module names
1542 if ( typeof module[m] === 'string' ) {
1543 mw.loader.register( module[m] );
1544 // module is an array of arrays
1545 } else if ( typeof module[m] === 'object' ) {
1546 mw.loader.register.apply( mw.loader, module[m] );
1552 if ( typeof module !== 'string' ) {
1553 throw new Error( 'module must be a string, not a ' + typeof module );
1555 if ( registry[module] !== undefined ) {
1556 throw new Error( 'module already registered: ' + module );
1558 // List the module as registered
1559 registry[module] = {
1560 version: version !== undefined ? parseInt( version, 10 ) : 0,
1562 group: typeof group === 'string' ? group : null,
1563 source: typeof source === 'string' ? source: 'local',
1566 if ( typeof dependencies === 'string' ) {
1567 // Allow dependencies to be given as a single module name
1568 registry[module].dependencies = [ dependencies ];
1569 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1570 // Allow dependencies to be given as an array of module names
1571 // or a function which returns an array
1572 registry[module].dependencies = dependencies;
1577 * Implement a module given the components that make up the module.
1579 * When #load or #using requests one or more modules, the server
1580 * response contain calls to this function.
1582 * All arguments are required.
1584 * @param {string} module Name of module
1585 * @param {Function|Array} script Function with module code or Array of URLs to
1586 * be used as the src attribute of a new `<script>` tag.
1587 * @param {Object} style Should follow one of the following patterns:
1589 * { "css": [css, ..] }
1590 * { "url": { <media>: [url, ..] } }
1592 * And for backwards compatibility (needs to be supported forever due to caching):
1595 * { <media>: [url, ..] }
1597 * The reason css strings are not concatenated anymore is bug 31676. We now check
1598 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1600 * @param {Object} msgs List of key/value pairs to be added to mw#messages.
1602 implement: function ( module, script, style, msgs ) {
1604 if ( typeof module !== 'string' ) {
1605 throw new Error( 'module must be a string, not a ' + typeof module );
1607 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1608 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1610 if ( !$.isPlainObject( style ) ) {
1611 throw new Error( 'style must be an object, not a ' + typeof style );
1613 if ( !$.isPlainObject( msgs ) ) {
1614 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1616 // Automatically register module
1617 if ( registry[module] === undefined ) {
1618 mw.loader.register( module );
1620 // Check for duplicate implementation
1621 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1622 throw new Error( 'module already implemented: ' + module );
1624 // Attach components
1625 registry[module].script = script;
1626 registry[module].style = style;
1627 registry[module].messages = msgs;
1628 // The module may already have been marked as erroneous
1629 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1630 registry[module].state = 'loaded';
1631 if ( allReady( registry[module].dependencies ) ) {
1638 * Execute a function as soon as one or more required modules are ready.
1640 * Example of inline dependency on OOjs:
1642 * mw.loader.using( 'oojs', function () {
1643 * OO.compare( [ 1 ], [ 1 ] );
1646 * @param {string|Array} dependencies Module name or array of modules names the callback
1647 * dependends on to be ready before executing
1648 * @param {Function} [ready] callback to execute when all dependencies are ready
1649 * @param {Function} [error] callback to execute when if dependencies have a errors
1651 using: function ( dependencies, ready, error ) {
1652 var tod = typeof dependencies;
1654 if ( tod !== 'object' && tod !== 'string' ) {
1655 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1657 // Allow calling with a single dependency as a string
1658 if ( tod === 'string' ) {
1659 dependencies = [ dependencies ];
1661 // Resolve entire dependency map
1662 dependencies = resolve( dependencies );
1663 if ( allReady( dependencies ) ) {
1664 // Run ready immediately
1665 if ( $.isFunction( ready ) ) {
1668 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1669 // Execute error immediately if any dependencies have errors
1670 if ( $.isFunction( error ) ) {
1671 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1675 // Not all dependencies are ready: queue up a request
1676 request( dependencies, ready, error );
1681 * Load an external script or one or more modules.
1683 * @param {string|Array} modules Either the name of a module, array of modules,
1684 * or a URL of an external script or style
1685 * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
1686 * external script or style; acceptable values are "text/css" and
1687 * "text/javascript"; if no type is provided, text/javascript is assumed.
1688 * @param {boolean} [async] If true, load modules asynchronously
1689 * even if document ready has not yet occurred. If false, block before
1690 * document ready and load async after. If not set, true will be
1691 * assumed if loading a URL, and false will be assumed otherwise.
1693 load: function ( modules, type, async ) {
1694 var filtered, m, module, l;
1697 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1698 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1700 // Allow calling with an external url or single dependency as a string
1701 if ( typeof modules === 'string' ) {
1702 // Support adding arbitrary external scripts
1703 if ( /^(https?:)?\/\//.test( modules ) ) {
1704 if ( async === undefined ) {
1705 // Assume async for bug 34542
1708 if ( type === 'text/css' ) {
1709 // IE7-8 throws security warnings when inserting a <link> tag
1710 // with a protocol-relative URL set though attributes (instead of
1711 // properties) - when on HTTPS. See also bug #.
1712 l = document.createElement( 'link' );
1713 l.rel = 'stylesheet';
1715 $( 'head' ).append( l );
1718 if ( type === 'text/javascript' || type === undefined ) {
1719 addScript( modules, null, async );
1723 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1725 // Called with single module
1726 modules = [ modules ];
1729 // Filter out undefined modules, otherwise resolve() will throw
1730 // an exception for trying to load an undefined module.
1731 // Undefined modules are acceptable here in load(), because load() takes
1732 // an array of unrelated modules, whereas the modules passed to
1733 // using() are related and must all be loaded.
1734 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1735 module = registry[modules[m]];
1736 if ( module !== undefined ) {
1737 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1738 filtered[filtered.length] = modules[m];
1743 if ( filtered.length === 0 ) {
1746 // Resolve entire dependency map
1747 filtered = resolve( filtered );
1748 // If all modules are ready, nothing to be done
1749 if ( allReady( filtered ) ) {
1752 // If any modules have errors: also quit.
1753 if ( filter( ['error', 'missing'], filtered ).length ) {
1756 // Since some modules are not yet ready, queue up a request.
1757 request( filtered, undefined, undefined, async );
1761 * Change the state of one or more modules.
1763 * @param {string|Object} module Module name or object of module name/state pairs
1764 * @param {string} state State name
1766 state: function ( module, state ) {
1769 if ( typeof module === 'object' ) {
1770 for ( m in module ) {
1771 mw.loader.state( m, module[m] );
1775 if ( registry[module] === undefined ) {
1776 mw.loader.register( module );
1778 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1779 && registry[module].state !== state ) {
1780 // Make sure pending modules depending on this one get executed if their
1781 // dependencies are now fulfilled!
1782 registry[module].state = state;
1783 handlePending( module );
1785 registry[module].state = state;
1790 * Get the version of a module.
1792 * @param {string} module Name of module to get version for
1793 * @return {string|null} The version, or null if the module (or its version) is not
1796 getVersion: function ( module ) {
1797 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1798 return formatVersionNumber( registry[module].version );
1804 * @inheritdoc #getVersion
1805 * @deprecated since 1.18 use #getVersion instead
1807 version: function () {
1808 return mw.loader.getVersion.apply( mw.loader, arguments );
1812 * Get the state of a module.
1814 * @param {string} module Name of module to get state for
1816 getState: function ( module ) {
1817 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1818 return registry[module].state;
1824 * Get the names of all registered modules.
1828 getModuleNames: function () {
1829 return $.map( registry, function ( i, key ) {
1835 * Load the `mediawiki.user` module.
1837 * For backwards-compatibility with cached pages from before 2013 where:
1839 * - the `mediawiki.user` module didn't exist yet
1840 * - `mw.user` was still part of mediawiki.js
1841 * - `mw.loader.go` still existed and called after `mw.loader.load()`
1844 mw.loader.load( 'mediawiki.user' );
1848 * @inheritdoc mw.inspect#runReports
1851 inspect: function () {
1852 var args = slice.call( arguments );
1853 mw.loader.using( 'mediawiki.inspect', function () {
1854 mw.inspect.runReports.apply( mw.inspect, args );
1859 * On browsers that implement the localStorage API, the module store serves as a
1860 * smart complement to the browser cache. Unlike the browser cache, the module store
1861 * can slice a concatenated response from ResourceLoader into its constituent
1862 * modules and cache each of them separately, using each module's versioning scheme
1863 * to determine when the cache should be invalidated.
1866 * @class mw.loader.store
1869 // Whether the store is in use on this page.
1872 // The contents of the store, mapping '[module name]@[version]' keys
1873 // to module implementations.
1877 stats: { hits: 0, misses: 0, expired: 0 },
1880 * Construct a JSON-serializable object representing the content of the store.
1881 * @return {Object} Module store contents.
1883 toJSON: function () {
1884 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
1888 * Get the localStorage key for the entire module store. The key references
1889 * $wgDBname to prevent clashes between wikis which share a common host.
1891 * @return {string} localStorage item key
1893 getStoreKey: function () {
1894 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
1898 * Get a string key on which to vary the module cache.
1899 * @return {string} String of concatenated vary conditions.
1901 getVary: function () {
1903 mw.config.get( 'skin' ),
1904 mw.config.get( 'wgResourceLoaderStorageVersion' ),
1905 mw.config.get( 'wgUserLanguage' )
1910 * Get a string key for a specific module. The key format is '[name]@[version]'.
1912 * @param {string} module Module name
1913 * @return {string|null} Module key or null if module does not exist
1915 getModuleKey: function ( module ) {
1916 return typeof registry[module] === 'object' ?
1917 ( module + '@' + registry[module].version ) : null;
1921 * Initialize the store.
1923 * Retrieves store from localStorage and (if successfully retrieved) decoding
1924 * the stored JSON value to a plain object.
1926 * The try / catch block is used for JSON & localStorage feature detection.
1927 * See the in-line documentation for Modernizr's localStorage feature detection
1928 * code for a full account of why we need a try / catch:
1929 * https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796
1934 if ( mw.loader.store.enabled !== null ) {
1939 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) {
1940 // Disabled by configuration, or because debug mode is set
1941 mw.loader.store.enabled = false;
1946 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
1947 // If we get here, localStorage is available; mark enabled
1948 mw.loader.store.enabled = true;
1949 data = JSON.parse( raw );
1950 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
1951 mw.loader.store.items = data.items;
1956 if ( raw === undefined ) {
1957 // localStorage failed; disable store
1958 mw.loader.store.enabled = false;
1960 mw.loader.store.update();
1965 * Retrieve a module from the store and update cache hit stats.
1967 * @param {string} module Module name
1968 * @return {string|boolean} Module implementation or false if unavailable
1970 get: function ( module ) {
1973 if ( !mw.loader.store.enabled ) {
1977 key = mw.loader.store.getModuleKey( module );
1978 if ( key in mw.loader.store.items ) {
1979 mw.loader.store.stats.hits++;
1980 return mw.loader.store.items[key];
1982 mw.loader.store.stats.misses++;
1987 * Stringify a module and queue it for storage.
1989 * @param {string} module Module name
1990 * @param {Object} descriptor The module's descriptor as set in the registry
1992 set: function ( module, descriptor ) {
1995 if ( !mw.loader.store.enabled ) {
1999 key = mw.loader.store.getModuleKey( module );
2002 // Already stored a copy of this exact version
2003 key in mw.loader.store.items ||
2004 // Module failed to load
2005 descriptor.state !== 'ready' ||
2006 // Unversioned, private, or site-/user-specific
2007 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
2008 // Partial descriptor
2009 $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1
2017 JSON.stringify( module ),
2018 typeof descriptor.script === 'function' ?
2019 String( descriptor.script ) :
2020 JSON.stringify( descriptor.script ),
2021 JSON.stringify( descriptor.style ),
2022 JSON.stringify( descriptor.messages )
2024 // Attempted workaround for a possible Opera bug (bug 57567).
2025 // This regex should never match under sane conditions.
2026 if ( /^\s*\(/.test( args[1] ) ) {
2027 args[1] = 'function' + args[1];
2028 log( 'Detected malformed function stringification (bug 57567)' );
2034 mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');';
2035 mw.loader.store.update();
2039 * Iterate through the module store, removing any item that does not correspond
2040 * (in name and version) to an item in the module registry.
2042 prune: function () {
2045 if ( !mw.loader.store.enabled ) {
2049 for ( key in mw.loader.store.items ) {
2050 module = key.substring( 0, key.indexOf( '@' ) );
2051 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2052 mw.loader.store.stats.expired++;
2053 delete mw.loader.store.items[key];
2059 * Sync modules to localStorage.
2061 * This function debounces localStorage updates. When called multiple times in
2062 * quick succession, the calls are coalesced into a single update operation.
2063 * This allows us to call #update without having to consider the module load
2064 * queue; the call to localStorage.setItem will be naturally deferred until the
2065 * page is quiescent.
2067 * Because localStorage is shared by all pages with the same origin, if multiple
2068 * pages are loaded with different module sets, the possibility exists that
2069 * modules saved by one page will be clobbered by another. But the impact would
2070 * be minor and the problem would be corrected by subsequent page views.
2074 update: ( function () {
2079 key = mw.loader.store.getStoreKey();
2081 if ( !mw.loader.store.enabled ) {
2084 mw.loader.store.prune();
2086 // Replacing the content of the module store might fail if the new
2087 // contents would exceed the browser's localStorage size limit. To
2088 // avoid clogging the browser with stale data, always remove the old
2089 // value before attempting to set the new one.
2090 localStorage.removeItem( key );
2091 data = JSON.stringify( mw.loader.store );
2092 localStorage.setItem( key, data );
2096 return function () {
2097 clearTimeout( timer );
2098 timer = setTimeout( flush, 2000 );
2106 * HTML construction helper functions
2113 * output = Html.element( 'div', {}, new Html.Raw(
2114 * Html.element( 'img', { src: '<' } )
2116 * mw.log( output ); // <div><img src="<"/></div>
2121 html: ( function () {
2122 function escapeCallback( s ) {
2139 * Escape a string for HTML. Converts special characters to HTML entities.
2141 * mw.html.escape( '< > \' & "' );
2142 * // Returns < > ' & "
2144 * @param {string} s The string to escape
2146 escape: function ( s ) {
2147 return s.replace( /['"<>&]/g, escapeCallback );
2151 * Create an HTML element string, with safe escaping.
2153 * @param {string} name The tag name.
2154 * @param {Object} attrs An object with members mapping element names to values
2155 * @param {Mixed} contents The contents of the element. May be either:
2156 * - string: The string is escaped.
2157 * - null or undefined: The short closing form is used, e.g. <br/>.
2158 * - this.Raw: The value attribute is included without escaping.
2159 * - this.Cdata: The value attribute is included, and an exception is
2160 * thrown if it contains an illegal ETAGO delimiter.
2161 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
2163 element: function ( name, attrs, contents ) {
2164 var v, attrName, s = '<' + name;
2166 for ( attrName in attrs ) {
2167 v = attrs[attrName];
2168 // Convert name=true, to name=name
2172 } else if ( v === false ) {
2175 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2177 if ( contents === undefined || contents === null ) {
2184 switch ( typeof contents ) {
2187 s += this.escape( contents );
2191 // Convert to string
2192 s += String( contents );
2195 if ( contents instanceof this.Raw ) {
2196 // Raw HTML inclusion
2197 s += contents.value;
2198 } else if ( contents instanceof this.Cdata ) {
2200 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2201 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2203 s += contents.value;
2205 throw new Error( 'mw.html.element: Invalid type of contents' );
2208 s += '</' + name + '>';
2213 * Wrapper object for raw HTML passed to mw.html.element().
2214 * @class mw.html.Raw
2216 Raw: function ( value ) {
2221 * Wrapper object for CDATA element contents passed to mw.html.element()
2222 * @class mw.html.Cdata
2224 Cdata: function ( value ) {
2230 // Skeleton user object. mediawiki.user.js extends this
2237 * Registry and firing of events.
2239 * MediaWiki has various interface components that are extended, enhanced
2240 * or manipulated in some other way by extensions, gadgets and even
2243 * This framework helps streamlining the timing of when these other
2244 * code paths fire their plugins (instead of using document-ready,
2245 * which can and should be limited to firing only once).
2247 * Features like navigating to other wiki pages, previewing an edit
2248 * and editing itself – without a refresh – can then retrigger these
2249 * hooks accordingly to ensure everything still works as expected.
2253 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2254 * mw.hook( 'wikipage.content' ).fire( $content );
2256 * Handlers can be added and fired for arbitrary event names at any time. The same
2257 * event can be fired multiple times. The last run of an event is memorized
2258 * (similar to `$(document).ready` and `$.Deferred().done`).
2259 * This means if an event is fired, and a handler added afterwards, the added
2260 * function will be fired right away with the last given event data.
2262 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2263 * Thus allowing flexible use and optimal maintainability and authority control.
2264 * You can pass around the `add` and/or `fire` method to another piece of code
2265 * without it having to know the event name (or `mw.hook` for that matter).
2267 * var h = mw.hook( 'bar.ready' );
2268 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2270 * Note: Events are documented with an underscore instead of a dot in the event
2271 * name due to jsduck not supporting dots in that position.
2275 hook: ( function () {
2279 * Create an instance of mw.hook.
2283 * @param {string} name Name of hook.
2286 return function ( name ) {
2287 var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) );
2291 * Register a hook handler
2292 * @param {Function...} handler Function to bind.
2298 * Unregister a hook handler
2299 * @param {Function...} handler Function to unbind.
2302 remove: list.remove,
2306 * @param {Mixed...} data
2310 return list.fireWith( null, slice.call( arguments ) );
2319 // Alias $j to jQuery for backwards compatibility
2322 // Attach to window and globally alias
2323 window.mw = window.mediaWiki = mw;
2325 // Auto-register from pre-loaded startup scripts
2326 if ( jQuery.isFunction( window.startUp ) ) {
2328 window.startUp = undefined;