2 * Core MediaWiki JavaScript Library
5 var mw = ( function ( $, undefined ) {
10 var hasOwn = Object.prototype.hasOwnProperty,
11 slice = Array.prototype.slice;
13 /* Object constructors */
18 * Creates an object that can be read from or written to from prototype functions
19 * that allow both single and multiple variables at once.
21 * @param global boolean Whether to store the values in the global window
22 * object or a exclusively in the object property 'values'.
25 function Map( global ) {
26 this.values = global === true ? window : {};
32 * Get the value of one or multiple a keys.
34 * If called with no arguments, all values will be returned.
36 * @param selection mixed String key or array of keys to get values for.
37 * @param fallback mixed Value to use in case key(s) do not exist (optional).
38 * @return mixed If selection was a string returns the value or null,
39 * If selection was an array, returns an object of key/values (value is null if not found),
40 * If selection was not passed or invalid, will return the 'values' object member (be careful as
41 * objects are always passed by reference in JavaScript!).
42 * @return Values as a string or object, null if invalid/inexistant.
44 get: function ( selection, fallback ) {
47 if ( $.isArray( selection ) ) {
48 selection = slice.call( selection );
50 for ( i = 0; i < selection.length; i += 1 ) {
51 results[selection[i]] = this.get( selection[i], fallback );
56 if ( typeof selection === 'string' ) {
57 if ( this.values[selection] === undefined ) {
58 if ( fallback !== undefined ) {
63 return this.values[selection];
66 if ( selection === undefined ) {
70 // invalid selection key
75 * Sets one or multiple key/value pairs.
77 * @param selection {mixed} String key or array of keys to set values for.
78 * @param value {mixed} Value to set (optional, only in use when key is a string)
79 * @return {Boolean} This returns true on success, false on failure.
81 set: function ( selection, value ) {
84 if ( $.isPlainObject( selection ) ) {
85 for ( s in selection ) {
86 this.values[s] = selection[s];
90 if ( typeof selection === 'string' && value !== undefined ) {
91 this.values[selection] = value;
98 * Checks if one or multiple keys exist.
100 * @param selection {mixed} String key or array of keys to check
101 * @return {Boolean} Existence of key(s)
103 exists: function ( selection ) {
106 if ( $.isArray( selection ) ) {
107 for ( s = 0; s < selection.length; s += 1 ) {
108 if ( this.values[selection[s]] === undefined ) {
114 return this.values[selection] !== undefined;
121 * Object constructor for messages,
122 * similar to the Message class in MediaWiki PHP.
124 * @param map Map Instance of mw.Map
126 * @param parameters Array
129 function Message( map, key, parameters ) {
130 this.format = 'plain';
133 this.parameters = parameters === undefined ? [] : slice.call( parameters );
137 Message.prototype = {
139 * Simple message parser, does $N replacement and nothing else.
140 * This may be overridden to provide a more complex message parser.
142 * This function will not be called for nonexistent messages.
144 parser: function () {
145 var parameters = this.parameters;
146 return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
147 var index = parseInt( match, 10 ) - 1;
148 return parameters[index] !== undefined ? parameters[index] : '$' + match;
153 * Appends (does not replace) parameters for replacement to the .parameters property.
155 * @param parameters Array
158 params: function ( parameters ) {
160 for ( i = 0; i < parameters.length; i += 1 ) {
161 this.parameters.push( parameters[i] );
167 * Converts message object to it's string form based on the state of format.
169 * @return string Message as a string in the current form or <key> if key does not exist.
171 toString: function () {
174 if ( !this.exists() ) {
175 // Use <key> as text if key does not exist
176 if ( this.format !== 'plain' ) {
177 // format 'escape' and 'parse' need to have the brackets and key html escaped
178 return mw.html.escape( '<' + this.key + '>' );
180 return '<' + this.key + '>';
183 if ( this.format === 'plain' ) {
184 // @todo FIXME: Although not applicable to core Message,
185 // Plugins like jQueryMsg should be able to distinguish
186 // between 'plain' (only variable replacement and plural/gender)
187 // and actually parsing wikitext to HTML.
188 text = this.parser();
191 if ( this.format === 'escaped' ) {
192 text = this.parser();
193 text = mw.html.escape( text );
196 if ( this.format === 'parse' ) {
197 text = this.parser();
204 * Changes format to parse and converts message to string
206 * @return {string} String form of parsed message
209 this.format = 'parse';
210 return this.toString();
214 * Changes format to plain and converts message to string
216 * @return {string} String form of plain message
219 this.format = 'plain';
220 return this.toString();
224 * Changes the format to html escaped and converts message to string
226 * @return {string} String form of html escaped message
228 escaped: function () {
229 this.format = 'escaped';
230 return this.toString();
234 * Checks if message exists
236 * @return {string} String form of parsed message
238 exists: function () {
239 return this.map.exists( this.key );
247 * Dummy function which in debug mode can be replaced with a function that
248 * emulates console.log in console-less environments.
250 log: function () { },
253 * @var constructor Make the Map constructor publicly available.
258 * @var constructor Make the Message constructor publicly available.
263 * List of configuration values
265 * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map().
266 * If $wgLegacyJavaScriptGlobals is true, this Map will have its values
267 * in the global window object.
274 * Empty object that plugins can be installed in.
278 /* Extension points */
283 * Localization system
290 * Gets a message object, similar to wfMessage()
292 * @param key string Key of message to get
293 * @param parameter_1 mixed First argument in a list of variadic arguments,
294 * each a parameter for $N replacement in messages.
297 message: function ( key, parameter_1 /* [, parameter_2] */ ) {
299 // Support variadic arguments
300 if ( parameter_1 !== undefined ) {
301 parameters = slice.call( arguments );
306 return new Message( mw.messages, key, parameters );
310 * Gets a message string, similar to wfMessage()
312 * @param key string Key of message to get
313 * @param parameters mixed First argument in a list of variadic arguments,
314 * each a parameter for $N replacement in messages.
317 msg: function ( /* key, parameter_1, parameter_2, .. */ ) {
318 return mw.message.apply( mw.message, arguments ).toString();
322 * Client-side module loader which integrates with the MediaWiki ResourceLoader
324 loader: ( function () {
326 /* Private Members */
329 * Mapping of registered modules
331 * The jquery module is pre-registered, because it must have already
332 * been provided for this object to have been built, and in debug mode
333 * jquery would have been provided through a unique loader request,
334 * making it impossible to hold back registration of jquery until after
337 * For exact details on support for script, style and messages, look at
338 * mw.loader.implement.
343 * 'version': ############## (unix timestamp),
344 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
345 * 'group': 'somegroup', (or) null,
346 * 'source': 'local', 'someforeignwiki', (or) null
347 * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing'
350 * 'messages': { 'key': 'value' },
356 * Mapping of sources, keyed by source-id, values are objects.
360 * 'loadScript': 'http://foo.bar/w/load.php'
365 // List of modules which will be loaded as when ready
367 // List of modules to be loaded
369 // List of callback functions waiting for modules to be ready to be called
371 // Selector cache for the marker element. Use getMarker() to get/use the marker!
374 /* Private methods */
376 function getMarker() {
382 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
383 if ( $marker.length ) {
386 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
387 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
393 * Create a new style tag and add it to the DOM.
395 * @param text String: CSS text
396 * @param nextnode mixed: [optional] An Element or jQuery object for an element where
397 * the style tag should be inserted before. Otherwise appended to the <head>.
398 * @return HTMLStyleElement
400 function addStyleTag( text, nextnode ) {
401 var s = document.createElement( 'style' );
402 // Insert into document before setting cssText (bug 33305)
404 // Must be inserted with native insertBefore, not $.fn.before.
405 // When using jQuery to insert it, like $nextnode.before( s ),
406 // then IE6 will throw "Access is denied" when trying to append
407 // to .cssText later. Some kind of weird security measure.
408 // http://stackoverflow.com/q/12586482/319266
409 // Works: jsfiddle.net/zJzMy/1
410 // Fails: jsfiddle.net/uJTQz
411 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
412 if ( nextnode.jquery ) {
413 nextnode = nextnode.get( 0 );
415 nextnode.parentNode.insertBefore( s, nextnode );
417 document.getElementsByTagName( 'head' )[0].appendChild( s );
419 if ( s.styleSheet ) {
421 s.styleSheet.cssText = text;
424 // (Safari sometimes borks on non-string values,
425 // play safe by casting to a string, just in case.)
426 s.appendChild( document.createTextNode( String( text ) ) );
432 * Checks if certain cssText is safe to append to
435 * Right now it only makes sure that cssText containing @import
436 * rules will end up in a new stylesheet (as those only work when
437 * placed at the start of a stylesheet; bug 35562).
438 * This could later be extended to take care of other bugs, such as
439 * the IE cssRules limit - not the same as the IE styleSheets limit).
441 function canExpandStylesheetWith( $style, cssText ) {
442 return cssText.indexOf( '@import' ) === -1;
445 function addEmbeddedCSS( cssText ) {
447 $style = getMarker().prev();
448 // Re-use <style> tags if possible, this to try to stay
449 // under the IE stylesheet limit (bug 31676).
450 // Also verify that the the element before Marker actually is one
451 // that came from ResourceLoader, and not a style tag that some
452 // other script inserted before our marker, or, more importantly,
453 // it may not be a style tag at all (could be <meta> or <script>).
455 $style.data( 'ResourceLoaderDynamicStyleTag' ) === true &&
456 canExpandStylesheetWith( $style, cssText )
458 // There's already a dynamic <style> tag present and
459 // canExpandStylesheetWith() gave a green light to append more to it.
460 styleEl = $style.get( 0 );
461 if ( styleEl.styleSheet ) {
463 styleEl.styleSheet.cssText += cssText; // IE
465 log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e );
468 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
471 $( addStyleTag( cssText, getMarker() ) )
472 .data( 'ResourceLoaderDynamicStyleTag', true );
476 function compare( a, b ) {
478 if ( a.length !== b.length ) {
481 for ( i = 0; i < b.length; i += 1 ) {
482 if ( $.isArray( a[i] ) ) {
483 if ( !compare( a[i], b[i] ) ) {
487 if ( a[i] !== b[i] ) {
495 * Generates an ISO8601 "basic" string from a UNIX timestamp
497 function formatVersionNumber( timestamp ) {
498 var pad = function ( a, b, c ) {
499 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
502 d.setTime( timestamp * 1000 );
504 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
505 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
510 * Resolves dependencies and detects circular references.
512 * @param module String Name of the top-level module whose dependencies shall be
513 * resolved and sorted.
514 * @param resolved Array Returns a topological sort of the given module and its
515 * dependencies, such that later modules depend on earlier modules. The array
516 * contains the module names. If the array contains already some module names,
517 * this function appends its result to the pre-existing array.
518 * @param unresolved Object [optional] Hash used to track the current dependency
519 * chain; used to report loops in the dependency graph.
520 * @throws Error if any unregistered module or a dependency loop is encountered
522 function sortDependencies( module, resolved, unresolved ) {
525 if ( registry[module] === undefined ) {
526 throw new Error( 'Unknown dependency: ' + module );
528 // Resolves dynamic loader function and replaces it with its own results
529 if ( $.isFunction( registry[module].dependencies ) ) {
530 registry[module].dependencies = registry[module].dependencies();
531 // Ensures the module's dependencies are always in an array
532 if ( typeof registry[module].dependencies !== 'object' ) {
533 registry[module].dependencies = [registry[module].dependencies];
536 if ( $.inArray( module, resolved ) !== -1 ) {
537 // Module already resolved; nothing to do.
540 // unresolved is optional, supply it if not passed in
544 // Tracks down dependencies
545 deps = registry[module].dependencies;
547 for ( n = 0; n < len; n += 1 ) {
548 if ( $.inArray( deps[n], resolved ) === -1 ) {
549 if ( unresolved[deps[n]] ) {
551 'Circular reference detected: ' + module +
557 unresolved[module] = true;
558 sortDependencies( deps[n], resolved, unresolved );
559 delete unresolved[module];
562 resolved[resolved.length] = module;
566 * Gets a list of module names that a module depends on in their proper dependency
569 * @param module string module name or array of string module names
570 * @return list of dependencies, including 'module'.
571 * @throws Error if circular reference is detected
573 function resolve( module ) {
576 // Allow calling with an array of module names
577 if ( $.isArray( module ) ) {
579 for ( m = 0; m < module.length; m += 1 ) {
580 sortDependencies( module[m], resolved );
585 if ( typeof module === 'string' ) {
587 sortDependencies( module, resolved );
591 throw new Error( 'Invalid module argument: ' + module );
595 * Narrows a list of module names down to those matching a specific
596 * state (see comment on top of this scope for a list of valid states).
597 * One can also filter for 'unregistered', which will return the
598 * modules names that don't have a registry entry.
600 * @param states string or array of strings of module states to filter by
601 * @param modules array list of module names to filter (optional, by default the entire
603 * @return array list of filtered module names
605 function filter( states, modules ) {
606 var list, module, s, m;
608 // Allow states to be given as a string
609 if ( typeof states === 'string' ) {
612 // If called without a list of modules, build and use a list of all modules
614 if ( modules === undefined ) {
616 for ( module in registry ) {
617 modules[modules.length] = module;
620 // Build a list of modules which are in one of the specified states
621 for ( s = 0; s < states.length; s += 1 ) {
622 for ( m = 0; m < modules.length; m += 1 ) {
623 if ( registry[modules[m]] === undefined ) {
624 // Module does not exist
625 if ( states[s] === 'unregistered' ) {
627 list[list.length] = modules[m];
630 // Module exists, check state
631 if ( registry[modules[m]].state === states[s] ) {
633 list[list.length] = modules[m];
642 * Determine whether all dependencies are in state 'ready', which means we may
643 * execute the module or job now.
645 * @param dependencies Array dependencies (module names) to be checked.
647 * @return Boolean true if all dependencies are in state 'ready', false otherwise
649 function allReady( dependencies ) {
650 return filter( 'ready', dependencies ).length === dependencies.length;
654 * Log a message to window.console, if possible. Useful to force logging of some
655 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
656 * Gets console references in each invocation, so that delayed debugging tools work
657 * fine. No need for optimization here, which would only result in losing logs.
659 * @param msg String text for the log entry.
660 * @param e Error [optional] to also log.
662 function log( msg, e ) {
663 var console = window.console;
664 if ( console && console.log ) {
666 // If we have an exception object, log it through .error() to trigger
667 // proper stacktraces in browsers that support it. There are no (known)
668 // browsers that don't support .error(), that do support .log() and
669 // have useful exception handling through .log().
670 if ( e && console.error ) {
677 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
678 * and modules that depend upon this module. if the given module failed, propagate the 'error'
679 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
680 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
682 * @param module String name of module that entered one of the states 'ready', 'error', or 'missing'.
684 function handlePending( module ) {
685 var j, job, hasErrors, m, stateChange;
688 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
689 // If the current module failed, mark all dependent modules also as failed.
690 // Iterate until steady-state to propagate the error state upwards in the
694 for ( m in registry ) {
695 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
696 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
697 registry[m].state = 'error';
702 } while ( stateChange );
705 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
706 for ( j = 0; j < jobs.length; j += 1 ) {
707 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
708 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
709 // All dependencies satisfied, or some have errors
715 throw new Error ("Module " + module + " failed.");
717 if ( $.isFunction( job.ready ) ) {
722 if ( $.isFunction( job.error ) ) {
724 job.error( e, [module] );
726 // A user-defined operation raised an exception. Swallow to protect
727 // our state machine!
728 log( 'Exception thrown by job.error()', ex );
735 if ( registry[module].state === 'ready' ) {
736 // The current module became 'ready'. Recursively execute all dependent modules that are loaded
737 // and now have all dependencies satisfied.
738 for ( m in registry ) {
739 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
747 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
748 * depending on whether document-ready has occurred yet and whether we are in async mode.
750 * @param src String: URL to script, will be used as the src attribute in the script tag
751 * @param callback Function: Optional callback which will be run when the script is done
753 function addScript( src, callback, async ) {
754 /*jshint evil:true */
758 // Using isReady directly instead of storing it locally from
759 // a $.fn.ready callback (bug 31895).
760 if ( $.isReady || async ) {
761 // Can't use jQuery.getScript because that only uses <script> for cross-domain,
762 // it uses XHR and eval for same-domain scripts, which we don't want because it
763 // messes up line numbers.
764 // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
766 // IE-safe way of getting the <head>. document.head isn't supported
767 // in old IE, and doesn't work when in the <head>.
768 head = document.getElementsByTagName( 'head' )[0] || document.body;
770 script = document.createElement( 'script' );
773 if ( $.isFunction( callback ) ) {
774 script.onload = script.onreadystatechange = function () {
779 || /loaded|complete/.test( script.readyState )
784 // Handle memory leak in IE
785 script.onload = script.onreadystatechange = null;
788 if ( script.parentNode ) {
789 script.parentNode.removeChild( script );
792 // Dereference the script
800 if ( window.opera ) {
801 // Appending to the <head> blocks rendering completely in Opera,
802 // so append to the <body> after document ready. This means the
803 // scripts only start loading after the document has been rendered,
804 // but so be it. Opera users don't deserve faster web pages if their
805 // browser makes it impossible.
807 document.body.appendChild( script );
810 head.appendChild( script );
813 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
814 if ( $.isFunction( callback ) ) {
815 // Document.write is synchronous, so this is called when it's done
816 // FIXME: that's a lie. doc.write isn't actually synchronous
823 * Executes a loaded module, making it ready to use
825 * @param module string module name to execute
827 function execute( module ) {
828 var key, value, media, i, urls, script, markModuleReady, nestedAddScript;
830 if ( registry[module] === undefined ) {
831 throw new Error( 'Module has not been registered yet: ' + module );
832 } else if ( registry[module].state === 'registered' ) {
833 throw new Error( 'Module has not been requested from the server yet: ' + module );
834 } else if ( registry[module].state === 'loading' ) {
835 throw new Error( 'Module has not completed loading yet: ' + module );
836 } else if ( registry[module].state === 'ready' ) {
837 throw new Error( 'Module has already been loaded: ' + module );
841 * Define loop-function here for efficiency
842 * and to avoid re-using badly scoped variables.
844 function addLink( media, url ) {
845 var el = document.createElement( 'link' );
846 getMarker().before( el ); // IE: Insert in dom before setting href
847 el.rel = 'stylesheet';
848 if ( media && media !== 'all' ) {
854 // Process styles (see also mw.loader.implement)
855 // * back-compat: { <media>: css }
856 // * back-compat: { <media>: [url, ..] }
857 // * { "css": [css, ..] }
858 // * { "url": { <media>: [url, ..] } }
859 if ( $.isPlainObject( registry[module].style ) ) {
860 for ( key in registry[module].style ) {
861 value = registry[module].style[key];
864 if ( key !== 'url' && key !== 'css' ) {
865 // Backwards compatibility, key is a media-type
866 if ( typeof value === 'string' ) {
867 // back-compat: { <media>: css }
868 // Ignore 'media' because it isn't supported (nor was it used).
869 // Strings are pre-wrapped in "@media". The media-type was just ""
870 // (because it had to be set to something).
871 // This is one of the reasons why this format is no longer used.
872 addEmbeddedCSS( value );
874 // back-compat: { <media>: [url, ..] }
880 // Array of css strings in key 'css',
881 // or back-compat array of urls from media-type
882 if ( $.isArray( value ) ) {
883 for ( i = 0; i < value.length; i += 1 ) {
884 if ( key === 'bc-url' ) {
885 // back-compat: { <media>: [url, ..] }
886 addLink( media, value[i] );
887 } else if ( key === 'css' ) {
888 // { "css": [css, ..] }
889 addEmbeddedCSS( value[i] );
892 // Not an array, but a regular object
893 // Array of urls inside media-type key
894 } else if ( typeof value === 'object' ) {
895 // { "url": { <media>: [url, ..] } }
896 for ( media in value ) {
898 for ( i = 0; i < urls.length; i += 1 ) {
899 addLink( media, urls[i] );
906 // Add localizations to message system
907 if ( $.isPlainObject( registry[module].messages ) ) {
908 mw.messages.set( registry[module].messages );
913 script = registry[module].script;
914 markModuleReady = function () {
915 registry[module].state = 'ready';
916 handlePending( module );
918 nestedAddScript = function ( arr, callback, async, i ) {
919 // Recursively call addScript() in its own callback
920 // for each element of arr.
921 if ( i >= arr.length ) {
922 // We're at the end of the array
927 addScript( arr[i], function () {
928 nestedAddScript( arr, callback, async, i + 1 );
932 if ( $.isArray( script ) ) {
933 registry[module].state = 'loading';
934 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
935 } else if ( $.isFunction( script ) ) {
936 registry[module].state = 'ready';
938 handlePending( module );
941 // This needs to NOT use mw.log because these errors are common in production mode
942 // and not in debug mode, such as when a symbol that should be global isn't exported
943 log( 'Exception thrown by ' + module + ': ' + e.message, e );
944 registry[module].state = 'error';
945 handlePending( module );
950 * Adds a dependencies to the queue with optional callbacks to be run
951 * when the dependencies are ready or fail
953 * @param dependencies string module name or array of string module names
954 * @param ready function callback to execute when all dependencies are ready
955 * @param error function callback to execute when any dependency fails
956 * @param async (optional) If true, load modules asynchronously even if
957 * document ready has not yet occurred
959 function request( dependencies, ready, error, async ) {
960 var regItemDeps, regItemDepLen, n;
962 // Allow calling by single module name
963 if ( typeof dependencies === 'string' ) {
964 dependencies = [dependencies];
967 // Add ready and error callbacks if they were given
968 if ( ready !== undefined || error !== undefined ) {
969 jobs[jobs.length] = {
970 'dependencies': filter(
971 ['registered', 'loading', 'loaded'],
979 // Queue up any dependencies that are registered
980 dependencies = filter( ['registered'], dependencies );
981 for ( n = 0; n < dependencies.length; n += 1 ) {
982 if ( $.inArray( dependencies[n], queue ) === -1 ) {
983 queue[queue.length] = dependencies[n];
985 // Mark this module as async in the registry
986 registry[dependencies[n]].async = true;
995 function sortQuery(o) {
996 var sorted = {}, key, a = [];
998 if ( hasOwn.call( o, key ) ) {
1003 for ( key = 0; key < a.length; key += 1 ) {
1004 sorted[a[key]] = o[a[key]];
1010 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1011 * to a query string of the form foo.bar,baz|bar.baz,quux
1013 function buildModulesString( moduleMap ) {
1014 var arr = [], p, prefix;
1015 for ( prefix in moduleMap ) {
1016 p = prefix === '' ? '' : prefix + '.';
1017 arr.push( p + moduleMap[prefix].join( ',' ) );
1019 return arr.join( '|' );
1023 * Asynchronously append a script tag to the end of the body
1024 * that invokes load.php
1025 * @param moduleMap {Object}: Module map, see buildModulesString()
1026 * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request
1027 * @param sourceLoadScript {String}: URL of load.php
1028 * @param async {Boolean}: If true, use an asynchrounous request even if document ready has not yet occurred
1030 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1031 var request = $.extend(
1032 { 'modules': buildModulesString( moduleMap ) },
1035 request = sortQuery( request );
1036 // Asynchronously append a script tag to the end of the body
1037 // Append &* to avoid triggering the IE6 extension check
1038 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1041 /* Public Methods */
1043 addStyleTag: addStyleTag,
1046 * Requests dependencies from server, loading and executing when things when ready.
1049 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1050 source, group, g, i, modules, maxVersion, sourceLoadScript,
1051 currReqBase, currReqBaseLength, moduleMap, l,
1052 lastDotIndex, prefix, suffix, bytesAdded, async;
1054 // Build a list of request parameters common to all requests.
1056 skin: mw.config.get( 'skin' ),
1057 lang: mw.config.get( 'wgUserLanguage' ),
1058 debug: mw.config.get( 'debug' )
1060 // Split module batch by source and by group.
1062 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1064 // Appends a list of modules from the queue to the batch
1065 for ( q = 0; q < queue.length; q += 1 ) {
1066 // Only request modules which are registered
1067 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1068 // Prevent duplicate entries
1069 if ( $.inArray( queue[q], batch ) === -1 ) {
1070 batch[batch.length] = queue[q];
1071 // Mark registered modules as loading
1072 registry[queue[q]].state = 'loading';
1076 // Early exit if there's nothing to load...
1077 if ( !batch.length ) {
1081 // The queue has been processed into the batch, clear up the queue.
1084 // Always order modules alphabetically to help reduce cache
1085 // misses for otherwise identical content.
1088 // Split batch by source and by group.
1089 for ( b = 0; b < batch.length; b += 1 ) {
1090 bSource = registry[batch[b]].source;
1091 bGroup = registry[batch[b]].group;
1092 if ( splits[bSource] === undefined ) {
1093 splits[bSource] = {};
1095 if ( splits[bSource][bGroup] === undefined ) {
1096 splits[bSource][bGroup] = [];
1098 bSourceGroup = splits[bSource][bGroup];
1099 bSourceGroup[bSourceGroup.length] = batch[b];
1102 // Clear the batch - this MUST happen before we append any
1103 // script elements to the body or it's possible that a script
1104 // will be locally cached, instantly load, and work the batch
1105 // again, all before we've cleared it causing each request to
1106 // include modules which are already loaded.
1109 for ( source in splits ) {
1111 sourceLoadScript = sources[source].loadScript;
1113 for ( group in splits[source] ) {
1115 // Cache access to currently selected list of
1116 // modules for this group from this source.
1117 modules = splits[source][group];
1119 // Calculate the highest timestamp
1121 for ( g = 0; g < modules.length; g += 1 ) {
1122 if ( registry[modules[g]].version > maxVersion ) {
1123 maxVersion = registry[modules[g]].version;
1127 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1128 // For user modules append a user name to the request.
1129 if ( group === "user" && mw.config.get( 'wgUserName' ) !== null ) {
1130 currReqBase.user = mw.config.get( 'wgUserName' );
1132 currReqBaseLength = $.param( currReqBase ).length;
1134 // We may need to split up the request to honor the query string length limit,
1135 // so build it piece by piece.
1136 l = currReqBaseLength + 9; // '&modules='.length == 9
1138 moduleMap = {}; // { prefix: [ suffixes ] }
1140 for ( i = 0; i < modules.length; i += 1 ) {
1141 // Determine how many bytes this module would add to the query string
1142 lastDotIndex = modules[i].lastIndexOf( '.' );
1143 // Note that these substr() calls work even if lastDotIndex == -1
1144 prefix = modules[i].substr( 0, lastDotIndex );
1145 suffix = modules[i].substr( lastDotIndex + 1 );
1146 bytesAdded = moduleMap[prefix] !== undefined
1147 ? suffix.length + 3 // '%2C'.length == 3
1148 : modules[i].length + 3; // '%7C'.length == 3
1150 // If the request would become too long, create a new one,
1151 // but don't create empty requests
1152 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1153 // This request would become too long, create a new one
1154 // and fire off the old one
1155 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1158 l = currReqBaseLength + 9;
1160 if ( moduleMap[prefix] === undefined ) {
1161 moduleMap[prefix] = [];
1163 moduleMap[prefix].push( suffix );
1164 if ( !registry[modules[i]].async ) {
1165 // If this module is blocking, make the entire request blocking
1166 // This is slightly suboptimal, but in practice mixing of blocking
1167 // and async modules will only occur in debug mode.
1172 // If there's anything left in moduleMap, request that too
1173 if ( !$.isEmptyObject( moduleMap ) ) {
1174 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1181 * Register a source.
1183 * @param id {String}: Short lowercase a-Z string representing a source, only used internally.
1184 * @param props {Object}: Object containing only the loadScript property which is a url to
1185 * the load.php location of the source.
1188 addSource: function ( id, props ) {
1190 // Allow multiple additions
1191 if ( typeof id === 'object' ) {
1192 for ( source in id ) {
1193 mw.loader.addSource( source, id[source] );
1198 if ( sources[id] !== undefined ) {
1199 throw new Error( 'source already registered: ' + id );
1202 sources[id] = props;
1208 * Registers a module, letting the system know about it and its
1209 * properties. Startup modules contain calls to this function.
1211 * @param module {String}: Module name
1212 * @param version {Number}: Module version number as a timestamp (falls backs to 0)
1213 * @param dependencies {String|Array|Function}: One string or array of strings of module
1214 * names on which this module depends, or a function that returns that array.
1215 * @param group {String}: Group which the module is in (optional, defaults to null)
1216 * @param source {String}: Name of the source. Defaults to local.
1218 register: function ( module, version, dependencies, group, source ) {
1220 // Allow multiple registration
1221 if ( typeof module === 'object' ) {
1222 for ( m = 0; m < module.length; m += 1 ) {
1223 // module is an array of module names
1224 if ( typeof module[m] === 'string' ) {
1225 mw.loader.register( module[m] );
1226 // module is an array of arrays
1227 } else if ( typeof module[m] === 'object' ) {
1228 mw.loader.register.apply( mw.loader, module[m] );
1234 if ( typeof module !== 'string' ) {
1235 throw new Error( 'module must be a string, not a ' + typeof module );
1237 if ( registry[module] !== undefined ) {
1238 throw new Error( 'module already registered: ' + module );
1240 // List the module as registered
1241 registry[module] = {
1242 version: version !== undefined ? parseInt( version, 10 ) : 0,
1244 group: typeof group === 'string' ? group : null,
1245 source: typeof source === 'string' ? source: 'local',
1248 if ( typeof dependencies === 'string' ) {
1249 // Allow dependencies to be given as a single module name
1250 registry[module].dependencies = [ dependencies ];
1251 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1252 // Allow dependencies to be given as an array of module names
1253 // or a function which returns an array
1254 registry[module].dependencies = dependencies;
1259 * Implements a module, giving the system a course of action to take
1260 * upon loading. Results of a request for one or more modules contain
1261 * calls to this function.
1263 * All arguments are required.
1265 * @param {String} module Name of module
1266 * @param {Function|Array} script Function with module code or Array of URLs to
1267 * be used as the src attribute of a new <script> tag.
1268 * @param {Object} style Should follow one of the following patterns:
1269 * { "css": [css, ..] }
1270 * { "url": { <media>: [url, ..] } }
1271 * And for backwards compatibility (needs to be supported forever due to caching):
1273 * { <media>: [url, ..] }
1275 * The reason css strings are not concatenated anymore is bug 31676. We now check
1276 * whether it's safe to extend the stylesheet (see canExpandStylesheetWith).
1278 * @param {Object} msgs List of key/value pairs to be passed through mw.messages.set
1280 implement: function ( module, script, style, msgs ) {
1282 if ( typeof module !== 'string' ) {
1283 throw new Error( 'module must be a string, not a ' + typeof module );
1285 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1286 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1288 if ( !$.isPlainObject( style ) ) {
1289 throw new Error( 'style must be an object, not a ' + typeof style );
1291 if ( !$.isPlainObject( msgs ) ) {
1292 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1294 // Automatically register module
1295 if ( registry[module] === undefined ) {
1296 mw.loader.register( module );
1298 // Check for duplicate implementation
1299 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1300 throw new Error( 'module already implemented: ' + module );
1302 // Attach components
1303 registry[module].script = script;
1304 registry[module].style = style;
1305 registry[module].messages = msgs;
1306 // The module may already have been marked as erroneous
1307 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1308 registry[module].state = 'loaded';
1309 if ( allReady( registry[module].dependencies ) ) {
1316 * Executes a function as soon as one or more required modules are ready
1318 * @param dependencies {String|Array} Module name or array of modules names the callback
1319 * dependends on to be ready before executing
1320 * @param ready {Function} callback to execute when all dependencies are ready (optional)
1321 * @param error {Function} callback to execute when if dependencies have a errors (optional)
1323 using: function ( dependencies, ready, error ) {
1324 var tod = typeof dependencies;
1326 if ( tod !== 'object' && tod !== 'string' ) {
1327 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1329 // Allow calling with a single dependency as a string
1330 if ( tod === 'string' ) {
1331 dependencies = [ dependencies ];
1333 // Resolve entire dependency map
1334 dependencies = resolve( dependencies );
1335 if ( allReady( dependencies ) ) {
1336 // Run ready immediately
1337 if ( $.isFunction( ready ) ) {
1340 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1341 // Execute error immediately if any dependencies have errors
1342 if ( $.isFunction( error ) ) {
1343 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1347 // Not all dependencies are ready: queue up a request
1348 request( dependencies, ready, error );
1353 * Loads an external script or one or more modules for future use
1355 * @param modules {mixed} Either the name of a module, array of modules,
1356 * or a URL of an external script or style
1357 * @param type {String} mime-type to use if calling with a URL of an
1358 * external script or style; acceptable values are "text/css" and
1359 * "text/javascript"; if no type is provided, text/javascript is assumed.
1360 * @param async {Boolean} (optional) If true, load modules asynchronously
1361 * even if document ready has not yet occurred. If false (default),
1362 * block before document ready and load async after. If not set, true will
1363 * be assumed if loading a URL, and false will be assumed otherwise.
1365 load: function ( modules, type, async ) {
1366 var filtered, m, module, l;
1369 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1370 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1372 // Allow calling with an external url or single dependency as a string
1373 if ( typeof modules === 'string' ) {
1374 // Support adding arbitrary external scripts
1375 if ( /^(https?:)?\/\//.test( modules ) ) {
1376 if ( async === undefined ) {
1377 // Assume async for bug 34542
1380 if ( type === 'text/css' ) {
1381 // IE7-8 throws security warnings when inserting a <link> tag
1382 // with a protocol-relative URL set though attributes (instead of
1383 // properties) - when on HTTPS. See also bug #.
1384 l = document.createElement( 'link' );
1385 l.rel = 'stylesheet';
1387 $( 'head' ).append( l );
1390 if ( type === 'text/javascript' || type === undefined ) {
1391 addScript( modules, null, async );
1395 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1397 // Called with single module
1398 modules = [ modules ];
1401 // Filter out undefined modules, otherwise resolve() will throw
1402 // an exception for trying to load an undefined module.
1403 // Undefined modules are acceptable here in load(), because load() takes
1404 // an array of unrelated modules, whereas the modules passed to
1405 // using() are related and must all be loaded.
1406 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1407 module = registry[modules[m]];
1408 if ( module !== undefined ) {
1409 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1410 filtered[filtered.length] = modules[m];
1415 if ( filtered.length === 0 ) {
1418 // Resolve entire dependency map
1419 filtered = resolve( filtered );
1420 // If all modules are ready, nothing to be done
1421 if ( allReady( filtered ) ) {
1424 // If any modules have errors: also quit.
1425 if ( filter( ['error', 'missing'], filtered ).length ) {
1428 // Since some modules are not yet ready, queue up a request.
1429 request( filtered, null, null, async );
1433 * Changes the state of a module
1435 * @param module {String|Object} module name or object of module name/state pairs
1436 * @param state {String} state name
1438 state: function ( module, state ) {
1441 if ( typeof module === 'object' ) {
1442 for ( m in module ) {
1443 mw.loader.state( m, module[m] );
1447 if ( registry[module] === undefined ) {
1448 mw.loader.register( module );
1450 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1451 && registry[module].state !== state ) {
1452 // Make sure pending modules depending on this one get executed if their
1453 // dependencies are now fulfilled!
1454 registry[module].state = state;
1455 handlePending( module );
1457 registry[module].state = state;
1462 * Gets the version of a module
1464 * @param module string name of module to get version for
1466 getVersion: function ( module ) {
1467 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1468 return formatVersionNumber( registry[module].version );
1474 * @deprecated since 1.18 use mw.loader.getVersion() instead
1476 version: function () {
1477 return mw.loader.getVersion.apply( mw.loader, arguments );
1481 * Gets the state of a module
1483 * @param module string name of module to get state for
1485 getState: function ( module ) {
1486 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1487 return registry[module].state;
1493 * Get names of all registered modules.
1497 getModuleNames: function () {
1498 return $.map( registry, function ( i, key ) {
1504 * For backwards-compatibility with Squid-cached pages. Loads mw.user
1507 mw.loader.load( 'mediawiki.user' );
1512 /** HTML construction helper functions */
1513 html: ( function () {
1514 function escapeCallback( s ) {
1531 * Escape a string for HTML. Converts special characters to HTML entities.
1532 * @param s The string to escape
1534 escape: function ( s ) {
1535 return s.replace( /['"<>&]/g, escapeCallback );
1539 * Wrapper object for raw HTML passed to mw.html.element().
1542 Raw: function ( value ) {
1547 * Wrapper object for CDATA element contents passed to mw.html.element()
1550 Cdata: function ( value ) {
1555 * Create an HTML element string, with safe escaping.
1557 * @param name The tag name.
1558 * @param attrs An object with members mapping element names to values
1559 * @param contents The contents of the element. May be either:
1560 * - string: The string is escaped.
1561 * - null or undefined: The short closing form is used, e.g. <br/>.
1562 * - this.Raw: The value attribute is included without escaping.
1563 * - this.Cdata: The value attribute is included, and an exception is
1564 * thrown if it contains an illegal ETAGO delimiter.
1565 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1569 * return h.element( 'div', {},
1570 * new h.Raw( h.element( 'img', {src: '<'} ) ) );
1571 * Returns <div><img src="<"/></div>
1573 element: function ( name, attrs, contents ) {
1574 var v, attrName, s = '<' + name;
1576 for ( attrName in attrs ) {
1577 v = attrs[attrName];
1578 // Convert name=true, to name=name
1582 } else if ( v === false ) {
1585 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
1587 if ( contents === undefined || contents === null ) {
1594 switch ( typeof contents ) {
1597 s += this.escape( contents );
1601 // Convert to string
1602 s += String( contents );
1605 if ( contents instanceof this.Raw ) {
1606 // Raw HTML inclusion
1607 s += contents.value;
1608 } else if ( contents instanceof this.Cdata ) {
1610 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1611 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1613 s += contents.value;
1615 throw new Error( 'mw.html.element: Invalid type of contents' );
1618 s += '</' + name + '>';
1624 // Skeleton user object. mediawiki.user.js extends this
1633 // Alias $j to jQuery for backwards compatibility
1636 // Attach to window and globally alias
1637 window.mw = window.mediaWiki = mw;
1639 // Auto-register from pre-loaded startup scripts
1640 if ( jQuery.isFunction( window.startUp ) ) {
1642 window.startUp = undefined;