2 * Base library for MediaWiki.
5 * @alternateClassName mediaWiki
9 var mw = ( function ( $, undefined ) {
14 var hasOwn = Object.prototype.hasOwnProperty,
15 slice = Array.prototype.slice;
18 * Log a message to window.console, if possible. Useful to force logging of some
19 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
20 * Gets console references in each invocation, so that delayed debugging tools work
21 * fine. No need for optimization here, which would only result in losing logs.
25 * @param {string} msg text for the log entry.
28 function log( msg, e ) {
29 var console = window.console;
30 if ( console && console.log ) {
32 // If we have an exception object, log it through .error() to trigger
33 // proper stacktraces in browsers that support it. There are no (known)
34 // browsers that don't support .error(), that do support .log() and
35 // have useful exception handling through .log().
36 if ( e && console.error ) {
37 console.error( String( e ), e );
42 /* Object constructors */
45 * Creates an object that can be read from or written to from prototype functions
46 * that allow both single and multiple variables at once.
50 * var addies, wanted, results;
52 * // Create your address book
53 * addies = new mw.Map();
55 * // This data could be coming from an external source (eg. API/AJAX)
57 * 'John Doe' : '10 Wall Street, New York, USA',
58 * 'Jane Jackson' : '21 Oxford St, London, UK',
59 * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
62 * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
64 * // You can detect missing keys first
65 * if ( !addies.exists( wanted ) ) {
66 * // One or more are missing (in this case: "George Johnson")
67 * mw.log( 'One or more names were not found in your address book' );
70 * // Or just let it give you what it can
71 * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
72 * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
73 * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
78 * @param {Object|boolean} [values] Value-bearing object to map, or boolean
79 * true to map over the global object. Defaults to an empty object.
81 function Map( values ) {
82 this.values = values === true ? window : ( values || {} );
88 * Get the value of one or multiple a keys.
90 * If called with no arguments, all values will be returned.
92 * @param {string|Array} selection String key or array of keys to get values for.
93 * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
94 * @return mixed If selection was a string returns the value or null,
95 * If selection was an array, returns an object of key/values (value is null if not found),
96 * If selection was not passed or invalid, will return the 'values' object member (be careful as
97 * objects are always passed by reference in JavaScript!).
98 * @return {string|Object|null} Values as a string or object, null if invalid/inexistant.
100 get: function ( selection, fallback ) {
102 // If we only do this in the `return` block, it'll fail for the
103 // call to get() from the mutli-selection block.
104 fallback = arguments.length > 1 ? fallback : null;
106 if ( $.isArray( selection ) ) {
107 selection = slice.call( selection );
109 for ( i = 0; i < selection.length; i++ ) {
110 results[selection[i]] = this.get( selection[i], fallback );
115 if ( typeof selection === 'string' ) {
116 if ( !hasOwn.call( this.values, selection ) ) {
119 return this.values[selection];
122 if ( selection === undefined ) {
126 // invalid selection key
131 * Sets one or multiple key/value pairs.
133 * @param {string|Object} selection String key to set value for, or object mapping keys to values.
134 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
135 * @return {Boolean} This returns true on success, false on failure.
137 set: function ( selection, value ) {
140 if ( $.isPlainObject( selection ) ) {
141 for ( s in selection ) {
142 this.values[s] = selection[s];
146 if ( typeof selection === 'string' && arguments.length > 1 ) {
147 this.values[selection] = value;
154 * Checks if one or multiple keys exist.
156 * @param {Mixed} selection String key or array of keys to check
157 * @return {boolean} Existence of key(s)
159 exists: function ( selection ) {
162 if ( $.isArray( selection ) ) {
163 for ( s = 0; s < selection.length; s++ ) {
164 if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
170 return typeof selection === 'string' && hasOwn.call( this.values, selection );
175 * Object constructor for messages.
177 * Similar to the Message class in MediaWiki PHP.
179 * Format defaults to 'text'.
184 * @param {mw.Map} map Message storage
185 * @param {string} key
186 * @param {Array} [parameters]
188 function Message( map, key, parameters ) {
189 this.format = 'text';
192 this.parameters = parameters === undefined ? [] : slice.call( parameters );
196 Message.prototype = {
198 * Simple message parser, does $N replacement and nothing else.
200 * This may be overridden to provide a more complex message parser.
202 * The primary override is in mediawiki.jqueryMsg.
204 * This function will not be called for nonexistent messages.
206 parser: function () {
207 var parameters = this.parameters;
208 return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
209 var index = parseInt( match, 10 ) - 1;
210 return parameters[index] !== undefined ? parameters[index] : '$' + match;
215 * Appends (does not replace) parameters for replacement to the .parameters property.
217 * @param {Array} parameters
220 params: function ( parameters ) {
222 for ( i = 0; i < parameters.length; i += 1 ) {
223 this.parameters.push( parameters[i] );
229 * Converts message object to its string form based on the state of format.
231 * @return {string} Message as a string in the current form or `<key>` if key does not exist.
233 toString: function () {
236 if ( !this.exists() ) {
237 // Use <key> as text if key does not exist
238 if ( this.format === 'escaped' || this.format === 'parse' ) {
239 // format 'escaped' and 'parse' need to have the brackets and key html escaped
240 return mw.html.escape( '<' + this.key + '>' );
242 return '<' + this.key + '>';
245 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
246 text = this.parser();
249 if ( this.format === 'escaped' ) {
250 text = this.parser();
251 text = mw.html.escape( text );
258 * Changes format to 'parse' and converts message to string
260 * If jqueryMsg is loaded, this parses the message text from wikitext
261 * (where supported) to HTML
263 * Otherwise, it is equivalent to plain.
265 * @return {string} String form of parsed message
268 this.format = 'parse';
269 return this.toString();
273 * Changes format to 'plain' and converts message to string
275 * This substitutes parameters, but otherwise does not change the
278 * @return {string} String form of plain message
281 this.format = 'plain';
282 return this.toString();
286 * Changes format to 'text' and converts message to string
288 * If jqueryMsg is loaded, {{-transformation is done where supported
289 * (such as {{plural:}}, {{gender:}}, {{int:}}).
291 * Otherwise, it is equivalent to plain.
294 this.format = 'text';
295 return this.toString();
299 * Changes the format to 'escaped' and converts message to string
301 * This is equivalent to using the 'text' format (see text method), then
302 * HTML-escaping the output.
304 * @return {string} String form of html escaped message
306 escaped: function () {
307 this.format = 'escaped';
308 return this.toString();
312 * Checks if message exists
317 exists: function () {
318 return this.map.exists( this.key );
329 * Dummy placeholder for {@link mw.log}
333 var log = function () {};
334 log.warn = function () {};
335 log.deprecate = function ( obj, key, val ) {
341 // Make the Map constructor publicly available.
344 // Make the Message constructor publicly available.
348 * Map of configuration values
350 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
353 * If `$wgLegacyJavaScriptGlobals` is true, this Map will put its values in the
354 * global window object.
356 * @property {mw.Map} config
358 // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule with an instance of `mw.Map`.
362 * Empty object that plugins can be installed in.
368 * Access container for deprecated functionality that can be moved from
369 * from their legacy location and attached to this object (e.g. a global
370 * function that is deprecated and as stop-gap can be exposed through here).
372 * This was reserved for future use but never ended up being used.
374 * @deprecated since 1.22: Let deprecated identifiers keep their original name
375 * and use mw.log#deprecate to create an access container for tracking.
381 * Localization system
389 * Get a message object.
391 * Similar to wfMessage() in MediaWiki PHP.
393 * @param {string} key Key of message to get
394 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
395 * @return {mw.Message}
397 message: function ( key ) {
398 // Variadic arguments
399 var parameters = slice.call( arguments, 1 );
400 return new Message( mw.messages, key, parameters );
404 * Get a message string using 'text' format.
406 * Similar to wfMsg() in MediaWiki PHP.
409 * @param {string} key Key of message to get
410 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
414 return mw.message.apply( mw.message, arguments ).toString();
418 * Client-side module loader which integrates with the MediaWiki ResourceLoader
422 loader: ( function () {
424 /* Private Members */
427 * Mapping of registered modules
429 * The jquery module is pre-registered, because it must have already
430 * been provided for this object to have been built, and in debug mode
431 * jquery would have been provided through a unique loader request,
432 * making it impossible to hold back registration of jquery until after
435 * For exact details on support for script, style and messages, look at
436 * mw.loader.implement.
441 * 'version': ############## (unix timestamp),
442 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
443 * 'group': 'somegroup', (or) null,
444 * 'source': 'local', 'someforeignwiki', (or) null
445 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
448 * 'messages': { 'key': 'value' },
457 // Mapping of sources, keyed by source-id, values are objects.
461 // 'loadScript': 'http://foo.bar/w/load.php'
466 // List of modules which will be loaded as when ready
468 // List of modules to be loaded
470 // List of callback functions waiting for modules to be ready to be called
472 // Selector cache for the marker element. Use getMarker() to get/use the marker!
474 // Buffer for addEmbeddedCSS.
476 // Callbacks for addEmbeddedCSS.
477 cssCallbacks = $.Callbacks();
479 /* Private methods */
481 function getMarker() {
487 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
488 if ( $marker.length ) {
491 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
492 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
498 * Create a new style tag and add it to the DOM.
501 * @param {string} text CSS text
502 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
503 * inserted before. Otherwise it will be appended to `<head>`.
504 * @return {HTMLElement} Reference to the created `<style>` element.
506 function newStyleTag( text, nextnode ) {
507 var s = document.createElement( 'style' );
508 // Insert into document before setting cssText (bug 33305)
510 // Must be inserted with native insertBefore, not $.fn.before.
511 // When using jQuery to insert it, like $nextnode.before( s ),
512 // then IE6 will throw "Access is denied" when trying to append
513 // to .cssText later. Some kind of weird security measure.
514 // http://stackoverflow.com/q/12586482/319266
515 // Works: jsfiddle.net/zJzMy/1
516 // Fails: jsfiddle.net/uJTQz
517 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
518 if ( nextnode.jquery ) {
519 nextnode = nextnode.get( 0 );
521 nextnode.parentNode.insertBefore( s, nextnode );
523 document.getElementsByTagName( 'head' )[0].appendChild( s );
525 if ( s.styleSheet ) {
527 s.styleSheet.cssText = text;
530 // (Safari sometimes borks on non-string values,
531 // play safe by casting to a string, just in case.)
532 s.appendChild( document.createTextNode( String( text ) ) );
538 * Checks whether it is safe to add this css to a stylesheet.
541 * @param {string} cssText
542 * @return {boolean} False if a new one must be created.
544 function canExpandStylesheetWith( cssText ) {
545 // Makes sure that cssText containing `@import`
546 // rules will end up in a new stylesheet (as those only work when
547 // placed at the start of a stylesheet; bug 35562).
548 return cssText.indexOf( '@import' ) === -1;
552 * Add a bit of CSS text to the current browser page.
554 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
555 * or create a new one based on whether the given `cssText` is safe for extension.
557 * @param {string} [cssText=cssBuffer] If called without cssText,
558 * the internal buffer will be inserted instead.
559 * @param {Function} [callback]
561 function addEmbeddedCSS( cssText, callback ) {
565 cssCallbacks.add( callback );
568 // Yield once before inserting the <style> tag. There are likely
569 // more calls coming up which we can combine this way.
570 // Appending a stylesheet and waiting for the browser to repaint
571 // is fairly expensive, this reduces it (bug 45810)
573 // Be careful not to extend the buffer with css that needs a new stylesheet
574 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
575 // Linebreak for somewhat distinguishable sections
576 // (the rl-cachekey comment separating each)
577 cssBuffer += '\n' + cssText;
578 // TODO: Use requestAnimationFrame in the future which will
579 // perform even better by not injecting styles while the browser
581 setTimeout( function () {
582 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
583 // (below version 13) has the non-standard behaviour of passing a
584 // numerical "lateness" value as first argument to this callback
585 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
591 // This is a delayed call and we got a buffer still
592 } else if ( cssBuffer ) {
596 // This is a delayed call, but buffer is already cleared by
597 // another delayed call.
601 // By default, always create a new <style>. Appending text
602 // to a <style> tag means the contents have to be re-parsed (bug 45810).
603 // Except, of course, in IE below 9, in there we default to
604 // re-using and appending to a <style> tag due to the
605 // IE stylesheet limit (bug 31676).
606 if ( 'documentMode' in document && document.documentMode <= 9 ) {
608 $style = getMarker().prev();
609 // Verify that the the element before Marker actually is a
610 // <style> tag and one that came from ResourceLoader
611 // (not some other style tag or even a `<meta>` or `<script>`).
612 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
613 // There's already a dynamic <style> tag present and
614 // canExpandStylesheetWith() gave a green light to append more to it.
615 styleEl = $style.get( 0 );
616 if ( styleEl.styleSheet ) {
618 styleEl.styleSheet.cssText += cssText; // IE
620 log( 'addEmbeddedCSS fail', e );
623 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
625 cssCallbacks.fire().empty();
630 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
632 cssCallbacks.fire().empty();
636 * Generates an ISO8601 "basic" string from a UNIX timestamp
639 function formatVersionNumber( timestamp ) {
641 function pad( a, b, c ) {
642 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
644 d.setTime( timestamp * 1000 );
646 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
647 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
652 * Resolves dependencies and detects circular references.
655 * @param {string} module Name of the top-level module whose dependencies shall be
656 * resolved and sorted.
657 * @param {Array} resolved Returns a topological sort of the given module and its
658 * dependencies, such that later modules depend on earlier modules. The array
659 * contains the module names. If the array contains already some module names,
660 * this function appends its result to the pre-existing array.
661 * @param {Object} [unresolved] Hash used to track the current dependency
662 * chain; used to report loops in the dependency graph.
663 * @throws {Error} If any unregistered module or a dependency loop is encountered
665 function sortDependencies( module, resolved, unresolved ) {
668 if ( registry[module] === undefined ) {
669 throw new Error( 'Unknown dependency: ' + module );
671 // Resolves dynamic loader function and replaces it with its own results
672 if ( $.isFunction( registry[module].dependencies ) ) {
673 registry[module].dependencies = registry[module].dependencies();
674 // Ensures the module's dependencies are always in an array
675 if ( typeof registry[module].dependencies !== 'object' ) {
676 registry[module].dependencies = [registry[module].dependencies];
679 if ( $.inArray( module, resolved ) !== -1 ) {
680 // Module already resolved; nothing to do.
683 // unresolved is optional, supply it if not passed in
687 // Tracks down dependencies
688 deps = registry[module].dependencies;
690 for ( n = 0; n < len; n += 1 ) {
691 if ( $.inArray( deps[n], resolved ) === -1 ) {
692 if ( unresolved[deps[n]] ) {
694 'Circular reference detected: ' + module +
700 unresolved[module] = true;
701 sortDependencies( deps[n], resolved, unresolved );
702 delete unresolved[module];
705 resolved[resolved.length] = module;
709 * Gets a list of module names that a module depends on in their proper dependency
713 * @param {string} module Module name or array of string module names
714 * @return {Array} list of dependencies, including 'module'.
715 * @throws {Error} If circular reference is detected
717 function resolve( module ) {
720 // Allow calling with an array of module names
721 if ( $.isArray( module ) ) {
723 for ( m = 0; m < module.length; m += 1 ) {
724 sortDependencies( module[m], resolved );
729 if ( typeof module === 'string' ) {
731 sortDependencies( module, resolved );
735 throw new Error( 'Invalid module argument: ' + module );
739 * Narrows a list of module names down to those matching a specific
740 * state (see comment on top of this scope for a list of valid states).
741 * One can also filter for 'unregistered', which will return the
742 * modules names that don't have a registry entry.
745 * @param {string|string[]} states Module states to filter by
746 * @param {Array} [modules] List of module names to filter (optional, by default the entire
748 * @return {Array} List of filtered module names
750 function filter( states, modules ) {
751 var list, module, s, m;
753 // Allow states to be given as a string
754 if ( typeof states === 'string' ) {
757 // If called without a list of modules, build and use a list of all modules
759 if ( modules === undefined ) {
761 for ( module in registry ) {
762 modules[modules.length] = module;
765 // Build a list of modules which are in one of the specified states
766 for ( s = 0; s < states.length; s += 1 ) {
767 for ( m = 0; m < modules.length; m += 1 ) {
768 if ( registry[modules[m]] === undefined ) {
769 // Module does not exist
770 if ( states[s] === 'unregistered' ) {
772 list[list.length] = modules[m];
775 // Module exists, check state
776 if ( registry[modules[m]].state === states[s] ) {
778 list[list.length] = modules[m];
787 * Determine whether all dependencies are in state 'ready', which means we may
788 * execute the module or job now.
791 * @param {Array} dependencies Dependencies (module names) to be checked.
792 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
794 function allReady( dependencies ) {
795 return filter( 'ready', dependencies ).length === dependencies.length;
799 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
800 * and modules that depend upon this module. if the given module failed, propagate the 'error'
801 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
802 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
805 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
807 function handlePending( module ) {
808 var j, job, hasErrors, m, stateChange;
811 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
812 // If the current module failed, mark all dependent modules also as failed.
813 // Iterate until steady-state to propagate the error state upwards in the
817 for ( m in registry ) {
818 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
819 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
820 registry[m].state = 'error';
825 } while ( stateChange );
828 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
829 for ( j = 0; j < jobs.length; j += 1 ) {
830 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
831 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
832 // All dependencies satisfied, or some have errors
838 if ( $.isFunction( job.error ) ) {
839 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
842 if ( $.isFunction( job.ready ) ) {
847 // A user-defined callback raised an exception.
848 // Swallow it to protect our state machine!
849 log( 'Exception thrown by job.error', e );
854 if ( registry[module].state === 'ready' ) {
855 // The current module became 'ready'. Set it in the module store, and recursively execute all
856 // dependent modules that are loaded and now have all dependencies satisfied.
857 mw.loader.store.set( module, registry[module] );
858 for ( m in registry ) {
859 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
867 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
868 * depending on whether document-ready has occurred yet and whether we are in async mode.
871 * @param {string} src URL to script, will be used as the src attribute in the script tag
872 * @param {Function} [callback] Callback which will be run when the script is done
874 function addScript( src, callback, async ) {
875 /*jshint evil:true */
876 var script, head, done;
878 // Using isReady directly instead of storing it locally from
879 // a $.fn.ready callback (bug 31895).
880 if ( $.isReady || async ) {
881 // Can't use jQuery.getScript because that only uses <script> for cross-domain,
882 // it uses XHR and eval for same-domain scripts, which we don't want because it
883 // messes up line numbers.
884 // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
886 // IE-safe way of getting the <head>. document.head isn't supported
887 // in old IE, and doesn't work when in the <head>.
889 head = document.getElementsByTagName( 'head' )[0] || document.body;
891 script = document.createElement( 'script' );
894 if ( $.isFunction( callback ) ) {
895 script.onload = script.onreadystatechange = function () {
900 || /loaded|complete/.test( script.readyState )
905 // Handle memory leak in IE
906 script.onload = script.onreadystatechange = null;
908 // Detach the element from the document
909 if ( script.parentNode ) {
910 script.parentNode.removeChild( script );
913 // Dereference the element from javascript
921 if ( window.opera ) {
922 // Appending to the <head> blocks rendering completely in Opera,
923 // so append to the <body> after document ready. This means the
924 // scripts only start loading after the document has been rendered,
925 // but so be it. Opera users don't deserve faster web pages if their
926 // browser makes it impossible.
928 document.body.appendChild( script );
931 head.appendChild( script );
934 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
935 if ( $.isFunction( callback ) ) {
936 // Document.write is synchronous, so this is called when it's done
937 // FIXME: that's a lie. doc.write isn't actually synchronous
944 * Executes a loaded module, making it ready to use
947 * @param {string} module Module name to execute
949 function execute( module ) {
950 var key, value, media, i, urls, cssHandle, checkCssHandles,
951 cssHandlesRegistered = false;
953 if ( registry[module] === undefined ) {
954 throw new Error( 'Module has not been registered yet: ' + module );
955 } else if ( registry[module].state === 'registered' ) {
956 throw new Error( 'Module has not been requested from the server yet: ' + module );
957 } else if ( registry[module].state === 'loading' ) {
958 throw new Error( 'Module has not completed loading yet: ' + module );
959 } else if ( registry[module].state === 'ready' ) {
960 throw new Error( 'Module has already been executed: ' + module );
964 * Define loop-function here for efficiency
965 * and to avoid re-using badly scoped variables.
968 function addLink( media, url ) {
969 var el = document.createElement( 'link' );
970 getMarker().before( el ); // IE: Insert in dom before setting href
971 el.rel = 'stylesheet';
972 if ( media && media !== 'all' ) {
978 function runScript() {
979 var script, markModuleReady, nestedAddScript;
981 script = registry[module].script;
982 markModuleReady = function () {
983 registry[module].state = 'ready';
984 handlePending( module );
986 nestedAddScript = function ( arr, callback, async, i ) {
987 // Recursively call addScript() in its own callback
988 // for each element of arr.
989 if ( i >= arr.length ) {
990 // We're at the end of the array
995 addScript( arr[i], function () {
996 nestedAddScript( arr, callback, async, i + 1 );
1000 if ( $.isArray( script ) ) {
1001 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1002 } else if ( $.isFunction( script ) ) {
1003 registry[module].state = 'ready';
1005 handlePending( module );
1008 // This needs to NOT use mw.log because these errors are common in production mode
1009 // and not in debug mode, such as when a symbol that should be global isn't exported
1010 log( 'Exception thrown by ' + module, e );
1011 registry[module].state = 'error';
1012 handlePending( module );
1016 // This used to be inside runScript, but since that is now fired asychronously
1017 // (after CSS is loaded) we need to set it here right away. It is crucial that
1018 // when execute() is called this is set synchronously, otherwise modules will get
1019 // executed multiple times as the registry will state that it isn't loading yet.
1020 registry[module].state = 'loading';
1022 // Add localizations to message system
1023 if ( $.isPlainObject( registry[module].messages ) ) {
1024 mw.messages.set( registry[module].messages );
1027 if ( $.isReady || registry[module].async ) {
1028 // Make sure we don't run the scripts until all (potentially asynchronous)
1029 // stylesheet insertions have completed.
1032 checkCssHandles = function () {
1033 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1034 // one of the cssHandles is fired while we're still creating more handles.
1035 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1037 runScript = undefined; // Revoke
1040 cssHandle = function () {
1041 var check = checkCssHandles;
1043 return function () {
1047 check = undefined; // Revoke
1053 // We are in blocking mode, and so we can't afford to wait for CSS
1054 cssHandle = function () {};
1056 checkCssHandles = runScript;
1059 // Process styles (see also mw.loader.implement)
1060 // * back-compat: { <media>: css }
1061 // * back-compat: { <media>: [url, ..] }
1062 // * { "css": [css, ..] }
1063 // * { "url": { <media>: [url, ..] } }
1064 if ( $.isPlainObject( registry[module].style ) ) {
1065 for ( key in registry[module].style ) {
1066 value = registry[module].style[key];
1069 if ( key !== 'url' && key !== 'css' ) {
1070 // Backwards compatibility, key is a media-type
1071 if ( typeof value === 'string' ) {
1072 // back-compat: { <media>: css }
1073 // Ignore 'media' because it isn't supported (nor was it used).
1074 // Strings are pre-wrapped in "@media". The media-type was just ""
1075 // (because it had to be set to something).
1076 // This is one of the reasons why this format is no longer used.
1077 addEmbeddedCSS( value, cssHandle() );
1079 // back-compat: { <media>: [url, ..] }
1085 // Array of css strings in key 'css',
1086 // or back-compat array of urls from media-type
1087 if ( $.isArray( value ) ) {
1088 for ( i = 0; i < value.length; i += 1 ) {
1089 if ( key === 'bc-url' ) {
1090 // back-compat: { <media>: [url, ..] }
1091 addLink( media, value[i] );
1092 } else if ( key === 'css' ) {
1093 // { "css": [css, ..] }
1094 addEmbeddedCSS( value[i], cssHandle() );
1097 // Not an array, but a regular object
1098 // Array of urls inside media-type key
1099 } else if ( typeof value === 'object' ) {
1100 // { "url": { <media>: [url, ..] } }
1101 for ( media in value ) {
1102 urls = value[media];
1103 for ( i = 0; i < urls.length; i += 1 ) {
1104 addLink( media, urls[i] );
1112 cssHandlesRegistered = true;
1117 * Adds a dependencies to the queue with optional callbacks to be run
1118 * when the dependencies are ready or fail
1121 * @param {string|string[]} dependencies Module name or array of string module names
1122 * @param {Function} [ready] Callback to execute when all dependencies are ready
1123 * @param {Function} [error] Callback to execute when any dependency fails
1124 * @param {boolean} [async] If true, load modules asynchronously even if
1125 * document ready has not yet occurred.
1127 function request( dependencies, ready, error, async ) {
1130 // Allow calling by single module name
1131 if ( typeof dependencies === 'string' ) {
1132 dependencies = [dependencies];
1135 // Add ready and error callbacks if they were given
1136 if ( ready !== undefined || error !== undefined ) {
1137 jobs[jobs.length] = {
1138 'dependencies': filter(
1139 ['registered', 'loading', 'loaded'],
1147 // Queue up any dependencies that are registered
1148 dependencies = filter( ['registered'], dependencies );
1149 for ( n = 0; n < dependencies.length; n += 1 ) {
1150 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1151 queue[queue.length] = dependencies[n];
1153 // Mark this module as async in the registry
1154 registry[dependencies[n]].async = true;
1163 function sortQuery(o) {
1164 var sorted = {}, key, a = [];
1166 if ( hasOwn.call( o, key ) ) {
1171 for ( key = 0; key < a.length; key += 1 ) {
1172 sorted[a[key]] = o[a[key]];
1178 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1179 * to a query string of the form foo.bar,baz|bar.baz,quux
1182 function buildModulesString( moduleMap ) {
1183 var arr = [], p, prefix;
1184 for ( prefix in moduleMap ) {
1185 p = prefix === '' ? '' : prefix + '.';
1186 arr.push( p + moduleMap[prefix].join( ',' ) );
1188 return arr.join( '|' );
1192 * Asynchronously append a script tag to the end of the body
1193 * that invokes load.php
1195 * @param {Object} moduleMap Module map, see #buildModulesString
1196 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1197 * @param {string} sourceLoadScript URL of load.php
1198 * @param {boolean} async If true, use an asynchronous request even if document ready has not yet occurred
1200 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1201 var request = $.extend(
1202 { modules: buildModulesString( moduleMap ) },
1205 request = sortQuery( request );
1206 // Asynchronously append a script tag to the end of the body
1207 // Append &* to avoid triggering the IE6 extension check
1208 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1211 /* Public Members */
1214 * The module registry is exposed as an aid for debugging and inspecting page
1215 * state; it is not a public interface for modifying the registry.
1221 moduleRegistry: registry,
1224 * @inheritdoc #newStyleTag
1227 addStyleTag: newStyleTag,
1230 * Batch-request queued dependencies from the server.
1233 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1234 source, concatSource, group, g, i, modules, maxVersion, sourceLoadScript,
1235 currReqBase, currReqBaseLength, moduleMap, l,
1236 lastDotIndex, prefix, suffix, bytesAdded, async;
1238 // Build a list of request parameters common to all requests.
1240 skin: mw.config.get( 'skin' ),
1241 lang: mw.config.get( 'wgUserLanguage' ),
1242 debug: mw.config.get( 'debug' )
1244 // Split module batch by source and by group.
1246 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1248 // Appends a list of modules from the queue to the batch
1249 for ( q = 0; q < queue.length; q += 1 ) {
1250 // Only request modules which are registered
1251 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1252 // Prevent duplicate entries
1253 if ( $.inArray( queue[q], batch ) === -1 ) {
1254 batch[batch.length] = queue[q];
1255 // Mark registered modules as loading
1256 registry[queue[q]].state = 'loading';
1261 mw.loader.store.init();
1262 if ( mw.loader.store.enabled ) {
1264 batch = $.grep( batch, function ( module ) {
1265 var source = mw.loader.store.get( module );
1267 concatSource.push( source );
1272 $.globalEval( concatSource.join( ';' ) );
1275 // Early exit if there's nothing to load...
1276 if ( !batch.length ) {
1280 // The queue has been processed into the batch, clear up the queue.
1283 // Always order modules alphabetically to help reduce cache
1284 // misses for otherwise identical content.
1287 // Split batch by source and by group.
1288 for ( b = 0; b < batch.length; b += 1 ) {
1289 bSource = registry[batch[b]].source;
1290 bGroup = registry[batch[b]].group;
1291 if ( splits[bSource] === undefined ) {
1292 splits[bSource] = {};
1294 if ( splits[bSource][bGroup] === undefined ) {
1295 splits[bSource][bGroup] = [];
1297 bSourceGroup = splits[bSource][bGroup];
1298 bSourceGroup[bSourceGroup.length] = batch[b];
1301 // Clear the batch - this MUST happen before we append any
1302 // script elements to the body or it's possible that a script
1303 // will be locally cached, instantly load, and work the batch
1304 // again, all before we've cleared it causing each request to
1305 // include modules which are already loaded.
1308 for ( source in splits ) {
1310 sourceLoadScript = sources[source].loadScript;
1312 for ( group in splits[source] ) {
1314 // Cache access to currently selected list of
1315 // modules for this group from this source.
1316 modules = splits[source][group];
1318 // Calculate the highest timestamp
1320 for ( g = 0; g < modules.length; g += 1 ) {
1321 if ( registry[modules[g]].version > maxVersion ) {
1322 maxVersion = registry[modules[g]].version;
1326 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1327 // For user modules append a user name to the request.
1328 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1329 currReqBase.user = mw.config.get( 'wgUserName' );
1331 currReqBaseLength = $.param( currReqBase ).length;
1333 // We may need to split up the request to honor the query string length limit,
1334 // so build it piece by piece.
1335 l = currReqBaseLength + 9; // '&modules='.length == 9
1337 moduleMap = {}; // { prefix: [ suffixes ] }
1339 for ( i = 0; i < modules.length; i += 1 ) {
1340 // Determine how many bytes this module would add to the query string
1341 lastDotIndex = modules[i].lastIndexOf( '.' );
1342 // Note that these substr() calls work even if lastDotIndex == -1
1343 prefix = modules[i].substr( 0, lastDotIndex );
1344 suffix = modules[i].substr( lastDotIndex + 1 );
1345 bytesAdded = moduleMap[prefix] !== undefined
1346 ? suffix.length + 3 // '%2C'.length == 3
1347 : modules[i].length + 3; // '%7C'.length == 3
1349 // If the request would become too long, create a new one,
1350 // but don't create empty requests
1351 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1352 // This request would become too long, create a new one
1353 // and fire off the old one
1354 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1357 l = currReqBaseLength + 9;
1359 if ( moduleMap[prefix] === undefined ) {
1360 moduleMap[prefix] = [];
1362 moduleMap[prefix].push( suffix );
1363 if ( !registry[modules[i]].async ) {
1364 // If this module is blocking, make the entire request blocking
1365 // This is slightly suboptimal, but in practice mixing of blocking
1366 // and async modules will only occur in debug mode.
1371 // If there's anything left in moduleMap, request that too
1372 if ( !$.isEmptyObject( moduleMap ) ) {
1373 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1380 * Register a source.
1382 * @param {string} id Short lowercase a-Z string representing a source, only used internally.
1383 * @param {Object} props Object containing only the loadScript property which is a url to
1384 * the load.php location of the source.
1387 addSource: function ( id, props ) {
1389 // Allow multiple additions
1390 if ( typeof id === 'object' ) {
1391 for ( source in id ) {
1392 mw.loader.addSource( source, id[source] );
1397 if ( sources[id] !== undefined ) {
1398 throw new Error( 'source already registered: ' + id );
1401 sources[id] = props;
1407 * Register a module, letting the system know about it and its
1408 * properties. Startup modules contain calls to this function.
1410 * @param {string} module Module name
1411 * @param {number} version Module version number as a timestamp (falls backs to 0)
1412 * @param {string|Array|Function} dependencies One string or array of strings of module
1413 * names on which this module depends, or a function that returns that array.
1414 * @param {string} [group=null] Group which the module is in
1415 * @param {string} [source='local'] Name of the source
1417 register: function ( module, version, dependencies, group, source ) {
1419 // Allow multiple registration
1420 if ( typeof module === 'object' ) {
1421 for ( m = 0; m < module.length; m += 1 ) {
1422 // module is an array of module names
1423 if ( typeof module[m] === 'string' ) {
1424 mw.loader.register( module[m] );
1425 // module is an array of arrays
1426 } else if ( typeof module[m] === 'object' ) {
1427 mw.loader.register.apply( mw.loader, module[m] );
1433 if ( typeof module !== 'string' ) {
1434 throw new Error( 'module must be a string, not a ' + typeof module );
1436 if ( registry[module] !== undefined ) {
1437 throw new Error( 'module already registered: ' + module );
1439 // List the module as registered
1440 registry[module] = {
1441 version: version !== undefined ? parseInt( version, 10 ) : 0,
1443 group: typeof group === 'string' ? group : null,
1444 source: typeof source === 'string' ? source: 'local',
1447 if ( typeof dependencies === 'string' ) {
1448 // Allow dependencies to be given as a single module name
1449 registry[module].dependencies = [ dependencies ];
1450 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1451 // Allow dependencies to be given as an array of module names
1452 // or a function which returns an array
1453 registry[module].dependencies = dependencies;
1458 * Implement a module given the components that make up the module.
1460 * When #load or #using requests one or more modules, the server
1461 * response contain calls to this function.
1463 * All arguments are required.
1465 * @param {string} module Name of module
1466 * @param {Function|Array} script Function with module code or Array of URLs to
1467 * be used as the src attribute of a new `<script>` tag.
1468 * @param {Object} style Should follow one of the following patterns:
1470 * { "css": [css, ..] }
1471 * { "url": { <media>: [url, ..] } }
1473 * And for backwards compatibility (needs to be supported forever due to caching):
1476 * { <media>: [url, ..] }
1478 * The reason css strings are not concatenated anymore is bug 31676. We now check
1479 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1481 * @param {Object} msgs List of key/value pairs to be added to mw#messages.
1483 implement: function ( module, script, style, msgs ) {
1485 if ( typeof module !== 'string' ) {
1486 throw new Error( 'module must be a string, not a ' + typeof module );
1488 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1489 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1491 if ( !$.isPlainObject( style ) ) {
1492 throw new Error( 'style must be an object, not a ' + typeof style );
1494 if ( !$.isPlainObject( msgs ) ) {
1495 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1497 // Automatically register module
1498 if ( registry[module] === undefined ) {
1499 mw.loader.register( module );
1501 // Check for duplicate implementation
1502 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1503 throw new Error( 'module already implemented: ' + module );
1505 // Attach components
1506 registry[module].script = script;
1507 registry[module].style = style;
1508 registry[module].messages = msgs;
1509 // The module may already have been marked as erroneous
1510 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1511 registry[module].state = 'loaded';
1512 if ( allReady( registry[module].dependencies ) ) {
1519 * Execute a function as soon as one or more required modules are ready.
1521 * @param {string|Array} dependencies Module name or array of modules names the callback
1522 * dependends on to be ready before executing
1523 * @param {Function} [ready] callback to execute when all dependencies are ready
1524 * @param {Function} [error] callback to execute when if dependencies have a errors
1526 using: function ( dependencies, ready, error ) {
1527 var tod = typeof dependencies;
1529 if ( tod !== 'object' && tod !== 'string' ) {
1530 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1532 // Allow calling with a single dependency as a string
1533 if ( tod === 'string' ) {
1534 dependencies = [ dependencies ];
1536 // Resolve entire dependency map
1537 dependencies = resolve( dependencies );
1538 if ( allReady( dependencies ) ) {
1539 // Run ready immediately
1540 if ( $.isFunction( ready ) ) {
1543 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1544 // Execute error immediately if any dependencies have errors
1545 if ( $.isFunction( error ) ) {
1546 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1550 // Not all dependencies are ready: queue up a request
1551 request( dependencies, ready, error );
1556 * Load an external script or one or more modules.
1558 * @param {string|Array} modules Either the name of a module, array of modules,
1559 * or a URL of an external script or style
1560 * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
1561 * external script or style; acceptable values are "text/css" and
1562 * "text/javascript"; if no type is provided, text/javascript is assumed.
1563 * @param {boolean} [async] If true, load modules asynchronously
1564 * even if document ready has not yet occurred. If false, block before
1565 * document ready and load async after. If not set, true will be
1566 * assumed if loading a URL, and false will be assumed otherwise.
1568 load: function ( modules, type, async ) {
1569 var filtered, m, module, l;
1572 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1573 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1575 // Allow calling with an external url or single dependency as a string
1576 if ( typeof modules === 'string' ) {
1577 // Support adding arbitrary external scripts
1578 if ( /^(https?:)?\/\//.test( modules ) ) {
1579 if ( async === undefined ) {
1580 // Assume async for bug 34542
1583 if ( type === 'text/css' ) {
1584 // IE7-8 throws security warnings when inserting a <link> tag
1585 // with a protocol-relative URL set though attributes (instead of
1586 // properties) - when on HTTPS. See also bug #.
1587 l = document.createElement( 'link' );
1588 l.rel = 'stylesheet';
1590 $( 'head' ).append( l );
1593 if ( type === 'text/javascript' || type === undefined ) {
1594 addScript( modules, null, async );
1598 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1600 // Called with single module
1601 modules = [ modules ];
1604 // Filter out undefined modules, otherwise resolve() will throw
1605 // an exception for trying to load an undefined module.
1606 // Undefined modules are acceptable here in load(), because load() takes
1607 // an array of unrelated modules, whereas the modules passed to
1608 // using() are related and must all be loaded.
1609 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1610 module = registry[modules[m]];
1611 if ( module !== undefined ) {
1612 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1613 filtered[filtered.length] = modules[m];
1618 if ( filtered.length === 0 ) {
1621 // Resolve entire dependency map
1622 filtered = resolve( filtered );
1623 // If all modules are ready, nothing to be done
1624 if ( allReady( filtered ) ) {
1627 // If any modules have errors: also quit.
1628 if ( filter( ['error', 'missing'], filtered ).length ) {
1631 // Since some modules are not yet ready, queue up a request.
1632 request( filtered, undefined, undefined, async );
1636 * Change the state of one or more modules.
1638 * @param {string|Object} module module name or object of module name/state pairs
1639 * @param {string} state state name
1641 state: function ( module, state ) {
1644 if ( typeof module === 'object' ) {
1645 for ( m in module ) {
1646 mw.loader.state( m, module[m] );
1650 if ( registry[module] === undefined ) {
1651 mw.loader.register( module );
1653 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1654 && registry[module].state !== state ) {
1655 // Make sure pending modules depending on this one get executed if their
1656 // dependencies are now fulfilled!
1657 registry[module].state = state;
1658 handlePending( module );
1660 registry[module].state = state;
1665 * Get the version of a module.
1667 * @param {string} module Name of module to get version for
1669 getVersion: function ( module ) {
1670 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1671 return formatVersionNumber( registry[module].version );
1677 * @inheritdoc #getVersion
1678 * @deprecated since 1.18 use #getVersion instead
1680 version: function () {
1681 return mw.loader.getVersion.apply( mw.loader, arguments );
1685 * Get the state of a module.
1687 * @param {string} module name of module to get state for
1689 getState: function ( module ) {
1690 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1691 return registry[module].state;
1697 * Get names of all registered modules.
1701 getModuleNames: function () {
1702 return $.map( registry, function ( i, key ) {
1708 * Load the `mediawiki.user` module.
1710 * For backwards-compatibility with cached pages from before 2013 where:
1712 * - the `mediawiki.user` module didn't exist yet
1713 * - `mw.user` was still part of mediawiki.js
1714 * - `mw.loader.go` still existed and called after `mw.loader.load()`
1717 mw.loader.load( 'mediawiki.user' );
1721 * @inheritdoc mw.inspect#runReports
1724 inspect: function () {
1725 var args = slice.call( arguments );
1726 mw.loader.using( 'mediawiki.inspect', function () {
1727 mw.inspect.runReports.apply( mw.inspect, args );
1732 * On browsers that implement the localStorage API, the module store serves as a
1733 * smart complement to the browser cache. Unlike the browser cache, the module store
1734 * can slice a concatenated response from ResourceLoader into its constituent
1735 * modules and cache each of them separately, using each module's versioning scheme
1736 * to determine when the cache should be invalidated.
1739 * @class mw.loader.store
1742 // Whether the store is in use on this page.
1745 // The contents of the store, mapping '[module name]@[version]' keys
1746 // to module implementations.
1750 stats: { hits: 0, misses: 0, expired: 0 },
1753 * Construct a JSON-serializable object representing the content of the store.
1754 * @return {Object} Module store contents.
1756 toJSON: function () {
1757 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
1761 * Get the localStorage key for the entire module store. The key references
1762 * $wgDBname to prevent clashes between wikis which share a common host.
1764 * @return {string} localStorage item key
1766 getStoreKey: function () {
1767 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
1771 * Get a string key on which to vary the module cache.
1772 * @return {string} String of concatenated vary conditions.
1774 getVary: function () {
1776 mw.config.get( 'skin' ),
1777 mw.config.get( 'wgResourceLoaderStorageVersion' ),
1778 mw.config.get( 'wgUserLanguage' )
1783 * Get a string key for a specific module. The key format is '[name]@[version]'.
1785 * @param {string} module Module name
1786 * @return {string|null} Module key or null if module does not exist
1788 getModuleKey: function ( module ) {
1789 return typeof registry[module] === 'object' ?
1790 ( module + '@' + registry[module].version ) : null;
1794 * Initialize the store by retrieving it from localStorage and (if successfully
1795 * retrieved) decoding the stored JSON value to a plain object.
1797 * The try / catch block is used for JSON & localStorage feature detection.
1798 * See the in-line documentation for Modernizr's localStorage feature detection
1799 * code for a full account of why we need a try / catch: <http://git.io/4NEwKg>.
1804 if ( mw.loader.store.enabled !== null ) {
1805 // #init already ran.
1809 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) || mw.config.get( 'debug' ) ) {
1810 // Disabled by configuration, or because debug mode is set.
1811 mw.loader.store.enabled = false;
1816 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
1817 // If we get here, localStorage is available; mark enabled.
1818 mw.loader.store.enabled = true;
1819 data = JSON.parse( raw );
1820 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
1821 mw.loader.store.items = data.items;
1826 if ( raw === undefined ) {
1827 mw.loader.store.enabled = false; // localStorage failed; disable store.
1829 mw.loader.store.update();
1834 * Retrieve a module from the store and update cache hit stats.
1836 * @param {string} module Module name
1837 * @return {string|boolean} Module implementation or false if unavailable
1839 get: function ( module ) {
1842 if ( mw.loader.store.enabled !== true ) {
1846 key = mw.loader.store.getModuleKey( module );
1847 if ( key in mw.loader.store.items ) {
1848 mw.loader.store.stats.hits++;
1849 return mw.loader.store.items[key];
1851 mw.loader.store.stats.misses++;
1856 * Stringify a module and queue it for storage.
1858 * @param {string} module Module name
1859 * @param {Object} descriptor The module's descriptor as set in the registry
1861 set: function ( module, descriptor ) {
1864 if ( mw.loader.store.enabled !== true ) {
1868 key = mw.loader.store.getModuleKey( module );
1870 if ( key in mw.loader.store.items ) {
1871 // Already set; decline to store.
1875 if ( descriptor.state !== 'ready' ) {
1876 // Module failed to load; decline to store.
1880 if ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) {
1881 // Unversioned, private, or site-/user-specific; decline to store.
1885 if ( $.inArray( undefined, [ descriptor.script, descriptor.style, descriptor.messages ] ) !== -1 ) {
1886 // Partial descriptor; decline to store.
1892 JSON.stringify( module ),
1893 typeof descriptor.script === 'function' ?
1894 String( descriptor.script ) : JSON.stringify( descriptor.script ),
1895 JSON.stringify( descriptor.style ),
1896 JSON.stringify( descriptor.messages )
1901 mw.loader.store.items[key] = 'mw.loader.implement(' + args.join(',') + ');';
1902 mw.loader.store.update();
1906 * Iterate through the module store, removing any item that does not correspond
1907 * (in name and version) to an item in the module registry.
1909 prune: function () {
1912 if ( mw.loader.store.enabled !== true ) {
1916 for ( key in mw.loader.store.items ) {
1917 module = key.substring( 0, key.indexOf( '@' ) );
1918 if ( mw.loader.store.getModuleKey( module ) !== key ) {
1919 mw.loader.store.stats.expired++;
1920 delete mw.loader.store.items[key];
1926 * Sync modules to localStorage.
1928 * This function debounces localStorage updates. When called multiple times in
1929 * quick succession, the calls are coalesced into a single update operation.
1930 * This allows us to call #update without having to consider the module load
1931 * queue; the call to localStorage.setItem will be naturally deferred until the
1932 * page is quiescent.
1934 * Because localStorage is shared by all pages with the same origin, if multiple
1935 * pages are loaded with different module sets, the possibility exists that
1936 * modules saved by one page will be clobbered by another. But the impact would
1937 * be minor and the problem would be corrected by subsequent page views.
1939 update: ( function () {
1943 var data, key = mw.loader.store.getStoreKey();
1944 if ( mw.loader.store.enabled !== true ) {
1947 mw.loader.store.prune();
1949 // Replacing the content of the module store might fail if the new
1950 // contents would exceed the browser's localStorage size limit. To
1951 // avoid clogging the browser with stale data, always remove the old
1952 // value before attempting to set the new one.
1953 localStorage.removeItem( key );
1954 data = JSON.stringify( mw.loader.store );
1955 localStorage.setItem( key, data );
1959 return function () {
1960 clearTimeout( timer );
1961 timer = setTimeout( flush, 2000 );
1969 * HTML construction helper functions
1976 * output = Html.element( 'div', {}, new Html.Raw(
1977 * Html.element( 'img', { src: '<' } )
1979 * mw.log( output ); // <div><img src="<"/></div>
1984 html: ( function () {
1985 function escapeCallback( s ) {
2002 * Escape a string for HTML. Converts special characters to HTML entities.
2003 * @param {string} s The string to escape
2005 escape: function ( s ) {
2006 return s.replace( /['"<>&]/g, escapeCallback );
2010 * Create an HTML element string, with safe escaping.
2012 * @param {string} name The tag name.
2013 * @param {Object} attrs An object with members mapping element names to values
2014 * @param {Mixed} contents The contents of the element. May be either:
2015 * - string: The string is escaped.
2016 * - null or undefined: The short closing form is used, e.g. <br/>.
2017 * - this.Raw: The value attribute is included without escaping.
2018 * - this.Cdata: The value attribute is included, and an exception is
2019 * thrown if it contains an illegal ETAGO delimiter.
2020 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
2022 element: function ( name, attrs, contents ) {
2023 var v, attrName, s = '<' + name;
2025 for ( attrName in attrs ) {
2026 v = attrs[attrName];
2027 // Convert name=true, to name=name
2031 } else if ( v === false ) {
2034 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2036 if ( contents === undefined || contents === null ) {
2043 switch ( typeof contents ) {
2046 s += this.escape( contents );
2050 // Convert to string
2051 s += String( contents );
2054 if ( contents instanceof this.Raw ) {
2055 // Raw HTML inclusion
2056 s += contents.value;
2057 } else if ( contents instanceof this.Cdata ) {
2059 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2060 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2062 s += contents.value;
2064 throw new Error( 'mw.html.element: Invalid type of contents' );
2067 s += '</' + name + '>';
2072 * Wrapper object for raw HTML passed to mw.html.element().
2073 * @class mw.html.Raw
2075 Raw: function ( value ) {
2080 * Wrapper object for CDATA element contents passed to mw.html.element()
2081 * @class mw.html.Cdata
2083 Cdata: function ( value ) {
2089 // Skeleton user object. mediawiki.user.js extends this
2096 * Registry and firing of events.
2098 * MediaWiki has various interface components that are extended, enhanced
2099 * or manipulated in some other way by extensions, gadgets and even
2102 * This framework helps streamlining the timing of when these other
2103 * code paths fire their plugins (instead of using document-ready,
2104 * which can and should be limited to firing only once).
2106 * Features like navigating to other wiki pages, previewing an edit
2107 * and editing itself – without a refresh – can then retrigger these
2108 * hooks accordingly to ensure everything still works as expected.
2112 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2113 * mw.hook( 'wikipage.content' ).fire( $content );
2115 * Handlers can be added and fired for arbitrary event names at any time. The same
2116 * event can be fired multiple times. The last run of an event is memorized
2117 * (similar to `$(document).ready` and `$.Deferred().done`).
2118 * This means if an event is fired, and a handler added afterwards, the added
2119 * function will be fired right away with the last given event data.
2121 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2122 * Thus allowing flexible use and optimal maintainability and authority control.
2123 * You can pass around the `add` and/or `fire` method to another piece of code
2124 * without it having to know the event name (or `mw.hook` for that matter).
2126 * var h = mw.hook( 'bar.ready' );
2127 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2129 * Note: Events are documented with an underscore instead of a dot in the event
2130 * name due to jsduck not supporting dots in that position.
2134 hook: ( function () {
2138 * Create an instance of mw.hook.
2142 * @param {string} name Name of hook.
2145 return function ( name ) {
2146 var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) );
2150 * Register a hook handler
2151 * @param {Function...} handler Function to bind.
2157 * Unregister a hook handler
2158 * @param {Function...} handler Function to unbind.
2161 remove: list.remove,
2165 * @param {Mixed...} data
2169 return list.fireWith( null, slice.call( arguments ) );
2178 // Alias $j to jQuery for backwards compatibility
2181 // Attach to window and globally alias
2182 window.mw = window.mediaWiki = mw;
2184 // Auto-register from pre-loaded startup scripts
2185 if ( jQuery.isFunction( window.startUp ) ) {
2187 window.startUp = undefined;