2 * Core MediaWiki JavaScript Library
5 var mw = ( function ( $, undefined ) {
10 var hasOwn = Object.prototype.hasOwnProperty,
11 slice = Array.prototype.slice;
13 /* Object constructors */
16 * Creates an object that can be read from or written to from prototype functions
17 * that allow both single and multiple variables at once.
21 * @param {boolean} global Whether to store the values in the global window
22 * object or a exclusively in the object property 'values'.
24 function Map( global ) {
25 this.values = global === true ? window : {};
31 * Get the value of one or multiple a keys.
33 * If called with no arguments, all values will be returned.
35 * @param {string|Array} selection String key or array of keys to get values for.
36 * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
37 * @return mixed If selection was a string returns the value or null,
38 * If selection was an array, returns an object of key/values (value is null if not found),
39 * If selection was not passed or invalid, will return the 'values' object member (be careful as
40 * objects are always passed by reference in JavaScript!).
41 * @return {string|Object|null} Values as a string or object, null if invalid/inexistant.
43 get: function ( selection, fallback ) {
45 // If we only do this in the `return` block, it'll fail for the
46 // call to get() from the mutli-selection block.
47 fallback = arguments.length > 1 ? fallback : null;
49 if ( $.isArray( selection ) ) {
50 selection = slice.call( selection );
52 for ( i = 0; i < selection.length; i++ ) {
53 results[selection[i]] = this.get( selection[i], fallback );
58 if ( typeof selection === 'string' ) {
59 if ( !hasOwn.call( this.values, selection ) ) {
62 return this.values[selection];
65 if ( selection === undefined ) {
69 // invalid selection key
74 * Sets one or multiple key/value pairs.
76 * @param {string|Object} selection String key to set value for, or object mapping keys to values.
77 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
78 * @return {Boolean} This returns true on success, false on failure.
80 set: function ( selection, value ) {
83 if ( $.isPlainObject( selection ) ) {
84 for ( s in selection ) {
85 this.values[s] = selection[s];
89 if ( typeof selection === 'string' && arguments.length > 1 ) {
90 this.values[selection] = value;
97 * Checks if one or multiple keys exist.
99 * @param {Mixed} selection String key or array of keys to check
100 * @return {boolean} Existence of key(s)
102 exists: function ( selection ) {
105 if ( $.isArray( selection ) ) {
106 for ( s = 0; s < selection.length; s++ ) {
107 if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
113 return typeof selection === 'string' && hasOwn.call( this.values, selection );
118 * Object constructor for messages,
119 * similar to the Message class in MediaWiki PHP.
123 * @param {mw.Map} map Message storage
124 * @param {string} key
125 * @param {Array} [parameters]
127 function Message( map, key, parameters ) {
128 this.format = 'text';
131 this.parameters = parameters === undefined ? [] : slice.call( parameters );
135 Message.prototype = {
137 * Simple message parser, does $N replacement and nothing else.
139 * This may be overridden to provide a more complex message parser.
141 * The primary override is in mediawiki.jqueryMsg.
143 * This function will not be called for nonexistent messages.
145 parser: function () {
146 var parameters = this.parameters;
147 return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
148 var index = parseInt( match, 10 ) - 1;
149 return parameters[index] !== undefined ? parameters[index] : '$' + match;
154 * Appends (does not replace) parameters for replacement to the .parameters property.
156 * @param {Array} parameters
159 params: function ( parameters ) {
161 for ( i = 0; i < parameters.length; i += 1 ) {
162 this.parameters.push( parameters[i] );
168 * Converts message object to it's string form based on the state of format.
170 * @return {string} Message as a string in the current form or `<key>` if key does not exist.
172 toString: function () {
175 if ( !this.exists() ) {
176 // Use <key> as text if key does not exist
177 if ( this.format === 'escaped' || this.format === 'parse' ) {
178 // format 'escaped' and 'parse' need to have the brackets and key html escaped
179 return mw.html.escape( '<' + this.key + '>' );
181 return '<' + this.key + '>';
184 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
185 text = this.parser();
188 if ( this.format === 'escaped' ) {
189 text = this.parser();
190 text = mw.html.escape( text );
197 * Changes format to 'parse' and converts message to string
199 * If jqueryMsg is loaded, this parses the message text from wikitext
200 * (where supported) to HTML
202 * Otherwise, it is equivalent to plain.
204 * @return {string} String form of parsed message
207 this.format = 'parse';
208 return this.toString();
212 * Changes format to 'plain' and converts message to string
214 * This substitutes parameters, but otherwise does not change the
217 * @return {string} String form of plain message
220 this.format = 'plain';
221 return this.toString();
225 * Changes format to 'text' and converts message to string
227 * If jqueryMsg is loaded, {{-transformation is done where supported
228 * (such as {{plural:}}, {{gender:}}, {{int:}}).
230 * Otherwise, it is equivalent to plain.
233 this.format = 'text';
234 return this.toString();
238 * Changes the format to 'escaped' and converts message to string
240 * This is equivalent to using the 'text' format (see text method), then
241 * HTML-escaping the output.
243 * @return {string} String form of html escaped message
245 escaped: function () {
246 this.format = 'escaped';
247 return this.toString();
251 * Checks if message exists
256 exists: function () {
257 return this.map.exists( this.key );
263 * @alternateClassName mediaWiki
270 * Dummy function which in debug mode can be replaced with a function that
271 * emulates console.log in console-less environments.
273 log: function () { },
275 // Make the Map constructor publicly available.
278 // Make the Message constructor publicly available.
282 * List of configuration values
284 * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map().
285 * If `$wgLegacyJavaScriptGlobals` is true, this Map will have its values
286 * in the global window object.
292 * Empty object that plugins can be installed in.
297 /* Extension points */
305 * Localization system
313 * Gets a message object, similar to wfMessage().
315 * @param {string} key Key of message to get
316 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
317 * @return {mw.Message}
319 message: function ( key ) {
320 // Variadic arguments
321 var parameters = slice.call( arguments, 1 );
322 return new Message( mw.messages, key, parameters );
326 * Gets a message string, similar to wfMessage()
328 * @see mw.Message#toString
329 * @param {string} key Key of message to get
330 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
333 msg: function ( /* key, parameters... */ ) {
334 return mw.message.apply( mw.message, arguments ).toString();
338 * Client-side module loader which integrates with the MediaWiki ResourceLoader
342 loader: ( function () {
344 /* Private Members */
347 * Mapping of registered modules
349 * The jquery module is pre-registered, because it must have already
350 * been provided for this object to have been built, and in debug mode
351 * jquery would have been provided through a unique loader request,
352 * making it impossible to hold back registration of jquery until after
355 * For exact details on support for script, style and messages, look at
356 * mw.loader.implement.
361 * 'version': ############## (unix timestamp),
362 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
363 * 'group': 'somegroup', (or) null,
364 * 'source': 'local', 'someforeignwiki', (or) null
365 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
368 * 'messages': { 'key': 'value' },
377 // Mapping of sources, keyed by source-id, values are objects.
381 // 'loadScript': 'http://foo.bar/w/load.php'
386 // List of modules which will be loaded as when ready
388 // List of modules to be loaded
390 // List of callback functions waiting for modules to be ready to be called
392 // Selector cache for the marker element. Use getMarker() to get/use the marker!
394 // Buffer for addEmbeddedCSS.
396 // Callbacks for addEmbeddedCSS.
397 cssCallbacks = $.Callbacks();
399 /* Private methods */
401 function getMarker() {
407 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
408 if ( $marker.length ) {
411 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
412 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
418 * Create a new style tag and add it to the DOM.
421 * @param {string} text CSS text
422 * @param {Mixed} [nextnode] An Element or jQuery object for an element where
423 * the style tag should be inserted before. Otherwise appended to the `<head>`.
424 * @return {HTMLElement} Node reference to the created `<style>` tag.
426 function addStyleTag( text, nextnode ) {
427 var s = document.createElement( 'style' );
428 // Insert into document before setting cssText (bug 33305)
430 // Must be inserted with native insertBefore, not $.fn.before.
431 // When using jQuery to insert it, like $nextnode.before( s ),
432 // then IE6 will throw "Access is denied" when trying to append
433 // to .cssText later. Some kind of weird security measure.
434 // http://stackoverflow.com/q/12586482/319266
435 // Works: jsfiddle.net/zJzMy/1
436 // Fails: jsfiddle.net/uJTQz
437 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
438 if ( nextnode.jquery ) {
439 nextnode = nextnode.get( 0 );
441 nextnode.parentNode.insertBefore( s, nextnode );
443 document.getElementsByTagName( 'head' )[0].appendChild( s );
445 if ( s.styleSheet ) {
447 s.styleSheet.cssText = text;
450 // (Safari sometimes borks on non-string values,
451 // play safe by casting to a string, just in case.)
452 s.appendChild( document.createTextNode( String( text ) ) );
458 * Checks whether it is safe to add this css to a stylesheet.
461 * @param {string} cssText
462 * @return {boolean} False if a new one must be created.
464 function canExpandStylesheetWith( cssText ) {
465 // Makes sure that cssText containing `@import`
466 // rules will end up in a new stylesheet (as those only work when
467 // placed at the start of a stylesheet; bug 35562).
468 return cssText.indexOf( '@import' ) === -1;
472 * @param {string} [cssText=cssBuffer] If called without cssText,
473 * the internal buffer will be inserted instead.
474 * @param {Function} [callback]
476 function addEmbeddedCSS( cssText, callback ) {
480 cssCallbacks.add( callback );
483 // Yield once before inserting the <style> tag. There are likely
484 // more calls coming up which we can combine this way.
485 // Appending a stylesheet and waiting for the browser to repaint
486 // is fairly expensive, this reduces it (bug 45810)
488 // Be careful not to extend the buffer with css that needs a new stylesheet
489 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
490 // Linebreak for somewhat distinguishable sections
491 // (the rl-cachekey comment separating each)
492 cssBuffer += '\n' + cssText;
493 // TODO: Use requestAnimationFrame in the future which will
494 // perform even better by not injecting styles while the browser
496 setTimeout( function () {
497 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
498 // (below version 13) has the non-standard behaviour of passing a
499 // numerical "lateness" value as first argument to this callback
500 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
506 // This is a delayed call and we got a buffer still
507 } else if ( cssBuffer ) {
511 // This is a delayed call, but buffer is already cleared by
512 // another delayed call.
516 // By default, always create a new <style>. Appending text
517 // to a <style> tag means the contents have to be re-parsed (bug 45810).
518 // Except, of course, in IE below 9, in there we default to
519 // re-using and appending to a <style> tag due to the
520 // IE stylesheet limit (bug 31676).
521 if ( 'documentMode' in document && document.documentMode <= 9 ) {
523 $style = getMarker().prev();
524 // Verify that the the element before Marker actually is a
525 // <style> tag and one that came from ResourceLoader
526 // (not some other style tag or even a `<meta>` or `<script>`).
527 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
528 // There's already a dynamic <style> tag present and
529 // canExpandStylesheetWith() gave a green light to append more to it.
530 styleEl = $style.get( 0 );
531 if ( styleEl.styleSheet ) {
533 styleEl.styleSheet.cssText += cssText; // IE
535 log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e );
538 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
540 cssCallbacks.fire().empty();
545 $( addStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
547 cssCallbacks.fire().empty();
551 * Generates an ISO8601 "basic" string from a UNIX timestamp
554 function formatVersionNumber( timestamp ) {
556 function pad( a, b, c ) {
557 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
559 d.setTime( timestamp * 1000 );
561 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
562 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
567 * Resolves dependencies and detects circular references.
570 * @param {string} module Name of the top-level module whose dependencies shall be
571 * resolved and sorted.
572 * @param {Array} resolved Returns a topological sort of the given module and its
573 * dependencies, such that later modules depend on earlier modules. The array
574 * contains the module names. If the array contains already some module names,
575 * this function appends its result to the pre-existing array.
576 * @param {Object} [unresolved] Hash used to track the current dependency
577 * chain; used to report loops in the dependency graph.
578 * @throws {Error} If any unregistered module or a dependency loop is encountered
580 function sortDependencies( module, resolved, unresolved ) {
583 if ( registry[module] === undefined ) {
584 throw new Error( 'Unknown dependency: ' + module );
586 // Resolves dynamic loader function and replaces it with its own results
587 if ( $.isFunction( registry[module].dependencies ) ) {
588 registry[module].dependencies = registry[module].dependencies();
589 // Ensures the module's dependencies are always in an array
590 if ( typeof registry[module].dependencies !== 'object' ) {
591 registry[module].dependencies = [registry[module].dependencies];
594 if ( $.inArray( module, resolved ) !== -1 ) {
595 // Module already resolved; nothing to do.
598 // unresolved is optional, supply it if not passed in
602 // Tracks down dependencies
603 deps = registry[module].dependencies;
605 for ( n = 0; n < len; n += 1 ) {
606 if ( $.inArray( deps[n], resolved ) === -1 ) {
607 if ( unresolved[deps[n]] ) {
609 'Circular reference detected: ' + module +
615 unresolved[module] = true;
616 sortDependencies( deps[n], resolved, unresolved );
617 delete unresolved[module];
620 resolved[resolved.length] = module;
624 * Gets a list of module names that a module depends on in their proper dependency
628 * @param {string} module Module name or array of string module names
629 * @return {Array} list of dependencies, including 'module'.
630 * @throws {Error} If circular reference is detected
632 function resolve( module ) {
635 // Allow calling with an array of module names
636 if ( $.isArray( module ) ) {
638 for ( m = 0; m < module.length; m += 1 ) {
639 sortDependencies( module[m], resolved );
644 if ( typeof module === 'string' ) {
646 sortDependencies( module, resolved );
650 throw new Error( 'Invalid module argument: ' + module );
654 * Narrows a list of module names down to those matching a specific
655 * state (see comment on top of this scope for a list of valid states).
656 * One can also filter for 'unregistered', which will return the
657 * modules names that don't have a registry entry.
660 * @param {string|string[]} states Module states to filter by
661 * @param {Array} [modules] List of module names to filter (optional, by default the entire
663 * @return {Array} List of filtered module names
665 function filter( states, modules ) {
666 var list, module, s, m;
668 // Allow states to be given as a string
669 if ( typeof states === 'string' ) {
672 // If called without a list of modules, build and use a list of all modules
674 if ( modules === undefined ) {
676 for ( module in registry ) {
677 modules[modules.length] = module;
680 // Build a list of modules which are in one of the specified states
681 for ( s = 0; s < states.length; s += 1 ) {
682 for ( m = 0; m < modules.length; m += 1 ) {
683 if ( registry[modules[m]] === undefined ) {
684 // Module does not exist
685 if ( states[s] === 'unregistered' ) {
687 list[list.length] = modules[m];
690 // Module exists, check state
691 if ( registry[modules[m]].state === states[s] ) {
693 list[list.length] = modules[m];
702 * Determine whether all dependencies are in state 'ready', which means we may
703 * execute the module or job now.
706 * @param {Array} dependencies Dependencies (module names) to be checked.
707 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
709 function allReady( dependencies ) {
710 return filter( 'ready', dependencies ).length === dependencies.length;
714 * Log a message to window.console, if possible. Useful to force logging of some
715 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
716 * Gets console references in each invocation, so that delayed debugging tools work
717 * fine. No need for optimization here, which would only result in losing logs.
720 * @param {string} msg text for the log entry.
723 function log( msg, e ) {
724 var console = window.console;
725 if ( console && console.log ) {
727 // If we have an exception object, log it through .error() to trigger
728 // proper stacktraces in browsers that support it. There are no (known)
729 // browsers that don't support .error(), that do support .log() and
730 // have useful exception handling through .log().
731 if ( e && console.error ) {
738 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
739 * and modules that depend upon this module. if the given module failed, propagate the 'error'
740 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
741 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
744 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
746 function handlePending( module ) {
747 var j, job, hasErrors, m, stateChange;
750 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
751 // If the current module failed, mark all dependent modules also as failed.
752 // Iterate until steady-state to propagate the error state upwards in the
756 for ( m in registry ) {
757 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
758 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
759 registry[m].state = 'error';
764 } while ( stateChange );
767 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
768 for ( j = 0; j < jobs.length; j += 1 ) {
769 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
770 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
771 // All dependencies satisfied, or some have errors
777 throw new Error( 'Module ' + module + ' failed.');
779 if ( $.isFunction( job.ready ) ) {
784 if ( $.isFunction( job.error ) ) {
786 job.error( e, [module] );
788 // A user-defined operation raised an exception. Swallow to protect
789 // our state machine!
790 log( 'Exception thrown by job.error()', ex );
797 if ( registry[module].state === 'ready' ) {
798 // The current module became 'ready'. Recursively execute all dependent modules that are loaded
799 // and now have all dependencies satisfied.
800 for ( m in registry ) {
801 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
809 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
810 * depending on whether document-ready has occurred yet and whether we are in async mode.
813 * @param {string} src URL to script, will be used as the src attribute in the script tag
814 * @param {Function} [callback] Callback which will be run when the script is done
816 function addScript( src, callback, async ) {
817 /*jshint evil:true */
821 // Using isReady directly instead of storing it locally from
822 // a $.fn.ready callback (bug 31895).
823 if ( $.isReady || async ) {
824 // Can't use jQuery.getScript because that only uses <script> for cross-domain,
825 // it uses XHR and eval for same-domain scripts, which we don't want because it
826 // messes up line numbers.
827 // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
829 // IE-safe way of getting the <head>. document.head isn't supported
830 // in old IE, and doesn't work when in the <head>.
831 head = document.getElementsByTagName( 'head' )[0] || document.body;
833 script = document.createElement( 'script' );
836 if ( $.isFunction( callback ) ) {
837 script.onload = script.onreadystatechange = function () {
842 || /loaded|complete/.test( script.readyState )
847 // Handle memory leak in IE
848 script.onload = script.onreadystatechange = null;
851 if ( script.parentNode ) {
852 script.parentNode.removeChild( script );
855 // Dereference the script
863 if ( window.opera ) {
864 // Appending to the <head> blocks rendering completely in Opera,
865 // so append to the <body> after document ready. This means the
866 // scripts only start loading after the document has been rendered,
867 // but so be it. Opera users don't deserve faster web pages if their
868 // browser makes it impossible.
870 document.body.appendChild( script );
873 head.appendChild( script );
876 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
877 if ( $.isFunction( callback ) ) {
878 // Document.write is synchronous, so this is called when it's done
879 // FIXME: that's a lie. doc.write isn't actually synchronous
886 * Executes a loaded module, making it ready to use
889 * @param {string} module Module name to execute
891 function execute( module ) {
892 var key, value, media, i, urls, cssHandle, checkCssHandles,
893 cssHandlesRegistered = false;
895 if ( registry[module] === undefined ) {
896 throw new Error( 'Module has not been registered yet: ' + module );
897 } else if ( registry[module].state === 'registered' ) {
898 throw new Error( 'Module has not been requested from the server yet: ' + module );
899 } else if ( registry[module].state === 'loading' ) {
900 throw new Error( 'Module has not completed loading yet: ' + module );
901 } else if ( registry[module].state === 'ready' ) {
902 throw new Error( 'Module has already been executed: ' + module );
906 * Define loop-function here for efficiency
907 * and to avoid re-using badly scoped variables.
910 function addLink( media, url ) {
911 var el = document.createElement( 'link' );
912 getMarker().before( el ); // IE: Insert in dom before setting href
913 el.rel = 'stylesheet';
914 if ( media && media !== 'all' ) {
920 function runScript() {
921 var script, markModuleReady, nestedAddScript;
923 script = registry[module].script;
924 markModuleReady = function () {
925 registry[module].state = 'ready';
926 handlePending( module );
928 nestedAddScript = function ( arr, callback, async, i ) {
929 // Recursively call addScript() in its own callback
930 // for each element of arr.
931 if ( i >= arr.length ) {
932 // We're at the end of the array
937 addScript( arr[i], function () {
938 nestedAddScript( arr, callback, async, i + 1 );
942 if ( $.isArray( script ) ) {
943 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
944 } else if ( $.isFunction( script ) ) {
945 registry[module].state = 'ready';
947 handlePending( module );
950 // This needs to NOT use mw.log because these errors are common in production mode
951 // and not in debug mode, such as when a symbol that should be global isn't exported
952 log( 'Exception thrown by ' + module + ': ' + e.message, e );
953 registry[module].state = 'error';
954 handlePending( module );
958 // This used to be inside runScript, but since that is now fired asychronously
959 // (after CSS is loaded) we need to set it here right away. It is crucial that
960 // when execute() is called this is set synchronously, otherwise modules will get
961 // executed multiple times as the registry will state that it isn't loading yet.
962 registry[module].state = 'loading';
964 // Add localizations to message system
965 if ( $.isPlainObject( registry[module].messages ) ) {
966 mw.messages.set( registry[module].messages );
969 // Make sure we don't run the scripts until all (potentially asynchronous)
970 // stylesheet insertions have completed.
973 checkCssHandles = function () {
974 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
975 // one of the cssHandles is fired while we're still creating more handles.
976 if ( cssHandlesRegistered && pending === 0 && runScript ) {
978 runScript = undefined; // Revoke
981 cssHandle = function () {
982 var check = checkCssHandles;
988 check = undefined; // Revoke
994 // Process styles (see also mw.loader.implement)
995 // * back-compat: { <media>: css }
996 // * back-compat: { <media>: [url, ..] }
997 // * { "css": [css, ..] }
998 // * { "url": { <media>: [url, ..] } }
999 if ( $.isPlainObject( registry[module].style ) ) {
1000 for ( key in registry[module].style ) {
1001 value = registry[module].style[key];
1004 if ( key !== 'url' && key !== 'css' ) {
1005 // Backwards compatibility, key is a media-type
1006 if ( typeof value === 'string' ) {
1007 // back-compat: { <media>: css }
1008 // Ignore 'media' because it isn't supported (nor was it used).
1009 // Strings are pre-wrapped in "@media". The media-type was just ""
1010 // (because it had to be set to something).
1011 // This is one of the reasons why this format is no longer used.
1012 addEmbeddedCSS( value, cssHandle() );
1014 // back-compat: { <media>: [url, ..] }
1020 // Array of css strings in key 'css',
1021 // or back-compat array of urls from media-type
1022 if ( $.isArray( value ) ) {
1023 for ( i = 0; i < value.length; i += 1 ) {
1024 if ( key === 'bc-url' ) {
1025 // back-compat: { <media>: [url, ..] }
1026 addLink( media, value[i] );
1027 } else if ( key === 'css' ) {
1028 // { "css": [css, ..] }
1029 addEmbeddedCSS( value[i], cssHandle() );
1032 // Not an array, but a regular object
1033 // Array of urls inside media-type key
1034 } else if ( typeof value === 'object' ) {
1035 // { "url": { <media>: [url, ..] } }
1036 for ( media in value ) {
1037 urls = value[media];
1038 for ( i = 0; i < urls.length; i += 1 ) {
1039 addLink( media, urls[i] );
1047 cssHandlesRegistered = true;
1052 * Adds a dependencies to the queue with optional callbacks to be run
1053 * when the dependencies are ready or fail
1056 * @param {string|string[]} dependencies Module name or array of string module names
1057 * @param {Function} [ready] Callback to execute when all dependencies are ready
1058 * @param {Function} [error] Callback to execute when any dependency fails
1059 * @param {boolean} [async] If true, load modules asynchronously even if
1060 * document ready has not yet occurred.
1062 function request( dependencies, ready, error, async ) {
1065 // Allow calling by single module name
1066 if ( typeof dependencies === 'string' ) {
1067 dependencies = [dependencies];
1070 // Add ready and error callbacks if they were given
1071 if ( ready !== undefined || error !== undefined ) {
1072 jobs[jobs.length] = {
1073 'dependencies': filter(
1074 ['registered', 'loading', 'loaded'],
1082 // Queue up any dependencies that are registered
1083 dependencies = filter( ['registered'], dependencies );
1084 for ( n = 0; n < dependencies.length; n += 1 ) {
1085 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1086 queue[queue.length] = dependencies[n];
1088 // Mark this module as async in the registry
1089 registry[dependencies[n]].async = true;
1098 function sortQuery(o) {
1099 var sorted = {}, key, a = [];
1101 if ( hasOwn.call( o, key ) ) {
1106 for ( key = 0; key < a.length; key += 1 ) {
1107 sorted[a[key]] = o[a[key]];
1113 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1114 * to a query string of the form foo.bar,baz|bar.baz,quux
1117 function buildModulesString( moduleMap ) {
1118 var arr = [], p, prefix;
1119 for ( prefix in moduleMap ) {
1120 p = prefix === '' ? '' : prefix + '.';
1121 arr.push( p + moduleMap[prefix].join( ',' ) );
1123 return arr.join( '|' );
1127 * Asynchronously append a script tag to the end of the body
1128 * that invokes load.php
1130 * @param {Object} moduleMap Module map, see #buildModulesString
1131 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1132 * @param {string} sourceLoadScript URL of load.php
1133 * @param {boolean} async If true, use an asynchrounous request even if document ready has not yet occurred
1135 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1136 var request = $.extend(
1137 { modules: buildModulesString( moduleMap ) },
1140 request = sortQuery( request );
1141 // Asynchronously append a script tag to the end of the body
1142 // Append &* to avoid triggering the IE6 extension check
1143 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1146 /* Public Methods */
1148 addStyleTag: addStyleTag,
1151 * Requests dependencies from server, loading and executing when things when ready.
1154 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1155 source, group, g, i, modules, maxVersion, sourceLoadScript,
1156 currReqBase, currReqBaseLength, moduleMap, l,
1157 lastDotIndex, prefix, suffix, bytesAdded, async;
1159 // Build a list of request parameters common to all requests.
1161 skin: mw.config.get( 'skin' ),
1162 lang: mw.config.get( 'wgUserLanguage' ),
1163 debug: mw.config.get( 'debug' )
1165 // Split module batch by source and by group.
1167 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1169 // Appends a list of modules from the queue to the batch
1170 for ( q = 0; q < queue.length; q += 1 ) {
1171 // Only request modules which are registered
1172 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1173 // Prevent duplicate entries
1174 if ( $.inArray( queue[q], batch ) === -1 ) {
1175 batch[batch.length] = queue[q];
1176 // Mark registered modules as loading
1177 registry[queue[q]].state = 'loading';
1181 // Early exit if there's nothing to load...
1182 if ( !batch.length ) {
1186 // The queue has been processed into the batch, clear up the queue.
1189 // Always order modules alphabetically to help reduce cache
1190 // misses for otherwise identical content.
1193 // Split batch by source and by group.
1194 for ( b = 0; b < batch.length; b += 1 ) {
1195 bSource = registry[batch[b]].source;
1196 bGroup = registry[batch[b]].group;
1197 if ( splits[bSource] === undefined ) {
1198 splits[bSource] = {};
1200 if ( splits[bSource][bGroup] === undefined ) {
1201 splits[bSource][bGroup] = [];
1203 bSourceGroup = splits[bSource][bGroup];
1204 bSourceGroup[bSourceGroup.length] = batch[b];
1207 // Clear the batch - this MUST happen before we append any
1208 // script elements to the body or it's possible that a script
1209 // will be locally cached, instantly load, and work the batch
1210 // again, all before we've cleared it causing each request to
1211 // include modules which are already loaded.
1214 for ( source in splits ) {
1216 sourceLoadScript = sources[source].loadScript;
1218 for ( group in splits[source] ) {
1220 // Cache access to currently selected list of
1221 // modules for this group from this source.
1222 modules = splits[source][group];
1224 // Calculate the highest timestamp
1226 for ( g = 0; g < modules.length; g += 1 ) {
1227 if ( registry[modules[g]].version > maxVersion ) {
1228 maxVersion = registry[modules[g]].version;
1232 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1233 // For user modules append a user name to the request.
1234 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1235 currReqBase.user = mw.config.get( 'wgUserName' );
1237 currReqBaseLength = $.param( currReqBase ).length;
1239 // We may need to split up the request to honor the query string length limit,
1240 // so build it piece by piece.
1241 l = currReqBaseLength + 9; // '&modules='.length == 9
1243 moduleMap = {}; // { prefix: [ suffixes ] }
1245 for ( i = 0; i < modules.length; i += 1 ) {
1246 // Determine how many bytes this module would add to the query string
1247 lastDotIndex = modules[i].lastIndexOf( '.' );
1248 // Note that these substr() calls work even if lastDotIndex == -1
1249 prefix = modules[i].substr( 0, lastDotIndex );
1250 suffix = modules[i].substr( lastDotIndex + 1 );
1251 bytesAdded = moduleMap[prefix] !== undefined
1252 ? suffix.length + 3 // '%2C'.length == 3
1253 : modules[i].length + 3; // '%7C'.length == 3
1255 // If the request would become too long, create a new one,
1256 // but don't create empty requests
1257 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1258 // This request would become too long, create a new one
1259 // and fire off the old one
1260 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1263 l = currReqBaseLength + 9;
1265 if ( moduleMap[prefix] === undefined ) {
1266 moduleMap[prefix] = [];
1268 moduleMap[prefix].push( suffix );
1269 if ( !registry[modules[i]].async ) {
1270 // If this module is blocking, make the entire request blocking
1271 // This is slightly suboptimal, but in practice mixing of blocking
1272 // and async modules will only occur in debug mode.
1277 // If there's anything left in moduleMap, request that too
1278 if ( !$.isEmptyObject( moduleMap ) ) {
1279 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1286 * Register a source.
1288 * @param {string} id Short lowercase a-Z string representing a source, only used internally.
1289 * @param {Object} props Object containing only the loadScript property which is a url to
1290 * the load.php location of the source.
1293 addSource: function ( id, props ) {
1295 // Allow multiple additions
1296 if ( typeof id === 'object' ) {
1297 for ( source in id ) {
1298 mw.loader.addSource( source, id[source] );
1303 if ( sources[id] !== undefined ) {
1304 throw new Error( 'source already registered: ' + id );
1307 sources[id] = props;
1313 * Registers a module, letting the system know about it and its
1314 * properties. Startup modules contain calls to this function.
1316 * @param {string} module Module name
1317 * @param {number} version Module version number as a timestamp (falls backs to 0)
1318 * @param {string|Array|Function} dependencies One string or array of strings of module
1319 * names on which this module depends, or a function that returns that array.
1320 * @param {string} [group=null] Group which the module is in
1321 * @param {string} [source='local'] Name of the source
1323 register: function ( module, version, dependencies, group, source ) {
1325 // Allow multiple registration
1326 if ( typeof module === 'object' ) {
1327 for ( m = 0; m < module.length; m += 1 ) {
1328 // module is an array of module names
1329 if ( typeof module[m] === 'string' ) {
1330 mw.loader.register( module[m] );
1331 // module is an array of arrays
1332 } else if ( typeof module[m] === 'object' ) {
1333 mw.loader.register.apply( mw.loader, module[m] );
1339 if ( typeof module !== 'string' ) {
1340 throw new Error( 'module must be a string, not a ' + typeof module );
1342 if ( registry[module] !== undefined ) {
1343 throw new Error( 'module already registered: ' + module );
1345 // List the module as registered
1346 registry[module] = {
1347 version: version !== undefined ? parseInt( version, 10 ) : 0,
1349 group: typeof group === 'string' ? group : null,
1350 source: typeof source === 'string' ? source: 'local',
1353 if ( typeof dependencies === 'string' ) {
1354 // Allow dependencies to be given as a single module name
1355 registry[module].dependencies = [ dependencies ];
1356 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1357 // Allow dependencies to be given as an array of module names
1358 // or a function which returns an array
1359 registry[module].dependencies = dependencies;
1364 * Implements a module, giving the system a course of action to take
1365 * upon loading. Results of a request for one or more modules contain
1366 * calls to this function.
1368 * All arguments are required.
1370 * @param {string} module Name of module
1371 * @param {Function|Array} script Function with module code or Array of URLs to
1372 * be used as the src attribute of a new `<script>` tag.
1373 * @param {Object} style Should follow one of the following patterns:
1374 * { "css": [css, ..] }
1375 * { "url": { <media>: [url, ..] } }
1376 * And for backwards compatibility (needs to be supported forever due to caching):
1378 * { <media>: [url, ..] }
1380 * The reason css strings are not concatenated anymore is bug 31676. We now check
1381 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1383 * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}.
1385 implement: function ( module, script, style, msgs ) {
1387 if ( typeof module !== 'string' ) {
1388 throw new Error( 'module must be a string, not a ' + typeof module );
1390 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1391 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1393 if ( !$.isPlainObject( style ) ) {
1394 throw new Error( 'style must be an object, not a ' + typeof style );
1396 if ( !$.isPlainObject( msgs ) ) {
1397 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1399 // Automatically register module
1400 if ( registry[module] === undefined ) {
1401 mw.loader.register( module );
1403 // Check for duplicate implementation
1404 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1405 throw new Error( 'module already implemented: ' + module );
1407 // Attach components
1408 registry[module].script = script;
1409 registry[module].style = style;
1410 registry[module].messages = msgs;
1411 // The module may already have been marked as erroneous
1412 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1413 registry[module].state = 'loaded';
1414 if ( allReady( registry[module].dependencies ) ) {
1421 * Executes a function as soon as one or more required modules are ready
1423 * @param {string|Array} dependencies Module name or array of modules names the callback
1424 * dependends on to be ready before executing
1425 * @param {Function} [ready] callback to execute when all dependencies are ready
1426 * @param {Function} [error] callback to execute when if dependencies have a errors
1428 using: function ( dependencies, ready, error ) {
1429 var tod = typeof dependencies;
1431 if ( tod !== 'object' && tod !== 'string' ) {
1432 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1434 // Allow calling with a single dependency as a string
1435 if ( tod === 'string' ) {
1436 dependencies = [ dependencies ];
1438 // Resolve entire dependency map
1439 dependencies = resolve( dependencies );
1440 if ( allReady( dependencies ) ) {
1441 // Run ready immediately
1442 if ( $.isFunction( ready ) ) {
1445 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1446 // Execute error immediately if any dependencies have errors
1447 if ( $.isFunction( error ) ) {
1448 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1452 // Not all dependencies are ready: queue up a request
1453 request( dependencies, ready, error );
1458 * Loads an external script or one or more modules for future use
1460 * @param {string|Array} modules Either the name of a module, array of modules,
1461 * or a URL of an external script or style
1462 * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
1463 * external script or style; acceptable values are "text/css" and
1464 * "text/javascript"; if no type is provided, text/javascript is assumed.
1465 * @param {boolean} [async] If true, load modules asynchronously
1466 * even if document ready has not yet occurred. If false, block before
1467 * document ready and load async after. If not set, true will be
1468 * assumed if loading a URL, and false will be assumed otherwise.
1470 load: function ( modules, type, async ) {
1471 var filtered, m, module, l;
1474 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1475 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1477 // Allow calling with an external url or single dependency as a string
1478 if ( typeof modules === 'string' ) {
1479 // Support adding arbitrary external scripts
1480 if ( /^(https?:)?\/\//.test( modules ) ) {
1481 if ( async === undefined ) {
1482 // Assume async for bug 34542
1485 if ( type === 'text/css' ) {
1486 // IE7-8 throws security warnings when inserting a <link> tag
1487 // with a protocol-relative URL set though attributes (instead of
1488 // properties) - when on HTTPS. See also bug #.
1489 l = document.createElement( 'link' );
1490 l.rel = 'stylesheet';
1492 $( 'head' ).append( l );
1495 if ( type === 'text/javascript' || type === undefined ) {
1496 addScript( modules, null, async );
1500 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1502 // Called with single module
1503 modules = [ modules ];
1506 // Filter out undefined modules, otherwise resolve() will throw
1507 // an exception for trying to load an undefined module.
1508 // Undefined modules are acceptable here in load(), because load() takes
1509 // an array of unrelated modules, whereas the modules passed to
1510 // using() are related and must all be loaded.
1511 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1512 module = registry[modules[m]];
1513 if ( module !== undefined ) {
1514 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1515 filtered[filtered.length] = modules[m];
1520 if ( filtered.length === 0 ) {
1523 // Resolve entire dependency map
1524 filtered = resolve( filtered );
1525 // If all modules are ready, nothing to be done
1526 if ( allReady( filtered ) ) {
1529 // If any modules have errors: also quit.
1530 if ( filter( ['error', 'missing'], filtered ).length ) {
1533 // Since some modules are not yet ready, queue up a request.
1534 request( filtered, undefined, undefined, async );
1538 * Changes the state of a module
1540 * @param {string|Object} module module name or object of module name/state pairs
1541 * @param {string} state state name
1543 state: function ( module, state ) {
1546 if ( typeof module === 'object' ) {
1547 for ( m in module ) {
1548 mw.loader.state( m, module[m] );
1552 if ( registry[module] === undefined ) {
1553 mw.loader.register( module );
1555 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1556 && registry[module].state !== state ) {
1557 // Make sure pending modules depending on this one get executed if their
1558 // dependencies are now fulfilled!
1559 registry[module].state = state;
1560 handlePending( module );
1562 registry[module].state = state;
1567 * Gets the version of a module
1569 * @param {string} module name of module to get version for
1571 getVersion: function ( module ) {
1572 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1573 return formatVersionNumber( registry[module].version );
1579 * @deprecated since 1.18 use mw.loader.getVersion() instead
1581 version: function () {
1582 return mw.loader.getVersion.apply( mw.loader, arguments );
1586 * Gets the state of a module
1588 * @param {string} module name of module to get state for
1590 getState: function ( module ) {
1591 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1592 return registry[module].state;
1598 * Get names of all registered modules.
1602 getModuleNames: function () {
1603 return $.map( registry, function ( i, key ) {
1609 * For backwards-compatibility with Squid-cached pages. Loads mw.user
1612 mw.loader.load( 'mediawiki.user' );
1618 * HTML construction helper functions
1622 html: ( function () {
1623 function escapeCallback( s ) {
1640 * Escape a string for HTML. Converts special characters to HTML entities.
1641 * @param {string} s The string to escape
1643 escape: function ( s ) {
1644 return s.replace( /['"<>&]/g, escapeCallback );
1648 * Wrapper object for raw HTML passed to mw.html.element().
1649 * @class mw.html.Raw
1651 Raw: function ( value ) {
1656 * Wrapper object for CDATA element contents passed to mw.html.element()
1657 * @class mw.html.Cdata
1659 Cdata: function ( value ) {
1664 * Create an HTML element string, with safe escaping.
1666 * @param {string} name The tag name.
1667 * @param {Object} attrs An object with members mapping element names to values
1668 * @param {Mixed} contents The contents of the element. May be either:
1669 * - string: The string is escaped.
1670 * - null or undefined: The short closing form is used, e.g. <br/>.
1671 * - this.Raw: The value attribute is included without escaping.
1672 * - this.Cdata: The value attribute is included, and an exception is
1673 * thrown if it contains an illegal ETAGO delimiter.
1674 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1678 * return h.element( 'div', {},
1679 * new h.Raw( h.element( 'img', {src: '<'} ) ) );
1680 * Returns <div><img src="<"/></div>
1682 element: function ( name, attrs, contents ) {
1683 var v, attrName, s = '<' + name;
1685 for ( attrName in attrs ) {
1686 v = attrs[attrName];
1687 // Convert name=true, to name=name
1691 } else if ( v === false ) {
1694 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
1696 if ( contents === undefined || contents === null ) {
1703 switch ( typeof contents ) {
1706 s += this.escape( contents );
1710 // Convert to string
1711 s += String( contents );
1714 if ( contents instanceof this.Raw ) {
1715 // Raw HTML inclusion
1716 s += contents.value;
1717 } else if ( contents instanceof this.Cdata ) {
1719 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1720 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1722 s += contents.value;
1724 throw new Error( 'mw.html.element: Invalid type of contents' );
1727 s += '</' + name + '>';
1733 // Skeleton user object. mediawiki.user.js extends this
1742 // Alias $j to jQuery for backwards compatibility
1745 // Attach to window and globally alias
1746 window.mw = window.mediaWiki = mw;
1748 // Auto-register from pre-loaded startup scripts
1749 if ( jQuery.isFunction( window.startUp ) ) {
1751 window.startUp = undefined;